implemented list servers + ping + reboot + example shown in example.rhai
This commit is contained in:
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| /target | ||||
							
								
								
									
										2280
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										2280
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										12
									
								
								Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								Cargo.toml
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										37
									
								
								example.rhai
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										60
									
								
								src/async_handler.rs
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										0
									
								
								src/error.rs
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										72
									
								
								src/hetzner_api.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								src/hetzner_api.rs
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										50
									
								
								src/main.rs
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										16
									
								
								src/ping.rs
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										178
									
								
								src/rhai_api.rs
									
									
									
									
									
										Normal 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) | ||||
|             }, | ||||
|         ); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user