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
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);
// // 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);

View File

@ -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");
// 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;
#[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<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 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::<T>());
if status == StatusCode::OK {
if status.is_success() {
serde_json::from_str::<T>(&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<Server, AppError> {
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<Vec<Server>, AppError> {
@ -65,16 +59,80 @@ impl Client {
.basic_auth(&self.config.username, Some(&self.config.password))
.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> {
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<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> {
@ -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<SshKey, AppError> {
@ -95,15 +154,12 @@ impl Client {
.basic_auth(&self.config.username, Some(&self.config.password))
.form(&params)
.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<SshKey, AppError> {
pub fn update_ssh_key_name(&self, fingerprint: &str, name: &str) -> Result<SshKey, AppError> {
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(&params)
.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<Rescue, AppError> {
pub fn get_rescue_boot_configuration(&self, server_number: i32) -> Result<Rescue, AppError> {
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(&params)
.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> {
@ -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)
}
}
}

View File

@ -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<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>
where
D: Deserializer<'de>,

View File

@ -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::<Windows>();
engine.build_type::<Plesk>();
engine.build_type::<Cpanel>();
engine.build_type::<Cancellation>();
server::register(&mut engine);
ssh_keys::register(&mut engine);

View File

@ -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!("<empty table>");
return;

View File

@ -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())
}
}
#[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())
}
}