parent
37197a9b07
commit
163d0ec657
@ -0,0 +1,297 @@
|
||||
use ureq;
|
||||
use crate::success;
|
||||
use crate::external;
|
||||
|
||||
use serde_json;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use serde_with::{
|
||||
DisplayFromStr,
|
||||
serde_as,
|
||||
};
|
||||
use uuid;
|
||||
|
||||
/// Defines a VM (used for requests using the VM section)
|
||||
#[serde_as]
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
||||
pub struct VirtualMachine {
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
vm_id: uuid::Uuid, // should be UUIDv4
|
||||
name: String, // the name set by the user
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
plan_id: i32, // should be more strict
|
||||
hostname: String, // the hostname set by the user
|
||||
#[serde(rename="primaryip")]
|
||||
primary_ip: std::net::Ipv4Addr,
|
||||
#[serde(rename="privateip")]
|
||||
private_ip: std::net::Ipv4Addr,
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
ram: i32, // in GB, may need to be a float
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
vcpu: i32, // CPU cores
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
storage: i32, // in GB
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
bandwidth: i32, // in GB/month
|
||||
region: String, // could be more strict
|
||||
os_status: String, // should be nmore strict
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct VMListResponse {
|
||||
vms: Vec<VirtualMachine>,
|
||||
#[serde(with="success")]
|
||||
success: bool, // should be more strict "yes" or "no" as optiobs
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct VMInfoExtra {
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
/// bandwidth allowed over a month in GB
|
||||
bandwidth: i32,
|
||||
/// the hostname set by the user
|
||||
hostname: String, // sufficiently vague
|
||||
/// the name set by the user
|
||||
name: String, // sufficiently vague
|
||||
/// the status of the operating system; possible values: "active", ... TODO
|
||||
os_status: String, // should be stricter
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
/// id of the plan being used on the VM
|
||||
plan_id: i32,
|
||||
#[serde(rename="primaryip")]
|
||||
/// primary (floating) IP
|
||||
primary_ip: std::net::Ipv4Addr,
|
||||
#[serde(rename="privateip")]
|
||||
/// the private (non-floating) IP to connect between nodes
|
||||
private_ip: std::net::Ipv4Addr,
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
/// RAM meassured in MB
|
||||
ram: i32,
|
||||
/// the datacentre location the VM is running in
|
||||
region: LNRegion,
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
/// the storage size of the VM in GB
|
||||
storage: i32,
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
/// number of virtual CPU cores allocated to the VM
|
||||
vcpu: i32,
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
/// UUIDv4 of the VM
|
||||
vm_id: uuid::Uuid,
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum IPAddress {
|
||||
V4(std::net::Ipv4Addr),
|
||||
V6(std::net::Ipv6Addr),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
// TODO: (de)serialize as "4"/"6" respectively
|
||||
pub enum IPAddressType {
|
||||
#[serde(rename="4")]
|
||||
V4,
|
||||
#[serde(rename="6")]
|
||||
V6,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct VMAddress {
|
||||
addr: IPAddress,
|
||||
#[serde(with="external")]
|
||||
external: bool,
|
||||
version: IPAddressType,
|
||||
reverse: Option<String>, // optional rDNS; string is sufficient, since the server will not be able to set it to something invalid.
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct VMInfo {
|
||||
#[serde(rename="additionalip")]
|
||||
additional_ip: Vec<VMAddress>,
|
||||
#[serde(rename="additionalprivateip")]
|
||||
additional_private_ip: Vec<VMAddress>,
|
||||
addresses: Vec<VMAddress>,
|
||||
/// a possibly empty string containing an error message
|
||||
error_detail: Option<String>,
|
||||
/// some unkown string that doesn't seem to be a UUID... maybe the checksum?
|
||||
host_id: String,
|
||||
/// the name of the VM set at creation by the user
|
||||
hostname: String,
|
||||
/// the name of the image being used by the VM
|
||||
image: String,
|
||||
/// the primary, external IP of the VM
|
||||
ip: std::net::Ipv4Addr,
|
||||
/// a list of IPv6 addresses assigned to the VM
|
||||
ipv6: Vec<std::net::Ipv6Addr>,
|
||||
/// Login details from the VM; this could potentially be a bit more strict to support the storing of username and password separately.
|
||||
/// Also, it's optional. The server may not even report this value at all, not only give a blank one. Fair enough. Seems more secure.
|
||||
login_details: Option<String>,
|
||||
/// the operating system (orignal image used to load the machine)
|
||||
os: String, // sufficiently vague
|
||||
#[serde(rename="privateip")]
|
||||
/// the primary private IP assigned to the machine
|
||||
private_ip: std::net::Ipv4Addr,
|
||||
/// security groups by id that the VM belongs to; should be a vec of i32, but it might take some custom implementations
|
||||
security_group_ids: Vec<String>,
|
||||
/// security groups by name that the VM belongs to
|
||||
security_groups: Vec<String>,
|
||||
#[serde(rename="securitygroups")]
|
||||
/// why is there a second one of these with a different name. This is stupid.
|
||||
security_groups2: Vec<String>,
|
||||
/// an HTML status of the machine
|
||||
status: String,
|
||||
/// the color of the status; this could be stricter
|
||||
status_color: String, // could be more strict
|
||||
/// the non-HTML status, this could potentially be more strict
|
||||
status_nohtml: String,
|
||||
/// A raw status-code string; this could for sure be more strict.
|
||||
status_raw: String,
|
||||
/// A possibly empty string, idk what task_state is tho
|
||||
task_state: String, // a possibly empty string
|
||||
/// A possibly empty string of attached volumes.
|
||||
volumes: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct VMInfoResponse {
|
||||
extra: VMInfoExtra,
|
||||
info: VMInfo,
|
||||
#[serde(with="success")]
|
||||
success: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum LNPlanCategory {
|
||||
#[serde(rename="Compute-Optimized")]
|
||||
ComputeOptimized,
|
||||
#[serde(rename="General Purpose")]
|
||||
GeneralPurpose,
|
||||
#[serde(rename="Memory-Optimized")]
|
||||
MemoryOptimized,
|
||||
#[serde(rename="SSD-Cached High-Memory")]
|
||||
SSDCacheHighMemory,
|
||||
#[serde(rename="SSD-Cached Standard")]
|
||||
SSDCacheStandard,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum LNRegion {
|
||||
Montreal,
|
||||
Roubaix,
|
||||
Toronto
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct LNPlan {
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
all_regions: i32, // may need to be more strict?
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
bandwidth: i32, // in Mbps
|
||||
category: LNPlanCategory, // could be more strict, "General Purpose", "Compute Optimized", "RAM Optimized"
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
cpu_points: f32, // no idea what this meas
|
||||
name: String, // plan name, this could potentially be strictly typed as the types of plans don't often change "s.half", "s.1", "m.1", "m.2", etc.
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
plan_id: i32, // can be strictly typed, if needed
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
price: f32, // up to 7 decmial points (f32), and this is the number of US dollars per hour
|
||||
price_monthly_nice: String, // instead of calculating it on your own, this provides a nice reading of the price for clients
|
||||
price_nice: String, // same as above, but for the hour
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
ram: i32, // in MB
|
||||
regions: Vec<LNRegion>, // list of regions by name
|
||||
regions_nice: String, // list of regions concatonated with commans
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
storage: i32, // per GB
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
vcpu: i32, // number of vCPU cores
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct PlanListResponse {
|
||||
plans: Vec<LNPlan>,
|
||||
#[serde(with="success")]
|
||||
success: bool, // should be more strict: "yes" or "no" as options
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct LNImage {
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
image_id: i32, // id of the image
|
||||
name: String, // set by the user or LunaNode
|
||||
region: LNRegion,
|
||||
status: String, // should be stricter, at least "active", "inactive"(?)
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct LNImageDetails {
|
||||
cache_mode: String, // should be stricter, at least "writeback",
|
||||
checksum: String, // should be stricter, to check of checksum type
|
||||
disk_format: String, // should be stricter, at least "iso"
|
||||
hw_disk_bus: String, // should be stricter, at least "ide",
|
||||
hw_video_model: String, // could, in theory, be stricter, at least: "cirrus",
|
||||
hw_vif_model: String, // appropriately vague
|
||||
#[serde(with="success")]
|
||||
is_read_only: bool, // "yes"/"no"
|
||||
libvrt_cpu_mode: String, // should be stricter, at least: "host-model",
|
||||
metadata: Vec<()>, // vec of what?
|
||||
name: String, // sufficiently vague
|
||||
region: LNRegion, // sufficiently typed
|
||||
size: i32, // in MB, maybe?
|
||||
status: String, // should be stricter, at least: "active",
|
||||
time_created: String, // should be a datetime
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct LNImageDetailResponse {
|
||||
#[serde(with="success")]
|
||||
success: bool,
|
||||
details: LNImageDetails,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct LNImageListResponse {
|
||||
images: Vec<LNImage>,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct BillingCreditResponse {
|
||||
/// Money left in the account in USD
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
credit: f32,
|
||||
#[serde(with="success")]
|
||||
success: bool, // this should be stricter
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct LNErrorResponse {
|
||||
#[serde(with="success")]
|
||||
success: bool,
|
||||
error: String, // proper type, full accoutnign of the error from the API
|
||||
}
|
||||
impl std::fmt::Display for LNErrorResponse {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "An API error has occured! {}", self.error)
|
||||
}
|
||||
}
|
||||
impl std::error::Error for LNErrorResponse {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LNError {
|
||||
RequestError(ureq::Error),
|
||||
DeserializationError(std::io::Error),
|
||||
TimeError(std::time::SystemTimeError),
|
||||
LunaNodeError(LNErrorResponse),
|
||||
SerdeError(serde_json::Error, String), // the serde error, accompanied by a raw string
|
||||
GetFuckedError,
|
||||
}
|
||||
impl std::fmt::Display for LNError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "An error has occured! {}", self)
|
||||
}
|
||||
}
|
||||
impl std::error::Error for LNError {}
|
@ -0,0 +1 @@
|
||||
|
@ -0,0 +1,34 @@
|
||||
use serde::{
|
||||
de::{
|
||||
self,
|
||||
Unexpected,
|
||||
},
|
||||
Serializer,
|
||||
Deserializer,
|
||||
Deserialize,
|
||||
};
|
||||
|
||||
/// Seiralize bool into "yes"/"no", just like the LunaNode API does.
|
||||
pub fn serialize<S>(succ: &bool, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer {
|
||||
match succ {
|
||||
true => serializer.serialize_str("1"),
|
||||
false => serializer.serialize_str(""),
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserialize bool from String with custom value mapping "yes" => true, "no" => false
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<bool, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
match String::deserialize(deserializer)?.as_ref() {
|
||||
"1" => Ok(true),
|
||||
"" => Ok(false),
|
||||
other => Err(de::Error::invalid_value(
|
||||
Unexpected::Str(other),
|
||||
&"\"1\" or \"\"",
|
||||
)),
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
pub mod success;
|
||||
pub mod external;
|
||||
pub mod requests;
|
||||
pub mod responses;
|
||||
pub mod types;
|
||||
|
||||
use ureq::{
|
||||
json,
|
||||
post
|
||||
};
|
||||
use sha2::Sha512;
|
||||
use hmac::{Hmac, Mac};
|
||||
use responses::{
|
||||
VMListResponse,
|
||||
PlanListResponse,
|
||||
LNImageListResponse,
|
||||
BillingCreditResponse,
|
||||
VMInfoResponse,
|
||||
};
|
||||
use serde::{
|
||||
de::DeserializeOwned,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
};
|
||||
use requests::{
|
||||
Args,
|
||||
VMListRequest,
|
||||
};
|
||||
use types::{
|
||||
ApiKeys,
|
||||
};
|
||||
use clap::Parser;
|
||||
|
||||
/*
|
||||
Define types for all different types of API requests.
|
||||
Every valid enum value is the type of API request, and within it, a structure representing that API call.
|
||||
No inner struct is required for requests that do not take arguments.
|
||||
*/
|
||||
#[derive(Serialize, Deserialize, Eq, PartialEq, Hash, Debug)]
|
||||
enum LunaNodeAPIRequest {
|
||||
BillingCredit,
|
||||
ImageList,
|
||||
PlanList,
|
||||
RegionList,
|
||||
VmInfo,
|
||||
VmList(VMListRequest),
|
||||
}
|
||||
impl LunaNodeAPIRequest {
|
||||
fn url_endpoint(&self) -> &str {
|
||||
match self {
|
||||
Self::BillingCredit => "billing/credit/",
|
||||
Self::ImageList => "image/list/",
|
||||
Self::PlanList => "plan/list/",
|
||||
Self::RegionList => "region/list/",
|
||||
Self::VmInfo => "vm/info/",
|
||||
Self::VmList(_) => "vm/list/",
|
||||
}
|
||||
}
|
||||
}
|
||||
impl ToString for LunaNodeAPIRequest {
|
||||
// TODO: don't do this; this should actually serialzie the object somehow
|
||||
fn to_string(&self) -> String {
|
||||
self.url_endpoint().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Hello, world!");
|
||||
let args = Args::parse();
|
||||
args.make_request();
|
||||
Ok(())
|
||||
}
|
@ -0,0 +1,154 @@
|
||||
use crate::{
|
||||
responses::{
|
||||
VMListResponse,
|
||||
BillingCreditResponse,
|
||||
LNImageListResponse,
|
||||
},
|
||||
types::{
|
||||
LNError,
|
||||
LNErrorResponse,
|
||||
LunaNodeRequest,
|
||||
LunaNodeResponse,
|
||||
Requestable,
|
||||
ApiKeys,
|
||||
},
|
||||
};
|
||||
use ureq::post;
|
||||
use serde::{
|
||||
Serialize,
|
||||
Deserialize,
|
||||
de::DeserializeOwned,
|
||||
};
|
||||
use hmac::{Hmac, Mac};
|
||||
use sha2::Sha512;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, clap::Subcommand)]
|
||||
#[serde(untagged)]
|
||||
pub enum ImageSubArgs {
|
||||
List(ImageListRequest),
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Debug, clap::Subcommand)]
|
||||
#[serde(untagged)]
|
||||
pub enum VmSubArgs {
|
||||
List(VMListRequest),
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Debug, clap::Subcommand)]
|
||||
#[serde(untagged)]
|
||||
pub enum BillingSubArgs {
|
||||
Credit(BillingCreditRequest),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, clap::Parser)]
|
||||
#[serde(untagged)]
|
||||
pub enum Args {
|
||||
#[clap(subcommand)]
|
||||
Image(ImageSubArgs),
|
||||
#[clap(subcommand)]
|
||||
Vm(VmSubArgs),
|
||||
#[clap(subcommand)]
|
||||
Billing(BillingSubArgs),
|
||||
}
|
||||
impl ToString for Args {
|
||||
fn to_string(&self) -> String {
|
||||
"Not implemented TODO!".to_string()
|
||||
}
|
||||
}
|
||||
impl Args {
|
||||
pub fn make_request(&self) -> Result<(), LNError> {
|
||||
match self {
|
||||
Self::Vm(VmSubArgs::List(vm_list)) => {
|
||||
let list = vm_list.make_request()?;
|
||||
println!("{:#?}", list);
|
||||
},
|
||||
Self::Image(ImageSubArgs::List(image_list)) => {
|
||||
let list = image_list.make_request()?;
|
||||
println!("{:#?}", list);
|
||||
},
|
||||
Self::Billing(BillingSubArgs::Credit(billing_credit)) => {
|
||||
let credit =billing_credit.make_request()?;
|
||||
println!("{:#?}", credit);
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn get_info(&self) -> (&str, ApiKeys) {
|
||||
match self {
|
||||
Self::Vm(VmSubArgs::List(vm_list)) => ("vm/list/", vm_list.keys.clone()),
|
||||
Self::Image(ImageSubArgs::List(image_list)) => ("image/list/", image_list.keys.clone()),
|
||||
Self::Billing(BillingSubArgs::Credit(credit)) => ("billing/credit/", credit.keys.clone()),
|
||||
}
|
||||
}
|
||||
fn get_keys(&self) -> ApiKeys {
|
||||
self.get_info().1
|
||||
}
|
||||
fn url_endpoint(&self) -> &str {
|
||||
self.get_info().0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Hash, PartialEq, Eq, clap::Args)]
|
||||
/// ImageListRequest is used to create a new request for the /vm/list endpoint.
|
||||
pub struct ImageListRequest {
|
||||
#[serde(flatten)]
|
||||
#[clap(flatten)]
|
||||
pub keys: ApiKeys,
|
||||
}
|
||||
impl ToString for ImageListRequest {
|
||||
fn to_string(&self) -> String {
|
||||
"N/A".to_string()
|
||||
}
|
||||
}
|
||||
impl Requestable for ImageListRequest {}
|
||||
impl LunaNodeRequest for ImageListRequest {
|
||||
type response = LNImageListResponse;
|
||||
fn url_endpoint(&self) -> String {
|
||||
"image/list/".to_string()
|
||||
}
|
||||
fn get_keys(&self) -> ApiKeys {
|
||||
self.keys.clone()
|
||||
}
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Debug, Hash, PartialEq, Eq, clap::Args)]
|
||||
/// BillingCreditRequest handles the /billing/credits/ endpoint. It will produce a BillingCreditResponse.
|
||||
pub struct BillingCreditRequest {
|
||||
#[serde(flatten)]
|
||||
#[clap(flatten)]
|
||||
pub keys: ApiKeys,
|
||||
}
|
||||
impl ToString for BillingCreditRequest {
|
||||
fn to_string(&self) -> String {
|
||||
"N/A".to_string()
|
||||
}
|
||||
}
|
||||
impl Requestable for BillingCreditRequest {}
|
||||
impl LunaNodeRequest for BillingCreditRequest {
|
||||
type response = BillingCreditResponse;
|
||||
fn get_keys(&self) -> ApiKeys {
|
||||
self.keys.clone()
|
||||
}
|
||||
fn url_endpoint(&self) -> String {
|
||||
"vm/list/".to_string()
|
||||
}
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Debug, Hash, PartialEq, Eq, clap::Args)]
|
||||
/// VMListRequest is used to create a new request for the /vm/list endpoint.
|
||||
pub struct VMListRequest {
|
||||
#[serde(flatten)]
|
||||
#[clap(flatten)]
|
||||
pub keys: ApiKeys,
|
||||
}
|
||||
impl ToString for VMListRequest {
|
||||
fn to_string(&self) -> String {
|
||||
"N/A".to_string()
|
||||
}
|
||||
}
|
||||
impl Requestable for VMListRequest {}
|
||||
impl LunaNodeRequest for VMListRequest {
|
||||
type response = VMListResponse;
|
||||
fn get_keys(&self) -> ApiKeys {
|
||||
self.keys.clone()
|
||||
}
|
||||
fn url_endpoint(&self) -> String {
|
||||
"vm/list/".to_string()
|
||||
}
|
||||
}
|
@ -0,0 +1,297 @@
|
||||
use ureq;
|
||||
use crate::success;
|
||||
use crate::external;
|
||||
use crate::types::{
|
||||
LunaNodeResponse,
|
||||
LunaNodeRequest,
|
||||
Requestable,
|
||||
};
|
||||
|
||||
use serde_json;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use serde_with::{
|
||||
DisplayFromStr,
|
||||
serde_as,
|
||||
};
|
||||
use uuid;
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
||||
/// Defines a VM (used for requests using the VM section)
|
||||
pub struct VirtualMachine {
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
/// the UUID of the VM
|
||||
vm_id: uuid::Uuid, // should be UUIDv4
|
||||
/// the name of the VM set by the user
|
||||
name: String, // the name set by the user
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
/// The plan ID, cross-reference with /plan/list/
|
||||
plan_id: i32, // should be more strict
|
||||
/// The hostname set by the user
|
||||
hostname: String, // the hostname set by the user
|
||||
#[serde(rename="primaryip")]
|
||||
/// The primary (public) IP address of the VM.
|
||||
primary_ip: std::net::Ipv4Addr,
|
||||
#[serde(rename="privateip")]
|
||||
/// The private IP address of the VM.
|
||||
private_ip: std::net::Ipv4Addr,
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
/// RAM of the VM in GB
|
||||
ram: i32, // in GB, may need to be a float
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
/// The number of virtual CPU cores
|
||||
vcpu: i32, // CPU cores
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
/// The storage amount of the VM in GB
|
||||
storage: i32, // in GB
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
/// bandwidth in GB/month
|
||||
bandwidth: i32, // in GB/month
|
||||
/// The region where the VM is located
|
||||
region: LNRegion, // could be more strict
|
||||
/// The status, which should be more strict than a String.
|
||||
os_status: String, // should be nmore strict
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct VMListResponse {
|
||||
vms: Vec<VirtualMachine>,
|
||||
#[serde(with="success")]
|
||||
success: bool, // should be more strict "yes" or "no" as optiobs
|
||||
}
|
||||
impl ToString for VMListResponse {
|
||||
fn to_string(&self) -> String {
|
||||
"N/A".to_string()
|
||||
}
|
||||
}
|
||||
impl LunaNodeResponse for VMListResponse {}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct VMInfoExtra {
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
/// bandwidth allowed over a month in GB
|
||||
bandwidth: i32,
|
||||
/// the hostname set by the user
|
||||
hostname: String, // sufficiently vague
|
||||
/// the name set by the user
|
||||
name: String, // sufficiently vague
|
||||
/// the status of the operating system; possible values: "active", ... TODO
|
||||
os_status: String, // should be stricter
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
/// id of the plan being used on the VM
|
||||
plan_id: i32,
|
||||
#[serde(rename="primaryip")]
|
||||
/// primary (floating) IP
|
||||
primary_ip: std::net::Ipv4Addr,
|
||||
#[serde(rename="privateip")]
|
||||
/// the private (non-floating) IP to connect between nodes
|
||||
private_ip: std::net::Ipv4Addr,
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
/// RAM meassured in MB
|
||||
ram: i32,
|
||||
/// the datacentre location the VM is running in
|
||||
region: LNRegion,
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
/// the storage size of the VM in GB
|
||||
storage: i32,
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
/// number of virtual CPU cores allocated to the VM
|
||||
vcpu: i32,
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
/// UUIDv4 of the VM
|
||||
vm_id: uuid::Uuid,
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(untagged)]
|
||||
/// A generic IP address type, with two variants: V4, and V6. This is used for generic types returned from the API. For example, a list of IP addresses without the IP type specified.
|
||||
pub enum IPAddress {
|
||||
V4(std::net::Ipv4Addr),
|
||||
V6(std::net::Ipv6Addr),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum IPAddressType {
|
||||
#[serde(rename="4")]
|
||||
V4,
|
||||
#[serde(rename="6")]
|
||||
V6,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct VMAddress {
|
||||
addr: IPAddress,
|
||||
#[serde(with="external")]
|
||||
external: bool,
|
||||
version: IPAddressType,
|
||||
#[serde(rename="reverse")]
|
||||
/// The reverse DNS assigned to the VM. This is optional.
|
||||
rdns: Option<String>,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct VMInfo {
|
||||
#[serde(rename="additionalip")]
|
||||
additional_ip: Vec<VMAddress>,
|
||||
#[serde(rename="additionalprivateip")]
|
||||
additional_private_ip: Vec<VMAddress>,
|
||||
addresses: Vec<VMAddress>,
|
||||
/// a possibly empty string containing an error message
|
||||
error_detail: Option<String>,
|
||||
/// some unkown string that doesn't seem to be a UUID... maybe the checksum?
|
||||
host_id: String,
|
||||
/// the name of the VM set at creation by the user
|
||||
hostname: String,
|
||||
/// the name of the image being used by the VM
|
||||
image: String,
|
||||
/// the primary, external IP of the VM
|
||||
ip: std::net::Ipv4Addr,
|
||||
/// a list of IPv6 addresses assigned to the VM
|
||||
ipv6: Vec<std::net::Ipv6Addr>,
|
||||
/// Login details from the VM; this could potentially be a bit more strict to support the storing of username and password separately.
|
||||
/// Also, it's optional. The server may not even report this value at all, not only give a blank one. Fair enough. Seems more secure.
|
||||
login_details: Option<String>,
|
||||
/// the operating system (orignal image used to load the machine)
|
||||
os: String, // sufficiently vague
|
||||
#[serde(rename="privateip")]
|
||||
/// the primary private IP assigned to the machine
|
||||
private_ip: std::net::Ipv4Addr,
|
||||
/// security groups by id that the VM belongs to; should be a vec of i32, but it might take some custom implementations
|
||||
security_group_ids: Vec<String>,
|
||||
/// security groups by name that the VM belongs to
|
||||
security_groups: Vec<String>,
|
||||
#[serde(rename="securitygroups")]
|
||||
/// why is there a second one of these with a different name. This is stupid.
|
||||
security_groups2: Vec<String>,
|
||||
/// an HTML status of the machine
|
||||
status: String,
|
||||
/// the color of the status; this could be stricter
|
||||
status_color: String, // could be more strict
|
||||
/// the non-HTML status, this could potentially be more strict
|
||||
status_nohtml: String,
|
||||
/// A raw status-code string; this could for sure be more strict.
|
||||
status_raw: String,
|
||||
/// A possibly empty string, idk what task_state is tho
|
||||
task_state: String, // a possibly empty string
|
||||
/// A possibly empty string of attached volumes.
|
||||
volumes: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct VMInfoResponse {
|
||||
extra: VMInfoExtra,
|
||||
info: VMInfo,
|
||||
#[serde(with="success")]
|
||||
success: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum LNPlanCategory {
|
||||
#[serde(rename="Compute-Optimized")]
|
||||
ComputeOptimized,
|
||||
#[serde(rename="General Purpose")]
|
||||
GeneralPurpose,
|
||||
#[serde(rename="Memory-Optimized")]
|
||||
MemoryOptimized,
|
||||
#[serde(rename="SSD-Cached High-Memory")]
|
||||
SSDCacheHighMemory,
|
||||
#[serde(rename="SSD-Cached Standard")]
|
||||
SSDCacheStandard,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Hash, Eq, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum LNRegion {
|
||||
Montreal,
|
||||
Roubaix,
|
||||
Toronto
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct LNPlan {
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
all_regions: i32, // may need to be more strict?
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
bandwidth: i32, // in Mbps
|
||||
category: LNPlanCategory,
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
cpu_points: f32, // no idea what this meas
|
||||
name: String, // plan name, this could potentially be strictly typed as the types of plans don't often change "s.half", "s.1", "m.1", "m.2", etc.
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
plan_id: i32, // can be strictly typed, if needed
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
price: f32, // up to 7 decmial points (f32), and this is the number of US dollars per hour
|
||||
price_monthly_nice: String, // instead of calculating it on your own, this provides a nice reading of the price for clients
|
||||
price_nice: String, // same as above, but for the hour
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
ram: i32, // in MB
|
||||
regions: Vec<LNRegion>, // list of regions by name
|
||||
regions_nice: String, // list of regions concatonated with commans
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
storage: i32, // per GB
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
vcpu: i32, // number of vCPU cores
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct PlanListResponse {
|
||||
plans: Vec<LNPlan>,
|
||||
#[serde(with="success")]
|
||||
success: bool, // should be more strict: "yes" or "no" as options
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct LNImage {
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
image_id: i32, // id of the image
|
||||
name: String, // set by the user or LunaNode
|
||||
region: LNRegion,
|
||||
status: String, // should be stricter, at least "active", "inactive"(?)
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct LNImageDetails {
|
||||
cache_mode: String, // should be stricter, at least "writeback",
|
||||
checksum: String, // should be stricter, to check of checksum type
|
||||
disk_format: String, // should be stricter, at least "iso"
|
||||
hw_disk_bus: String, // should be stricter, at least "ide",
|
||||
hw_video_model: String, // could, in theory, be stricter, at least: "cirrus",
|
||||
hw_vif_model: String, // appropriately vague
|
||||
#[serde(with="success")]
|
||||
is_read_only: bool, // "yes"/"no"
|
||||
libvrt_cpu_mode: String, // should be stricter, at least: "host-model",
|
||||
metadata: Vec<()>, // vec of what?
|
||||
name: String, // sufficiently vague
|
||||
region: LNRegion, // sufficiently typed
|
||||
size: i32, // in MB, maybe?
|
||||
status: String, // should be stricter, at least: "active",
|
||||
time_created: String, // should be a datetime
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct LNImageDetailResponse {
|
||||
#[serde(with="success")]
|
||||
success: bool,
|
||||
details: LNImageDetails,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct LNImageListResponse {
|
||||
images: Vec<LNImage>,
|
||||
#[serde(with="success")]
|
||||
success: bool,
|
||||
}
|
||||
impl LunaNodeResponse for LNImageListResponse {}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct BillingCreditResponse {
|
||||
/// Money left in the account in USD
|
||||
#[serde_as(as="DisplayFromStr")]
|
||||
credit: f32,
|
||||
#[serde(with="success")]
|
||||
success: bool, // this should be stricter
|
||||
}
|
||||
impl LunaNodeResponse for BillingCreditResponse {}
|
@ -0,0 +1,34 @@
|
||||
use serde::{
|
||||
de::{
|
||||
self,
|
||||
Unexpected,
|
||||
},
|
||||
Serializer,
|
||||
Deserializer,
|
||||
Deserialize,
|
||||
};
|
||||
|
||||
/// Seiralize bool into "yes"/"no", just like the LunaNode API does.
|
||||
pub fn serialize<S>(succ: &bool, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer {
|
||||
match succ {
|
||||
true => serializer.serialize_str("yes"),
|
||||
false => serializer.serialize_str("no"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserialize bool from String with custom value mapping "yes" => true, "no" => false
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<bool, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
match String::deserialize(deserializer)?.as_ref() {
|
||||
"yes" => Ok(true),
|
||||
"no" => Ok(false),
|
||||
other => Err(de::Error::invalid_value(
|
||||
Unexpected::Str(other),
|
||||
&"\"yes\" or \"no\"",
|
||||
)),
|
||||
}
|
||||
}
|
@ -0,0 +1,135 @@
|
||||
use crate::success;
|
||||
use clap;
|
||||
use hmac::{Hmac, Mac};
|
||||
use serde::{
|
||||
Serialize,
|
||||
Deserialize,
|
||||
de::DeserializeOwned,
|
||||
};
|
||||
use sha2::Sha512;
|
||||
use ureq::post;
|
||||
type HmacSHA512 = Hmac<Sha512>;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, Hash, PartialEq, clap::Args, Clone)]
|
||||
/// Used to specify API credentials. These are not required as long as LUNANODE_KEY_ID and LUNANODE_API_KEY are specified in the environment.
|
||||
pub struct ApiKeys {
|
||||
#[clap(long, takes_value=true, env="LUNANODE_KEY_ID", help="The API key ID from the Lunanode Dashboard.", long_help="The API key ID from the Lunanode dashboard. You may also specify this option via the LUNANODE_KEY_ID environment variable.")]
|
||||
/// The API id string as used by LunaNode. This should be exactly 16 bytes. This will get enforced at a later date.
|
||||
pub api_id: String,
|
||||
#[serde(skip)]
|
||||
#[clap(long, takes_value=true, env="LUNANODE_API_KEY", help="The Lunanode API key received from the dashboard.", long_help="The Lunanode API key recieved from the dashboard. This option may also be set by the LUNANODE_API_KEY environemnt variable.")]
|
||||
/// Used for convenience. It is not serialized nor deserializes; this means that this object can NOT be deserialized at all, since this is not an Option<String>.
|
||||
/// This is used to fill in the api_paritlakey field, and to give the request a way to access the api key locally.
|
||||
pub api_key: String,
|
||||
#[clap(long, takes_value=true, env="LUNANODE_API_PARTIALKEY", help="The Lunanode API key received from the dashboard, but only the first 64 bytes of it.", long_help="The Lunanode API key recieved from the dashboard. This option may also be set by the LUNANODE_API_PARTIALKEY environemnt variable. This should be autogenerated based on the LUNANODE_API_KEY, but it isn't. TODO.")]
|
||||
/// The api_paritalkey field as used by LunaNode (the first 64 bytes of the api_key). Note that this field is autofilled.
|
||||
/// Since this should always be a 64 byte array, this will be enforced at some point in the future. TODO
|
||||
pub api_partialkey: String,
|
||||
}
|
||||
impl ApiKeys {
|
||||
pub fn new(api_id: String, api_key: String) -> Self {
|
||||
Self {
|
||||
api_id,
|
||||
api_key: api_key.clone(),
|
||||
api_partialkey: api_key[..64].to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Requestable: Serialize + ToString {}
|
||||
pub trait LunaNodeResponse: DeserializeOwned {}
|
||||
pub trait LunaNodeRequest: Requestable {
|
||||
/// The resposne type you expect after making this request.
|
||||
/// Setting this will allow acces to a generic function, make_request, that will expect this type to be recieved.
|
||||
/// See: make_request
|
||||
type response: LunaNodeResponse;
|
||||
|
||||
/// Can't genericize this one out. You need to implenent a function which can return a set of API keys for creating an API requst.
|
||||
fn get_keys(&self) -> ApiKeys;
|
||||
/// The LunaNode API endpoint you'll be using.
|
||||
/// Note: do *NOT* add a leading slash, but *DO* include a trailing slash.
|
||||
/// The three wrong ways: "/vm/list", "vm/list", "/vm/list",
|
||||
/// The only correct way: "vm/list/".
|
||||
fn url_endpoint(&self) -> String;
|
||||
/// A generic function to recieve a specific relsponse type from the server.
|
||||
/// The Self::response type is the type you're expecting. In the case that this type is incorrect, or the server returns bad data, you will receive an LNError::SerdeError(serde::Error, String), with the String being a raw response from the server.
|
||||
/// You may also recieve any other error defined in LNError, including timezone errors due to not being able to create a nonce value for the request.
|
||||
/// See LNError.
|
||||
fn make_request(&self) -> Result<Self::response, LNError> {
|
||||
make_request::<Self, Self::response>(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct LNErrorResponse {
|
||||
#[serde(with="success")]
|
||||
success: bool,
|
||||
error: String, // proper type, full accoutnign of the error from the API
|
||||
}
|
||||
impl std::fmt::Display for LNErrorResponse {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "An API error has occured! {}", self.error)
|
||||
}
|
||||
}
|
||||
impl std::error::Error for LNErrorResponse {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LNError {
|
||||
RequestError(ureq::Error),
|
||||
DeserializationError(std::io::Error),
|
||||
TimeError(std::time::SystemTimeError),
|
||||
LunaNodeError(LNErrorResponse),
|
||||
SerdeError(serde_json::Error, String), // the serde error, accompanied by a raw string
|
||||
GetFuckedError,
|
||||
}
|
||||
impl std::fmt::Display for LNError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "An error has occured! {}", self)
|
||||
}
|
||||
}
|
||||
impl std::error::Error for LNError {}
|
||||
|
||||
fn make_request<S, D>(req: &S) -> Result<D, LNError>
|
||||
where S: LunaNodeRequest + ?Sized,
|
||||
D: LunaNodeResponse
|
||||
{
|
||||
let json_req_data = match serde_json::to_string(req) {
|
||||
Ok(jrd) => jrd,
|
||||
Err(e) => return Err(LNError::SerdeError(e, req.to_string())),
|
||||
};
|
||||
println!("RAW: {}", json_req_data);
|
||||
let epoch = match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
|
||||
Err(e) => return Err(LNError::TimeError(e)),
|
||||
Ok(ue) => ue,
|
||||
}.as_secs();
|
||||
let nonce = format!("{}", epoch);
|
||||
let mut hmac = HmacSHA512::new_from_slice(req.get_keys().api_key.as_bytes()).expect("SHA512 accepts text of any size.");
|
||||
let hmac_input = format!("{}|{}|{}", req.url_endpoint(), &json_req_data, nonce);
|
||||
hmac.update(hmac_input.as_bytes());
|
||||
let signature = hex::encode(hmac.finalize().into_bytes());
|
||||
let full_url = format!("https://dynamic.lunanode.com/api/{}", req.url_endpoint());
|
||||
println!("FULL URL: {}", full_url);
|
||||
match post(&full_url)
|
||||
.send_form(&[
|
||||
("req", &json_req_data),
|
||||
("signature", &signature),
|
||||
("nonce", &nonce),
|
||||
]) {
|
||||
Err(e) => Err(LNError::RequestError(e)),
|
||||
Ok(resp) => {
|
||||
let resp_str = match resp.into_string() {
|
||||
Err(e) => return Err(LNError::DeserializationError(e)),
|
||||
Ok(s) => s,
|
||||
};
|
||||
match serde_json::from_str::<LNErrorResponse>(&resp_str) {
|
||||
Ok(e) => return Err(LNError::LunaNodeError(e)),
|
||||
Err(e) => false,
|
||||
};
|
||||
match serde_json::from_str::<D>(&resp_str) {
|
||||
Ok(s) => Ok(s),
|
||||
Err(e) => Err(LNError::SerdeError(e, resp_str)),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in new issue