From 2e999a3e7e7cceacf9bf125881ca91d2b96140bf Mon Sep 17 00:00:00 2001 From: Maxime Van Hees Date: Tue, 22 Jul 2025 15:04:43 +0200 Subject: [PATCH] add more server ordering / transaction calls --- examples/server_ordering.rhai | 18 +++- src/api/mod.rs | 65 +++++++++++- src/api/models.rs | 169 ++++++++++++++++++++++++++++++- src/scripting/mod.rs | 6 +- src/scripting/server_ordering.rs | 69 ++++++++++++- 5 files changed, 317 insertions(+), 10 deletions(-) diff --git a/examples/server_ordering.rhai b/examples/server_ordering.rhai index b741f36..b328f49 100644 --- a/examples/server_ordering.rhai +++ b/examples/server_ordering.rhai @@ -1,3 +1,15 @@ -// Get all available products (servers) that we can order and print them in a table -let available_server_products = hetzner.get_server_ordering_product_overview(); -available_server_products.pretty_print(); \ No newline at end of file +// // Get all available products (servers) that we can order and print them in a table +// let available_server_products = hetzner.get_server_ordering_product_overview(); +// available_server_products.pretty_print(); + +// // List the details from a specific sever product based on the ID +// let example_server_product = hetzner.get_server_ordering_product_by_id("AX41-NVMe"); +// print(example_server_product); + +// List all the transactions from the past 30 days +// let transactions_last_30 = hetzner.get_transactions(); +// print(transactions_last_30); + +let example_transaction = hetzner.get_transaction_by_id("2111181"); +print(example_transaction); + diff --git a/src/api/mod.rs b/src/api/mod.rs index c64ce8d..4ede250 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -4,11 +4,12 @@ pub mod models; use self::models::{Boot, Rescue, Server, SshKey}; use crate::api::error::ApiError; use crate::api::models::{ - BootWrapper, Cancellation, CancellationWrapper, OrderServerProduct, OrderServerProductWrapper, RescueWrapped, ServerWrapper, SshKeyWrapper + BootWrapper, Cancellation, CancellationWrapper, OrderServerProduct, OrderServerProductWrapper, RescueWrapped, ServerWrapper, SshKeyWrapper, Transaction, TransactionWrapper }; use crate::config::Config; use error::AppError; use reqwest::blocking::Client as HttpClient; +use serde_json::json; #[derive(Clone)] pub struct Client { @@ -256,4 +257,66 @@ impl Client { let products = wrapped.into_iter().map(|sop| sop.product).collect(); Ok(products) } + + pub fn get_server_ordering_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, + product_id: &str, + dist: &str, + location: &str, + authorized_keys: Vec, + addons: Option>, + ) -> Result { + let mut params = json!({ + "product_id": product_id, + "dist": dist, + "location": location, + "authorized_key": authorized_keys, + }); + + if let Some(addons) = addons { + params["addon"] = json!(addons); + } + + 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) + } } diff --git a/src/api/models.rs b/src/api/models.rs index d430001..9f1af5a 100644 --- a/src/api/models.rs +++ b/src/api/models.rs @@ -830,8 +830,173 @@ impl fmt::Display for OrderServerProduct { 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(", ")]); - table.add_row(row!["Prices", format!("{:?}", self.prices)]); - table.add_row(row!["Orderable Addons", format!("{:?}", self.orderable_addons)]); + 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: String, + pub fingerprint: String, + #[serde(rename = "type")] + pub key_type: String, + pub size: i32, +} + +impl From for AuthorizedKey { + fn from(key: SshKey) -> Self { + Self { + name: key.name, + fingerprint: key.fingerprint, + key_type: key.key_type, + size: 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: String, + #[serde(rename = "type")] + pub key_type: String, + pub size: i32, +} + +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); + } } \ No newline at end of file diff --git a/src/scripting/mod.rs b/src/scripting/mod.rs index 25a8d12..13646d3 100644 --- a/src/scripting/mod.rs +++ b/src/scripting/mod.rs @@ -1,5 +1,5 @@ use crate::api::Client; -use crate::api::models::{Rescue, Linux, Vnc, Windows, Plesk, Cpanel, Boot, Server, SshKey, Cancellation, OrderServerProduct}; +use crate::api::models::{Rescue, Linux, Vnc, Windows, Plesk, Cpanel, Boot, Server, SshKey, Cancellation, OrderServerProduct, Transaction, AuthorizedKey, TransactionProduct, HostKey}; use rhai::{Engine, Scope}; pub mod server; @@ -23,6 +23,10 @@ pub fn setup_engine(client: Client) -> (Engine, Scope<'static>) { engine.build_type::(); engine.build_type::(); engine.build_type::(); + engine.build_type::(); + engine.build_type::(); + engine.build_type::(); + engine.build_type::(); server::register(&mut engine); ssh_keys::register(&mut engine); diff --git a/src/scripting/server_ordering.rs b/src/scripting/server_ordering.rs index 967a739..2b0d3dc 100644 --- a/src/scripting/server_ordering.rs +++ b/src/scripting/server_ordering.rs @@ -1,4 +1,4 @@ -use crate::api::{Client, models::OrderServerProduct}; +use crate::api::{Client, models::{OrderServerProduct, Transaction, SshKey}}; use rhai::{Array, Dynamic, plugin::*}; pub fn register(engine: &mut Engine) { @@ -8,8 +8,6 @@ pub fn register(engine: &mut Engine) { #[export_module] pub mod server_order_api { - // use super::*; - // use rhai::EvalAltResult; #[rhai_fn(name = "get_server_ordering_product_overview", return_raw)] pub fn get_server_ordering_product_overview( @@ -20,4 +18,69 @@ pub mod server_order_api { .map_err(|e| Into::>::into(e.to_string()))?; Ok(overview_servers.into_iter().map(Dynamic::from).collect()) } + + #[rhai_fn(name = "get_server_ordering_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_ordering_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, + product_id: &str, + dist: &str, + location: &str, + authorized_keys: Array, + addons: Array, + ) -> Result> { + let authorized_keys: Vec = if authorized_keys.is_empty() { + vec![] + } else if authorized_keys[0].is::() { + authorized_keys + .into_iter() + .map(|k| k.cast::().fingerprint) + .collect() + } else { + authorized_keys + .into_iter() + .map(|k| k.into_string().unwrap()) + .collect() + }; + + let addons = if addons.is_empty() { + None + } else { + Some(addons.into_iter().map(|a| a.into_string().unwrap()).collect()) + }; + + let transaction = client + .order_server(product_id, dist, location, authorized_keys, addons) + .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()) + } }