implemented additional server functions

This commit is contained in:
Maxime Van Hees 2025-07-18 15:40:42 +02:00
parent 6082ecc6d1
commit 84aa8cc11d
8 changed files with 319 additions and 63 deletions

View File

@ -1,9 +1,21 @@
// Get all servers and print them in a table // Get all servers and print them in a table
let servers = hetzner.get_servers(); // let servers = hetzner.get_servers();
print(servers); // servers.pretty_print();
servers.pretty_print();
// Get a specific server and print its details // // Get a specific server and print its details
// Replace 1825193 with the server number you want to fetch // // Replace 2550253 with the server number you want to fetch
let server = hetzner.get_server(1825193); // let server = hetzner.get_server(2550253);
print(server); // 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);

View File

@ -1,22 +1,22 @@
// Get all SSH keys and print them in a table // Get all SSH keys and print them in a table
let keys = hetzner.get_ssh_keys(); let keys = hetzner.get_ssh_keys();
print(keys); keys.pretty_print();
// Get a specific SSH key // 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 // 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"); // let key = hetzner.get_ssh_key("13:dc:a2:1e:a9:d2:1d:a9:39:f4:44:c5:f1:00:ec:c7");
print(key); // print(key);
// Add a new SSH key // Add a new SSH key
// Replace "my-new-key" with the desired name and "ssh-rsa ..." with your public key data // 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 ..."); // let new_key = hetzner.add_ssh_key("vanheesm@incubaid.com", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFyZJCEsvRc0eitsOoq+ywC5Lmqejvk3hXMVbO0AxPrd");
print(new_key); // print(new_key);
// Update an SSH key's name // 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 // 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"); // 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); // print(updated_key);
// Delete an SSH 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 // 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"); // hetzner.delete_ssh_key("e1:a7:27:ed:12:77:6a:4c:3a:cd:30:18:c4:f3:d0:88");

View File

@ -1,12 +1,54 @@
use crate::api::models::ApiError; use std::fmt;
use serde::Deserialize;
use thiserror::Error; use thiserror::Error;
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum AppError { pub enum AppError {
#[error("Request failed: {0}")] #[error("Request failed: {0}")]
RequestError(#[from] reqwest::Error), RequestError(#[from] reqwest::Error),
#[error("API error: {0:?}")] #[error("API error: {0}")]
ApiError(ApiError), ApiError(ApiError),
#[error("Deserialization Error: {0:?}")] #[error("Deserialization Error: {0:?}")]
SerdeJsonError(#[from] serde_json::Error), SerdeJsonError(#[from] serde_json::Error),
} }
#[derive(Debug, Deserialize)]
pub struct ApiError {
pub status: u16,
pub message: String,
}
impl From<reqwest::blocking::Response> 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::<HetznerApiErrorWrapper>(&self.message) {
write!(
f,
"Status: {}, Code: {}, Message: {}",
self.status, wrapper.error.code, wrapper.error.message
)
} else {
write!(f, "Status: {}: {}", self.status, self.message)
}
}
}

View File

@ -1,15 +1,14 @@
pub mod error; pub mod error;
pub mod models; pub mod models;
use std::any::type_name; use self::models::{Boot, Rescue, Server, SshKey};
use crate::api::error::ApiError;
use self::models::{ use crate::api::models::{
Boot, Rescue, Server, SshKey, BootWrapper, Cancellation, CancellationWrapper, RescueWrapped, ServerWrapper, SshKeyWrapper,
}; };
use crate::config::Config; use crate::config::Config;
use error::AppError; use error::AppError;
use reqwest::blocking::Client as HttpClient; use reqwest::blocking::Client as HttpClient;
use reqwest::StatusCode;
#[derive(Clone)] #[derive(Clone)]
pub struct Client { pub struct Client {
@ -32,13 +31,10 @@ impl Client {
let status = response.status(); let status = response.status();
let body = response.text()?; let body = response.text()?;
println!("RESPONSE: \n{}", &body); if status.is_success() {
println!("Type of T to handle_response: {:#?}", type_name::<T>());
if status == StatusCode::OK {
serde_json::from_str::<T>(&body).map_err(Into::into) serde_json::from_str::<T>(&body).map_err(Into::into)
} else { } else {
Err(AppError::ApiError(models::ApiError { Err(AppError::ApiError(ApiError {
status: status.as_u16(), status: status.as_u16(),
message: body, message: body,
})) }))
@ -48,14 +44,12 @@ impl Client {
pub fn get_server(&self, server_number: i32) -> Result<Server, AppError> { pub fn get_server(&self, server_number: i32) -> Result<Server, AppError> {
let response = self let response = self
.http_client .http_client
.get(format!( .get(format!("{}/server/{}", self.config.api_url, server_number))
"{}/server/{}",
self.config.api_url, server_number
))
.basic_auth(&self.config.username, Some(&self.config.password)) .basic_auth(&self.config.username, Some(&self.config.password))
.send()?; .send()?;
self.handle_response(response) let wrapped: ServerWrapper = self.handle_response(response)?;
Ok(wrapped.server)
} }
pub fn get_servers(&self) -> Result<Vec<Server>, AppError> { pub fn get_servers(&self) -> Result<Vec<Server>, AppError> {
@ -65,8 +59,70 @@ impl Client {
.basic_auth(&self.config.username, Some(&self.config.password)) .basic_auth(&self.config.username, Some(&self.config.password))
.send()?; .send()?;
self.handle_response(response) let wrapped: Vec<ServerWrapper> = 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<Server, AppError> {
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(&params)
.send()?;
let wrapped: ServerWrapper = self.handle_response(response)?;
Ok(wrapped.server)
}
pub fn get_cancellation_data(&self, server_number: i32) -> Result<Cancellation, AppError> {
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<Cancellation, AppError> {
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(&params)
.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<Vec<SshKey>, AppError> { pub fn get_ssh_keys(&self) -> Result<Vec<SshKey>, AppError> {
let response = self let response = self
.http_client .http_client
@ -74,7 +130,9 @@ impl Client {
.basic_auth(&self.config.username, Some(&self.config.password)) .basic_auth(&self.config.username, Some(&self.config.password))
.send()?; .send()?;
self.handle_response(response) let wrapped: Vec<SshKeyWrapper> = 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<SshKey, AppError> { pub fn get_ssh_key(&self, fingerprint: &str) -> Result<SshKey, AppError> {
@ -84,7 +142,8 @@ impl Client {
.basic_auth(&self.config.username, Some(&self.config.password)) .basic_auth(&self.config.username, Some(&self.config.password))
.send()?; .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<SshKey, AppError> { pub fn add_ssh_key(&self, name: &str, data: &str) -> Result<SshKey, AppError> {
@ -96,14 +155,11 @@ impl Client {
.form(&params) .form(&params)
.send()?; .send()?;
self.handle_response(response) let wrapped: SshKeyWrapper = self.handle_response(response)?;
Ok(wrapped.key)
} }
pub fn update_ssh_key_name( pub fn update_ssh_key_name(&self, fingerprint: &str, name: &str) -> Result<SshKey, AppError> {
&self,
fingerprint: &str,
name: &str,
) -> Result<SshKey, AppError> {
let params = [("name", name)]; let params = [("name", name)];
let response = self let response = self
.http_client .http_client
@ -112,7 +168,8 @@ impl Client {
.form(&params) .form(&params)
.send()?; .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> { pub fn delete_ssh_key(&self, fingerprint: &str) -> Result<(), AppError> {
@ -130,13 +187,11 @@ impl Client {
.basic_auth(&self.config.username, Some(&self.config.password)) .basic_auth(&self.config.username, Some(&self.config.password))
.send()?; .send()?;
self.handle_response(response) let wrapped: BootWrapper = self.handle_response(response)?;
Ok(wrapped.boot)
} }
pub fn get_rescue_boot_configuration( pub fn get_rescue_boot_configuration(&self, server_number: i32) -> Result<Rescue, AppError> {
&self,
server_number: i32,
) -> Result<Rescue, AppError> {
let response = self let response = self
.http_client .http_client
.get(format!( .get(format!(
@ -146,7 +201,8 @@ impl Client {
.basic_auth(&self.config.username, Some(&self.config.password)) .basic_auth(&self.config.username, Some(&self.config.password))
.send()?; .send()?;
self.handle_response(response) let wrapped: RescueWrapped = self.handle_response(response)?;
Ok(wrapped.rescue)
} }
pub fn enable_rescue_mode( pub fn enable_rescue_mode(
@ -171,7 +227,8 @@ impl Client {
.form(&params) .form(&params)
.send()?; .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<Rescue, AppError> { pub fn disable_rescue_mode(&self, server_number: i32) -> Result<Rescue, AppError> {
@ -184,6 +241,7 @@ impl Client {
.basic_auth(&self.config.username, Some(&self.config.password)) .basic_auth(&self.config.username, Some(&self.config.password))
.send()?; .send()?;
self.handle_response(response) let wrapped: RescueWrapped = self.handle_response(response)?;
Ok(wrapped.rescue)
} }
} }

View File

@ -88,6 +88,10 @@ impl fmt::Display for Server {
} }
} }
#[derive(Deserialize)]
struct SubnetWrapper {
_subnet: Subnet,
}
#[derive(Debug, Deserialize, Clone, CustomType)] #[derive(Debug, Deserialize, Clone, CustomType)]
#[rhai_type(extra = Self::build_rhai_type)] #[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)] #[derive(Debug, Deserialize, Clone, CustomType)]
#[rhai_type(extra = Self::build_rhai_type)] #[rhai_type(extra = Self::build_rhai_type)]
pub struct SshKey { 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)] #[derive(Debug, Deserialize, Clone, CustomType)]
#[rhai_type(extra = Self::build_rhai_type)] #[rhai_type(extra = Self::build_rhai_type)]
pub struct Rescue { 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<String>,
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<String>,
pub cancellation_reason: Vec<String>,
}
impl Cancellation {
fn build_rhai_type(builder: &mut TypeBuilder<Self>) {
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<Vec<String>, D::Error> fn string_or_seq_string<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where where
D: Deserializer<'de>, D: Deserializer<'de>,

View File

@ -1,5 +1,5 @@
use crate::api::Client; 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}; use rhai::{Engine, Scope};
pub mod server; pub mod server;
@ -20,6 +20,7 @@ pub fn setup_engine(client: Client) -> (Engine, Scope<'static>) {
engine.build_type::<Windows>(); engine.build_type::<Windows>();
engine.build_type::<Plesk>(); engine.build_type::<Plesk>();
engine.build_type::<Cpanel>(); engine.build_type::<Cpanel>();
engine.build_type::<Cancellation>();
server::register(&mut engine); server::register(&mut engine);
ssh_keys::register(&mut engine); ssh_keys::register(&mut engine);

View File

@ -5,6 +5,7 @@ mod servers_table;
mod ssh_keys_table; mod ssh_keys_table;
pub fn pretty_print_dispatch(array: Array) { pub fn pretty_print_dispatch(array: Array) {
println!("pretty print dispatch");
if array.is_empty() { if array.is_empty() {
println!("<empty table>"); println!("<empty table>");
return; return;

View File

@ -1,6 +1,5 @@
use crate::api::{models::Server, Client}; use crate::api::{Client, models::Server};
use prettytable::{row, Table}; use rhai::{Array, Dynamic, plugin::*};
use rhai::{plugin::*, Array, Dynamic};
pub fn register(engine: &mut Engine) { pub fn register(engine: &mut Engine) {
let server_module = exported_module!(server_api); let server_module = exported_module!(server_api);
@ -9,6 +8,8 @@ pub fn register(engine: &mut Engine) {
#[export_module] #[export_module]
pub mod server_api { pub mod server_api {
use crate::api::models::Cancellation;
use super::*; use super::*;
use rhai::EvalAltResult; use rhai::EvalAltResult;
@ -30,4 +31,46 @@ pub mod server_api {
Ok(servers.into_iter().map(Dynamic::from).collect()) 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<Server, Box<EvalAltResult>> {
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<Cancellation, Box<EvalAltResult>> {
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<Cancellation, Box<EvalAltResult>> {
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<EvalAltResult>> {
client
.withdraw_cancellation(server_number as i32)
.map_err(|e| e.to_string().into())
}
} }