From 871d895e173625b3e83b3eea1b43cc1b4448ae74 Mon Sep 17 00:00:00 2001 From: "Azzam S.A" Date: Thu, 4 Apr 2024 11:10:13 +0700 Subject: [PATCH] Initial commit --- .gitignore | 4 + Cargo.toml | 21 +++- README.md | 51 ++++++++ configs/cliff.toml | 4 +- configs/typos.toml | 2 +- docs/README.md | 12 ++ examples/.example.env | 4 + examples/README.md | 7 ++ examples/create_snapshot.rs | 114 ++++++++++++++++++ examples/create_vm.rs | 87 ++++++++++++++ examples/keypair.rs | 55 +++++++++ examples/plan.rs | 90 ++++++++++++++ examples/snapshot.rs | 50 ++++++++ examples/vm.rs | 69 +++++++++++ justfile | 37 +----- rust-toolchain.toml | 2 +- src/client.rs | 92 +++++++++++++++ src/config.rs | 14 +++ src/domain/account.rs | 160 +++++++++++++++++++++++++ src/domain/keypair.rs | 52 ++++++++ src/domain/lite.rs | 35 ++++++ src/domain/mod.rs | 7 ++ src/domain/products/ip.rs | 34 ++++++ src/domain/products/mod.rs | 3 + src/domain/products/os.rs | 46 ++++++++ src/domain/products/plan.rs | 113 ++++++++++++++++++ src/domain/snapshot.rs | 136 +++++++++++++++++++++ src/domain/vm.rs | 230 ++++++++++++++++++++++++++++++++++++ src/error.rs | 31 +++++ src/lib.rs | 15 ++- 30 files changed, 1533 insertions(+), 44 deletions(-) create mode 100644 docs/README.md create mode 100644 examples/.example.env create mode 100644 examples/README.md create mode 100644 examples/create_snapshot.rs create mode 100644 examples/create_vm.rs create mode 100644 examples/keypair.rs create mode 100644 examples/plan.rs create mode 100644 examples/snapshot.rs create mode 100644 examples/vm.rs create mode 100644 src/client.rs create mode 100644 src/config.rs create mode 100644 src/domain/account.rs create mode 100644 src/domain/keypair.rs create mode 100644 src/domain/lite.rs create mode 100644 src/domain/mod.rs create mode 100644 src/domain/products/ip.rs create mode 100644 src/domain/products/mod.rs create mode 100644 src/domain/products/os.rs create mode 100644 src/domain/products/plan.rs create mode 100644 src/domain/snapshot.rs create mode 100644 src/domain/vm.rs create mode 100644 src/error.rs diff --git a/.gitignore b/.gitignore index 96ef6c0..cdca302 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ /target Cargo.lock + +.env +.ignore +examples/playground.rs diff --git a/Cargo.toml b/Cargo.toml index 9746793..cba23a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,15 +1,28 @@ [package] -name = "neolite-sdk" +name = "neolite" version = "0.1.0" authors = ["azzamsa "] edition = "2021" license = "MIT" readme = "README.md" -repository = "https://github.com/BiznetGIO/neolite-sdk" -rust-version = "1.73.0" -description = "Neo Lite SDK" +repository = "https://github.com/BiznetGIO/neolite" +rust-version = "1.77.1" +description = "NEO Lite SDK" [dependencies] +http = "1.1.0" +log = "0.4.21" +reqwest = { version = "0.12.2", default-features = false, features = ["rustls-tls", "json", "multipart"] } +serde = { version = "1.0.197", features = ["derive"] } +serde-aux = "4.5.0" +serde_json = "1.0.115" +thiserror = "1.0.58" + +[dev-dependencies] +anyhow = "1.0.81" +dotenvy = "0.15.7" +env_logger = "0.11.3" +tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread"] } [package.metadata.release] sign-commit = true diff --git a/README.md b/README.md index e69de29..3fb180c 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,51 @@ +
+

neolite

+ +NEO Lite SDK. + + + + + + + + +
+ +--- + +The `neolite` SDK makes it easy to work with Biznet Gio's [NEO Lite](https://www.biznetgio.com/product/neo-lite) service. With NEO Lite SDK, developers can effortlessly manage and control their VPS instances for hosting websites and applications. + +## Usage + +```rust +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let url = "https://api.portal.biznetgio.dev/v1/neolites".parse::()?; + let token = env::var("TOKEN").context("TOKEN env not found.")?; + + let config = Config::new(url, &token); + let client = Client::new(config)?; + + let keypair = Lite::new(client).keypair().await?; + let key = keypair.create("gandalf0").await?; + println!("{}", key.name); + Ok(()) +} +``` + +To learn more, see other [examples](/examples). + +## Development + +```bash +git clone https://github.com/BiznetGIO/neolite +cd neolite + +# Run unit tests and integration tests +cargo test +``` + +## Contributing + +To learn more read the [contributing guide](docs/dev/README.md) diff --git a/configs/cliff.toml b/configs/cliff.toml index 6ebfbd8..92a0a0c 100644 --- a/configs/cliff.toml +++ b/configs/cliff.toml @@ -16,7 +16,7 @@ body = """ ### {{ group | striptags | trim | upper_first }} {% for commit in commits %} {%- if commit.scope -%} - - **{{ commit.scope }}:** {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}](https://github.com/BiznegGio/neolite-sdk/commit/{{ commit.id }})) + - **{{ commit.scope }}:** {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}](https://github.com/BiznegGio/neolite/commit/{{ commit.id }})) {% if commit.breaking -%} {% raw %} {% endraw %}- **BREAKING!** ⚠️ : {{ commit.breaking_description }} {% endif -%} @@ -24,7 +24,7 @@ body = """ {% raw %}\n{% endraw %}{% raw %} {% endraw %}{{ commit.body | indent(width=4) }}{% raw %}\n{% endraw %} {% endif -%} {% else -%} - - {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}](https://github.com/BiznegGio/neolite-sdk/commit/{{ commit.id }})) + - {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}](https://github.com/BiznegGio/neolite/commit/{{ commit.id }})) {% if commit.breaking -%} {% raw %} {% endraw %}- **BREAKING!** ⚠️ : {{ commit.breaking_description }} {% endif -%} diff --git a/configs/typos.toml b/configs/typos.toml index bf27f28..ab1bb7c 100644 --- a/configs/typos.toml +++ b/configs/typos.toml @@ -1,2 +1,2 @@ [files] -extend-exclude = ["CHANGELOG.md"] +extend-exclude = ["CHANGELOG.md", "playground.rs"] diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..1a6c448 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,12 @@ +# Guide + +## Common workflow + +- Create NEO Lite Virtual Machine. + - Check available products `product.list()`. + - Select preferred product `product.get(1538)`. + - Select preferred billing cycle `product_resource.get_billing("Monthly")`. + - Check IP availability `ip.is_available()`. + - Select preferred OS `os.get(1001)`. + - Create or Select existing keypair `keypair.create("gandalf0")`. + - Create a virtual Machine `lite.create()`. diff --git a/examples/.example.env b/examples/.example.env new file mode 100644 index 0000000..6692e04 --- /dev/null +++ b/examples/.example.env @@ -0,0 +1,4 @@ +TOKEN='eyJhbG...' +VM_ID=123 +PRODUCT_ID=123 +KEYPAIR_ID=123 diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..8a8e855 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,7 @@ +# Examples + +Sorted by dependency order. + +1. Keypair +2. Product +3. Virtual Machine (vm) diff --git a/examples/create_snapshot.rs b/examples/create_snapshot.rs new file mode 100644 index 0000000..245f6b3 --- /dev/null +++ b/examples/create_snapshot.rs @@ -0,0 +1,114 @@ +#![allow(dead_code)] +use std::env; + +use anyhow::Context; +use neolite::{client::Client, config::Config, lite::Lite, snapshot::SnapshotOpts}; + +async fn create(client: Client, vm_id: u32) -> anyhow::Result<()> { + let lite = Lite::new(client); + + // (1) Select preferred billing cycle + let product = lite.plan().await?; + let product_resource = product.get_vm(1538).await?; + let billing_resource = product_resource.get_billing("Monthly").await?; + println!( + "::: Billing. label: {}, price: {}", + billing_resource.label, billing_resource.price, + ); + + // (2) Create a virtual machine snapshot + let snapshot = lite.snapshot().await?; + let opts = SnapshotOpts { + billing: billing_resource, + use_credit_card: false, + promocode: None, + }; + let billing_resource = snapshot + .create( + vm_id, + "snapshot-from-sdk".to_string(), + Some("Snapshot from SDK".to_string()), + &opts, + ) + .await?; + println!( + "::: Snapshot. account id: {}, order id: {}", + billing_resource.account_id, billing_resource.order_id + ); + + Ok(()) +} + +async fn restore_with(client: Client) -> anyhow::Result<()> { + let lite = Lite::new(client); + + // (1) Select preferred plan + // let resource = plan.list().await?; + let plan = lite.plan().await?; + let plan_resource = plan.get_vm(1538).await?; + println!( + "::: plan. id: {}, name: {}", + plan_resource.id, plan_resource.name, + ); + + // (2) Select preferred billing cycle + let billing_resource = plan_resource.get_billing("Monthly").await?; + println!( + "::: Billing. label: {}, price: {}", + billing_resource.label, billing_resource.price, + ); + + // (3) Create or Select existing keypair + let keypair = lite.keypair().await?; + let keypair_resource = keypair.create("gandalf0").await?; + println!( + "::: Keypair. id: {}, name: {}", + keypair_resource.id, keypair_resource.name, + ); + + // (4) Create a virtual machine snapshot + let snapshot = lite.snapshot().await?; + let opts = neolite::snapshot::RestoreVirtualMachineOptions { + plan: plan_resource, + keypair: keypair_resource, + billing: billing_resource, + use_credit_card: false, + promocode: None, + }; + let snapshot_id = 123; + let billing_resource = snapshot + .restore_with( + snapshot_id, + "thorin-os2".to_string(), + Some("Thorin Virtual Machine".to_string()), + "thethorin".to_string(), + "SpeakFriendAndEnter123".to_string(), + &opts, + ) + .await?; + println!( + "::: VM created. account id: {}, order id: {}", + billing_resource.account_id, billing_resource.order_id + ); + + Ok(()) +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + env_logger::init(); + dotenvy::from_filename("./examples/.env")?; + + let url = "https://api.portal.biznetgio.dev/v1/neolites".parse::()?; + let token = env::var("TOKEN").context("TOKEN env not found.")?; + let id = env::var("VM_ID").context("VM_ID env not found.")?; + let id: u32 = id.parse()?; + + let config = Config::new(url, &token); + let client = Client::new(config)?; + + create(client, id).await?; + // restore_with(client).await?; + + Ok(()) +} diff --git a/examples/create_vm.rs b/examples/create_vm.rs new file mode 100644 index 0000000..9de1e05 --- /dev/null +++ b/examples/create_vm.rs @@ -0,0 +1,87 @@ +use std::env; + +use anyhow::Context; +use neolite::{client::Client, config::Config, lite::Lite, vm::VirtualMachineOptions}; + +async fn create(client: Client) -> anyhow::Result<()> { + let lite = Lite::new(client); + + // (1) Select preferred plan + // let resource = plan.list().await?; + let plan = lite.plan().await?; + let plan_resource = plan.get_vm(1538).await?; + println!( + "::: plan. id: {}, name: {}", + plan_resource.id, plan_resource.name, + ); + + // (2) Select preferred billing cycle + let billing_resource = plan_resource.get_billing("Monthly").await?; + println!( + "::: Billing. label: {}, price: {}", + billing_resource.label, billing_resource.price, + ); + + // (3) Check IP availability + let ip = plan_resource.ip().await?; + let is_ip_available = ip.is_available().await?; + if !is_ip_available { + return Err(anyhow::anyhow!("IP is not available")); + } + println!("::: IP availability: {}", is_ip_available); + + // (4) Select preferred OS + let os = plan_resource.os().await?; + // let oses = os.list().await?; + let os_resource = os.get(1001).await?; + println!("::: OS. id: {}, name: {}", os_resource.id, os_resource.name); + + // (5) Create or Select existing keypair + let keypair = lite.keypair().await?; + let keypair_resource = keypair.create("gandalf0").await?; + println!( + "::: Keypair. id: {}, name: {}", + keypair_resource.id, keypair_resource.name, + ); + + // (6) Create a virtual Machine + let vm = lite.vm().await?; + let opts = VirtualMachineOptions { + plan: plan_resource, + os: os_resource, + keypair: keypair_resource, + billing: billing_resource, + use_credit_card: false, + promocode: None, + }; + let billing_resource = vm + .create( + "thorin-os2".to_string(), + Some("Thorin Virtual Machine".to_string()), + "thethorin".to_string(), + "SpeakFriendAndEnter123".to_string(), + &opts, + ) + .await?; + println!( + "::: NeoLite VM. account id: {}, order id: {}", + billing_resource.account_id, billing_resource.order_id + ); + + Ok(()) +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + env_logger::init(); + dotenvy::from_filename("./examples/.env")?; + + let url = "https://api.portal.biznetgio.dev/v1/neolites".parse::()?; + let token = env::var("TOKEN").context("TOKEN env not found.")?; + let config = Config::new(url, &token); + + let client = Client::new(config)?; + create(client).await?; + + Ok(()) +} diff --git a/examples/keypair.rs b/examples/keypair.rs new file mode 100644 index 0000000..d7bb448 --- /dev/null +++ b/examples/keypair.rs @@ -0,0 +1,55 @@ +#![allow(dead_code)] +use std::env; + +use anyhow::Context; +use neolite::{client::Client, config::Config, keypair::Keypair, lite::Lite}; + +async fn list(keypair: Keypair) -> anyhow::Result<()> { + let keys = keypair.list().await?; + for key in keys { + println!("{}: {}", key.id, key.name); + } + Ok(()) +} + +async fn get(keypair: Keypair, id: u32) -> anyhow::Result<()> { + let key = keypair.get(id).await?; + println!("id: {}, name: {}", key.id, key.name); + Ok(()) +} + +async fn create(keypair: Keypair) -> anyhow::Result<()> { + let key = keypair.create("gandalf0").await?; + println!("{:?}", key); + println!("{}", key.name); + Ok(()) +} + +async fn delete(keypair: Keypair, id: u32) -> anyhow::Result<()> { + keypair.delete(id).await?; + println!("::: Keypair deleted."); + + Ok(()) +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + env_logger::init(); + dotenvy::from_filename("./examples/.env")?; + + let url = "https://api.portal.biznetgio.dev/v1/neolites".parse::()?; + let token = env::var("TOKEN").context("TOKEN env not found.")?; + let id = env::var("KEYPAIR_ID").context("KEYPAIR_ID env not found.")?; + let id: u32 = id.parse()?; + + let config = Config::new(url, &token); + let client = Client::new(config)?; + let keypair = Lite::new(client).keypair().await?; + + // list(keypair).await?; + // create(keypair).await?; + get(keypair, id).await?; + // delete(keypair, id).await?; + + Ok(()) +} diff --git a/examples/plan.rs b/examples/plan.rs new file mode 100644 index 0000000..6a7124e --- /dev/null +++ b/examples/plan.rs @@ -0,0 +1,90 @@ +#![allow(dead_code)] +use std::env; + +use anyhow::Context; +use neolite::{client::Client, config::Config, lite::Lite, plan::Plan}; + +async fn list(plan: Plan) -> anyhow::Result<()> { + let plans = plan.list_vm().await?; + for p in plans { + println!("{}: {}", p.name, p.id); + } + Ok(()) +} + +async fn get(plan: Plan, id: u32) -> anyhow::Result<()> { + let plan = plan.get_vm(id).await?; + println!("{}: {}", plan.name, plan.id); + Ok(()) +} + +/// List available OS for the VM +async fn list_os(plan: Plan, id: u32) -> anyhow::Result<()> { + let plan = plan.get_vm(id).await?; + let os = plan.os().await?; + let all_oses = os.list().await?; + println!("{:?}", all_oses); + Ok(()) +} + +async fn get_os(plan: Plan, id: u32) -> anyhow::Result<()> { + let plan = plan.get_vm(id).await?; + let os = plan.os().await?; + let os = os.get(1001).await?; + println!("{}", os.name); + Ok(()) +} + +async fn check_ip_availability(plan: Plan, id: u32) -> anyhow::Result<()> { + let plan = plan.get_vm(id).await?; + let ip = plan.ip().await?; + println!( + "Ip public availability status: {:?}", + ip.is_available().await? + ); + Ok(()) +} + +// +// Snapshots +// + +async fn list_snapshot(plan: Plan) -> anyhow::Result<()> { + let plans = plan.list_snapshot().await?; + for p in plans { + println!("{}: {}", p.name, p.id); + } + Ok(()) +} + +async fn get_snapshot(plan: Plan, id: u32) -> anyhow::Result<()> { + let p = plan.get_snapshot(id).await?; + println!("{}: {}", p.name, p.id); + Ok(()) +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + env_logger::init(); + dotenvy::from_filename("./examples/.env")?; + + let url = "https://api.portal.biznetgio.dev/v1/neolites".parse::()?; + let token = env::var("TOKEN").context("TOKEN env not found.")?; + let config = Config::new(url, &token); + let client = Client::new(config)?; + let id = env::var("PLAN_ID").context("PLAN_ID env not found.")?; + let id: u32 = id.parse()?; + + let plan = Lite::new(client).plan().await?; + + // list(plan).await?; + // get(plan, id).await?; + // list_os(plan, id).await?; + // get_os(plan, id).await?; + // check_ip_availability(plan, id).await?; + + // list_snapshot(plan).await?; + get_snapshot(plan, id).await?; + + Ok(()) +} diff --git a/examples/snapshot.rs b/examples/snapshot.rs new file mode 100644 index 0000000..3bb562c --- /dev/null +++ b/examples/snapshot.rs @@ -0,0 +1,50 @@ +#![allow(dead_code)] +use std::env; + +use anyhow::Context; +use neolite::{client::Client, config::Config, lite::Lite, snapshot::Snapshot}; + +async fn list(snapshot: Snapshot) -> anyhow::Result<()> { + let snapshots = snapshot.list().await?; + for snapshot in snapshots { + println!("id: {}, name: {}", snapshot.id, snapshot.name); + } + + Ok(()) +} + +async fn get(snapshot: Snapshot, id: u32) -> anyhow::Result<()> { + let snapshot = snapshot.get(id).await?; + println!("{}: {}", snapshot.id, snapshot.name); + + Ok(()) +} + +async fn delete(snapshot: Snapshot, id: u32) -> anyhow::Result<()> { + snapshot.delete(id).await?; + println!("::: Snapshot deleted."); + + Ok(()) +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + env_logger::init(); + dotenvy::from_filename("./examples/.env")?; + + let url = "https://api.portal.biznetgio.dev/v1/neolites".parse::()?; + let token = env::var("TOKEN").context("TOKEN env not found.")?; + let id = env::var("VM_ID").context("VM_ID env not found.")?; + let id: u32 = id.parse()?; + + let config = Config::new(url, &token); + let client = Client::new(config)?; + let snapshot = Lite::new(client).snapshot().await?; + + // list(snapshot).await?; + get(snapshot, id).await?; + // list_with_status(vm).await?; + // delete(vm, id).await?; + + Ok(()) +} diff --git a/examples/vm.rs b/examples/vm.rs new file mode 100644 index 0000000..a1c8fef --- /dev/null +++ b/examples/vm.rs @@ -0,0 +1,69 @@ +#![allow(dead_code)] +use std::env; + +use anyhow::Context; +use neolite::{ + client::Client, config::Config, lite::Lite, vm::VirtualMachine, vm::VirtualMachineStatus, +}; + +async fn list(vm: VirtualMachine) -> anyhow::Result<()> { + let vms = vm.list().await?; + for vm in vms { + println!("id: {}, name: {}, status: {}", vm.id, vm.name, vm.status,); + } + + Ok(()) +} + +async fn list_with_status(vm: VirtualMachine) -> anyhow::Result<()> { + let vms = vm.list_with_status(VirtualMachineStatus::Active).await?; + for vm in vms { + println!("id: {}, name: {}, status: {}", vm.id, vm.name, vm.status,); + } + + Ok(()) +} + +async fn get(vm: VirtualMachine, id: u32) -> anyhow::Result<()> { + let vm = vm.get(id).await?; + println!("{}: {}", vm.id, vm.name); + + Ok(()) +} + +async fn delete(vm: VirtualMachine, id: u32) -> anyhow::Result<()> { + vm.delete(id).await?; + println!("::: Virtual machine deleted."); + + Ok(()) +} + +async fn stop(vm: VirtualMachine, id: u32) -> anyhow::Result<()> { + let vm = vm.get(id).await?; + vm.stop().await?; + println!("::: Virtual machine stopped."); + + Ok(()) +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + env_logger::init(); + dotenvy::from_filename("./examples/.env")?; + + let url = "https://api.portal.biznetgio.dev/v1/neolites".parse::()?; + let token = env::var("TOKEN").context("TOKEN env not found.")?; + let id = env::var("VM_ID").context("VM_ID env not found.")?; + let id: u32 = id.parse()?; + + let config = Config::new(url, &token); + let client = Client::new(config)?; + let vm = Lite::new(client).vm().await?; + + get(vm, id).await?; + // list(vm).await?; + // list_with_status(vm).await?; + // delete(vm, id).await?; + + Ok(()) +} diff --git a/justfile b/justfile index 66d8b9e..fee680e 100755 --- a/justfile +++ b/justfile @@ -15,8 +15,8 @@ _default: just --list --unsorted # Setup the repository. -setup: _areyousure - just _cargo-install 'cargo-edit cargo-nextest cargo-outdated dprint git-cliff bacon typos-cli' +setup: + # Please install: 'cargo-edit cargo-nextest cargo-outdated dprint git-cliff bacon typos-cli' # Tasks to make the code-base comply with the rules. Mostly used in git hooks. comply: _doc-check fmt lint test @@ -94,36 +94,3 @@ up arg="": else { cargo outdated --root-deps-only } - -# -# Helper -# - -[unix] -_cargo-install tool: - #!/usr/bin/env bash - if command -v cargo-binstall >/dev/null 2>&1; then - echo "cargo-binstall..." - cargo binstall --no-confirm --no-symlinks {{ tool }} - else - echo "Building from source" - cargo install --locked {{ tool }} - fi - -[unix] -_areyousure: - #!/usr/bin/env bash - echo -e "This command will alter your system. ⚠️ - You are advised to run in inside containerized environment. - Such as [toolbx](https://containertoolbx.org/). - - If you are unsure. Run the installation commands manually. - Take a look at the 'setup' recipe in the Justfile.\n" - - read -p "Are you sure you want to proceed? (Y/n) " response; - if [[ $response =~ ^[Yy] ]]; then - echo "Continue!"; - else - echo "Cancelled!"; - exit 1; - fi diff --git a/rust-toolchain.toml b/rust-toolchain.toml index b44c3df..8d033cb 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.73.0" +channel = "1.77.1" components = ["rustfmt", "clippy"] diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..7b72283 --- /dev/null +++ b/src/client.rs @@ -0,0 +1,92 @@ +use crate::config::Config; + +use reqwest::{Method, StatusCode}; +use serde::{Deserialize, Serialize}; +use serde_json as json; + +#[derive(Debug, Default)] +pub struct Client { + config: Config, + requester: reqwest::Client, +} + +impl Client { + pub fn new(config: Config) -> Result { + let requester = reqwest::Client::builder().build()?; + Ok(Self { config, requester }) + } + pub async fn get(&self, path: &str) -> Result { + self.send(Method::GET, path, None).await + } + pub async fn post(&self, path: &str, body: json::Value) -> Result { + self.send(Method::POST, path, Some(body)).await + } + pub async fn put(&self, path: &str) -> Result { + self.send(Method::PUT, path, None).await + } + pub async fn put_with_body( + &self, + path: &str, + body: json::Value, + ) -> Result { + self.send(Method::PUT, path, Some(body)).await + } + pub async fn delete(&self, path: &str) -> Result { + self.send(Method::DELETE, path, None).await + } + async fn send( + &self, + method: reqwest::Method, + path: &str, + body: Option, + ) -> Result { + let url = format!("{}{}", self.config.base_url, path); + log::debug!("URL: {:?}", url); + + let response = self + .requester + .request(method, url) + .header("x-token", &self.config.token) + .header("Content-Type", "application/json") + .json(&body) + .send() + .await?; + let status = response.status(); + let text = response.text().await?; + log::trace!("Response: {:?}", &text); + + match status { + StatusCode::OK | StatusCode::CREATED => { + let response: json::Value = json::from_str(&text)?; + match response.get("data") { + Some(_) => { + let response: Response = json::from_str(&text)?; + Ok(response.data.to_owned()) + } + None => { + log::error!("status: {}, body: {:?}", status, text); + Err(crate::Error::InvalidArgument(format!( + "Incomplete response. Status: {}. Body: {}", + status, text + ))) + } + } + } + StatusCode::NOT_FOUND => Err(crate::Error::NotFound("Resource not found".into())), + _ => { + log::error!("status: {}, body: {:?}", status, text); + Err(crate::Error::InvalidArgument(format!( + "Request failed. Status: {}. Body: {}", + status, text + ))) + } + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +struct Response { + success: bool, + code: i32, + data: json::Value, +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..2160e2c --- /dev/null +++ b/src/config.rs @@ -0,0 +1,14 @@ +#[derive(Debug, Default)] +pub struct Config { + pub base_url: http::Uri, + pub token: String, +} + +impl Config { + pub fn new(url: http::Uri, token: &str) -> Self { + Self { + base_url: url, + token: token.to_string(), + } + } +} diff --git a/src/domain/account.rs b/src/domain/account.rs new file mode 100644 index 0000000..cf532e7 --- /dev/null +++ b/src/domain/account.rs @@ -0,0 +1,160 @@ +use std::sync::Arc; + +use serde::{Deserialize, Serialize}; +use serde_aux::field_attributes::deserialize_number_from_string; +use serde_json as json; + +use crate::client::Client; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub enum AccountStatus { + Active, + Pending, + Suspended, + Terminated, +} + +impl AccountStatus { + pub fn as_str(&self) -> &'static str { + match self { + Self::Active => "Active", + Self::Pending => "Pending", + Self::Suspended => "Suspended", + Self::Terminated => "Terminated", + } + } +} + +pub struct Account { + client: Arc, +} + +impl Account { + pub fn new(client: Arc) -> Self { + Self { client } + } + pub async fn list(&self) -> Result, crate::Error> { + let response = self.client.get("/accounts").await?; + let response: Vec = json::from_value(response)?; + Ok(response) + } + pub async fn list_with_status( + &self, + status: AccountStatus, + ) -> Result, crate::Error> { + let response = self + .client + .get(&format!("/accounts?status={}", status.as_str())) + .await?; + let response: Vec = json::from_value(response)?; + Ok(response) + } + // pub async fn get(&self, id: u32) -> Result { + // let response = self.client.get(&format!("/accounts/{id}")).await; + // match response { + // Ok(response) => { + // let response: AccountResource = json::from_value(response)?; + // Ok(response) + // } + // Err(e) => match e { + // crate::Error::NotFound(_) => { + // Err(crate::Error::NotFound("Account not found".into())) + // } + // _ => Err(e), + // }, + // } + // } + + // + // Snapshots + // + pub async fn list_snapshot(&self) -> Result, crate::Error> { + let response = self.client.get("/snapshots/accounts").await?; + let response: Vec = json::from_value(response)?; + Ok(response) + } + + pub async fn get_snapshot(&self, id: u32) -> Result { + let response = self.client.get(&format!("/snapshots/accounts/{id}")).await; + match response { + Ok(response) => { + let response: SnapshotAccountResource = json::from_value(response)?; + Ok(response) + } + Err(e) => match e { + crate::Error::NotFound(_) => { + Err(crate::Error::NotFound("Snapshot not found".into())) + } + _ => Err(e), + }, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AccountResource { + #[serde(deserialize_with = "deserialize_number_from_string")] + #[serde(rename = "account_id")] + pub id: u32, + pub domain: String, + pub status: AccountStatus, + pub billingcycle: String, + pub date_created: String, + pub next_due: String, + pub recurring_amount: i32, + pub extra_details: ExtraDetails, + pub product_id: u32, + pub product_name: String, + pub description: String, + pub category_id: u32, + pub category_name: String, + pub last_invoice: LastInvoice, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct LastInvoice { + pub id: u32, + pub paid_id: u32, + pub status: String, + pub date: String, + pub duedate: String, + pub paybefore: String, + pub datepaid: String, + pub invoice_type: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ExtraDetails { + pub region: String, + pub region_label: String, + pub description: String, + pub name: String, + pub tenant_id: Option, + pub ciuser: String, + pub cipassword: String, + #[serde(rename = "neosshkey_id")] + pub keypair_id: u32, + // pub keypair_name: String, + pub sshkeys: String, + pub osname: String, + pub disk_size: String, +} + +// +// Snapshots +// + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct SnapshotAccountResource { + #[serde(rename = "account_id")] + pub id: u32, + pub status: AccountStatus, + pub extra_details: SnapshotExtraDetails, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct SnapshotExtraDetails { + pub name: String, + pub description: String, + pub region: String, +} diff --git a/src/domain/keypair.rs b/src/domain/keypair.rs new file mode 100644 index 0000000..bdd1c40 --- /dev/null +++ b/src/domain/keypair.rs @@ -0,0 +1,52 @@ +use std::sync::Arc; + +use serde::{Deserialize, Serialize}; +use serde_aux::field_attributes::deserialize_number_from_string; +use serde_json as json; + +use crate::client::Client; + +pub struct Keypair { + client: Arc, +} + +impl Keypair { + pub fn new(client: Arc) -> Self { + Self { client } + } + pub async fn list(&self) -> Result, crate::Error> { + let response = self.client.get("/keypairs").await?; + let response: Vec = json::from_value(response)?; + Ok(response) + } + + // NOTE: The NEOLite REST API doesn't have `get keypair` + pub async fn get(&self, id: u32) -> Result { + let keys = self.list().await?; + for key in keys { + if key.id == id { + return Ok(key); + } + } + Err(crate::Error::NotFound("Keypair is not found".to_string())) + } + pub async fn create(&self, name: &str) -> Result { + let body = json::json!({ "name": name }); + let response = self.client.post("/keypairs", body).await?; + let response: KeypairResource = json::from_value(response)?; + Ok(response) + } + pub async fn delete(&self, id: u32) -> Result<(), crate::Error> { + self.client.delete(&format!("/keypairs/{}", id)).await?; + Ok(()) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct KeypairResource { + #[serde(deserialize_with = "deserialize_number_from_string")] + #[serde(rename = "keypair_id")] + pub id: u32, + pub name: String, + pub public_key: String, +} diff --git a/src/domain/lite.rs b/src/domain/lite.rs new file mode 100644 index 0000000..8819633 --- /dev/null +++ b/src/domain/lite.rs @@ -0,0 +1,35 @@ +use std::sync::Arc; + +use serde::{Deserialize, Serialize}; + +use crate::{client::Client, keypair::Keypair, plan::Plan, snapshot::Snapshot, vm::VirtualMachine}; + +pub struct Lite { + client: Arc, +} + +impl Lite { + pub fn new(client: Client) -> Self { + Self { + client: Arc::new(client), + } + } + pub async fn keypair(&self) -> Result { + Ok(Keypair::new(Arc::clone(&self.client))) + } + pub async fn plan(&self) -> Result { + Ok(Plan::new(Arc::clone(&self.client))) + } + pub async fn vm(&self) -> Result { + Ok(VirtualMachine::new(Arc::clone(&self.client))) + } + pub async fn snapshot(&self) -> Result { + Ok(Snapshot::new(Arc::clone(&self.client))) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct BillingResource { + pub order_id: String, + pub account_id: String, +} diff --git a/src/domain/mod.rs b/src/domain/mod.rs new file mode 100644 index 0000000..13e5c4c --- /dev/null +++ b/src/domain/mod.rs @@ -0,0 +1,7 @@ +mod account; + +pub mod keypair; +pub mod lite; +pub mod products; +pub mod snapshot; +pub mod vm; diff --git a/src/domain/products/ip.rs b/src/domain/products/ip.rs new file mode 100644 index 0000000..c45364b --- /dev/null +++ b/src/domain/products/ip.rs @@ -0,0 +1,34 @@ +use std::sync::Arc; + +use serde::{Deserialize, Serialize}; +use serde_json as json; + +use crate::client::Client; + +#[derive(Debug, Default)] +pub struct Ip { + pub product_id: u32, + client: Arc, +} + +impl Ip { + pub fn new(client: Arc, product_id: u32) -> Self { + Self { + client, + product_id: product_id.to_owned(), + } + } + pub async fn is_available(&self) -> Result { + let response = self + .client + .get(&format!("/products/{}/ip-availability", self.product_id)) + .await?; + let response: IpResource = json::from_value(response)?; + Ok(response.available) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct IpResource { + pub available: bool, +} diff --git a/src/domain/products/mod.rs b/src/domain/products/mod.rs new file mode 100644 index 0000000..6e85e79 --- /dev/null +++ b/src/domain/products/mod.rs @@ -0,0 +1,3 @@ +pub mod ip; +pub mod os; +pub mod plan; diff --git a/src/domain/products/os.rs b/src/domain/products/os.rs new file mode 100644 index 0000000..015d83e --- /dev/null +++ b/src/domain/products/os.rs @@ -0,0 +1,46 @@ +use std::sync::Arc; + +use serde::{Deserialize, Serialize}; +use serde_json as json; + +use crate::client::Client; + +#[derive(Debug, Default)] +pub struct Os { + client: Arc, + pub product_id: u32, +} + +impl Os { + pub fn new(client: Arc, product_id: u32) -> Self { + Self { client, product_id } + } + pub async fn list(&self) -> Result, crate::Error> { + let response = self + .client + .get(&format!("/products/{}/oss", self.product_id)) + .await?; + let response: Vec = json::from_value(response)?; + Ok(response) + } + // NOTE: The NEOLite REST API doesn't have `get OS` + pub async fn get(&self, id: u32) -> Result { + let oses = self.list().await?; + for os in oses { + if os.id == id { + return Ok(os); + } + } + Err(crate::Error::NotFound("OS is not found".to_string())) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct OsResource { + #[serde(rename = "vmid")] + pub id: u32, + pub node: String, + pub name: String, + pub maxmem: u64, + pub maxcpu: u32, +} diff --git a/src/domain/products/plan.rs b/src/domain/products/plan.rs new file mode 100644 index 0000000..6878ab3 --- /dev/null +++ b/src/domain/products/plan.rs @@ -0,0 +1,113 @@ +use std::sync::Arc; + +use serde::{Deserialize, Serialize}; +use serde_json as json; + +use super::{ip::Ip, os::Os}; +use crate::client::Client; + +#[derive(Debug)] +pub struct Plan { + client: Arc, +} + +impl Plan { + pub fn new(client: Arc) -> Self { + Self { client } + } + pub async fn list_vm(&self) -> Result, crate::Error> { + let response = self.client.get("/products").await?; + let response: Vec = json::from_value(response)?; + Ok(response) + } + pub async fn get_vm(&self, id: u32) -> Result { + let response = self.client.get(&format!("/products/{id}")).await?; + let response: PlanResource = json::from_value(response)?; + let response = response.with_client(Arc::clone(&self.client)); + Ok(response) + } + // + // Snapshots + // + pub async fn list_snapshot(&self) -> Result, crate::Error> { + let response = self.client.get("/snapshots/products").await?; + let response: Vec = json::from_value(response)?; + Ok(response) + } + pub async fn get_snapshot(&self, id: u32) -> Result { + let response = self + .client + .get(&format!("/snapshots/products/{id}")) + .await?; + let response: PlanResource = json::from_value(response)?; + Ok(response) + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct PlanResource { + #[serde(rename = "product_id")] + pub id: u32, + pub name: String, + pub description: String, + pub category_id: u32, + pub category_name: String, + pub options: Options, + pub billing: Vec, + #[serde(skip)] + client: Arc, +} + +impl PlanResource { + fn with_client(mut self, client: Arc) -> Self { + self.client = client; + self + } + pub async fn os(&self) -> Result { + let os = Os::new(Arc::clone(&self.client), self.id); + Ok(os) + } + pub async fn ip(&self) -> Result { + let ip = Ip::new(Arc::clone(&self.client), self.id); + Ok(ip) + } + pub async fn get_billing(&self, label: &str) -> Result { + for billing in &self.billing { + if billing.label == label { + return Ok(billing.clone()); + } + } + Err(crate::Error::NotFound("Billing is not found".to_string())) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Billing { + pub label: String, + pub cycle: String, + pub price: u32, + pub components: Option>, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Component { + pub label: String, + pub field: String, + pub prices: Vec, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Price { + pub qty_min: i32, + pub qty_max: i32, + pub price: u32, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Options { + #[serde(rename = "type")] + pub r#type: String, + pub cores: u32, + pub memory: u32, + pub allow_downgrade: i32, +} diff --git a/src/domain/snapshot.rs b/src/domain/snapshot.rs new file mode 100644 index 0000000..3b18284 --- /dev/null +++ b/src/domain/snapshot.rs @@ -0,0 +1,136 @@ +use std::sync::Arc; + +use serde::{Deserialize, Serialize}; +use serde_json as json; + +use super::account::{Account, AccountStatus}; +use crate::{ + client::Client, + keypair::KeypairResource, + lite::BillingResource, + plan::{Billing, PlanResource}, +}; + +pub struct RestoreVirtualMachineOptions { + pub plan: PlanResource, + pub keypair: KeypairResource, + pub billing: Billing, + pub use_credit_card: bool, + pub promocode: Option, +} + +pub struct SnapshotOpts { + pub billing: Billing, + pub use_credit_card: bool, + pub promocode: Option, +} + +pub struct Snapshot { + client: Arc, +} + +impl Snapshot { + pub fn new(client: Arc) -> Self { + Self { client } + } + pub async fn list(&self) -> Result, crate::Error> { + let mut snapshots: Vec = Vec::new(); + + let account = Account::new(Arc::clone(&self.client)); + let accounts = account.list_snapshot().await?; + + for account in accounts { + // Skips terminated vm. It can't be accessed + if account.status == AccountStatus::Terminated { + continue; + } + let snapshot = SnapshotResource { + id: account.id, + name: account.extra_details.name, + }; + snapshots.push(snapshot); + } + Ok(snapshots) + } + pub async fn create( + &self, + vm_id: u32, + name: String, + description: Option, + opts: &SnapshotOpts, + ) -> Result { + let use_cc = match opts.use_credit_card { + true => "yes", + false => "no", + }; + let body = json::json!({ + "name": &name, + "description": &description, + "cycle": opts.billing.cycle, + "pay_invoice_with_cc": use_cc, + "promocode": opts.promocode.as_deref(), + }); + let response = self + .client + .post(&format!("/accounts/{vm_id}/snapshot"), body) + .await?; + let response: BillingResource = json::from_value(response)?; + Ok(response) + } + pub async fn get(&self, id: u32) -> Result { + let account = Account::new(Arc::clone(&self.client)); + let account = account.get_snapshot(id).await?; + let snapshot = SnapshotResource { + id: account.id, + name: account.extra_details.name, + }; + Ok(snapshot) + } + pub async fn delete(&self, id: u32) -> Result<(), crate::Error> { + self.client.delete(&format!("/snapshots/{}", id)).await?; + Ok(()) + } + pub async fn restore(&self, id: u32) -> Result<(), crate::Error> { + self.client + .put(&format!("/snapshots/accounts/{id}/restore")) + .await?; + Ok(()) + } + pub async fn restore_with( + &self, + snapshot_id: u32, + name: String, + description: Option, + username: String, + password: String, + opts: &RestoreVirtualMachineOptions, + ) -> Result { + let use_cc = match opts.use_credit_card { + true => "yes", + false => "no", + }; + let body = json::json!({ + "ssh_and_console_user": &username, + "console_password": &password, + "vm_name": &name, + "description": &description, + "product_id": opts.plan.id, + "keypair_id": opts.keypair.id, + "cycle": opts.billing.cycle, + "pay_invoice_with_cc": use_cc, + "promocode": opts.promocode, + }); + let response = self + .client + .post(&format!("/snapshots/accounts/{snapshot_id}/create"), body) + .await?; + let response: BillingResource = json::from_value(response)?; + Ok(response) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SnapshotResource { + pub id: u32, + pub name: String, +} diff --git a/src/domain/vm.rs b/src/domain/vm.rs new file mode 100644 index 0000000..0481eb6 --- /dev/null +++ b/src/domain/vm.rs @@ -0,0 +1,230 @@ +use std::sync::Arc; + +use serde::{Deserialize, Serialize}; +use serde_json as json; + +use super::account::{Account, AccountStatus}; +use crate::{ + client::Client, + keypair::KeypairResource, + lite::BillingResource, + os::OsResource, + plan::{Billing, PlanResource}, +}; + +pub struct VirtualMachineOptions { + pub plan: PlanResource, + pub keypair: KeypairResource, + pub os: OsResource, + pub billing: Billing, + pub use_credit_card: bool, + pub promocode: Option, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +/// Terminated is not on the list because termindated VM can't be accessed. +pub enum VirtualMachineStatus { + Active, + Pending, + Suspended, +} + +impl VirtualMachineStatus { + pub fn as_str(&self) -> &'static str { + match self { + Self::Active => "Active", + Self::Pending => "Pending", + Self::Suspended => "Suspended", + } + } +} + +pub struct VirtualMachine { + client: Arc, +} + +impl VirtualMachine { + pub fn new(client: Arc) -> Self { + Self { client } + } + pub async fn list(&self) -> Result, crate::Error> { + let mut vms: Vec = Vec::new(); + + let account = Account::new(Arc::clone(&self.client)); + let accounts = account.list().await?; + + for account in accounts { + // Skips terminated vm. It can't be accessed + if account.status == AccountStatus::Terminated { + continue; + } + let vm = self.get(account.id).await?; + vms.push(vm); + } + Ok(vms) + } + pub async fn list_with_status( + &self, + status: VirtualMachineStatus, + ) -> Result, crate::Error> { + let mut vms: Vec = Vec::new(); + + let status = match status { + VirtualMachineStatus::Active => AccountStatus::Active, + VirtualMachineStatus::Pending => AccountStatus::Pending, + VirtualMachineStatus::Suspended => AccountStatus::Suspended, + }; + let account = Account::new(Arc::clone(&self.client)); + let accounts = account.list_with_status(status).await?; + + for account in accounts { + // Skips terminated vm. It can't be accessed + if account.status == AccountStatus::Terminated { + continue; + } + let vm = self.get(account.id).await?; + vms.push(vm); + } + Ok(vms) + } + pub async fn get(&self, id: u32) -> Result { + let response = self + .client + .get(&format!("/accounts/{}/vm-details", id)) + .await?; + let response: VirtualMachineResource = json::from_value(response)?; + let response = response.with_client(Arc::clone(&self.client)); + // Set `id` to use `Account ID` instead of real `VM ID`. + // The `Account ID` is used throughout the upstream REST API. + let response = response.change_id(id); + Ok(response) + } + pub async fn create( + &self, + name: String, + description: Option, + username: String, + password: String, + opts: &VirtualMachineOptions, + ) -> Result { + let use_cc = match opts.use_credit_card { + true => "yes", + false => "no", + }; + let body = json::json!({ + "ssh_and_console_user": &username, + "console_password": &password, + "vm_name": &name, + "description": &description, + "product_id": opts.plan.id, + "select_os": &opts.os.name, + "keypair_id": opts.keypair.id, + "cycle": opts.billing.cycle, + "pay_invoice_with_cc": use_cc, + "promocode": opts.promocode, + }); + let response = self.client.post("", body).await?; + let response: BillingResource = json::from_value(response)?; + Ok(response) + } + pub async fn delete(&self, id: u32) -> Result<(), crate::Error> { + self.client.delete(&format!("/{}", id)).await?; + Ok(()) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct VirtualMachineResource { + #[serde(rename = "vmid")] + pub id: u32, + pub name: String, + pub status: String, + pub uptime: i32, + pub maxdisk: i64, + pub maxmem: i64, + pub mem: i32, + pub cpus: i32, + + #[serde(skip)] + client: Arc, +} + +impl VirtualMachineResource { + fn with_client(mut self, client: Arc) -> Self { + self.client = client; + self + } + fn change_id(mut self, id: u32) -> Self { + self.id = id; + self + } + pub async fn change_keypair(&self, keypair_id: u32) -> Result<(), crate::Error> { + let body = json::json!({ "keypair_id": keypair_id }); + self.client + .put_with_body(&format!("/accounts/{}/keypair", self.id), body) + .await?; + Ok(()) + } + pub async fn change_name(&self, name: &str) -> Result<(), crate::Error> { + let body = json::json!({ "name": name }); + self.client + .put_with_body(&format!("/accounts/{}/change-vm-name", self.id), body) + .await?; + Ok(()) + } + pub async fn change_plan(&self, plan_id: u32) -> Result { + let body = json::json!({ "new_product_id": plan_id }); + let response = self + .client + .post(&format!("/accounts/{}/change-package", self.id), body) + .await?; + let response: BillingResource = json::from_value(response)?; + Ok(response) + } + pub async fn change_storage(&self, size: u32) -> Result { + let body = json::json!({ "disk_size": size }); + let response = self + .client + .put_with_body(&format!("/accounts/{}/storage", self.id), body) + .await?; + let response: BillingResource = json::from_value(response)?; + Ok(response) + } + pub async fn start(&self) -> Result<(), crate::Error> { + self.change_state("start").await?; + Ok(()) + } + pub async fn suspend(&self) -> Result<(), crate::Error> { + self.change_state("suspend").await?; + Ok(()) + } + pub async fn resume(&self) -> Result<(), crate::Error> { + self.change_state("resume").await?; + Ok(()) + } + pub async fn reset(&self) -> Result<(), crate::Error> { + self.change_state("reset").await?; + Ok(()) + } + pub async fn shutdown(&self) -> Result<(), crate::Error> { + self.change_state("shutdown").await?; + Ok(()) + } + pub async fn stop(&self) -> Result<(), crate::Error> { + self.change_state("stop").await?; + Ok(()) + } + pub async fn rebuild(&self, os: &OsResource) -> Result<(), crate::Error> { + let body = json::json!({ "name": os.name }); + self.client + .put_with_body(&format!("/accounts/{}/rebuild", self.id), body) + .await?; + Ok(()) + } + async fn change_state(&self, state: &str) -> Result<(), crate::Error> { + self.client + .put(&format!("/accounts/{}/vm-state/{}", self.id, state)) + .await?; + Ok(()) + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..0d111de --- /dev/null +++ b/src/error.rs @@ -0,0 +1,31 @@ +use serde_json as json; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Internal error")] + Internal(String), + + #[error("{0}")] + NotFound(String), + + #[error("{0}")] + PermissionDenied(String), + + #[error("{0}")] + InvalidArgument(String), + + #[error("{0}")] + AlreadyExists(String), +} + +impl std::convert::From for Error { + fn from(err: json::Error) -> Self { + Error::InvalidArgument(format!("failed to parse json: {}", err)) + } +} + +impl std::convert::From for Error { + fn from(err: reqwest::Error) -> Self { + Error::InvalidArgument(err.to_string()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 55601f9..9ef368e 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,14 @@ -#![deny(clippy::unwrap_used)] +// #![deny(clippy::unwrap_used)] + +pub mod client; +pub mod config; +pub mod domain; +pub mod error; + +pub use error::Error; + +pub use domain::{ + keypair, lite, + products::{ip, os, plan}, + snapshot, vm, +};