diff --git a/Cargo.toml b/Cargo.toml index 8512fef..71575e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ members = [ "packages/system/virt", "rhai", "herodo", + "packages/clients/hetznerclient", ] resolver = "2" @@ -47,7 +48,7 @@ log = "0.4" once_cell = "1.18.0" rand = "0.8.5" regex = "1.8.1" -reqwest = { version = "0.12.15", features = ["json"] } +reqwest = { version = "0.12.15", features = ["json", "blocking"] } rhai = { version = "1.12.0", features = ["sync"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" @@ -102,6 +103,7 @@ sal-git = { path = "packages/system/git" } sal-kubernetes = { path = "packages/system/kubernetes" } sal-redisclient = { path = "packages/clients/redisclient" } sal-mycelium = { path = "packages/clients/myceliumclient" } +sal-hetzner = { path = "packages/clients/hetznerclient" } sal-text = { path = "packages/core/text" } sal-os = { path = "packages/system/os" } sal-net = { path = "packages/core/net" } @@ -122,6 +124,7 @@ sal-git = { workspace = true, optional = true } sal-kubernetes = { workspace = true, optional = true } sal-redisclient = { workspace = true, optional = true } sal-mycelium = { workspace = true, optional = true } +sal-hetzner = { workspace = true, optional = true } sal-text = { workspace = true, optional = true } sal-os = { workspace = true, optional = true } sal-net = { workspace = true, optional = true } @@ -141,6 +144,7 @@ git = ["dep:sal-git"] kubernetes = ["dep:sal-kubernetes"] redisclient = ["dep:sal-redisclient"] mycelium = ["dep:sal-mycelium"] +hetzner = ["dep:sal-hetzner"] text = ["dep:sal-text"] os = ["dep:sal-os"] net = ["dep:sal-net"] @@ -154,7 +158,7 @@ rhai = ["dep:sal-rhai"] # Convenience feature groups core = ["os", "process", "text", "net"] -clients = ["redisclient", "postgresclient", "zinit_client", "mycelium"] +clients = ["redisclient", "postgresclient", "zinit_client", "mycelium", "hetzner"] infrastructure = ["git", "vault", "kubernetes", "virt"] scripting = ["rhai"] all = [ @@ -162,6 +166,7 @@ all = [ "kubernetes", "redisclient", "mycelium", + "hetzner", "text", "os", "net", @@ -188,4 +193,3 @@ required-features = ["kubernetes"] name = "generic_cluster" path = "examples/kubernetes/clusters/generic.rs" required-features = ["kubernetes"] - diff --git a/herodo/src/lib.rs b/herodo/src/lib.rs index cf77755..8c00710 100644 --- a/herodo/src/lib.rs +++ b/herodo/src/lib.rs @@ -3,7 +3,7 @@ //! This library loads the Rhai engine, registers all SAL modules, //! and executes Rhai scripts from a specified directory in sorted order. -use rhai::Engine; +use rhai::{Engine, Scope}; use std::error::Error; use std::fs; use std::path::{Path, PathBuf}; @@ -29,6 +29,17 @@ pub fn run(script_path: &str) -> Result<(), Box> { // Create a new Rhai engine let mut engine = Engine::new(); + + // TODO: if we create a scope here we could clean up all the different functionsand types regsitered wit the engine + // We should generalize the way we add things to the scope for each module sepeartely + let mut scope = Scope::new(); + // TODO: this should be done for the other clients as well (but not here of course, in each module) + let hetzner_client = sal::hetzner::api::Client::new(sal::hetzner::config::Config::from_env().unwrap()); + scope.push("hetzner", hetzner_client); + // This makes it easy to call e.g. `hetzner.get_server()` or `mycelium.get_connected_peers()` + // --> without the need of manually created a client for each one first + // --> could be conditionally compiled to only use those who we need (we only push the things to the scope that we actually need to run the script) + // Register println function for output engine.register_fn("println", |s: &str| println!("{}", s)); @@ -78,19 +89,20 @@ pub fn run(script_path: &str) -> Result<(), Box> { let script = fs::read_to_string(&script_file)?; // Execute the script - match engine.eval::(&script) { - Ok(result) => { - println!("Script executed successfully"); - if !result.is_unit() { - println!("Result: {}", result); - } - } - Err(err) => { - eprintln!("Error executing script: {}", err); - // Exit with error code when a script fails - process::exit(1); - } - } + // match engine.eval::(&script) { + // Ok(result) => { + // println!("Script executed successfully"); + // if !result.is_unit() { + // println!("Result: {}", result); + // } + // } + // Err(err) => { + // eprintln!("Error executing script: {}", err); + // // Exit with error code when a script fails + // process::exit(1); + // } + // } + engine.run_with_scope(&mut scope, &script)?; } println!("\nAll scripts executed successfully!"); diff --git a/packages/clients/hetznerclient/Cargo.toml b/packages/clients/hetznerclient/Cargo.toml new file mode 100644 index 0000000..f0f0bf1 --- /dev/null +++ b/packages/clients/hetznerclient/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "sal-hetzner" +version = "0.1.0" +edition = "2024" + +[dependencies] +prettytable = "0.10.0" +reqwest.workspace = true +rhai = { workspace = true, features = ["serde"] } +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +thiserror.workspace = true diff --git a/packages/clients/hetznerclient/src/api/error.rs b/packages/clients/hetznerclient/src/api/error.rs new file mode 100644 index 0000000..818830b --- /dev/null +++ b/packages/clients/hetznerclient/src/api/error.rs @@ -0,0 +1,54 @@ +use std::fmt; + +use serde::Deserialize; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum AppError { + #[error("Request failed: {0}")] + RequestError(#[from] reqwest::Error), + #[error("API error: {0}")] + ApiError(ApiError), + #[error("Deserialization Error: {0:?}")] + SerdeJsonError(#[from] serde_json::Error), +} + +#[derive(Debug, Deserialize)] +pub struct ApiError { + pub status: u16, + pub message: String, +} + +impl From for ApiError { + fn from(value: reqwest::blocking::Response) -> Self { + ApiError { + status: value.status().into(), + message: value.text().unwrap_or("The API call returned an error.".to_string()), + } + } +} + +impl fmt::Display for ApiError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + #[derive(Deserialize)] + struct HetznerApiError { + code: String, + message: String, + } + + #[derive(Deserialize)] + struct HetznerApiErrorWrapper { + error: HetznerApiError, + } + + if let Ok(wrapper) = serde_json::from_str::(&self.message) { + write!( + f, + "Status: {}, Code: {}, Message: {}", + self.status, wrapper.error.code, wrapper.error.message + ) + } else { + write!(f, "Status: {}: {}", self.status, self.message) + } + } +} \ No newline at end of file diff --git a/packages/clients/hetznerclient/src/api/mod.rs b/packages/clients/hetznerclient/src/api/mod.rs new file mode 100644 index 0000000..d34406c --- /dev/null +++ b/packages/clients/hetznerclient/src/api/mod.rs @@ -0,0 +1,513 @@ +pub mod error; +pub mod models; + +use self::models::{ + Boot, Rescue, Server, SshKey, ServerAddonProduct, ServerAddonProductWrapper, + AuctionServerProduct, AuctionServerProductWrapper, AuctionTransaction, + AuctionTransactionWrapper, BootWrapper, Cancellation, CancellationWrapper, + OrderServerBuilder, OrderServerProduct, OrderServerProductWrapper, RescueWrapped, + ServerWrapper, SshKeyWrapper, Transaction, TransactionWrapper, + ServerAddonTransaction, ServerAddonTransactionWrapper, + OrderServerAddonBuilder, +}; +use crate::api::error::ApiError; +use crate::config::Config; +use error::AppError; +use reqwest::blocking::Client as HttpClient; +use serde_json::json; + +#[derive(Clone)] +pub struct Client { + http_client: HttpClient, + config: Config, +} + +impl Client { + pub fn new(config: Config) -> Self { + Self { + http_client: HttpClient::new(), + config, + } + } + + fn handle_response(&self, response: reqwest::blocking::Response) -> Result + where + T: serde::de::DeserializeOwned, + { + let status = response.status(); + let body = response.text()?; + + if status.is_success() { + serde_json::from_str::(&body).map_err(Into::into) + } else { + Err(AppError::ApiError(ApiError { + status: status.as_u16(), + message: body, + })) + } + } + + pub fn get_server(&self, server_number: i32) -> Result { + let response = self + .http_client + .get(format!("{}/server/{}", self.config.api_url, server_number)) + .basic_auth(&self.config.username, Some(&self.config.password)) + .send()?; + + let wrapped: ServerWrapper = self.handle_response(response)?; + Ok(wrapped.server) + } + + pub fn get_servers(&self) -> Result, AppError> { + let response = self + .http_client + .get(format!("{}/server", self.config.api_url)) + .basic_auth(&self.config.username, Some(&self.config.password)) + .send()?; + + let wrapped: Vec = self.handle_response(response)?; + let servers = wrapped.into_iter().map(|sw| sw.server).collect(); + Ok(servers) + } + + pub fn update_server_name(&self, server_number: i32, name: &str) -> Result { + let params = [("server_name", name)]; + let response = self + .http_client + .post(format!("{}/server/{}", self.config.api_url, server_number)) + .basic_auth(&self.config.username, Some(&self.config.password)) + .form(¶ms) + .send()?; + + let wrapped: ServerWrapper = self.handle_response(response)?; + Ok(wrapped.server) + } + + pub fn get_cancellation_data(&self, server_number: i32) -> Result { + let response = self + .http_client + .get(format!( + "{}/server/{}/cancellation", + self.config.api_url, server_number + )) + .basic_auth(&self.config.username, Some(&self.config.password)) + .send()?; + + let wrapped: CancellationWrapper = self.handle_response(response)?; + Ok(wrapped.cancellation) + } + + pub fn cancel_server( + &self, + server_number: i32, + cancellation_date: &str, + ) -> Result { + let params = [("cancellation_date", cancellation_date)]; + let response = self + .http_client + .post(format!( + "{}/server/{}/cancellation", + self.config.api_url, server_number + )) + .basic_auth(&self.config.username, Some(&self.config.password)) + .form(¶ms) + .send()?; + + let wrapped: CancellationWrapper = self.handle_response(response)?; + Ok(wrapped.cancellation) + } + + pub fn withdraw_cancellation(&self, server_number: i32) -> Result<(), AppError> { + self.http_client + .delete(format!( + "{}/server/{}/cancellation", + self.config.api_url, server_number + )) + .basic_auth(&self.config.username, Some(&self.config.password)) + .send()?; + + Ok(()) + } + + pub fn get_ssh_keys(&self) -> Result, AppError> { + let response = self + .http_client + .get(format!("{}/key", self.config.api_url)) + .basic_auth(&self.config.username, Some(&self.config.password)) + .send()?; + + let wrapped: Vec = self.handle_response(response)?; + let keys = wrapped.into_iter().map(|sk| sk.key).collect(); + Ok(keys) + } + + pub fn get_ssh_key(&self, fingerprint: &str) -> Result { + let response = self + .http_client + .get(format!("{}/key/{}", self.config.api_url, fingerprint)) + .basic_auth(&self.config.username, Some(&self.config.password)) + .send()?; + + let wrapped: SshKeyWrapper = self.handle_response(response)?; + Ok(wrapped.key) + } + + pub fn add_ssh_key(&self, name: &str, data: &str) -> Result { + let params = [("name", name), ("data", data)]; + let response = self + .http_client + .post(format!("{}/key", self.config.api_url)) + .basic_auth(&self.config.username, Some(&self.config.password)) + .form(¶ms) + .send()?; + + let wrapped: SshKeyWrapper = self.handle_response(response)?; + Ok(wrapped.key) + } + + pub fn update_ssh_key_name(&self, fingerprint: &str, name: &str) -> Result { + let params = [("name", name)]; + let response = self + .http_client + .post(format!("{}/key/{}", self.config.api_url, fingerprint)) + .basic_auth(&self.config.username, Some(&self.config.password)) + .form(¶ms) + .send()?; + + let wrapped: SshKeyWrapper = self.handle_response(response)?; + Ok(wrapped.key) + } + + pub fn delete_ssh_key(&self, fingerprint: &str) -> Result<(), AppError> { + self.http_client + .delete(format!("{}/key/{}", self.config.api_url, fingerprint)) + .basic_auth(&self.config.username, Some(&self.config.password)) + .send()?; + + Ok(()) + } + pub fn get_boot_configuration(&self, server_number: i32) -> Result { + let response = self + .http_client + .get(format!("{}/boot/{}", self.config.api_url, server_number)) + .basic_auth(&self.config.username, Some(&self.config.password)) + .send()?; + + let wrapped: BootWrapper = self.handle_response(response)?; + Ok(wrapped.boot) + } + + pub fn get_rescue_boot_configuration(&self, server_number: i32) -> Result { + let response = self + .http_client + .get(format!( + "{}/boot/{}/rescue", + self.config.api_url, server_number + )) + .basic_auth(&self.config.username, Some(&self.config.password)) + .send()?; + + let wrapped: RescueWrapped = self.handle_response(response)?; + Ok(wrapped.rescue) + } + + pub fn enable_rescue_mode( + &self, + server_number: i32, + os: &str, + authorized_keys: Option<&[String]>, + ) -> Result { + let mut params = vec![("os", os)]; + if let Some(keys) = authorized_keys { + for key in keys { + params.push(("authorized_key[]", key)); + } + } + let response = self + .http_client + .post(format!( + "{}/boot/{}/rescue", + self.config.api_url, server_number + )) + .basic_auth(&self.config.username, Some(&self.config.password)) + .form(¶ms) + .send()?; + + let wrapped: RescueWrapped = self.handle_response(response)?; + Ok(wrapped.rescue) + } + + pub fn disable_rescue_mode(&self, server_number: i32) -> Result { + let response = self + .http_client + .delete(format!( + "{}/boot/{}/rescue", + self.config.api_url, server_number + )) + .basic_auth(&self.config.username, Some(&self.config.password)) + .send()?; + + let wrapped: RescueWrapped = self.handle_response(response)?; + Ok(wrapped.rescue) + } + + pub fn get_server_products( + &self, + ) -> Result, AppError> { + let response = self + .http_client + .get(format!("{}/order/server/product", &self.config.api_url)) + .basic_auth(&self.config.username, Some(&self.config.password)) + .send()?; + + let wrapped: Vec = self.handle_response(response)?; + let products = wrapped.into_iter().map(|sop| sop.product).collect(); + Ok(products) + } + + pub fn get_server_product_by_id( + &self, + product_id: &str, + ) -> Result { + let response = self + .http_client + .get(format!( + "{}/order/server/product/{}", + &self.config.api_url, product_id + )) + .basic_auth(&self.config.username, Some(&self.config.password)) + .send()?; + + let wrapped: OrderServerProductWrapper = self.handle_response(response)?; + Ok(wrapped.product) + } + pub fn order_server(&self, order: OrderServerBuilder) -> Result { + let mut params = json!({ + "product_id": order.product_id, + "dist": order.dist, + "location": order.location, + "authorized_key": order.authorized_keys.unwrap_or_default(), + }); + + if let Some(addons) = order.addons { + params["addon"] = json!(addons); + } + + if let Some(test) = order.test { + if test { + params["test"] = json!(test); + } + } + + let response = self + .http_client + .post(format!("{}/order/server/transaction", &self.config.api_url)) + .basic_auth(&self.config.username, Some(&self.config.password)) + .json(¶ms) + .send()?; + + let wrapped: TransactionWrapper = self.handle_response(response)?; + Ok(wrapped.transaction) + } + + pub fn get_transaction_by_id(&self, transaction_id: &str) -> Result { + let response = self + .http_client + .get(format!( + "{}/order/server/transaction/{}", + &self.config.api_url, transaction_id + )) + .basic_auth(&self.config.username, Some(&self.config.password)) + .send()?; + + let wrapped: TransactionWrapper = self.handle_response(response)?; + Ok(wrapped.transaction) + } + pub fn get_transactions(&self) -> Result, AppError> { + let response = self + .http_client + .get(format!("{}/order/server/transaction", &self.config.api_url)) + .basic_auth(&self.config.username, Some(&self.config.password)) + .send()?; + + let wrapped: Vec = self.handle_response(response)?; + let transactions = wrapped.into_iter().map(|t| t.transaction).collect(); + Ok(transactions) + } + pub fn get_auction_server_products(&self) -> Result, AppError> { + let response = self + .http_client + .get(format!( + "{}/order/server_market/product", + &self.config.api_url + )) + .basic_auth(&self.config.username, Some(&self.config.password)) + .send()?; + + let wrapped: Vec = self.handle_response(response)?; + let products = wrapped.into_iter().map(|asp| asp.product).collect(); + Ok(products) + } + pub fn get_auction_server_product_by_id(&self, product_id: &str) -> Result { + let response = self + .http_client + .get(format!("{}/order/server_market/product/{}", &self.config.api_url, product_id)) + .basic_auth(&self.config.username, Some(&self.config.password)) + .send()?; + + let wrapped: AuctionServerProductWrapper = self.handle_response(response)?; + Ok(wrapped.product) + } + pub fn get_auction_transactions(&self) -> Result, AppError> { + let response = self + .http_client + .get(format!("{}/order/server_market/transaction", &self.config.api_url)) + .basic_auth(&self.config.username, Some(&self.config.password)) + .send()?; + + let wrapped: Vec = self.handle_response(response)?; + let transactions = wrapped.into_iter().map(|t| t.transaction).collect(); + Ok(transactions) + } + + pub fn get_auction_transaction_by_id(&self, transaction_id: &str) -> Result { + let response = self + .http_client + .get(format!("{}/order/server_market/transaction/{}", &self.config.api_url, transaction_id)) + .basic_auth(&self.config.username, Some(&self.config.password)) + .send()?; + + let wrapped: AuctionTransactionWrapper = self.handle_response(response)?; + Ok(wrapped.transaction) + } + + pub fn get_server_addon_products( + &self, + server_number: i64, + ) -> Result, AppError> { + let response = self + .http_client + .get(format!( + "{}/order/server_addon/{}/product", + &self.config.api_url, server_number + )) + .basic_auth(&self.config.username, Some(&self.config.password)) + .send()?; + + let wrapped: Vec = self.handle_response(response)?; + let products = wrapped.into_iter().map(|sap| sap.product).collect(); + Ok(products) + } + + pub fn order_auction_server( + &self, + product_id: i64, + authorized_keys: Vec, + dist: Option, + arch: Option, + lang: Option, + comment: Option, + addons: Option>, + test: Option, + ) -> Result { + let mut params: Vec<(&str, String)> = Vec::new(); + + params.push(("product_id", product_id.to_string())); + + for key in &authorized_keys { + params.push(("authorized_key[]", key.clone())); + } + + if let Some(dist) = dist { + params.push(("dist", dist)); + } + if let Some(arch) = arch { + params.push(("@deprecated arch", arch)); + } + if let Some(lang) = lang { + params.push(("lang", lang)); + } + if let Some(comment) = comment { + params.push(("comment", comment)); + } + if let Some(addons) = addons { + for addon in addons { + params.push(("addon[]", addon)); + } + } + if let Some(test) = test { + params.push(("test", test.to_string())); + } + + let response = self + .http_client + .post(format!("{}/order/server_market/transaction", &self.config.api_url)) + .basic_auth(&self.config.username, Some(&self.config.password)) + .form(¶ms) + .send()?; + + let wrapped: AuctionTransactionWrapper = self.handle_response(response)?; + Ok(wrapped.transaction) + } + + pub fn get_server_addon_transactions(&self) -> Result, AppError> { + let response = self + .http_client + .get(format!("{}/order/server_addon/transaction", &self.config.api_url)) + .basic_auth(&self.config.username, Some(&self.config.password)) + .send()?; + + let wrapped: Vec = self.handle_response(response)?; + let transactions = wrapped.into_iter().map(|satw| satw.transaction).collect(); + Ok(transactions) + } + + pub fn get_server_addon_transaction_by_id( + &self, + transaction_id: &str, + ) -> Result { + let response = self + .http_client + .get(format!( + "{}/order/server_addon/transaction/{}", + &self.config.api_url, transaction_id + )) + .basic_auth(&self.config.username, Some(&self.config.password)) + .send()?; + + let wrapped: ServerAddonTransactionWrapper = self.handle_response(response)?; + Ok(wrapped.transaction) + } + + pub fn order_server_addon( + &self, + order: OrderServerAddonBuilder, + ) -> Result { + let mut params = json!({ + "server_number": order.server_number, + "product_id": order.product_id, + }); + + if let Some(reason) = order.reason { + params["reason"] = json!(reason); + } + if let Some(gateway) = order.gateway { + params["gateway"] = json!(gateway); + } + if let Some(test) = order.test { + if test { + params["test"] = json!(test); + } + } + + let response = self + .http_client + .post(format!("{}/order/server_addon/transaction", &self.config.api_url)) + .basic_auth(&self.config.username, Some(&self.config.password)) + .form(¶ms) + .send()?; + + let wrapped: ServerAddonTransactionWrapper = self.handle_response(response)?; + Ok(wrapped.transaction) + } +} diff --git a/packages/clients/hetznerclient/src/api/models.rs b/packages/clients/hetznerclient/src/api/models.rs new file mode 100644 index 0000000..ad4f16e --- /dev/null +++ b/packages/clients/hetznerclient/src/api/models.rs @@ -0,0 +1,1894 @@ +use prettytable::{Table, row}; +use rhai::{Array, CustomType, TypeBuilder}; +use serde::{Deserialize, Deserializer}; +use serde_json::Value; +use std::fmt; + +#[derive(Debug, Deserialize, Clone)] +pub struct ServerWrapper { + pub server: Server, +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct Server { + pub server_ip: Option, + pub server_ipv6_net: Option, + pub server_number: i32, + pub server_name: String, + pub product: String, + pub dc: String, + pub traffic: String, + pub status: String, + pub cancelled: bool, + pub paid_until: String, + pub ip: Option>, + pub subnet: Option>, + pub reset: Option, + pub rescue: Option, + pub vnc: Option, + pub windows: Option, + pub plesk: Option, + pub cpanel: Option, + pub wol: Option, + pub hot_swap: Option, + pub linked_storagebox: Option, +} + +impl Server { + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("Server") + .with_get("server_ip", |s: &mut Server| { + s.server_ip.clone().unwrap_or_default() + }) + .with_get("server_ipv6_net", |s: &mut Server| { + s.server_ipv6_net.clone().unwrap_or_default() + }) + .with_get("server_number", |s: &mut Server| s.server_number) + .with_get("server_name", |s: &mut Server| s.server_name.clone()) + .with_get("product", |s: &mut Server| s.product.clone()) + .with_get("dc", |s: &mut Server| s.dc.clone()) + .with_get("traffic", |s: &mut Server| s.traffic.clone()) + .with_get("status", |s: &mut Server| s.status.clone()) + .with_get("cancelled", |s: &mut Server| s.cancelled) + .with_get("paid_until", |s: &mut Server| s.paid_until.clone()) + .with_get("ip", |s: &mut Server| s.ip.clone().unwrap_or_default()) + .with_get("subnet", |s: &mut Server| s.subnet.clone()) + .with_get("reset", |s: &mut Server| s.reset.clone()) + .with_get("rescue", |s: &mut Server| s.rescue.clone()) + .with_get("vnc", |s: &mut Server| s.vnc.clone()) + .with_get("windows", |s: &mut Server| s.windows.clone()) + .with_get("plesk", |s: &mut Server| s.plesk.clone()) + .with_get("cpanel", |s: &mut Server| s.cpanel.clone()) + .with_get("wol", |s: &mut Server| s.wol.clone()) + .with_get("hot_swap", |s: &mut Server| s.hot_swap.clone()) + .with_get("linked_storagebox", |s: &mut Server| { + s.linked_storagebox.clone() + }) + // when doing `print(server) in Rhai script, this will execute` + .on_print(|s: &mut Server| s.to_string()) + // also add the pretty_print function for convience + .with_fn("pretty_print", |s: &mut Server| s.to_string()); + } +} + +// Server should always be printed as a table, hence implement the Display trait to render the table +impl fmt::Display for Server { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut table = Table::new(); + table.add_row(row!["Property", "Value"]); + table.add_row(row!["Server Number", self.server_number.to_string()]); + table.add_row(row!["Server Name", self.server_name.clone()]); + table.add_row(row![ + "Server IP", + self.server_ip.as_deref().unwrap_or("N/A") + ]); + table.add_row(row![ + "IPv6 Network", + self.server_ipv6_net.as_deref().unwrap_or("N/A") + ]); + table.add_row(row!["Product", self.product.clone()]); + table.add_row(row!["Datacenter", self.dc.clone()]); + table.add_row(row!["Traffic", self.traffic.clone()]); + table.add_row(row!["Status", self.status.clone()]); + table.add_row(row!["Cancelled", self.cancelled.to_string()]); + table.add_row(row!["Paid Until", self.paid_until.clone()]); + table.add_row(row!["IP Addresses", self.ip.as_deref().unwrap_or_default().join(", ")]); + table.add_row(row!["Reset", self.reset.unwrap_or(false).to_string()]); + table.add_row(row!["VNC", self.vnc.unwrap_or(false).to_string()]); + table.add_row(row!["Windows", self.windows.is_some().to_string()]); + table.add_row(row!["Plesk", self.plesk.is_some().to_string()]); + table.add_row(row!["cPanel", self.cpanel.is_some().to_string()]); + table.add_row(row!["WOL", self.wol.unwrap_or(false).to_string()]); + table.add_row(row!["Hot Swap", self.hot_swap.unwrap_or(false).to_string()]); + table.add_row(row![ + "Linked Storagebox", + self.linked_storagebox + .map_or("N/A".to_string(), |id| id.to_string()) + ]); + write!(f, "{}", table) + } +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct Subnet { + pub ip: String, + pub mask: String, +} + +impl Subnet { + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("Subnet") + .with_get("ip", |s: &mut Subnet| s.ip.clone()) + .with_get("mask", |s: &mut Subnet| s.mask.clone()) + .on_print(|s: &mut Subnet| s.to_string()) + .with_fn("pretty_print", |s: &mut Subnet| s.to_string()); + } +} + +impl fmt::Display for Subnet { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "IP: {}, Mask: {}", self.ip, self.mask) + } +} + +#[derive(Deserialize)] +pub struct SshKeyWrapper { + pub key: SshKey, +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct SshKey { + pub name: String, + pub fingerprint: String, + #[serde(rename = "type")] + pub key_type: String, + pub size: i32, + pub data: String, + pub created_at: String, +} + +impl SshKey { + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("SshKey") + .with_get("name", |s: &mut SshKey| s.name.clone()) + .with_get("fingerprint", |s: &mut SshKey| s.fingerprint.clone()) + .with_get("key_type", |s: &mut SshKey| s.key_type.clone()) + .with_get("size", |s: &mut SshKey| s.size) + .with_get("data", |s: &mut SshKey| s.data.clone()) + .with_get("created_at", |s: &mut SshKey| s.created_at.clone()) + .on_print(|s: &mut SshKey| s.to_string()) + .with_fn("pretty_print", |s: &mut SshKey| s.to_string()); + } +} + +impl fmt::Display for SshKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut table = Table::new(); + table.add_row(row!["Name", "Fingerprint", "Type", "Size", "Created At"]); + table.add_row(row![ + self.name.clone(), + self.fingerprint.clone(), + self.key_type.clone(), + self.size.to_string(), + self.created_at.clone() + ]); + write!(f, "{}", table) + } +} +#[derive(Debug, Deserialize, Clone)] +pub struct BootWrapper { + pub boot: Boot, +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct Boot { + pub rescue: Rescue, + pub linux: Linux, + pub vnc: Vnc, + pub windows: Option, + pub plesk: Option, + pub cpanel: Option, +} + +impl Boot { + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("Boot") + .with_get("rescue", |b: &mut Boot| b.rescue.clone()) + .with_get("linux", |b: &mut Boot| b.linux.clone()) + .with_get("vnc", |b: &mut Boot| b.vnc.clone()) + .with_get("windows", |b: &mut Boot| b.windows.clone()) + .with_get("plesk", |b: &mut Boot| b.plesk.clone()) + .with_get("cpanel", |b: &mut Boot| b.cpanel.clone()) + .on_print(|b: &mut Boot| b.to_string()) + .with_fn("pretty_print", |b: &mut Boot| b.to_string()); + } +} + +impl fmt::Display for Boot { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut table = Table::new(); + table.add_row(row!["Configuration", "Details"]); + table.add_row(row!["Rescue", self.rescue.to_string()]); + table.add_row(row!["Linux", self.linux.to_string()]); + table.add_row(row!["VNC", self.vnc.to_string()]); + if let Some(windows) = &self.windows { + table.add_row(row!["Windows", windows.to_string()]); + } + if let Some(plesk) = &self.plesk { + table.add_row(row!["Plesk", plesk.to_string()]); + } + if let Some(cpanel) = &self.cpanel { + table.add_row(row!["cPanel", cpanel.to_string()]); + } + write!(f, "{}", table) + } +} + +#[derive(Debug, Deserialize, Clone)] +#[serde(untagged)] +pub enum Os { + Single(String), + Multiple(Vec), +} + +impl Os { + pub fn to_vec(&self) -> Vec { + match self { + Os::Single(s) => vec![s.clone()], + Os::Multiple(v) => v.clone(), + } + } +} +#[derive(Deserialize)] +pub struct RescueWrapped { + pub rescue: Rescue, +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct Rescue { + pub server_ip: String, + pub server_ipv6_net: String, + pub server_number: i32, + pub os: Os, + pub active: bool, + pub password: Option, + #[serde(rename = "authorized_key")] + pub authorized_keys: Vec, + pub host_key: Vec, +} + +impl Rescue { + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("Rescue") + .with_get("server_ip", |r: &mut Rescue| r.server_ip.clone()) + .with_get("server_ipv6_net", |r: &mut Rescue| { + r.server_ipv6_net.clone() + }) + .with_get("server_number", |r: &mut Rescue| r.server_number) + .with_get("os", |r: &mut Rescue| r.os.to_vec()) + .with_get("active", |r: &mut Rescue| r.active) + .with_get("password", |r: &mut Rescue| r.password.clone()) + .with_get("authorized_keys", |r: &mut Rescue| { + r.authorized_keys + .iter() + .map(|k| rhai::Dynamic::from(k.key.clone())) + .collect::() + }) + .with_get("host_key", |r: &mut Rescue| r.host_key.clone()) + .on_print(|r: &mut Rescue| r.to_string()) + .with_fn("pretty_print", |r: &mut Rescue| r.to_string()); + } +} + +impl fmt::Display for Rescue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut table = Table::new(); + table.add_row(row!["Property", "Value"]); + table.add_row(row!["Server IP", self.server_ip]); + table.add_row(row!["Server IPv6 Net", self.server_ipv6_net]); + table.add_row(row!["Server Number", self.server_number.to_string()]); + table.add_row(row!["OS", self.os.to_vec().join(", ")]); + table.add_row(row!["Active", self.active.to_string()]); + table.add_row(row!["Password", self.password.as_deref().unwrap_or("N/A")]); + + let authorized_keys: Vec = self + .authorized_keys + .iter() + .filter_map(|key| key.key.fingerprint.clone()) + .collect(); + table.add_row(row!["Authorized Keys", authorized_keys.join("\n")]); + + let host_keys: Vec = self + .host_key + .iter() + .filter_map(|key| key.fingerprint.clone()) + .collect(); + table.add_row(row!["Host Keys", host_keys.join("\n")]); + + write!(f, "{}", table) + } +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct Linux { + pub server_ip: String, + pub server_ipv6_net: String, + pub server_number: i32, + #[serde(deserialize_with = "string_or_seq_string")] + pub dist: Vec, + #[serde(deserialize_with = "string_or_seq_string")] + pub lang: Vec, + pub active: bool, + pub password: Option, + #[serde(rename = "authorized_key")] + pub authorized_keys: Vec, + pub host_key: Vec, +} + +impl Linux { + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("Linux") + .with_get("server_ip", |l: &mut Linux| l.server_ip.clone()) + .with_get("server_ipv6_net", |l: &mut Linux| l.server_ipv6_net.clone()) + .with_get("server_number", |l: &mut Linux| l.server_number) + .with_get("dist", |l: &mut Linux| l.dist.clone()) + .with_get("lang", |l: &mut Linux| l.lang.clone()) + .with_get("active", |l: &mut Linux| l.active) + .with_get("password", |l: &mut Linux| l.password.clone()) + .with_get("authorized_keys", |l: &mut Linux| { + l.authorized_keys + .iter() + .map(|k| rhai::Dynamic::from(k.key.clone())) + .collect::() + }) + .with_get("host_key", |l: &mut Linux| l.host_key.clone()) + .on_print(|l: &mut Linux| l.to_string()) + .with_fn("pretty_print", |l: &mut Linux| l.to_string()); + } +} + +impl fmt::Display for Linux { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut table = Table::new(); + table.add_row(row!["Property", "Value"]); + table.add_row(row!["Server IP", self.server_ip]); + table.add_row(row!["Server IPv6 Net", self.server_ipv6_net]); + table.add_row(row!["Server Number", self.server_number.to_string()]); + table.add_row(row!["Distribution", self.dist.join(", ")]); + table.add_row(row!["Language", self.lang.join(", ")]); + table.add_row(row!["Active", self.active.to_string()]); + table.add_row(row!["Password", self.password.as_deref().unwrap_or("N/A")]); + + let authorized_keys: Vec = self + .authorized_keys + .iter() + .filter_map(|key| key.key.fingerprint.clone()) + .collect(); + table.add_row(row!["Authorized Keys", authorized_keys.join("\n")]); + + let host_keys: Vec = self + .host_key + .iter() + .filter_map(|key| key.fingerprint.clone()) + .collect(); + table.add_row(row!["Host Keys", host_keys.join("\n")]); + write!(f, "{}", table) + } +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct Vnc { + pub server_ip: String, + pub server_ipv6_net: String, + pub server_number: i32, + #[serde(deserialize_with = "string_or_seq_string")] + pub dist: Vec, + #[serde(deserialize_with = "string_or_seq_string")] + pub lang: Vec, + pub active: bool, + pub password: Option, +} + +impl Vnc { + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("Vnc") + .with_get("server_ip", |v: &mut Vnc| v.server_ip.clone()) + .with_get("server_ipv6_net", |v: &mut Vnc| v.server_ipv6_net.clone()) + .with_get("server_number", |v: &mut Vnc| v.server_number) + .with_get("dist", |v: &mut Vnc| v.dist.clone()) + .with_get("lang", |v: &mut Vnc| v.lang.clone()) + .with_get("active", |v: &mut Vnc| v.active) + .with_get("password", |v: &mut Vnc| v.password.clone()) + .on_print(|v: &mut Vnc| v.to_string()) + .with_fn("pretty_print", |v: &mut Vnc| v.to_string()); + } +} + +impl fmt::Display for Vnc { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Dist: {}, Lang: {}, Active: {}", + self.dist.join(", "), + self.lang.join(", "), + self.active + ) + } +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct Windows { + pub server_ip: String, + pub server_ipv6_net: String, + pub server_number: i32, + #[serde(deserialize_with = "option_string_or_seq_string")] + pub dist: Option>, + #[serde(deserialize_with = "option_string_or_seq_string")] + pub lang: Option>, + pub active: bool, + pub password: Option, +} + +impl Windows { + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("Windows") + .with_get("server_ip", |w: &mut Windows| w.server_ip.clone()) + .with_get("server_ipv6_net", |w: &mut Windows| { + w.server_ipv6_net.clone() + }) + .with_get("server_number", |w: &mut Windows| w.server_number) + .with_get("dist", |w: &mut Windows| w.dist.clone()) + .with_get("lang", |w: &mut Windows| w.lang.clone()) + .with_get("active", |w: &mut Windows| w.active) + .with_get("password", |w: &mut Windows| w.password.clone()) + .on_print(|w: &mut Windows| w.to_string()) + .with_fn("pretty_print", |w: &mut Windows| w.to_string()); + } +} + +impl fmt::Display for Windows { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Dist: {}, Lang: {}, Active: {}", + self.dist.as_deref().unwrap_or_default().join(", "), + self.lang.as_deref().unwrap_or_default().join(", "), + self.active + ) + } +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct Plesk { + pub server_ip: String, + pub server_ipv6_net: String, + pub server_number: i32, + #[serde(deserialize_with = "string_or_seq_string")] + pub dist: Vec, + #[serde(deserialize_with = "string_or_seq_string")] + pub lang: Vec, + pub active: bool, + pub password: Option, + pub hostname: Option, +} + +impl Plesk { + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("Plesk") + .with_get("server_ip", |p: &mut Plesk| p.server_ip.clone()) + .with_get("server_ipv6_net", |p: &mut Plesk| p.server_ipv6_net.clone()) + .with_get("server_number", |p: &mut Plesk| p.server_number) + .with_get("dist", |p: &mut Plesk| p.dist.clone()) + .with_get("lang", |p: &mut Plesk| p.lang.clone()) + .with_get("active", |p: &mut Plesk| p.active) + .with_get("password", |p: &mut Plesk| p.password.clone()) + .with_get("hostname", |p: &mut Plesk| p.hostname.clone()) + .on_print(|p: &mut Plesk| p.to_string()) + .with_fn("pretty_print", |p: &mut Plesk| p.to_string()); + } +} + +impl fmt::Display for Plesk { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Dist: {}, Lang: {}, Active: {}, Hostname: {}", + self.dist.join(", "), + self.lang.join(", "), + self.active, + self.hostname.as_deref().unwrap_or("N/A") + ) + } +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct Cpanel { + pub server_ip: String, + pub server_ipv6_net: String, + pub server_number: i32, + #[serde(deserialize_with = "string_or_seq_string")] + pub dist: Vec, + #[serde(deserialize_with = "string_or_seq_string")] + pub lang: Vec, + pub active: bool, + pub password: Option, + pub hostname: Option, +} + +impl Cpanel { + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("Cpanel") + .with_get("server_ip", |c: &mut Cpanel| c.server_ip.clone()) + .with_get("server_ipv6_net", |c: &mut Cpanel| { + c.server_ipv6_net.clone() + }) + .with_get("server_number", |c: &mut Cpanel| c.server_number) + .with_get("dist", |c: &mut Cpanel| c.dist.clone()) + .with_get("lang", |c: &mut Cpanel| c.lang.clone()) + .with_get("active", |c: &mut Cpanel| c.active) + .with_get("password", |c: &mut Cpanel| c.password.clone()) + .with_get("hostname", |c: &mut Cpanel| c.hostname.clone()) + .on_print(|c: &mut Cpanel| c.to_string()) + .with_fn("pretty_print", |c: &mut Cpanel| c.to_string()); + } +} + +impl fmt::Display for Cpanel { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Dist: {}, Lang: {}, Active: {}, Hostname: {}", + self.dist.join(", "), + self.lang.join(", "), + self.active, + self.hostname.as_deref().unwrap_or("N/A") + ) + } +} + +#[derive(Debug, Deserialize, Clone)] +pub struct CancellationWrapper { + pub cancellation: Cancellation, +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct Cancellation { + pub server_ip: String, + pub server_ipv6_net: Option, + pub server_number: i32, + pub server_name: String, + pub earliest_cancellation_date: String, + pub cancelled: bool, + pub reservation_possible: bool, + pub reserved: bool, + pub cancellation_date: Option, + pub cancellation_reason: Vec, +} + +impl Cancellation { + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("Cancellation") + .with_get("server_ip", |c: &mut Cancellation| c.server_ip.clone()) + .with_get("server_ipv6_net", |c: &mut Cancellation| { + c.server_ipv6_net.clone() + }) + .with_get("server_number", |c: &mut Cancellation| c.server_number) + .with_get("server_name", |c: &mut Cancellation| c.server_name.clone()) + .with_get("earliest_cancellation_date", |c: &mut Cancellation| { + c.earliest_cancellation_date.clone() + }) + .with_get("cancelled", |c: &mut Cancellation| c.cancelled) + .with_get("reservation_possible", |c: &mut Cancellation| { + c.reservation_possible + }) + .with_get("reserved", |c: &mut Cancellation| c.reserved) + .with_get("cancellation_date", |c: &mut Cancellation| { + c.cancellation_date.clone() + }) + .with_get("cancellation_reason", |c: &mut Cancellation| { + c.cancellation_reason.clone() + }) + .on_print(|c: &mut Cancellation| c.to_string()) + .with_fn("pretty_print", |c: &mut Cancellation| c.to_string()); + } +} + +impl fmt::Display for Cancellation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut table = Table::new(); + table.add_row(row!["Property", "Value"]); + table.add_row(row!["Server IP", self.server_ip.clone()]); + table.add_row(row![ + "Server IPv6 Net", + self.server_ipv6_net.as_deref().unwrap_or("N/A").to_string() + ]); + table.add_row(row!["Server Number", self.server_number.to_string()]); + table.add_row(row!["Server Name", self.server_name.clone()]); + table.add_row(row![ + "Earliest Cancellation Date", + self.earliest_cancellation_date.clone() + ]); + table.add_row(row!["Cancelled", self.cancelled.to_string()]); + table.add_row(row![ + "Reservation Possible", + self.reservation_possible.to_string() + ]); + table.add_row(row!["Reserved", self.reserved.to_string()]); + table.add_row(row![ + "Cancellation Date", + self.cancellation_date + .as_deref() + .unwrap_or("N/A") + .to_string() + ]); + table.add_row(row![ + "Cancellation Reason", + self.cancellation_reason.join(", ") + ]); + write!(f, "{}", table) + } +} + + +#[derive(Debug, Deserialize)] +pub struct ApiError { + #[allow(dead_code)] + pub status: u16, + #[allow(dead_code)] + pub message: String, +} + +impl From for ApiError { + fn from(value: reqwest::blocking::Response) -> Self { + ApiError { + status: value.status().into(), + message: value + .text() + .unwrap_or("The API call returned an error.".to_string()), + } + } +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct Price { + pub net: String, + pub gross: String, + pub hourly_net: String, + pub hourly_gross: String, +} + +impl Price { + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("Price") + .with_get("net", |p: &mut Price| p.net.clone()) + .with_get("gross", |p: &mut Price| p.gross.clone()) + .with_get("hourly_net", |p: &mut Price| p.hourly_net.clone()) + .with_get("hourly_gross", |p: &mut Price| p.hourly_gross.clone()) + .on_print(|p: &mut Price| p.to_string()) + .with_fn("pretty_print", |p: &mut Price| p.to_string()); + } +} + +impl fmt::Display for Price { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Net: {}, Gross: {}, Hourly Net: {}, Hourly Gross: {}", + self.net, self.gross, self.hourly_net, self.hourly_gross + ) + } +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct PriceSetup { + pub net: String, + pub gross: String, +} + +impl PriceSetup { + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("PriceSetup") + .with_get("net", |p: &mut PriceSetup| p.net.clone()) + .with_get("gross", |p: &mut PriceSetup| p.gross.clone()) + .on_print(|p: &mut PriceSetup| p.to_string()) + .with_fn("pretty_print", |p: &mut PriceSetup| p.to_string()); + } +} + +impl fmt::Display for PriceSetup { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Net: {}, Gross: {}", self.net, self.gross) + } +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct ProductPrice { + pub location: String, + pub price: Price, + pub price_setup: PriceSetup, +} + +impl ProductPrice { + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("ProductPrice") + .with_get("location", |p: &mut ProductPrice| p.location.clone()) + .with_get("price", |p: &mut ProductPrice| p.price.clone()) + .with_get("price_setup", |p: &mut ProductPrice| p.price_setup.clone()) + .on_print(|p: &mut ProductPrice| p.to_string()) + .with_fn("pretty_print", |p: &mut ProductPrice| p.to_string()); + } +} + +impl fmt::Display for ProductPrice { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Location: {}, Price: ({}), Price Setup: ({})", + self.location, self.price, self.price_setup + ) + } +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct OrderableAddon { + pub id: String, + pub name: String, + pub min: i32, + pub max: i32, + pub prices: Vec, +} + +impl OrderableAddon { + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("OrderableAddon") + .with_get("id", |o: &mut OrderableAddon| o.id.clone()) + .with_get("name", |o: &mut OrderableAddon| o.name.clone()) + .with_get("min", |o: &mut OrderableAddon| o.min) + .with_get("max", |o: &mut OrderableAddon| o.max) + .with_get("prices", |o: &mut OrderableAddon| o.prices.clone()) + .on_print(|o: &mut OrderableAddon| o.to_string()) + .with_fn("pretty_print", |o: &mut OrderableAddon| o.to_string()); + } +} + +impl fmt::Display for OrderableAddon { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut table = Table::new(); + table.add_row(row!["Property", "Value"]); + table.add_row(row!["ID", self.id.clone()]); + table.add_row(row!["Name", self.name.clone()]); + table.add_row(row!["Min", self.min.to_string()]); + table.add_row(row!["Max", self.max.to_string()]); + table.add_row(row!["Prices", format!("{:?}", self.prices)]); + write!(f, "{}", table) + } +} + +#[derive(Debug, Deserialize, Clone)] +pub struct ServerAddonProductWrapper { + pub product: ServerAddonProduct, +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct ServerAddonProduct { + pub id: String, + pub name: String, + #[serde(rename = "type")] + pub product_type: String, + pub price: ProductPrice, +} + +impl ServerAddonProduct { + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("ServerAddonProduct") + .with_get("id", |p: &mut ServerAddonProduct| p.id.clone()) + .with_get("name", |p: &mut ServerAddonProduct| p.name.clone()) + .with_get("product_type", |p: &mut ServerAddonProduct| { + p.product_type.clone() + }) + .with_get("price", |p: &mut ServerAddonProduct| p.price.clone()) + .on_print(|p: &mut ServerAddonProduct| p.to_string()) + .with_fn("pretty_print", |p: &mut ServerAddonProduct| p.to_string()); + } +} + +impl fmt::Display for ServerAddonProduct { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut table = Table::new(); + table.add_row(row!["Property", "Value"]); + table.add_row(row!["ID", self.id.clone()]); + table.add_row(row!["Name", self.name.clone()]); + table.add_row(row!["Type", self.product_type.clone()]); + table.add_row(row!["Price", self.price.to_string()]); + write!(f, "{}", table) + } +} + +#[derive(Debug, Deserialize, Clone)] +pub struct ServerAddonTransactionWrapper { + pub transaction: ServerAddonTransaction, +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct ServerAddonTransaction { + pub id: String, + pub date: String, + pub status: String, + pub server_number: i32, + pub product: ServerAddonTransactionProduct, + pub resources: Vec, +} + +impl ServerAddonTransaction { + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("ServerAddonTransaction") + .with_get("id", |t: &mut ServerAddonTransaction| t.id.clone()) + .with_get("date", |t: &mut ServerAddonTransaction| t.date.clone()) + .with_get("status", |t: &mut ServerAddonTransaction| t.status.clone()) + .with_get("server_number", |t: &mut ServerAddonTransaction| { + t.server_number + }) + .with_get("product", |t: &mut ServerAddonTransaction| { + t.product.clone() + }) + .with_get("resources", |t: &mut ServerAddonTransaction| { + t.resources.clone() + }) + .on_print(|t: &mut ServerAddonTransaction| t.to_string()) + .with_fn("pretty_print", |t: &mut ServerAddonTransaction| { + t.to_string() + }); + } +} + +impl fmt::Display for ServerAddonTransaction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut table = Table::new(); + table.add_row(row!["Property", "Value"]); + table.add_row(row!["ID", self.id.clone()]); + table.add_row(row!["Date", self.date.clone()]); + table.add_row(row!["Status", self.status.clone()]); + table.add_row(row!["Server Number", self.server_number.to_string()]); + table.add_row(row!["Product ID", self.product.id.clone()]); + table.add_row(row!["Product Name", self.product.name.clone()]); + table.add_row(row!["Product Price", self.product.price.to_string()]); + + let mut resources_table = Table::new(); + resources_table.add_row(row![b => "Type", "ID"]); + for resource in &self.resources { + resources_table.add_row(row![resource.resource_type, resource.id]); + } + table.add_row(row!["Resources", resources_table]); + + write!(f, "{}", table) + } +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct ServerAddonTransactionProduct { + pub id: String, + pub name: String, + pub price: ProductPrice, +} + +impl ServerAddonTransactionProduct { + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("ServerAddonTransactionProduct") + .with_get("id", |p: &mut ServerAddonTransactionProduct| p.id.clone()) + .with_get("name", |p: &mut ServerAddonTransactionProduct| { + p.name.clone() + }) + .with_get("price", |p: &mut ServerAddonTransactionProduct| { + p.price.clone() + }) + .on_print(|p: &mut ServerAddonTransactionProduct| p.to_string()) + .with_fn("pretty_print", |p: &mut ServerAddonTransactionProduct| { + p.to_string() + }); + } +} + +impl fmt::Display for ServerAddonTransactionProduct { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "ID: {}, Name: {}, Price: ({})", + self.id, self.name, self.price + ) + } +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct ServerAddonResource { + #[serde(rename = "type")] + pub resource_type: String, + pub id: String, +} + +impl ServerAddonResource { + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("ServerAddonResource") + .with_get("resource_type", |r: &mut ServerAddonResource| { + r.resource_type.clone() + }) + .with_get("id", |r: &mut ServerAddonResource| r.id.clone()) + .on_print(|r: &mut ServerAddonResource| r.to_string()) + .with_fn("pretty_print", |r: &mut ServerAddonResource| { + r.to_string() + }); + } +} + +impl fmt::Display for ServerAddonResource { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Type: {}, ID: {}", self.resource_type, self.id) + } +} + +#[derive(Debug, Deserialize, Clone)] +pub struct OrderServerProductWrapper { + pub product: OrderServerProduct, +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct OrderServerProduct { + pub id: String, + pub name: String, + #[serde(deserialize_with = "string_or_seq_string")] + pub description: Vec, + pub traffic: String, + #[serde(deserialize_with = "string_or_seq_string")] + pub dist: Vec, + #[serde( + rename = "@deprecated arch", + default, + deserialize_with = "option_string_or_seq_string" + )] + #[deprecated(note = "use `dist` instead")] + pub arch: Option>, + #[serde(deserialize_with = "string_or_seq_string")] + pub lang: Vec, + #[serde(deserialize_with = "string_or_seq_string")] + pub location: Vec, + pub prices: Vec, + pub orderable_addons: Vec, +} + +impl OrderServerProduct { + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("OrderServerProduct") + .with_get("id", |o: &mut OrderServerProduct| o.id.clone()) + .with_get("name", |o: &mut OrderServerProduct| o.name.clone()) + .with_get("description", |o: &mut OrderServerProduct| { + o.description.clone() + }) + .with_get("traffic", |o: &mut OrderServerProduct| o.traffic.clone()) + .with_get("dist", |o: &mut OrderServerProduct| o.dist.clone()) + .with_get("arch", |o: &mut OrderServerProduct| o.arch.clone()) + .with_get("lang", |o: &mut OrderServerProduct| o.lang.clone()) + .with_get("location", |o: &mut OrderServerProduct| o.location.clone()) + .with_get("prices", |o: &mut OrderServerProduct| o.prices.clone()) + .with_get("orderable_addons", |o: &mut OrderServerProduct| { + o.orderable_addons.clone() + }) + .on_print(|o: &mut OrderServerProduct| o.to_string()) + .with_fn("pretty_print", |o: &mut OrderServerProduct| o.to_string()); + } +} + +impl fmt::Display for OrderServerProduct { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut table = Table::new(); + table.add_row(row!["Property", "Value"]); + table.add_row(row!["ID", self.id.clone()]); + table.add_row(row!["Name", self.name.clone()]); + table.add_row(row!["Description", self.description.join(", ")]); + table.add_row(row!["Traffic", self.traffic.clone()]); + table.add_row(row!["Distributions", self.dist.join(", ")]); + table.add_row(row![ + "Architectures", + self.arch.as_deref().unwrap_or_default().join(", ") + ]); + table.add_row(row!["Languages", self.lang.join(", ")]); + table.add_row(row!["Locations", self.location.join(", ")]); + let mut prices_table = Table::new(); + prices_table.add_row(row![b => "Location", "Net", "Gross", "Hourly Net", "Hourly Gross", "Setup Net", "Setup Gross"]); + for price in &self.prices { + prices_table.add_row(row![ + price.location, + price.price.net, + price.price.gross, + price.price.hourly_net, + price.price.hourly_gross, + price.price_setup.net, + price.price_setup.gross + ]); + } + table.add_row(row!["Prices", prices_table]); + + let mut addons_table = Table::new(); + addons_table.add_row(row![b => "ID", "Name", "Min", "Max", "Prices"]); + for addon in &self.orderable_addons { + let mut addon_prices_table = Table::new(); + addon_prices_table.add_row(row![b => "Location", "Net", "Gross", "Hourly Net", "Hourly Gross", "Setup Net", "Setup Gross"]); + for price in &addon.prices { + addon_prices_table.add_row(row![ + price.location, + price.price.net, + price.price.gross, + price.price.hourly_net, + price.price.hourly_gross, + price.price_setup.net, + price.price_setup.gross + ]); + } + addons_table.add_row(row![ + addon.id, + addon.name, + addon.min, + addon.max, + addon_prices_table + ]); + } + table.add_row(row!["Orderable Addons", addons_table]); + write!(f, "{}", table) + } +} + +#[derive(Debug, Deserialize, Clone)] +pub struct TransactionWrapper { + pub transaction: Transaction, +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct Transaction { + pub id: String, + pub date: String, + pub status: String, + pub server_number: Option, + pub server_ip: Option, + pub authorized_key: Vec, + pub host_key: Vec, + pub comment: Option, + pub product: TransactionProduct, + pub addons: Vec, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct AuthorizedKeyWrapper { + pub key: AuthorizedKey, +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct AuthorizedKey { + pub name: Option, + pub fingerprint: Option, + #[serde(rename = "type")] + pub key_type: Option, + pub size: Option, +} + +impl From for AuthorizedKey { + fn from(key: SshKey) -> Self { + Self { + name: Some(key.name), + fingerprint: Some(key.fingerprint), + key_type: Some(key.key_type), + size: Some(key.size), + } + } +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct TransactionProduct { + pub id: String, + pub name: String, + pub description: Vec, + pub traffic: String, + pub dist: String, + #[serde(rename = "@deprecated arch")] + pub arch: String, + pub lang: String, + pub location: String, +} + +impl Transaction { + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("Transaction") + .with_get("id", |t: &mut Transaction| t.id.clone()) + .with_get("date", |t: &mut Transaction| t.date.clone()) + .with_get("status", |t: &mut Transaction| t.status.clone()) + .with_get("server_number", |t: &mut Transaction| t.server_number) + .with_get("server_ip", |t: &mut Transaction| t.server_ip.clone()) + .with_get("authorized_key", |t: &mut Transaction| { + t.authorized_key.clone() + }) + .with_get("host_key", |t: &mut Transaction| t.host_key.clone()) + .with_get("comment", |t: &mut Transaction| t.comment.clone()) + .with_get("product", |t: &mut Transaction| t.product.clone()) + .with_get("addons", |t: &mut Transaction| t.addons.clone()); + } +} + +impl AuthorizedKey { + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("AuthorizedKey") + .with_get("name", |k: &mut AuthorizedKey| k.name.clone()) + .with_get("fingerprint", |k: &mut AuthorizedKey| k.fingerprint.clone()) + .with_get("key_type", |k: &mut AuthorizedKey| k.key_type.clone()) + .with_get("size", |k: &mut AuthorizedKey| k.size); + } +} + +impl TransactionProduct { + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("TransactionProduct") + .with_get("id", |p: &mut TransactionProduct| p.id.clone()) + .with_get("name", |p: &mut TransactionProduct| p.name.clone()) + .with_get("description", |p: &mut TransactionProduct| { + p.description.clone() + }) + .with_get("traffic", |p: &mut TransactionProduct| p.traffic.clone()) + .with_get("dist", |p: &mut TransactionProduct| p.dist.clone()) + .with_get("arch", |p: &mut TransactionProduct| p.arch.clone()) + .with_get("lang", |p: &mut TransactionProduct| p.lang.clone()) + .with_get("location", |p: &mut TransactionProduct| p.location.clone()); + } +} + +#[derive(Debug, Deserialize, Clone)] +pub struct HostKeyWrapper { + pub key: HostKey, +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct HostKey { + pub fingerprint: Option, + #[serde(rename = "type")] + pub key_type: Option, + pub size: Option, +} + + +#[derive(Debug, Deserialize, Clone)] +pub struct RescueKey { + pub key: HostKey, +} + +impl HostKey { + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("HostKey") + .with_get("fingerprint", |k: &mut HostKey| k.fingerprint.clone()) + .with_get("key_type", |k: &mut HostKey| k.key_type.clone()) + .with_get("size", |k: &mut HostKey| k.size); + } +} + +#[derive(Debug, Deserialize, Clone)] +pub struct AuctionServerProductWrapper { + pub product: AuctionServerProduct, +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct AuctionServerProduct { + pub id: i32, + pub name: String, + #[serde(deserialize_with = "string_or_seq_string")] + pub description: Vec, + pub traffic: String, + #[serde(deserialize_with = "string_or_seq_string")] + pub dist: Vec, + #[serde( + rename = "@deprecated arch", + default, + deserialize_with = "option_string_or_seq_string" + )] + #[deprecated(note = "use `dist` instead")] + pub arch: Option>, + #[serde(deserialize_with = "string_or_seq_string")] + pub lang: Vec, + pub cpu: String, + pub cpu_benchmark: i32, + pub memory_size: i32, + pub hdd_size: i32, + pub hdd_text: String, + pub hdd_count: i32, + pub datacenter: String, + pub network_speed: String, + pub price: String, + pub price_hourly: Option, + pub price_setup: String, + #[serde(rename = "price_vat")] + pub price_with_vat: String, + #[serde(rename = "price_hourly_vat")] + pub price_hourly_with_vat: Option, + #[serde(rename = "price_setup_vat")] + pub price_setup_with_vat: String, + pub fixed_price: bool, + pub next_reduce: i32, + pub next_reduce_date: String, + pub orderable_addons: Vec, +} + +impl AuctionServerProduct { + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("AuctionServerProduct") + .with_get("id", |p: &mut AuctionServerProduct| p.id) + .with_get("name", |p: &mut AuctionServerProduct| p.name.clone()) + .with_get("description", |p: &mut AuctionServerProduct| { + p.description.clone() + }) + .with_get("traffic", |p: &mut AuctionServerProduct| p.traffic.clone()) + .with_get("dist", |p: &mut AuctionServerProduct| p.dist.clone()) + .with_get("arch", |p: &mut AuctionServerProduct| p.arch.clone()) + .with_get("lang", |p: &mut AuctionServerProduct| p.lang.clone()) + .with_get("cpu", |p: &mut AuctionServerProduct| p.cpu.clone()) + .with_get("cpu_benchmark", |p: &mut AuctionServerProduct| { + p.cpu_benchmark + }) + .with_get("memory_size", |p: &mut AuctionServerProduct| p.memory_size) + .with_get("hdd_size", |p: &mut AuctionServerProduct| p.hdd_size) + .with_get("hdd_text", |p: &mut AuctionServerProduct| { + p.hdd_text.clone() + }) + .with_get("hdd_count", |p: &mut AuctionServerProduct| p.hdd_count) + .with_get("datacenter", |p: &mut AuctionServerProduct| { + p.datacenter.clone() + }) + .with_get("network_speed", |p: &mut AuctionServerProduct| { + p.network_speed.clone() + }) + .with_get("price", |p: &mut AuctionServerProduct| p.price.clone()) + .with_get("price_hourly", |p: &mut AuctionServerProduct| { + p.price_hourly.clone() + }) + .with_get("price_setup", |p: &mut AuctionServerProduct| { + p.price_setup.clone() + }) + .with_get("price_with_vat", |p: &mut AuctionServerProduct| { + p.price_with_vat.clone() + }) + .with_get("price_hourly_with_vat", |p: &mut AuctionServerProduct| { + p.price_hourly_with_vat.clone() + }) + .with_get("price_setup_with_vat", |p: &mut AuctionServerProduct| { + p.price_setup_with_vat.clone() + }) + .with_get("fixed_price", |p: &mut AuctionServerProduct| p.fixed_price) + .with_get("next_reduce", |p: &mut AuctionServerProduct| p.next_reduce) + .with_get("next_reduce_date", |p: &mut AuctionServerProduct| { + p.next_reduce_date.clone() + }) + .with_get("orderable_addons", |p: &mut AuctionServerProduct| { + p.orderable_addons.clone() + }) + .on_print(|p: &mut AuctionServerProduct| p.to_string()) + .with_fn("pretty_print", |p: &mut AuctionServerProduct| p.to_string()); + } +} + +impl fmt::Display for AuctionServerProduct { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut table = Table::new(); + table.add_row(row!["Property", "Value"]); + table.add_row(row!["ID", self.id.to_string()]); + table.add_row(row!["Name", self.name.clone()]); + table.add_row(row!["Description", self.description.join(", ")]); + table.add_row(row!["Traffic", self.traffic.clone()]); + table.add_row(row!["Distributions", self.dist.join(", ")]); + table.add_row(row![ + "Architectures", + self.arch.as_deref().unwrap_or_default().join(", ") + ]); + table.add_row(row!["Languages", self.lang.join(", ")]); + table.add_row(row!["CPU", self.cpu.clone()]); + table.add_row(row!["CPU Benchmark", self.cpu_benchmark.to_string()]); + table.add_row(row!["Memory Size (GB)", self.memory_size.to_string()]); + table.add_row(row!["HDD Size (GB)", self.hdd_size.to_string()]); + table.add_row(row!["HDD Text", self.hdd_text.clone()]); + table.add_row(row!["HDD Count", self.hdd_count.to_string()]); + table.add_row(row!["Datacenter", self.datacenter.clone()]); + table.add_row(row!["Network Speed", self.network_speed.clone()]); + table.add_row(row!["Price (Net)", self.price.clone()]); + table.add_row(row![ + "Price (Hourly Net)", + self.price_hourly.as_deref().unwrap_or("N/A").to_string() + ]); + table.add_row(row!["Price (Setup Net)", self.price_setup.clone()]); + table.add_row(row!["Price (VAT)", self.price_with_vat.clone()]); + table.add_row(row![ + "Price (Hourly VAT)", + self.price_hourly_with_vat + .as_deref() + .unwrap_or("N/A") + .to_string() + ]); + table.add_row(row!["Price (Setup VAT)", self.price_setup_with_vat.clone()]); + table.add_row(row!["Fixed Price", self.fixed_price.to_string()]); + table.add_row(row!["Next Reduce (seconds)", self.next_reduce.to_string()]); + table.add_row(row!["Next Reduce Date", self.next_reduce_date.clone()]); + + let mut addons_table = Table::new(); + addons_table.add_row(row![b => "ID", "Name", "Min", "Max", "Prices"]); + for addon in &self.orderable_addons { + let mut addon_prices_table = Table::new(); + addon_prices_table.add_row(row![b => "Location", "Net", "Gross", "Hourly Net", "Hourly Gross", "Setup Net", "Setup Gross"]); + for price in &addon.prices { + addon_prices_table.add_row(row![ + price.location, + price.price.net, + price.price.gross, + price.price.hourly_net, + price.price.hourly_gross, + price.price_setup.net, + price.price_setup.gross + ]); + } + addons_table.add_row(row![ + addon.id, + addon.name, + addon.min, + addon.max, + addon_prices_table + ]); + } + table.add_row(row!["Orderable Addons", addons_table]); + write!(f, "{}", table) + } +} + +#[derive(Debug, Deserialize, Clone)] +pub struct AuctionTransactionWrapper { + pub transaction: AuctionTransaction, +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct AuctionTransaction { + pub id: String, + pub date: String, + pub status: String, + pub server_number: Option, + pub server_ip: Option, + pub authorized_key: Vec, + pub host_key: Vec, + pub comment: Option, + pub product: AuctionTransactionProduct, + pub addons: Vec, +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct AuctionTransactionProduct { + pub id: i32, + pub name: String, + pub description: Vec, + pub traffic: String, + pub dist: String, + #[serde(rename = "@deprecated arch")] + pub arch: Option, + pub lang: String, + pub cpu: String, + pub cpu_benchmark: i32, + pub memory_size: i32, + pub hdd_size: i32, + pub hdd_text: String, + pub hdd_count: i32, + pub datacenter: String, + pub network_speed: String, + pub fixed_price: Option, + pub next_reduce: Option, + pub next_reduce_date: Option, +} + +impl AuctionTransaction { + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("AuctionTransaction") + .with_get("id", |t: &mut AuctionTransaction| t.id.clone()) + .with_get("date", |t: &mut AuctionTransaction| t.date.clone()) + .with_get("status", |t: &mut AuctionTransaction| t.status.clone()) + .with_get("server_number", |t: &mut AuctionTransaction| { + t.server_number + }) + .with_get("server_ip", |t: &mut AuctionTransaction| { + t.server_ip.clone() + }) + .with_get("authorized_key", |t: &mut AuctionTransaction| { + t.authorized_key.clone() + }) + .with_get("host_key", |t: &mut AuctionTransaction| t.host_key.clone()) + .with_get("comment", |t: &mut AuctionTransaction| t.comment.clone()) + .with_get("product", |t: &mut AuctionTransaction| t.product.clone()) + .with_get("addons", |t: &mut AuctionTransaction| t.addons.clone()) + .on_print(|t: &mut AuctionTransaction| t.to_string()) + .with_fn("pretty_print", |t: &mut AuctionTransaction| t.to_string()); + } +} + +impl fmt::Display for AuctionTransaction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut table = Table::new(); + table.add_row(row!["Property", "Value"]); + table.add_row(row!["ID", self.id.clone()]); + table.add_row(row!["Date", self.date.clone()]); + table.add_row(row!["Status", self.status.clone()]); + table.add_row(row![ + "Server Number", + self.server_number + .map_or("N/A".to_string(), |id| id.to_string()) + ]); + table.add_row(row![ + "Server IP", + self.server_ip.as_deref().unwrap_or("N/A").to_string() + ]); + table.add_row(row![ + "Comment", + self.comment.as_deref().unwrap_or("N/A").to_string() + ]); + table.add_row(row!["Product ID", self.product.id.to_string()]); + table.add_row(row!["Product Name", self.product.name.clone()]); + table.add_row(row![ + "Product Description", + self.product.description.join(", ") + ]); + table.add_row(row!["Product Traffic", self.product.traffic.clone()]); + table.add_row(row!["Product Distributions", self.product.dist.clone()]); + table.add_row(row![ + "Product Architectures", + self.product.arch.as_deref().unwrap_or("N/A") + ]); + table.add_row(row!["Product Languages", self.product.lang.clone()]); + table.add_row(row!["Product CPU", self.product.cpu.clone()]); + table.add_row(row![ + "Product CPU Benchmark", + self.product.cpu_benchmark.to_string() + ]); + table.add_row(row![ + "Product Memory Size (GB)", + self.product.memory_size.to_string() + ]); + table.add_row(row![ + "Product HDD Size (GB)", + self.product.hdd_size.to_string() + ]); + table.add_row(row!["Product HDD Text", self.product.hdd_text.clone()]); + table.add_row(row![ + "Product HDD Count", + self.product.hdd_count.to_string() + ]); + table.add_row(row!["Product Datacenter", self.product.datacenter.clone()]); + table.add_row(row![ + "Product Network Speed", + self.product.network_speed.clone() + ]); + table.add_row(row![ + "Product Fixed Price", + self.product.fixed_price.unwrap_or_default().to_string() + ]); + table.add_row(row![ + "Product Next Reduce (seconds)", + self.product + .next_reduce + .map_or("N/A".to_string(), |r| r.to_string()) + ]); + table.add_row(row![ + "Product Next Reduce Date", + self.product.next_reduce_date.as_deref().unwrap_or("N/A") + ]); + table.add_row(row!["Addons", self.addons.join(", ")]); + + let mut authorized_keys_table = Table::new(); + authorized_keys_table.add_row(row![b => "Name", "Fingerprint", "Type", "Size"]); + for key in &self.authorized_key { + authorized_keys_table.add_row(row![ + key.key.name.as_deref().unwrap_or("N/A"), + key.key.fingerprint.as_deref().unwrap_or("N/A"), + key.key.key_type.as_deref().unwrap_or("N/A"), + key.key.size.map_or("N/A".to_string(), |s| s.to_string()) + ]); + } + table.add_row(row!["Authorized Keys", authorized_keys_table]); + + let mut host_keys_table = Table::new(); + host_keys_table.add_row(row![b => "Fingerprint", "Type", "Size"]); + for key in &self.host_key { + host_keys_table.add_row(row![ + key.key.fingerprint.as_deref().unwrap_or("N/A"), + key.key.key_type.as_deref().unwrap_or("N/A"), + key.key.size.map_or("N/A".to_string(), |s| s.to_string()) + ]); + } + table.add_row(row!["Host Keys", host_keys_table]); + + write!(f, "{}", table) + } +} + +impl AuctionTransactionProduct { + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("AuctionTransactionProduct") + .with_get("id", |p: &mut AuctionTransactionProduct| p.id) + .with_get("name", |p: &mut AuctionTransactionProduct| p.name.clone()) + .with_get("description", |p: &mut AuctionTransactionProduct| { + p.description.clone() + }) + .with_get("traffic", |p: &mut AuctionTransactionProduct| { + p.traffic.clone() + }) + .with_get("dist", |p: &mut AuctionTransactionProduct| p.dist.clone()) + .with_get("arch", |p: &mut AuctionTransactionProduct| { + p.arch.clone().unwrap_or_default() + }) + .with_get("lang", |p: &mut AuctionTransactionProduct| p.lang.clone()) + .with_get("cpu", |p: &mut AuctionTransactionProduct| p.cpu.clone()) + .with_get("cpu_benchmark", |p: &mut AuctionTransactionProduct| { + p.cpu_benchmark + }) + .with_get("memory_size", |p: &mut AuctionTransactionProduct| { + p.memory_size + }) + .with_get("hdd_size", |p: &mut AuctionTransactionProduct| p.hdd_size) + .with_get("hdd_text", |p: &mut AuctionTransactionProduct| { + p.hdd_text.clone() + }) + .with_get("hdd_count", |p: &mut AuctionTransactionProduct| p.hdd_count) + .with_get("datacenter", |p: &mut AuctionTransactionProduct| { + p.datacenter.clone() + }) + .with_get("network_speed", |p: &mut AuctionTransactionProduct| { + p.network_speed.clone() + }) + .with_get("fixed_price", |p: &mut AuctionTransactionProduct| { + p.fixed_price.unwrap_or_default() + }) + .with_get("next_reduce", |p: &mut AuctionTransactionProduct| { + p.next_reduce.unwrap_or_default() + }) + .with_get("next_reduce_date", |p: &mut AuctionTransactionProduct| { + p.next_reduce_date.clone().unwrap_or_default() + }); + } +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct OrderServerBuilder { + pub product_id: String, + pub authorized_keys: Option>, + pub dist: Option, + pub location: Option, + pub lang: Option, + pub comment: Option, + pub addons: Option>, + pub test: Option, +} + +impl OrderServerBuilder { + pub fn new(product_id: &str) -> Self { + Self { + product_id: product_id.to_string(), + authorized_keys: None, + dist: None, + location: None, + lang: None, + comment: None, + addons: None, + test: Some(true), + } + } + + pub fn with_authorized_keys(mut self, keys: Array) -> Self { + let authorized_keys: Vec = if keys.is_empty() { + vec![] + } else if keys[0].is::() { + keys.into_iter() + .map(|k| k.cast::().fingerprint) + .collect() + } else { + keys.into_iter().map(|k| k.into_string().unwrap()).collect() + }; + self.authorized_keys = Some(authorized_keys); + self + } + + pub fn with_dist(mut self, dist: &str) -> Self { + self.dist = Some(dist.to_string()); + self + } + + pub fn with_location(mut self, location: &str) -> Self { + self.location = Some(location.to_string()); + self + } + + pub fn with_lang(mut self, lang: &str) -> Self { + self.lang = Some(lang.to_string()); + self + } + + pub fn with_comment(mut self, comment: &str) -> Self { + self.comment = Some(comment.to_string()); + self + } + + pub fn with_addons(mut self, addons: Array) -> Self { + let addon_list: Vec = addons + .into_iter() + .map(|a| a.into_string().unwrap()) + .collect(); + self.addons = Some(addon_list); + self + } + + pub fn with_test(mut self, test: bool) -> Self { + self.test = Some(test); + self + } + + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("OrderServerBuilder") + .with_fn("new_server_builder", Self::new) + .with_fn("with_authorized_keys", Self::with_authorized_keys) + .with_fn("with_dist", Self::with_dist) + .with_fn("with_location", Self::with_location) + .with_fn("with_lang", Self::with_lang) + .with_fn("with_comment", Self::with_comment) + .with_fn("with_addons", Self::with_addons) + .with_fn("with_test", Self::with_test) + .with_get("product_id", |b: &mut OrderServerBuilder| b.product_id.clone()) + .with_get("dist", |b: &mut OrderServerBuilder| b.dist.clone()) + .with_get("location", |b: &mut OrderServerBuilder| b.location.clone()) + .with_get("authorized_keys", |b: &mut OrderServerBuilder| { + b.authorized_keys.clone() + }) + .with_get("addons", |b: &mut OrderServerBuilder| b.addons.clone()) + .with_get("test", |b: &mut OrderServerBuilder| b.test.clone()); + } +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct OrderAuctionServerBuilder { + pub product_id: i64, + pub authorized_keys: Option>, + pub dist: Option, + pub lang: Option, + pub comment: Option, + pub addon: Option>, + pub test: Option, +} + +impl OrderAuctionServerBuilder { + pub fn new(product_id: i64) -> Self { + Self { + product_id, + authorized_keys: None, + dist: None, + lang: None, + comment: None, + addon: None, + // by default test is enabled + test: Some(true), + } + } + + pub fn with_authorized_keys(mut self, keys: Array) -> Self { + let authorized_keys: Vec = if keys.is_empty() { + vec![] + } else if keys[0].is::() { + keys.into_iter() + .map(|k| k.cast::().fingerprint) + .collect() + } else { + keys.into_iter().map(|k| k.into_string().unwrap()).collect() + }; + self.authorized_keys = Some(authorized_keys); + self + } + + pub fn with_dist(mut self, dist: &str) -> Self { + self.dist = Some(dist.to_string()); + self + } + + pub fn with_lang(mut self, lang: &str) -> Self { + self.lang = Some(lang.to_string()); + self + } + + pub fn with_comment(mut self, comment: &str) -> Self { + self.comment = Some(comment.to_string()); + self + } + + pub fn with_addon(mut self, addon: Array) -> Self { + let addons = addon + .into_iter() + .map(|a| a.into_string().unwrap()) + .collect(); + self.addon = Some(addons); + self + } + + pub fn with_test(mut self, test: bool) -> Self { + self.test = Some(test); + self + } + + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("OrderAuctionServerBuilder") + .with_fn("new_auction_server_builder", Self::new) + .with_fn("with_authorized_keys", Self::with_authorized_keys) + .with_fn("with_dist", Self::with_dist) + .with_fn("with_lang", Self::with_lang) + .with_fn("with_comment", Self::with_comment) + .with_fn("with_addon", Self::with_addon) + .with_fn("with_test", Self::with_test) + .with_get("authorized_keys", |b: &mut OrderAuctionServerBuilder| { + b.authorized_keys.clone() + }) + .with_get("dist", |b: &mut OrderAuctionServerBuilder| b.dist.clone()) + .with_get("lang", |b: &mut OrderAuctionServerBuilder| b.lang.clone()) + .with_get("comment", |b: &mut OrderAuctionServerBuilder| { + b.comment.clone().unwrap_or("".to_string()) + }) + .with_get("addon", |b: &mut OrderAuctionServerBuilder| b.addon.clone()) + .with_get("test", |b: &mut OrderAuctionServerBuilder| b.test.clone()); + } +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct OrderServerAddonBuilder { + pub server_number: i64, + pub product_id: String, + pub reason: Option, + pub gateway: Option, + pub test: Option, +} + +impl OrderServerAddonBuilder { + pub fn new(server_number: i64, product_id: &str) -> Self { + Self { + server_number, + product_id: product_id.to_string(), + reason: None, + gateway: None, + test: Some(true), // by default test is enabled + } + } + + pub fn with_reason(mut self, reason: &str) -> Self { + self.reason = Some(reason.to_string()); + self + } + + pub fn with_gateway(mut self, gateway: &str) -> Self { + self.gateway = Some(gateway.to_string()); + self + } + + pub fn with_test(mut self, test: bool) -> Self { + self.test = Some(test); + self + } + + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("OrderServerAddonBuilder") + .with_fn("new_server_addon_builder", Self::new) + .with_fn("with_reason", Self::with_reason) + .with_fn("with_gateway", Self::with_gateway) + .with_fn("with_test", Self::with_test) + .with_get("server_number", |b: &mut OrderServerAddonBuilder| { + b.server_number + }) + .with_get("product_id", |b: &mut OrderServerAddonBuilder| { + b.product_id.clone() + }) + .with_get("reason", |b: &mut OrderServerAddonBuilder| b.reason.clone()) + .with_get("gateway", |b: &mut OrderServerAddonBuilder| { + b.gateway.clone() + }) + .with_get("test", |b: &mut OrderServerAddonBuilder| b.test.clone()); + } +} + + +fn string_or_seq_string<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let value = Value::deserialize(deserializer)?; + match value { + Value::String(s) => Ok(vec![s]), + Value::Array(a) => a + .into_iter() + .map(|v| { + v.as_str() + .map(ToString::to_string) + .ok_or(serde::de::Error::custom("expected string")) + }) + .collect(), + _ => Err(serde::de::Error::custom( + "expected string or array of strings", + )), + } +} + +fn option_string_or_seq_string<'de, D>(deserializer: D) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + let value = Value::deserialize(deserializer)?; + match value { + Value::Null => Ok(None), + Value::String(s) => Ok(Some(vec![s])), + Value::Array(a) => Ok(Some( + a.into_iter() + .map(|v| { + v.as_str() + .map(ToString::to_string) + .ok_or(serde::de::Error::custom("expected string")) + }) + .collect::, _>>()?, + )), + _ => Err(serde::de::Error::custom( + "expected string or array of strings", + )), + } +} diff --git a/packages/clients/hetznerclient/src/config.rs b/packages/clients/hetznerclient/src/config.rs new file mode 100644 index 0000000..75e2950 --- /dev/null +++ b/packages/clients/hetznerclient/src/config.rs @@ -0,0 +1,25 @@ +use std::env; + +#[derive(Clone)] +pub struct Config { + pub username: String, + pub password: String, + pub api_url: String, +} + +impl Config { + pub fn from_env() -> Result { + let username = env::var("HETZNER_USERNAME") + .map_err(|_| "HETZNER_USERNAME environment variable not set".to_string())?; + let password = env::var("HETZNER_PASSWORD") + .map_err(|_| "HETZNER_PASSWORD environment variable not set".to_string())?; + let api_url = env::var("HETZNER_API_URL") + .unwrap_or_else(|_| "https://robot-ws.your-server.de".to_string()); + + Ok(Config { + username, + password, + api_url, + }) + } +} \ No newline at end of file diff --git a/packages/clients/hetznerclient/src/lib.rs b/packages/clients/hetznerclient/src/lib.rs new file mode 100644 index 0000000..72e6f98 --- /dev/null +++ b/packages/clients/hetznerclient/src/lib.rs @@ -0,0 +1,3 @@ +pub mod api; +pub mod config; +pub mod rhai; \ No newline at end of file diff --git a/packages/clients/hetznerclient/src/rhai/boot.rs b/packages/clients/hetznerclient/src/rhai/boot.rs new file mode 100644 index 0000000..2c1340a --- /dev/null +++ b/packages/clients/hetznerclient/src/rhai/boot.rs @@ -0,0 +1,63 @@ +use crate::api::{ + models::{Boot, Rescue}, + Client, +}; +use rhai::{plugin::*, Engine}; + +pub fn register(engine: &mut Engine) { + let boot_module = exported_module!(boot_api); + engine.register_global_module(boot_module.into()); +} + +#[export_module] +pub mod boot_api { + use super::*; + use rhai::EvalAltResult; + + #[rhai_fn(name = "get_boot_configuration", return_raw)] + pub fn get_boot_configuration( + client: &mut Client, + server_number: i64, + ) -> Result> { + client + .get_boot_configuration(server_number as i32) + .map_err(|e| e.to_string().into()) + } + + #[rhai_fn(name = "get_rescue_boot_configuration", return_raw)] + pub fn get_rescue_boot_configuration( + client: &mut Client, + server_number: i64, + ) -> Result> { + client + .get_rescue_boot_configuration(server_number as i32) + .map_err(|e| e.to_string().into()) + } + + #[rhai_fn(name = "enable_rescue_mode", return_raw)] + pub fn enable_rescue_mode( + client: &mut Client, + server_number: i64, + os: &str, + authorized_keys: rhai::Array, + ) -> Result> { + let keys: Vec = authorized_keys + .into_iter() + .map(|k| k.into_string().unwrap()) + .collect(); + + client + .enable_rescue_mode(server_number as i32, os, Some(&keys)) + .map_err(|e| e.to_string().into()) + } + + #[rhai_fn(name = "disable_rescue_mode", return_raw)] + pub fn disable_rescue_mode( + client: &mut Client, + server_number: i64, + ) -> Result> { + client + .disable_rescue_mode(server_number as i32) + .map_err(|e| e.to_string().into()) + } +} \ No newline at end of file diff --git a/packages/clients/hetznerclient/src/rhai/mod.rs b/packages/clients/hetznerclient/src/rhai/mod.rs new file mode 100644 index 0000000..f566b11 --- /dev/null +++ b/packages/clients/hetznerclient/src/rhai/mod.rs @@ -0,0 +1,54 @@ +use rhai::{Engine, EvalAltResult}; + +use crate::api::models::{ + AuctionServerProduct, AuctionTransaction, AuctionTransactionProduct, AuthorizedKey, Boot, + Cancellation, Cpanel, HostKey, Linux, OrderAuctionServerBuilder, OrderServerAddonBuilder, + OrderServerBuilder, OrderServerProduct, Plesk, Rescue, Server, ServerAddonProduct, + ServerAddonResource, ServerAddonTransaction, SshKey, Transaction, TransactionProduct, Vnc, + Windows, +}; + +pub mod boot; +pub mod printing; +pub mod server; +pub mod server_ordering; +pub mod ssh_keys; + +// here just register the hetzner module +pub fn register_hetzner_module(engine: &mut Engine) -> Result<(), Box> { + // TODO:register types + engine.build_type::(); + engine.build_type::(); + engine.build_type::(); + engine.build_type::(); + engine.build_type::(); + engine.build_type::(); + engine.build_type::(); + engine.build_type::(); + engine.build_type::(); + engine.build_type::(); + engine.build_type::(); + engine.build_type::(); + engine.build_type::(); + engine.build_type::(); + engine.build_type::(); + engine.build_type::(); + engine.build_type::(); + engine.build_type::(); + engine.build_type::(); + engine.build_type::(); + engine.build_type::(); + engine.build_type::(); + engine.build_type::(); + engine.build_type::(); + + server::register(engine); + ssh_keys::register(engine); + boot::register(engine); + server_ordering::register(engine); + + // TODO: push hetzner to scope as value client: + // scope.push("hetzner", client); + + Ok(()) +} diff --git a/packages/clients/hetznerclient/src/rhai/printing/mod.rs b/packages/clients/hetznerclient/src/rhai/printing/mod.rs new file mode 100644 index 0000000..b71c23a --- /dev/null +++ b/packages/clients/hetznerclient/src/rhai/printing/mod.rs @@ -0,0 +1,43 @@ +use rhai::{Array, Engine}; +use crate::{api::models::{OrderServerProduct, AuctionServerProduct, AuctionTransaction, ServerAddonProduct, ServerAddonTransaction, Server, SshKey}}; + +mod servers_table; +mod ssh_keys_table; +mod server_ordering_table; + +// This will be called when we print(...) or pretty_print() an Array (with Dynamic values) +pub fn pretty_print_dispatch(array: Array) { + if array.is_empty() { + println!(""); + return; + } + + let first = &array[0]; + + if first.is::() { + println!("Yeah first is server!"); + servers_table::pretty_print_servers(array); + } else if first.is::() { + ssh_keys_table::pretty_print_ssh_keys(array); + } + else if first.is::() { + server_ordering_table::pretty_print_server_products(array); + } else if first.is::() { + server_ordering_table::pretty_print_auction_server_products(array); + } else if first.is::() { + server_ordering_table::pretty_print_auction_transactions(array); + } else if first.is::() { + server_ordering_table::pretty_print_server_addon_products(array); + } else if first.is::() { + server_ordering_table::pretty_print_server_addon_transactions(array); + } else { + // Generic fallback for other types + for item in array { + println!("{}", item.to_string()); + } + } +} + +pub fn register(engine: &mut Engine) { + engine.register_fn("pretty_print", pretty_print_dispatch); +} diff --git a/packages/clients/hetznerclient/src/rhai/printing/server_ordering_table.rs b/packages/clients/hetznerclient/src/rhai/printing/server_ordering_table.rs new file mode 100644 index 0000000..e4cf48c --- /dev/null +++ b/packages/clients/hetznerclient/src/rhai/printing/server_ordering_table.rs @@ -0,0 +1,293 @@ +use prettytable::{row, Table}; +use crate::api::models::{OrderServerProduct, ServerAddonProduct, ServerAddonTransaction, ServerAddonResource}; + +pub fn pretty_print_server_products(products: rhai::Array) { + let mut table = Table::new(); + table.add_row(row![b => + "ID", + "Name", + "Description", + "Traffic", + "Location", + "Price (Net)", + "Price (Gross)", + ]); + + for product_dyn in products { + if let Some(product) = product_dyn.try_cast::() { + let mut price_net = "N/A".to_string(); + let mut price_gross = "N/A".to_string(); + + if let Some(first_price) = product.prices.first() { + price_net = first_price.price.net.clone(); + price_gross = first_price.price.gross.clone(); + } + + table.add_row(row![ + product.id, + product.name, + product.description.join(", "), + product.traffic, + product.location.join(", "), + price_net, + price_gross, + ]); + } + } + table.printstd(); +} + +pub fn pretty_print_auction_server_products(products: rhai::Array) { + let mut table = Table::new(); + table.add_row(row![b => + "ID", + "Name", + "Description", + "Traffic", + "Distributions", + "Architectures", + "Languages", + "CPU", + "CPU Benchmark", + "Memory Size (GB)", + "HDD Size (GB)", + "HDD Text", + "HDD Count", + "Datacenter", + "Network Speed", + "Price (Net)", + "Price (Hourly Net)", + "Price (Setup Net)", + "Price (VAT)", + "Price (Hourly VAT)", + "Price (Setup VAT)", + "Fixed Price", + "Next Reduce (seconds)", + "Next Reduce Date", + "Orderable Addons", + ]); + + for product_dyn in products { + if let Some(product) = product_dyn.try_cast::() { + let mut addons_table = Table::new(); + addons_table.add_row(row![b => "ID", "Name", "Min", "Max", "Prices"]); + for addon in &product.orderable_addons { + let mut addon_prices_table = Table::new(); + addon_prices_table.add_row(row![b => "Location", "Net", "Gross", "Hourly Net", "Hourly Gross", "Setup Net", "Setup Gross"]); + for price in &addon.prices { + addon_prices_table.add_row(row![ + price.location, + price.price.net, + price.price.gross, + price.price.hourly_net, + price.price.hourly_gross, + price.price_setup.net, + price.price_setup.gross + ]); + } + addons_table.add_row(row![ + addon.id, + addon.name, + addon.min, + addon.max, + addon_prices_table + ]); + } + + table.add_row(row![ + product.id, + product.name, + product.description.join(", "), + product.traffic, + product.dist.join(", "), + product.arch.as_deref().unwrap_or_default().join(", "), + product.lang.join(", "), + product.cpu, + product.cpu_benchmark, + product.memory_size, + product.hdd_size, + product.hdd_text, + product.hdd_count, + product.datacenter, + product.network_speed, + product.price, + product.price_hourly.as_deref().unwrap_or("N/A"), + product.price_setup, + product.price_with_vat, + product.price_hourly_with_vat.as_deref().unwrap_or("N/A"), + product.price_setup_with_vat, + product.fixed_price, + product.next_reduce, + product.next_reduce_date, + addons_table, + ]); + } + } + table.printstd(); +} + +pub fn pretty_print_server_addon_products(products: rhai::Array) { + let mut table = Table::new(); + table.add_row(row![b => + "ID", + "Name", + "Type", + "Location", + "Price (Net)", + "Price (Gross)", + "Hourly Net", + "Hourly Gross", + "Setup Net", + "Setup Gross", + ]); + + for product_dyn in products { + if let Some(product) = product_dyn.try_cast::() { + table.add_row(row![ + product.id, + product.name, + product.product_type, + product.price.location, + product.price.price.net, + product.price.price.gross, + product.price.price.hourly_net, + product.price.price.hourly_gross, + product.price.price_setup.net, + product.price.price_setup.gross, + ]); + } + } + table.printstd(); +} + +pub fn pretty_print_auction_transactions(transactions: rhai::Array) { + let mut table = Table::new(); + table.add_row(row![b => + "ID", + "Date", + "Status", + "Server Number", + "Server IP", + "Comment", + "Product ID", + "Product Name", + "Product Traffic", + "Product Distributions", + "Product Architectures", + "Product Languages", + "Product CPU", + "Product CPU Benchmark", + "Product Memory Size (GB)", + "Product HDD Size (GB)", + "Product HDD Text", + "Product HDD Count", + "Product Datacenter", + "Product Network Speed", + "Product Fixed Price", + "Product Next Reduce (seconds)", + "Product Next Reduce Date", + "Addons", + ]); + + for transaction_dyn in transactions { + if let Some(transaction) = transaction_dyn.try_cast::() { + let _authorized_keys_table = { + let mut table = Table::new(); + table.add_row(row![b => "Name", "Fingerprint", "Type", "Size"]); + for key in &transaction.authorized_key { + table.add_row(row![ + key.key.name.as_deref().unwrap_or("N/A"), + key.key.fingerprint.as_deref().unwrap_or("N/A"), + key.key.key_type.as_deref().unwrap_or("N/A"), + key.key.size.map_or("N/A".to_string(), |s| s.to_string()) + ]); + } + table + }; + + let _host_keys_table = { + let mut table = Table::new(); + table.add_row(row![b => "Fingerprint", "Type", "Size"]); + for key in &transaction.host_key { + table.add_row(row![ + key.key.fingerprint.as_deref().unwrap_or("N/A"), + key.key.key_type.as_deref().unwrap_or("N/A"), + key.key.size.map_or("N/A".to_string(), |s| s.to_string()) + ]); + } + table + }; + + table.add_row(row![ + transaction.id, + transaction.date, + transaction.status, + transaction.server_number.map_or("N/A".to_string(), |id| id.to_string()), + transaction.server_ip.as_deref().unwrap_or("N/A"), + transaction.comment.as_deref().unwrap_or("N/A"), + transaction.product.id, + transaction.product.name, + transaction.product.traffic, + transaction.product.dist, + transaction.product.arch.as_deref().unwrap_or("N/A"), + transaction.product.lang, + transaction.product.cpu, + transaction.product.cpu_benchmark, + transaction.product.memory_size, + transaction.product.hdd_size, + transaction.product.hdd_text, + transaction.product.hdd_count, + transaction.product.datacenter, + transaction.product.network_speed, + transaction.product.fixed_price.unwrap_or_default().to_string(), + transaction + .product + .next_reduce + .map_or("N/A".to_string(), |r| r.to_string()), + transaction + .product + .next_reduce_date + .as_deref() + .unwrap_or("N/A"), + transaction.addons.join(", "), + ]); + } + } + table.printstd(); +} + +pub fn pretty_print_server_addon_transactions(transactions: rhai::Array) { + let mut table = Table::new(); + table.add_row(row![b => + "ID", + "Date", + "Status", + "Server Number", + "Product ID", + "Product Name", + "Product Price", + "Resources", + ]); + + for transaction_dyn in transactions { + if let Some(transaction) = transaction_dyn.try_cast::() { + let mut resources_table = Table::new(); + resources_table.add_row(row![b => "Type", "ID"]); + for resource in &transaction.resources { + resources_table.add_row(row![resource.resource_type, resource.id]); + } + + table.add_row(row![ + transaction.id, + transaction.date, + transaction.status, + transaction.server_number, + transaction.product.id, + transaction.product.name, + transaction.product.price.to_string(), + resources_table, + ]); + } + } + table.printstd(); +} \ No newline at end of file diff --git a/packages/clients/hetznerclient/src/rhai/printing/servers_table.rs b/packages/clients/hetznerclient/src/rhai/printing/servers_table.rs new file mode 100644 index 0000000..446720a --- /dev/null +++ b/packages/clients/hetznerclient/src/rhai/printing/servers_table.rs @@ -0,0 +1,30 @@ +use prettytable::{row, Table}; +use rhai::Array; + +use super::Server; + +pub fn pretty_print_servers(servers: Array) { + let mut table = Table::new(); + table.add_row(row![b => + "Number", + "Name", + "IP", + "Product", + "DC", + "Status" + ]); + + for server_dyn in servers { + if let Some(server) = server_dyn.try_cast::() { + table.add_row(row![ + server.server_number.to_string(), + server.server_name, + server.server_ip.unwrap_or("N/A".to_string()), + server.product, + server.dc, + server.status + ]); + } + } + table.printstd(); +} \ No newline at end of file diff --git a/packages/clients/hetznerclient/src/rhai/printing/ssh_keys_table.rs b/packages/clients/hetznerclient/src/rhai/printing/ssh_keys_table.rs new file mode 100644 index 0000000..36c3a1c --- /dev/null +++ b/packages/clients/hetznerclient/src/rhai/printing/ssh_keys_table.rs @@ -0,0 +1,26 @@ +use prettytable::{row, Table}; +use super::SshKey; + +pub fn pretty_print_ssh_keys(keys: rhai::Array) { + let mut table = Table::new(); + table.add_row(row![b => + "Name", + "Fingerprint", + "Type", + "Size", + "Created At" + ]); + + for key_dyn in keys { + if let Some(key) = key_dyn.try_cast::() { + table.add_row(row![ + key.name, + key.fingerprint, + key.key_type, + key.size.to_string(), + key.created_at + ]); + } + } + table.printstd(); +} diff --git a/packages/clients/hetznerclient/src/rhai/server.rs b/packages/clients/hetznerclient/src/rhai/server.rs new file mode 100644 index 0000000..733122e --- /dev/null +++ b/packages/clients/hetznerclient/src/rhai/server.rs @@ -0,0 +1,76 @@ +use crate::api::{Client, models::Server}; +use rhai::{Array, Dynamic, plugin::*}; + +pub fn register(engine: &mut Engine) { + let server_module = exported_module!(server_api); + engine.register_global_module(server_module.into()); +} + +#[export_module] +pub mod server_api { + use crate::api::models::Cancellation; + + use super::*; + use rhai::EvalAltResult; + + #[rhai_fn(name = "get_server", return_raw)] + pub fn get_server( + client: &mut Client, + server_number: i64, + ) -> Result> { + client + .get_server(server_number as i32) + .map_err(|e| e.to_string().into()) + } + + #[rhai_fn(name = "get_servers", return_raw)] + pub fn get_servers(client: &mut Client) -> Result> { + let servers = client + .get_servers() + .map_err(|e| Into::>::into(e.to_string()))?; + println!("number of SERVERS we got: {:#?}", servers.len()); + Ok(servers.into_iter().map(Dynamic::from).collect()) + } + + #[rhai_fn(name = "update_server_name", return_raw)] + pub fn update_server_name( + client: &mut Client, + server_number: i64, + name: &str, + ) -> Result> { + client + .update_server_name(server_number as i32, name) + .map_err(|e| e.to_string().into()) + } + + #[rhai_fn(name = "get_cancellation_data", return_raw)] + pub fn get_cancellation_data( + client: &mut Client, + server_number: i64, + ) -> Result> { + client + .get_cancellation_data(server_number as i32) + .map_err(|e| e.to_string().into()) + } + + #[rhai_fn(name = "cancel_server", return_raw)] + pub fn cancel_server( + client: &mut Client, + server_number: i64, + cancellation_date: &str, + ) -> Result> { + client + .cancel_server(server_number as i32, cancellation_date) + .map_err(|e| e.to_string().into()) + } + + #[rhai_fn(name = "withdraw_cancellation", return_raw)] + pub fn withdraw_cancellation( + client: &mut Client, + server_number: i64, + ) -> Result<(), Box> { + client + .withdraw_cancellation(server_number as i32) + .map_err(|e| e.to_string().into()) + } +} diff --git a/packages/clients/hetznerclient/src/rhai/server_ordering.rs b/packages/clients/hetznerclient/src/rhai/server_ordering.rs new file mode 100644 index 0000000..a979161 --- /dev/null +++ b/packages/clients/hetznerclient/src/rhai/server_ordering.rs @@ -0,0 +1,170 @@ +use crate::api::{ + Client, + models::{ + AuctionServerProduct, AuctionTransaction, OrderAuctionServerBuilder, OrderServerBuilder, + OrderServerProduct, ServerAddonProduct, ServerAddonTransaction, Transaction, + }, +}; +use rhai::{Array, Dynamic, plugin::*}; + +pub fn register(engine: &mut Engine) { + let server_order_module = exported_module!(server_order_api); + engine.register_global_module(server_order_module.into()); +} + +#[export_module] +pub mod server_order_api { + use crate::api::models::OrderServerAddonBuilder; + + #[rhai_fn(name = "get_server_products", return_raw)] + pub fn get_server_ordering_product_overview( + client: &mut Client, + ) -> Result> { + let overview_servers = client + .get_server_products() + .map_err(|e| Into::>::into(e.to_string()))?; + Ok(overview_servers.into_iter().map(Dynamic::from).collect()) + } + + #[rhai_fn(name = "get_server_product_by_id", return_raw)] + pub fn get_server_ordering_product_by_id( + client: &mut Client, + product_id: &str, + ) -> Result> { + let product = client + .get_server_product_by_id(product_id) + .map_err(|e| Into::>::into(e.to_string()))?; + Ok(product) + } + + #[rhai_fn(name = "order_server", return_raw)] + pub fn order_server( + client: &mut Client, + order: OrderServerBuilder, + ) -> Result> { + let transaction = client + .order_server(order) + .map_err(|e| Into::>::into(e.to_string()))?; + Ok(transaction) + } + + #[rhai_fn(name = "get_transaction_by_id", return_raw)] + pub fn get_transaction_by_id( + client: &mut Client, + transaction_id: &str, + ) -> Result> { + let transaction = client + .get_transaction_by_id(transaction_id) + .map_err(|e| Into::>::into(e.to_string()))?; + Ok(transaction) + } + + #[rhai_fn(name = "get_transactions", return_raw)] + pub fn get_transactions(client: &mut Client) -> Result> { + let transactions = client + .get_transactions() + .map_err(|e| Into::>::into(e.to_string()))?; + Ok(transactions.into_iter().map(Dynamic::from).collect()) + } + + #[rhai_fn(name = "get_auction_server_products", return_raw)] + pub fn get_auction_server_products(client: &mut Client) -> Result> { + let products = client + .get_auction_server_products() + .map_err(|e| Into::>::into(e.to_string()))?; + Ok(products.into_iter().map(Dynamic::from).collect()) + } + + #[rhai_fn(name = "get_auction_server_product_by_id", return_raw)] + pub fn get_auction_server_product_by_id( + client: &mut Client, + product_id: &str, + ) -> Result> { + let product = client + .get_auction_server_product_by_id(product_id) + .map_err(|e| Into::>::into(e.to_string()))?; + Ok(product) + } + + #[rhai_fn(name = "get_auction_transactions", return_raw)] + pub fn get_auction_transactions(client: &mut Client) -> Result> { + let transactions = client + .get_auction_transactions() + .map_err(|e| Into::>::into(e.to_string()))?; + Ok(transactions.into_iter().map(Dynamic::from).collect()) + } + + #[rhai_fn(name = "get_auction_transaction_by_id", return_raw)] + pub fn get_auction_transaction_by_id( + client: &mut Client, + transaction_id: &str, + ) -> Result> { + let transaction = client + .get_auction_transaction_by_id(transaction_id) + .map_err(|e| Into::>::into(e.to_string()))?; + Ok(transaction) + } + + #[rhai_fn(name = "get_server_addon_products", return_raw)] + pub fn get_server_addon_products( + client: &mut Client, + server_number: i64, + ) -> Result> { + let products = client + .get_server_addon_products(server_number) + .map_err(|e| Into::>::into(e.to_string()))?; + Ok(products.into_iter().map(Dynamic::from).collect()) + } + + #[rhai_fn(name = "get_server_addon_transactions", return_raw)] + pub fn get_server_addon_transactions( + client: &mut Client, + ) -> Result> { + let transactions = client + .get_server_addon_transactions() + .map_err(|e| Into::>::into(e.to_string()))?; + Ok(transactions.into_iter().map(Dynamic::from).collect()) + } + + #[rhai_fn(name = "get_server_addon_transaction_by_id", return_raw)] + pub fn get_server_addon_transaction_by_id( + client: &mut Client, + transaction_id: &str, + ) -> Result> { + let transaction = client + .get_server_addon_transaction_by_id(transaction_id) + .map_err(|e| Into::>::into(e.to_string()))?; + Ok(transaction) + } + + #[rhai_fn(name = "order_auction_server", return_raw)] + pub fn order_auction_server( + client: &mut Client, + order: OrderAuctionServerBuilder, + ) -> Result> { + println!("Builder struct being used to order server: {:#?}", order); + let transaction = client.order_auction_server( + order.product_id, + order.authorized_keys.unwrap_or(vec![]), + order.dist, + None, + order.lang, + order.comment, + order.addon, + order.test, + ).map_err(|e| Into::>::into(e.to_string()))?; + Ok(transaction) + } + + #[rhai_fn(name = "order_server_addon", return_raw)] + pub fn order_server_addon( + client: &mut Client, + order: OrderServerAddonBuilder, + ) -> Result> { + println!("Builder struct being used to order server addon: {:#?}", order); + let transaction = client + .order_server_addon(order) + .map_err(|e| Into::>::into(e.to_string()))?; + Ok(transaction) + } +} diff --git a/packages/clients/hetznerclient/src/rhai/ssh_keys.rs b/packages/clients/hetznerclient/src/rhai/ssh_keys.rs new file mode 100644 index 0000000..cc84372 --- /dev/null +++ b/packages/clients/hetznerclient/src/rhai/ssh_keys.rs @@ -0,0 +1,89 @@ +use crate::api::{Client, models::SshKey}; +use prettytable::{Table, row}; +use rhai::{Array, Dynamic, Engine, plugin::*}; + +pub fn register(engine: &mut Engine) { + let ssh_keys_module = exported_module!(ssh_keys_api); + engine.register_global_module(ssh_keys_module.into()); +} + +#[export_module] +pub mod ssh_keys_api { + use super::*; + use rhai::EvalAltResult; + + #[rhai_fn(name = "get_ssh_keys", return_raw)] + pub fn get_ssh_keys(client: &mut Client) -> Result> { + let ssh_keys = client + .get_ssh_keys() + .map_err(|e| Into::>::into(e.to_string()))?; + Ok(ssh_keys.into_iter().map(Dynamic::from).collect()) + } + + #[rhai_fn(name = "get_ssh_key", return_raw)] + pub fn get_ssh_key( + client: &mut Client, + fingerprint: &str, + ) -> Result> { + client + .get_ssh_key(fingerprint) + .map_err(|e| e.to_string().into()) + } + + #[rhai_fn(name = "add_ssh_key", return_raw)] + pub fn add_ssh_key( + client: &mut Client, + name: &str, + data: &str, + ) -> Result> { + client + .add_ssh_key(name, data) + .map_err(|e| e.to_string().into()) + } + + #[rhai_fn(name = "update_ssh_key_name", return_raw)] + pub fn update_ssh_key_name( + client: &mut Client, + fingerprint: &str, + name: &str, + ) -> Result> { + client + .update_ssh_key_name(fingerprint, name) + .map_err(|e| e.to_string().into()) + } + + #[rhai_fn(name = "delete_ssh_key", return_raw)] + pub fn delete_ssh_key( + client: &mut Client, + fingerprint: &str, + ) -> Result<(), Box> { + client + .delete_ssh_key(fingerprint) + .map_err(|e| e.to_string().into()) + } + + #[rhai_fn(name = "pretty_print")] + pub fn pretty_print_ssh_keys(keys: Array) { + let mut table = Table::new(); + table.add_row(row![b => + "Name", + "Fingerprint", + "Type", + "Size", + "Created At" + ]); + + for key_dyn in keys { + if let Some(key) = key_dyn.try_cast::() { + table.add_row(row![ + key.name, + key.fingerprint, + key.key_type, + key.size.to_string(), + key.created_at + ]); + } + } + table.printstd(); + } +} diff --git a/rhai/Cargo.toml b/rhai/Cargo.toml index 3182714..f5a8fe6 100644 --- a/rhai/Cargo.toml +++ b/rhai/Cargo.toml @@ -26,6 +26,7 @@ sal-redisclient = { workspace = true } sal-postgresclient = { workspace = true } sal-virt = { workspace = true } sal-mycelium = { workspace = true } +sal-hetzner = { workspace = true } sal-text = { workspace = true } sal-net = { workspace = true } sal-zinit-client = { workspace = true } diff --git a/rhai/src/lib.rs b/rhai/src/lib.rs index 9b7094e..1327d5e 100644 --- a/rhai/src/lib.rs +++ b/rhai/src/lib.rs @@ -90,6 +90,9 @@ pub use sal_zinit_client::rhai::register_zinit_module; // Re-export mycelium module pub use sal_mycelium::rhai::register_mycelium_module; +// Re-export hetzner module +pub use sal_hetzner::rhai::register_hetzner_module; + // Re-export text module pub use sal_text::rhai::register_text_module; @@ -151,6 +154,9 @@ pub fn register(engine: &mut Engine) -> Result<(), Box> { // Register Mycelium module functions sal_mycelium::rhai::register_mycelium_module(engine)?; + // Register Hetzner module functions + sal_hetzner::rhai::register_hetzner_module(engine)?; + // Register Text module functions sal_text::rhai::register_text_module(engine)?; diff --git a/src/lib.rs b/src/lib.rs index 7ae9fdb..8414784 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,6 +46,9 @@ pub use sal_kubernetes as kubernetes; #[cfg(feature = "mycelium")] pub use sal_mycelium as mycelium; +#[cfg(feature = "hetzner")] +pub use sal_hetzner as hetzner; + #[cfg(feature = "net")] pub use sal_net as net;