From 53e7d91e3953412a4f83f6b60f3023f33e5df0cb Mon Sep 17 00:00:00 2001 From: Maxime Van Hees Date: Wed, 23 Jul 2025 17:01:09 +0200 Subject: [PATCH] added ability to also order regular servers (previously only auctioned ones) --- examples/server_ordering.rhai | 18 +- src/api/mod.rs | 30 ++-- src/api/models.rs | 280 ++++++++++++++++++++++++------- src/scripting/mod.rs | 3 +- src/scripting/server_ordering.rs | 40 +---- 5 files changed, 256 insertions(+), 115 deletions(-) diff --git a/examples/server_ordering.rhai b/examples/server_ordering.rhai index e688644..73326db 100644 --- a/examples/server_ordering.rhai +++ b/examples/server_ordering.rhai @@ -1,11 +1,23 @@ /// --- 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(); +// let available_server_products = hetzner.get_server_products(); // 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"); +// let example_server_product = hetzner.get_server_product_by_id("AX41-NVMe"); // print(example_server_product); +/// --- Order a server +// 1. Grab the SSH key to pass to the deployment +let ssh_key = hetzner.get_ssh_key("e0:73:80:26:80:46:f0:c8:bb:74:f4:d0:2d:10:2d:6f"); +// 2. Use the builder to bundle the details on what to order +let order_builder = new_server_builder("AX41-NVMe") + .with_authorized_keys([ssh_key.fingerprint]) + .with_test(true); + +let ordered_server_transaction = hetzner.order_server(order_builder); +print(ordered_server_transaction); + + /// --- List all the transactions from the past 30 days // let transactions_last_30 = hetzner.get_transactions(); // print(transactions_last_30); @@ -32,7 +44,7 @@ /// --- Order an auction server // 1. Grab the SSH key to pass to the deployment -let ssh_key = hetzner.get_ssh_key("e0:73:80:26:80:46:f0:c8:bb:74:f4:d0:2d:10:2d:6f"); +// let ssh_key = hetzner.get_ssh_key("e0:73:80:26:80:46:f0:c8:bb:74:f4:d0:2d:10:2d:6f"); // 2. Use the builder to bundle the details on what to order // let order_builder = new_auction_server_builder(2741558) // .with_authorized_keys([ssh_key.fingerprint]) diff --git a/src/api/mod.rs b/src/api/mod.rs index c4aced5..2de719a 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -4,7 +4,10 @@ pub mod models; use self::models::{Boot, Rescue, Server, SshKey}; use crate::api::error::ApiError; use crate::api::models::{ - AuctionServerProduct, AuctionServerProductWrapper, AuctionTransaction, AuctionTransactionWrapper, BootWrapper, Cancellation, CancellationWrapper, OrderServerProduct, OrderServerProductWrapper, RescueWrapped, ServerWrapper, SshKeyWrapper, Transaction, TransactionWrapper + AuctionServerProduct, AuctionServerProductWrapper, AuctionTransaction, + AuctionTransactionWrapper, BootWrapper, Cancellation, CancellationWrapper, + OrderServerBuilder, OrderServerProduct, OrderServerProductWrapper, RescueWrapped, + ServerWrapper, SshKeyWrapper, Transaction, TransactionWrapper, }; use crate::config::Config; use error::AppError; @@ -276,25 +279,24 @@ impl Client { 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 { + pub fn order_server(&self, order: OrderServerBuilder) -> Result { let mut params = json!({ - "product_id": product_id, - "dist": dist, - "location": location, - "authorized_key": authorized_keys, + "product_id": order.product_id, + "dist": order.dist, + "location": order.location, + "authorized_key": order.authorized_keys.unwrap_or_default(), }); - if let Some(addons) = addons { + 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)) diff --git a/src/api/models.rs b/src/api/models.rs index 43db3e0..e1ced43 100644 --- a/src/api/models.rs +++ b/src/api/models.rs @@ -51,6 +51,19 @@ impl Server { .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()) + .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 @@ -90,11 +103,6 @@ impl fmt::Display for Server { } } -#[derive(Deserialize)] -struct SubnetWrapper { - _subnet: Subnet, -} - #[derive(Debug, Deserialize, Clone, CustomType)] #[rhai_type(extra = Self::build_rhai_type)] pub struct Subnet { @@ -575,49 +583,6 @@ impl fmt::Display for Cancellation { } } -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", - )), - } -} #[derive(Debug, Deserialize)] pub struct ApiError { @@ -1255,27 +1220,69 @@ impl fmt::Display for AuctionTransaction { 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![ + "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 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 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 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 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![ + "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(); @@ -1348,6 +1355,102 @@ impl AuctionTransactionProduct { } } +#[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)] @@ -1428,9 +1531,60 @@ impl OrderAuctionServerBuilder { .with_fn("with_comment", Self::with_comment) .with_fn("with_addon", Self::with_addon) .with_fn("with_test", Self::with_test) - // TODO implement other getters + .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()); + } +} + + +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/src/scripting/mod.rs b/src/scripting/mod.rs index 1059742..3e36734 100644 --- a/src/scripting/mod.rs +++ b/src/scripting/mod.rs @@ -1,6 +1,6 @@ use crate::api::Client; use crate::api::models::{ - AuctionServerProduct, AuctionTransaction, AuctionTransactionProduct, AuthorizedKey, Boot, Cancellation, Cpanel, HostKey, Linux, OrderAuctionServerBuilder, OrderServerProduct, Plesk, Rescue, Server, SshKey, Transaction, TransactionProduct, Vnc, Windows + AuctionServerProduct, AuctionTransaction, AuctionTransactionProduct, AuthorizedKey, Boot, Cancellation, Cpanel, HostKey, Linux, OrderAuctionServerBuilder, OrderServerBuilder, OrderServerProduct, Plesk, Rescue, Server, SshKey, Transaction, TransactionProduct, Vnc, Windows }; use rhai::{Engine, Scope}; @@ -33,6 +33,7 @@ pub fn setup_engine(client: Client) -> (Engine, Scope<'static>) { 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 4fb5883..6df77e4 100644 --- a/src/scripting/server_ordering.rs +++ b/src/scripting/server_ordering.rs @@ -1,6 +1,9 @@ use crate::api::{ Client, - models::{AuctionServerProduct, AuctionTransaction, OrderServerProduct, SshKey, Transaction}, + models::{ + AuctionServerProduct, AuctionTransaction, OrderAuctionServerBuilder, OrderServerBuilder, + OrderServerProduct, Transaction, + }, }; use rhai::{Array, Dynamic, plugin::*}; @@ -11,8 +14,6 @@ pub fn register(engine: &mut Engine) { #[export_module] pub mod server_order_api { - use crate::api::models::OrderAuctionServerBuilder; - #[rhai_fn(name = "get_server_products", return_raw)] pub fn get_server_ordering_product_overview( client: &mut Client, @@ -37,39 +38,10 @@ pub mod server_order_api { #[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, + order: OrderServerBuilder, ) -> 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) + .order_server(order) .map_err(|e| Into::>::into(e.to_string()))?; Ok(transaction) }