diff --git a/examples/server_management.rhai b/examples/server_management.rhai index ef7855e..6c93e82 100644 --- a/examples/server_management.rhai +++ b/examples/server_management.rhai @@ -1,9 +1,21 @@ // Get all servers and print them in a table -let servers = hetzner.get_servers(); -print(servers); -servers.pretty_print(); +// let servers = hetzner.get_servers(); +// servers.pretty_print(); -// Get a specific server and print its details -// Replace 1825193 with the server number you want to fetch -let server = hetzner.get_server(1825193); -print(server); \ No newline at end of file +// // Get a specific server and print its details +// // Replace 2550253 with the server number you want to fetch +// let server = hetzner.get_server(2550253); +// print(server); + +// Update the name of a specific server and print it +// print(hetzner.update_server_name(2550253, "kristof-123456")); + +// Query cancellation data for a server +let c_d = hetzner.get_cancellation_data(2550253); +print(c_d); + +// Cancel a server +// Replace 2550253 with the server number you want to cancel +// Replace "2014-04-15" with the desired cancellation date +let cancelled_server = hetzner.cancel_server(2550253, "2014-04-15"); +print(cancelled_server); \ No newline at end of file diff --git a/examples/ssh_key_management.rhai b/examples/ssh_key_management.rhai index f6727ec..68bab33 100644 --- a/examples/ssh_key_management.rhai +++ b/examples/ssh_key_management.rhai @@ -1,22 +1,22 @@ // Get all SSH keys and print them in a table let keys = hetzner.get_ssh_keys(); -print(keys); +keys.pretty_print(); // Get a specific SSH key // Replace "13:dc:a2:1e:a9:d2:1d:a9:39:f4:44:c5:f1:00:ec:c7" with the fingerprint of the key you want to fetch -let key = hetzner.get_ssh_key("13:dc:a2:1e:a9:d2:1d:a9:39:f4:44:c5:f1:00:ec:c7"); -print(key); +// let key = hetzner.get_ssh_key("13:dc:a2:1e:a9:d2:1d:a9:39:f4:44:c5:f1:00:ec:c7"); +// print(key); // Add a new SSH key // Replace "my-new-key" with the desired name and "ssh-rsa ..." with your public key data -let new_key = hetzner.add_ssh_key("my-new-key", "ssh-rsa ..."); -print(new_key); +// let new_key = hetzner.add_ssh_key("vanheesm@incubaid.com", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFyZJCEsvRc0eitsOoq+ywC5Lmqejvk3hXMVbO0AxPrd"); +// print(new_key); // Update an SSH key's name // Replace "cb:8b:ef:a7:fe:04:87:3f:e5:55:cd:12:e3:e8:9f:99" with the fingerprint of the key you want to update -let updated_key = hetzner.update_ssh_key_name("cb:8b:ef:a7:fe:04:87:3f:e5:55:cd:12:e3:e8:9f:99", "my-updated-key-name"); -print(updated_key); +// let updated_key = hetzner.update_ssh_key_name("e0:73:80:26:80:46:f0:c8:bb:74:f4:d0:2d:10:2d:6f", "my-updated-key-name"); +// print(updated_key); // Delete an SSH key // Replace "cb:8b:ef:a7:fe:04:87:3f:e5:55:cd:12:e3:e8:9f:99" with the fingerprint of the key you want to delete -hetzner.delete_ssh_key("cb:8b:ef:a7:fe:04:87:3f:e5:55:cd:12:e3:e8:9f:99"); \ No newline at end of file +// hetzner.delete_ssh_key("e1:a7:27:ed:12:77:6a:4c:3a:cd:30:18:c4:f3:d0:88"); \ No newline at end of file diff --git a/src/api/error.rs b/src/api/error.rs index 1d51289..818830b 100644 --- a/src/api/error.rs +++ b/src/api/error.rs @@ -1,12 +1,54 @@ -use crate::api::models::ApiError; +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:?}")] + #[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/src/api/mod.rs b/src/api/mod.rs index 51de4e6..1762845 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,15 +1,14 @@ pub mod error; pub mod models; -use std::any::type_name; - -use self::models::{ - Boot, Rescue, Server, SshKey, +use self::models::{Boot, Rescue, Server, SshKey}; +use crate::api::error::ApiError; +use crate::api::models::{ + BootWrapper, Cancellation, CancellationWrapper, RescueWrapped, ServerWrapper, SshKeyWrapper, }; use crate::config::Config; use error::AppError; use reqwest::blocking::Client as HttpClient; -use reqwest::StatusCode; #[derive(Clone)] pub struct Client { @@ -32,13 +31,10 @@ impl Client { let status = response.status(); let body = response.text()?; - println!("RESPONSE: \n{}", &body); - println!("Type of T to handle_response: {:#?}", type_name::()); - - if status == StatusCode::OK { + if status.is_success() { serde_json::from_str::(&body).map_err(Into::into) } else { - Err(AppError::ApiError(models::ApiError { + Err(AppError::ApiError(ApiError { status: status.as_u16(), message: body, })) @@ -48,14 +44,12 @@ impl Client { pub fn get_server(&self, server_number: i32) -> Result { let response = self .http_client - .get(format!( - "{}/server/{}", - self.config.api_url, server_number - )) + .get(format!("{}/server/{}", self.config.api_url, server_number)) .basic_auth(&self.config.username, Some(&self.config.password)) .send()?; - self.handle_response(response) + let wrapped: ServerWrapper = self.handle_response(response)?; + Ok(wrapped.server) } pub fn get_servers(&self) -> Result, AppError> { @@ -65,16 +59,80 @@ impl Client { .basic_auth(&self.config.username, Some(&self.config.password)) .send()?; - self.handle_response(response) + 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()?; - - self.handle_response(response) + + 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 { @@ -83,8 +141,9 @@ impl Client { .get(format!("{}/key/{}", self.config.api_url, fingerprint)) .basic_auth(&self.config.username, Some(&self.config.password)) .send()?; - - self.handle_response(response) + + let wrapped: SshKeyWrapper = self.handle_response(response)?; + Ok(wrapped.key) } pub fn add_ssh_key(&self, name: &str, data: &str) -> Result { @@ -95,15 +154,12 @@ impl Client { .basic_auth(&self.config.username, Some(&self.config.password)) .form(¶ms) .send()?; - - self.handle_response(response) + + let wrapped: SshKeyWrapper = self.handle_response(response)?; + Ok(wrapped.key) } - pub fn update_ssh_key_name( - &self, - fingerprint: &str, - name: &str, - ) -> Result { + pub fn update_ssh_key_name(&self, fingerprint: &str, name: &str) -> Result { let params = [("name", name)]; let response = self .http_client @@ -111,8 +167,9 @@ impl Client { .basic_auth(&self.config.username, Some(&self.config.password)) .form(¶ms) .send()?; - - self.handle_response(response) + + let wrapped: SshKeyWrapper = self.handle_response(response)?; + Ok(wrapped.key) } pub fn delete_ssh_key(&self, fingerprint: &str) -> Result<(), AppError> { @@ -129,14 +186,12 @@ impl Client { .get(format!("{}/boot/{}", self.config.api_url, server_number)) .basic_auth(&self.config.username, Some(&self.config.password)) .send()?; - - self.handle_response(response) + + let wrapped: BootWrapper = self.handle_response(response)?; + Ok(wrapped.boot) } - pub fn get_rescue_boot_configuration( - &self, - server_number: i32, - ) -> Result { + pub fn get_rescue_boot_configuration(&self, server_number: i32) -> Result { let response = self .http_client .get(format!( @@ -146,7 +201,8 @@ impl Client { .basic_auth(&self.config.username, Some(&self.config.password)) .send()?; - self.handle_response(response) + let wrapped: RescueWrapped = self.handle_response(response)?; + Ok(wrapped.rescue) } pub fn enable_rescue_mode( @@ -170,8 +226,9 @@ impl Client { .basic_auth(&self.config.username, Some(&self.config.password)) .form(¶ms) .send()?; - - self.handle_response(response) + + let wrapped: RescueWrapped = self.handle_response(response)?; + Ok(wrapped.rescue) } pub fn disable_rescue_mode(&self, server_number: i32) -> Result { @@ -183,7 +240,8 @@ impl Client { )) .basic_auth(&self.config.username, Some(&self.config.password)) .send()?; - - self.handle_response(response) + + let wrapped: RescueWrapped = self.handle_response(response)?; + Ok(wrapped.rescue) } -} \ No newline at end of file +} diff --git a/src/api/models.rs b/src/api/models.rs index 4297f9f..8a7bb5d 100644 --- a/src/api/models.rs +++ b/src/api/models.rs @@ -88,6 +88,10 @@ impl fmt::Display for Server { } } +#[derive(Deserialize)] +struct SubnetWrapper { + _subnet: Subnet, +} #[derive(Debug, Deserialize, Clone, CustomType)] #[rhai_type(extra = Self::build_rhai_type)] @@ -113,6 +117,11 @@ impl fmt::Display for Subnet { } } +#[derive(Deserialize)] +pub struct SshKeyWrapper { + pub key: SshKey, +} + #[derive(Debug, Deserialize, Clone, CustomType)] #[rhai_type(extra = Self::build_rhai_type)] pub struct SshKey { @@ -211,6 +220,11 @@ impl fmt::Display for Boot { } } +#[derive(Deserialize)] +pub struct RescueWrapped { + pub rescue: Rescue, +} + #[derive(Debug, Deserialize, Clone, CustomType)] #[rhai_type(extra = Self::build_rhai_type)] pub struct Rescue { @@ -488,6 +502,91 @@ impl fmt::Display for Cpanel { } } +#[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) + } +} + fn string_or_seq_string<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, diff --git a/src/scripting/mod.rs b/src/scripting/mod.rs index 71fda3f..c2ef05b 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}; +use crate::api::models::{Rescue, Linux, Vnc, Windows, Plesk, Cpanel, Boot, Server, SshKey, Cancellation}; use rhai::{Engine, Scope}; pub mod server; @@ -20,6 +20,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/printing/mod.rs b/src/scripting/printing/mod.rs index 0c793c2..4f4a0fb 100644 --- a/src/scripting/printing/mod.rs +++ b/src/scripting/printing/mod.rs @@ -5,6 +5,7 @@ mod servers_table; mod ssh_keys_table; pub fn pretty_print_dispatch(array: Array) { + println!("pretty print dispatch"); if array.is_empty() { println!(""); return; diff --git a/src/scripting/server.rs b/src/scripting/server.rs index b217e02..da22519 100644 --- a/src/scripting/server.rs +++ b/src/scripting/server.rs @@ -1,6 +1,5 @@ -use crate::api::{models::Server, Client}; -use prettytable::{row, Table}; -use rhai::{plugin::*, Array, Dynamic}; +use crate::api::{Client, models::Server}; +use rhai::{Array, Dynamic, plugin::*}; pub fn register(engine: &mut Engine) { let server_module = exported_module!(server_api); @@ -9,6 +8,8 @@ pub fn register(engine: &mut Engine) { #[export_module] pub mod server_api { + use crate::api::models::Cancellation; + use super::*; use rhai::EvalAltResult; @@ -30,4 +31,46 @@ pub mod server_api { Ok(servers.into_iter().map(Dynamic::from).collect()) } -} \ No newline at end of file + #[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, + cancellation_date: &str, + ) -> Result<(), Box> { + client + .withdraw_cancellation(server_number as i32) + .map_err(|e| e.to_string().into()) + } +}