implemented list servers + ping + reboot + example shown in example.rhai

This commit is contained in:
Maxime Van Hees 2025-07-14 18:16:30 +02:00
commit bf3bcba164
10 changed files with 2706 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

2280
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

12
Cargo.toml Normal file
View File

@ -0,0 +1,12 @@
[package]
name = "hetzner_rhai"
version = "0.1.0"
edition = "2024"
[dependencies]
hcloud = "0.21.0"
reqwest = "0.12.22"
rhai = { version = "1.22.2", features = ["sync"] }
tokio = { version = "1.46.1", features = ["full"] }
ping = "0.6.1"

37
example.rhai Normal file
View File

@ -0,0 +1,37 @@
let client = new_hetzner_client(HETZNER_API_TOKEN);
try {
print("Listing servers...");
let servers = client.list_servers();
if servers.len() == 0 {
print("No servers found.");
} else {
for server in servers {
print(`Server: ${server.name} (${server.id}), Status: ${server.status}`);
}
let first_server = servers[0];
print(`Getting details for server: ${first_server.name}`);
let detailed_server = client.get_server(first_server.id);
print(detailed_server.show_details());
print(`Pinging server ${detailed_server.name}...`);
let is_online = detailed_server.ping();
if is_online {
print("Server is online.");
} else {
print("Server is offline.");
}
// To reboot the server, uncomment the following lines:
// print("\nAttempting to reboot the server...");
// try {
// first_server.reboot(client);
// print("Reboot command sent successfully.");
// } catch(e) {
// print(`Error during reboot: ${e}`);
// }
}
} catch (e) {
print(`An error occurred: ${e}`);
}

60
src/async_handler.rs Normal file
View File

@ -0,0 +1,60 @@
use crate::hetzner_api::{HetznerClient, WrappedServer};
use crate::ping::ping_server;
use std::net::IpAddr;
use std::sync::mpsc::{Receiver, Sender};
use tokio::runtime::Builder;
#[derive(Clone)]
pub enum Request {
ListServers(HetznerClient),
GetServerStatus(HetznerClient, i64),
GetServer(HetznerClient, i64),
RebootServer(HetznerClient, i64),
PingServer(IpAddr),
}
pub enum Response {
ListServers(Result<Vec<WrappedServer>, String>),
GetServerStatus(Result<String, String>),
GetServer(Result<WrappedServer, String>),
RebootServer(Result<(), String>),
PingServer(Result<bool, String>),
}
pub fn run_worker(
command_rx: Receiver<Request>,
reply_tx: Sender<Response>,
) {
std::thread::spawn(move || {
let rt = Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
while let Ok(request) = command_rx.recv() {
let response = match request {
Request::ListServers(client) => {
let result = rt.block_on(client.list_servers()).map_err(|e| e.to_string());
Response::ListServers(result)
}
Request::GetServerStatus(client, server_id) => {
let result = rt.block_on(client.get_server_status(server_id)).map_err(|e| e.to_string());
Response::GetServerStatus(result)
}
Request::GetServer(client, server_id) => {
let result = rt.block_on(client.get_server(server_id)).map_err(|e| e.to_string());
Response::GetServer(result)
}
Request::RebootServer(client, server_id) => {
let result = rt.block_on(client.reboot_server(server_id)).map_err(|e| e.to_string());
Response::RebootServer(result)
}
Request::PingServer(ip) => {
let result = ping_server(ip).map_err(|e| e.to_string());
Response::PingServer(result)
}
};
reply_tx.send(response).expect("Failed to send response");
}
});
}

0
src/error.rs Normal file
View File

72
src/hetzner_api.rs Normal file
View File

@ -0,0 +1,72 @@
use hcloud::apis::{configuration::Configuration, servers_api};
use hcloud::models::Server;
#[derive(Debug, Clone)]
pub struct HetznerClient {
pub configuration: Configuration,
}
#[derive(Clone)]
pub struct WrappedServer(pub Server);
impl HetznerClient {
pub fn new(api_token: &str) -> Self {
let mut configuration = Configuration::new();
configuration.bearer_access_token = Some(api_token.to_string());
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(5))
.build()
.unwrap();
configuration.client = client;
Self { configuration }
}
pub async fn list_servers(&self) -> Result<Vec<WrappedServer>, Box<dyn std::error::Error>> {
let servers = servers_api::list_servers(&self.configuration, Default::default())
.await?
.servers
.into_iter()
.map(WrappedServer)
.collect();
Ok(servers)
}
pub async fn get_server_status(&self, server_id: i64) -> Result<String, Box<dyn std::error::Error>> {
let params = servers_api::GetServerParams {
id: server_id,
};
let server_response = servers_api::get_server(&self.configuration, params)
.await?;
if let Some(server) = server_response.server {
Ok(format!("{:?}", server.status))
} else {
Err(Box::from(format!("Server with id {} not found", server_id)))
}
}
pub async fn get_server(&self, server_id: i64) -> Result<WrappedServer, Box<dyn std::error::Error>> {
let params = servers_api::GetServerParams {
id: server_id,
};
let server_response = servers_api::get_server(&self.configuration, params)
.await?;
if let Some(server) = server_response.server {
Ok(WrappedServer(*server))
} else {
Err(Box::from(format!("Server with id {} not found", server_id)))
}
}
pub async fn reboot_server(&self, server_id: i64) -> Result<(), Box<dyn std::error::Error>> {
let params = servers_api::SoftRebootServerParams {
id: server_id,
};
servers_api::soft_reboot_server(&self.configuration, params).await?;
Ok(())
}
}

50
src/main.rs Normal file
View File

@ -0,0 +1,50 @@
mod error;
mod hetzner_api;
mod rhai_api;
mod async_handler;
mod ping;
use crate::rhai_api::register_hetzner_api;
use rhai::{Engine, Scope};
use std::env;
use std::thread;
use std::sync::{Arc, Mutex, mpsc};
use rhai::EvalAltResult;
fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let (command_tx, command_rx) = mpsc::channel::<async_handler::Request>();
let (reply_tx, reply_rx) = mpsc::channel::<async_handler::Response>();
async_handler::run_worker(command_rx, reply_tx);
let rhai_thread = thread::spawn(move || -> Result<(), Box<EvalAltResult>> {
let reply_rx = Arc::new(Mutex::new(reply_rx));
let mut engine = Engine::new();
register_hetzner_api(&mut engine, command_tx, reply_rx);
let mut scope = Scope::new();
scope.push(
"HETZNER_API_TOKEN",
env::var("HETZNER_API_TOKEN").unwrap_or_else(|_| {
let args: Vec<String> = env::args().collect();
args.get(1).cloned().unwrap_or_default()
}),
);
let script = std::env::args().nth(2);
if let Some(s) = script {
engine.run_with_scope(&mut scope, &s)?;
} else {
engine.run_file_with_scope(&mut scope, "example.rhai".into())?;
}
Ok(())
});
if let Err(err) = rhai_thread.join().unwrap() {
eprintln!("Error in Rhai script: {}", *err);
}
Ok(())
}

16
src/ping.rs Normal file
View File

@ -0,0 +1,16 @@
use std::net::IpAddr;
use std::time::Duration;
pub fn ping_server(ip: IpAddr) -> Result<bool, Box<dyn std::error::Error>> {
match ping::new(ip)
.socket_type(ping::SocketType::DGRAM)
.timeout(Duration::from_secs(2))
.send()
{
Ok(_) => Ok(true),
Err(e) => {
eprintln!("Ping error: {}", e);
Ok(false)
}
}
}

178
src/rhai_api.rs Normal file
View File

@ -0,0 +1,178 @@
use crate::async_handler::Response;
use crate::async_handler::Request;
use crate::hetzner_api::{HetznerClient, WrappedServer};
use rhai::{Engine, EvalAltResult};
use rhai::EvalContext;
use std::sync::{Arc, Mutex, mpsc::{Sender, Receiver}};
pub fn register_hetzner_api(
engine: &mut Engine,
command_tx: Sender<Request>,
reply_rx: Arc<Mutex<Receiver<Response>>>,
) {
let list_servers_tx = command_tx.clone();
let list_servers_rx = reply_rx.clone();
let get_server_status_tx = command_tx.clone();
let get_server_status_rx = reply_rx.clone();
let get_server_tx = command_tx.clone();
let get_server_rx = reply_rx.clone();
let reboot_server_tx = command_tx.clone();
let reboot_server_rx = reply_rx.clone();
let ping_server_tx = command_tx.clone();
let ping_server_rx = reply_rx.clone();
engine
.register_type_with_name::<HetznerClient>("HetznerClient")
.register_fn("new_hetzner_client", HetznerClient::new)
.register_fn(
"list_servers",
move |client: &mut HetznerClient| -> Result<Vec<WrappedServer>, Box<EvalAltResult>> {
list_servers_tx.send(Request::ListServers(client.clone()))
.map_err(|e| e.to_string())?;
let response = list_servers_rx.lock().unwrap().recv()
.map_err(|e| e.to_string())?;
match response {
Response::ListServers(result) => result.map_err(|e| e.into()),
_ => Err("Unexpected response".into()),
}
},
)
.register_fn(
"get_server_status",
move |client: &mut HetznerClient,
server_id: i64|
-> Result<String, Box<EvalAltResult>> {
get_server_status_tx.send(Request::GetServerStatus(client.clone(), server_id))
.map_err(|e| e.to_string())?;
let response = get_server_status_rx.lock().unwrap().recv()
.map_err(|e| e.to_string())?;
match response {
Response::GetServerStatus(result) => result.map_err(|e| e.into()),
_ => Err("Unexpected response".into()),
}
},
)
.register_fn(
"get_server",
move |client: &mut HetznerClient,
server_id: i64|
-> Result<WrappedServer, Box<EvalAltResult>> {
get_server_tx.send(Request::GetServer(client.clone(), server_id))
.map_err(|e| e.to_string())?;
let response = get_server_rx.lock().unwrap().recv()
.map_err(|e| e.to_string())?;
match response {
Response::GetServer(result) => result.map_err(|e| e.into()),
_ => Err("Unexpected response".into()),
}
},
)
.register_type_with_name::<WrappedServer>("Server")
.register_get("id", |server: &mut WrappedServer| server.0.id)
.register_get("name", |server: &mut WrappedServer| {
server.0.name.clone()
})
.register_get("status", |server: &mut WrappedServer| {
format!("{:?}", server.0.status)
})
.register_get("created", |server: &mut WrappedServer| {
server.0.created.clone()
})
.register_get("public_ipv4", |server: &mut WrappedServer| {
server.0.public_net.ipv4.clone().unwrap().ip
})
.register_get("server_type", |server: &mut WrappedServer| {
server.0.server_type.clone().name
})
.register_get("included_traffic", |server: &mut WrappedServer| {
server.0.included_traffic.unwrap_or(0)
})
.register_get("ingoing_traffic", |server: &mut WrappedServer| {
server.0.ingoing_traffic.unwrap_or(0)
})
.register_get("outgoing_traffic", |server: &mut WrappedServer| {
server.0.outgoing_traffic.unwrap_or(0)
})
.register_get("primary_disk_size", |server: &mut WrappedServer| {
server.0.primary_disk_size
})
.register_get("rescue_enabled", |server: &mut WrappedServer| {
server.0.rescue_enabled
})
.register_fn(
"reboot",
move |server: &mut WrappedServer,
client: HetznerClient|
-> Result<(), Box<EvalAltResult>> {
reboot_server_tx
.send(Request::RebootServer(client, server.0.id))
.map_err(|e| e.to_string())?;
let response = reboot_server_rx
.lock()
.unwrap()
.recv()
.map_err(|e| e.to_string())?;
match response {
Response::RebootServer(result) => result.map_err(|e| e.into()),
_ => Err("Unexpected response".into()),
}
},
)
.register_iterator::<Vec<WrappedServer>>()
.register_fn("len", |list: &mut Vec<WrappedServer>| list.len() as i64)
.register_indexer_get(|list: &mut Vec<WrappedServer>, index: i64| list[index as usize].clone())
.register_fn(
"ping",
move |server: &mut WrappedServer| -> Result<bool, Box<EvalAltResult>> {
ping_server_tx
.send(Request::PingServer(
server
.0
.public_net
.ipv4
.clone()
.unwrap()
.ip
.parse()
.unwrap(),
))
.map_err(|e| e.to_string())?;
let response = ping_server_rx
.lock()
.unwrap()
.recv()
.map_err(|e| e.to_string())?;
match response {
Response::PingServer(result) => result.map_err(|e| e.into()),
_ => Err("Unexpected response".into()),
}
},
)
.register_fn(
"show_details",
|server: &mut WrappedServer| -> Result<String, Box<EvalAltResult>> {
let mut details = String::new();
details.push_str(&format!(" ID: {}\n", server.0.id));
details.push_str(&format!(" Status: {:?}\n", server.0.status));
details.push_str(&format!(" Created: {}\n", server.0.created));
details.push_str(&format!(" IPv4: {}\n", server.0.public_net.ipv4.clone().unwrap().ip));
details.push_str(&format!(" Type: {}\n", server.0.server_type.clone().name));
details.push_str(&format!(" Included Traffic: {} GB\n", server.0.included_traffic.unwrap_or(0) / 1024 / 1024 / 1024));
details.push_str(&format!(" Ingoing Traffic: {} MB\n", server.0.ingoing_traffic.unwrap_or(0) / 1024 / 1024));
details.push_str(&format!(" Outgoing Traffic: {} MB\n", server.0.outgoing_traffic.unwrap_or(0) / 1024 / 1024));
details.push_str(&format!(" Primary Disk Size: {} GB\n", server.0.primary_disk_size));
details.push_str(&format!(" Rescue Enabled: {}\n", server.0.rescue_enabled));
Ok(details)
},
);
}