Initial implementation; some generics, but needs some tweaking

master
Tait Hoyem 2 years ago
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,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…
Cancel
Save