added functionality to list different images
This commit is contained in:
		
							
								
								
									
										1
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -467,6 +467,7 @@ dependencies = [ | ||||
|  "reqwest", | ||||
|  "rhai", | ||||
|  "serde", | ||||
|  "serde_json", | ||||
|  "tokio", | ||||
| ] | ||||
|  | ||||
|   | ||||
| @@ -12,3 +12,4 @@ tokio = { version = "1.46.1", features = ["full"] } | ||||
| ping = "0.6.1" | ||||
| prettytable-rs = "0.10.0" | ||||
| serde = "1.0.219" | ||||
| serde_json = "1.0.122" | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| use crate::hetzner_api::{ | ||||
|     HetznerClient, ServerBuilder, WrappedCreateServerResponse, WrappedServer, WrappedSshKey, | ||||
|     ListImagesParamsBuilder, HetznerClient, ServerBuilder, WrappedCreateServerResponse, | ||||
|     WrappedServer, WrappedSshKey, WrappedImage, | ||||
| }; | ||||
| use std::sync::mpsc::{Receiver, Sender}; | ||||
| use tokio::runtime::Builder; | ||||
| @@ -16,6 +17,7 @@ pub enum Request { | ||||
|     EnableRescueModeWithAllKeys(HetznerClient, i64), | ||||
|     DisableRescueMode(HetznerClient, i64), | ||||
|     ListSshKeys(HetznerClient), | ||||
|     ListImages(HetznerClient, ListImagesParamsBuilder), | ||||
| } | ||||
|  | ||||
| pub enum Response { | ||||
| @@ -28,6 +30,7 @@ pub enum Response { | ||||
|     ResetServer(Result<(), String>), | ||||
|     EnableRescueMode(Result<String, String>), | ||||
|     DisableRescueMode(Result<(), String>), | ||||
|     ListImages(Result<Vec<WrappedImage>, String>), | ||||
| } | ||||
|  | ||||
| pub fn run_worker( | ||||
| @@ -42,6 +45,12 @@ pub fn run_worker( | ||||
|  | ||||
|         while let Ok(request) = command_rx.recv() { | ||||
|             let response = match request { | ||||
|                 Request::ListImages(client, builder) => { | ||||
|                     let result = rt | ||||
|                         .block_on(client.list_images(builder)) | ||||
|                         .map_err(|e| e.to_string()); | ||||
|                     Response::ListImages(result) | ||||
|                 } | ||||
|                 Request::CreateServer(client, builder) => { | ||||
|                     let result = rt | ||||
|                         .block_on(client.create_server(builder)) | ||||
|   | ||||
| @@ -1,15 +1,18 @@ | ||||
| use hcloud::apis::{ | ||||
|     configuration::Configuration, | ||||
|     images_api::{self, ListImagesParams}, | ||||
|     servers_api::{ | ||||
|         self, DisableRescueModeForServerParams, EnableRescueModeForServerParams, ListServersParams, | ||||
|         ResetServerParams, CreateServerParams, | ||||
|         self, CreateServerParams, DisableRescueModeForServerParams, | ||||
|         EnableRescueModeForServerParams, ListServersParams, ResetServerParams, | ||||
|     }, | ||||
|     ssh_keys_api::{self, ListSshKeysParams}, | ||||
| }; | ||||
| use hcloud::models::{ | ||||
|     CreateServerRequest, CreateServerResponse, EnableRescueModeForServerRequest, Server, SshKey, | ||||
|     CreateServerRequest, CreateServerResponse, EnableRescueModeForServerRequest, Image, Server, | ||||
|     SshKey, | ||||
| }; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use serde_json::Value; | ||||
|  | ||||
| #[derive(Debug, Clone)] | ||||
| pub struct HetznerClient { | ||||
| @@ -22,6 +25,9 @@ pub struct WrappedServer(pub Server); | ||||
| #[derive(Clone)] | ||||
| pub struct WrappedSshKey(pub SshKey); | ||||
|  | ||||
| #[derive(Clone)] | ||||
| pub struct WrappedImage(pub Image); | ||||
|  | ||||
| #[derive(Clone, Debug, Serialize, Deserialize)] | ||||
| pub struct ServerBuilder { | ||||
|     pub name: String, | ||||
| @@ -93,6 +99,79 @@ impl ServerBuilder { | ||||
| #[derive(Clone)] | ||||
| pub struct WrappedCreateServerResponse(pub CreateServerResponse); | ||||
|  | ||||
| #[derive(Clone, Debug, Serialize, Deserialize, Default)] | ||||
| pub struct ListImagesParamsBuilder { | ||||
|     pub sort: Option<String>, | ||||
|     pub r#type: Option<String>, | ||||
|     pub status: Option<String>, | ||||
|     pub bound_to: Option<String>, | ||||
|     pub include_deprecated: Option<bool>, | ||||
|     pub name: Option<String>, | ||||
|     pub label_selector: Option<String>, | ||||
|     pub architecture: Option<String>, | ||||
| } | ||||
|  | ||||
| impl ListImagesParamsBuilder { | ||||
|     pub fn new() -> Self { | ||||
|         Default::default() | ||||
|     } | ||||
|  | ||||
|     pub fn with_sort(mut self, sort: String) -> Self { | ||||
|         self.sort = Some(sort); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub fn with_type(mut self, r#type: String) -> Self { | ||||
|         self.r#type = Some(r#type); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub fn with_status(mut self, status: String) -> Self { | ||||
|         self.status = Some(status); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub fn with_bound_to(mut self, bound_to: String) -> Self { | ||||
|         self.bound_to = Some(bound_to); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub fn with_include_deprecated(mut self, include_deprecated: bool) -> Self { | ||||
|         self.include_deprecated = Some(include_deprecated); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub fn with_name(mut self, name: String) -> Self { | ||||
|         self.name = Some(name); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub fn with_label_selector(mut self, label_selector: String) -> Self { | ||||
|         self.label_selector = Some(label_selector); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub fn with_architecture(mut self, architecture: String) -> Self { | ||||
|         self.architecture = Some(architecture); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     fn build(self, page: i64, per_page: i64) -> ListImagesParams { | ||||
|         ListImagesParams { | ||||
|             sort: self.sort, | ||||
|             r#type: self.r#type, | ||||
|             status: self.status, | ||||
|             bound_to: self.bound_to, | ||||
|             include_deprecated: self.include_deprecated, | ||||
|             name: self.name, | ||||
|             label_selector: self.label_selector, | ||||
|             architecture: self.architecture, | ||||
|             page: Some(page), | ||||
|             per_page: Some(per_page), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl HetznerClient { | ||||
|     pub fn new(api_token: &str) -> Self { | ||||
|         let mut configuration = Configuration::new(); | ||||
| @@ -250,4 +329,98 @@ impl HetznerClient { | ||||
|  | ||||
|         Ok(all_keys) | ||||
|     } | ||||
|     // This function manually calls the Hetzner Cloud API to work around an issue in the | ||||
|     // `hcloud` crate where the `OsFlavor` enum is missing the `opensuse` variant. | ||||
|     // By parsing the JSON response ourselves, we can gracefully skip any images with | ||||
|     // unrecognized OS flavors, preventing deserialization errors. | ||||
|     pub async fn list_images( | ||||
|         &self, | ||||
|         builder: ListImagesParamsBuilder, | ||||
|     ) -> Result<Vec<WrappedImage>, Box<dyn std::error::Error>> { | ||||
|         let mut all_images = Vec::new(); | ||||
|         let mut page = 1; | ||||
|         let per_page = 50; | ||||
|      | ||||
|         loop { | ||||
|             let mut url = "https://api.hetzner.cloud/v1/images".to_string(); | ||||
|             let mut query_params = Vec::new(); | ||||
|      | ||||
|             if let Some(sort) = &builder.sort { | ||||
|                 query_params.push(format!("sort={}", sort)); | ||||
|             } | ||||
|             if let Some(r#type) = &builder.r#type { | ||||
|                 query_params.push(format!("type={}", r#type)); | ||||
|             } | ||||
|             if let Some(status) = &builder.status { | ||||
|                 query_params.push(format!("status={}", status)); | ||||
|             } | ||||
|             if let Some(bound_to) = &builder.bound_to { | ||||
|                 query_params.push(format!("bound_to={}", bound_to)); | ||||
|             } | ||||
|             if let Some(include_deprecated) = builder.include_deprecated { | ||||
|                 query_params.push(format!("include_deprecated={}", include_deprecated)); | ||||
|             } | ||||
|             if let Some(name) = &builder.name { | ||||
|                 query_params.push(format!("name={}", name)); | ||||
|             } | ||||
|             if let Some(label_selector) = &builder.label_selector { | ||||
|                 query_params.push(format!("label_selector={}", label_selector)); | ||||
|             } | ||||
|             if let Some(architecture) = &builder.architecture { | ||||
|                 query_params.push(format!("architecture={}", architecture)); | ||||
|             } | ||||
|      | ||||
|             query_params.push(format!("page={}", page)); | ||||
|             query_params.push(format!("per_page={}", per_page)); | ||||
|      | ||||
|             if !query_params.is_empty() { | ||||
|                 url.push('?'); | ||||
|                 url.push_str(&query_params.join("&")); | ||||
|             } | ||||
|      | ||||
|             let response: Value = self | ||||
|                 .configuration | ||||
|                 .client | ||||
|                 .get(&url) | ||||
|                 .bearer_auth(self.configuration.bearer_access_token.as_ref().unwrap()) | ||||
|                 .send() | ||||
|                 .await? | ||||
|                 .json() | ||||
|                 .await?; | ||||
|      | ||||
|             if let Some(images_json) = response.get("images").and_then(|i| i.as_array()) { | ||||
|                 if images_json.is_empty() { | ||||
|                     break; | ||||
|                 } | ||||
|      | ||||
|                 let images: Vec<WrappedImage> = images_json | ||||
|                     .iter() | ||||
|                     .filter_map(|image_value| { | ||||
|                         match serde_json::from_value::<Image>(image_value.clone()) { | ||||
|                             Ok(image) => Some(WrappedImage(image)), | ||||
|                             Err(_) => None, // Silently ignore images that can't be deserialized | ||||
|                         } | ||||
|                     }) | ||||
|                     .collect(); | ||||
|      | ||||
|                 all_images.extend(images); | ||||
|             } else { | ||||
|                 break; | ||||
|             } | ||||
|      | ||||
|             if response | ||||
|                 .get("meta") | ||||
|                 .and_then(|m| m.get("pagination")) | ||||
|                 .and_then(|p| p.get("next_page")) | ||||
|                 .and_then(|np| np.as_null()) | ||||
|                 .is_some() | ||||
|             { | ||||
|                 break; | ||||
|             } | ||||
|      | ||||
|             page += 1; | ||||
|         } | ||||
|      | ||||
|         Ok(all_images) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,7 +1,8 @@ | ||||
| use crate::async_handler::Request; | ||||
| use crate::async_handler::Response; | ||||
| use crate::hetzner_api::{ | ||||
|     HetznerClient, ServerBuilder, WrappedCreateServerResponse, WrappedServer, WrappedSshKey, | ||||
|     HetznerClient, ListImagesParamsBuilder, ServerBuilder, WrappedCreateServerResponse, | ||||
|     WrappedImage, WrappedServer, WrappedSshKey, | ||||
| }; | ||||
| use prettytable::{Cell, Row, Table}; | ||||
| use rhai::{Engine, EvalAltResult}; | ||||
| @@ -119,6 +120,83 @@ pub fn register_hetzner_api( | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|     engine | ||||
|         .register_type_with_name::<ListImagesParamsBuilder>("ListImagesParamsBuilder") | ||||
|         .register_fn("new_list_images_params_builder", ListImagesParamsBuilder::new) | ||||
|         .register_fn("with_sort", ListImagesParamsBuilder::with_sort) | ||||
|         .register_fn("with_type", ListImagesParamsBuilder::with_type) | ||||
|         .register_fn("with_status", ListImagesParamsBuilder::with_status) | ||||
|         .register_fn("with_bound_to", ListImagesParamsBuilder::with_bound_to) | ||||
|         .register_fn( | ||||
|             "with_include_deprecated", | ||||
|             ListImagesParamsBuilder::with_include_deprecated, | ||||
|         ) | ||||
|         .register_fn("with_name", ListImagesParamsBuilder::with_name) | ||||
|         .register_fn( | ||||
|             "with_label_selector", | ||||
|             ListImagesParamsBuilder::with_label_selector, | ||||
|         ) | ||||
|         .register_fn("with_architecture", ListImagesParamsBuilder::with_architecture); | ||||
|  | ||||
|     engine | ||||
|         .register_fn("list_images", { | ||||
|             let bridge = api_bridge.clone(); | ||||
|             move |client: &mut HetznerClient, builder: ListImagesParamsBuilder| { | ||||
|                 bridge.call(Request::ListImages(client.clone(), builder), |response| { | ||||
|                     match response { | ||||
|                         Response::ListImages(result) => result.map_err(|e| e.into()), | ||||
|                         _ => Err("Unexpected response".into()), | ||||
|                     } | ||||
|                 }) | ||||
|             } | ||||
|         }) | ||||
|         .register_type_with_name::<WrappedImage>("Image") | ||||
|         .register_get("id", |image: &mut WrappedImage| image.0.id) | ||||
|         .register_get("name", |image: &mut WrappedImage| image.0.name.clone()) | ||||
|         .register_get("description", |image: &mut WrappedImage| { | ||||
|             image.0.description.clone() | ||||
|         }) | ||||
|         .register_get("status", |image: &mut WrappedImage| { | ||||
|             format!("{:?}", image.0.status) | ||||
|         }) | ||||
|         .register_get("type", |image: &mut WrappedImage| format!("{:?}", image.0.r#type)) | ||||
|         .register_get("created", |image: &mut WrappedImage| image.0.created.clone()) | ||||
|         .register_get("os_flavor", |image: &mut WrappedImage| { | ||||
|             format!("{:?}", image.0.os_flavor) | ||||
|         }) | ||||
|         .register_get("os_version", |image: &mut WrappedImage| { | ||||
|             image.0.os_version.clone() | ||||
|         }); | ||||
|  | ||||
|     engine | ||||
|         .register_iterator::<Vec<WrappedImage>>() | ||||
|         .register_fn( | ||||
|             "show_table", | ||||
|             |images: &mut Vec<WrappedImage>| -> Result<String, Box<EvalAltResult>> { | ||||
|                 let mut table = Table::new(); | ||||
|                 table.set_titles(Row::new(vec![Cell::new("Images").style_spec("c")])); | ||||
|                 table.add_row(Row::new(vec![ | ||||
|                     Cell::new("ID"), | ||||
|                     Cell::new("Name"), | ||||
|                     Cell::new("Description"), | ||||
|                     Cell::new("Type"), | ||||
|                     Cell::new("OS Flavor"), | ||||
|                     Cell::new("OS Version"), | ||||
|                 ])); | ||||
|                 for image in images { | ||||
|                     table.add_row(Row::new(vec![ | ||||
|                         Cell::new(&image.0.id.to_string()), | ||||
|                         Cell::new(&image.0.name.clone().unwrap_or("".to_string())), | ||||
|                         Cell::new(&image.0.description), | ||||
|                         Cell::new(&format!("{:?}", image.0.r#type)), | ||||
|                         Cell::new(&format!("{:?}", image.0.os_flavor)), | ||||
|                         Cell::new(&image.0.os_version.clone().unwrap_or("".to_string())), | ||||
|                     ])); | ||||
|                 } | ||||
|                 Ok(table.to_string()) | ||||
|             }, | ||||
|         ); | ||||
|  | ||||
|     engine | ||||
|         .register_type_with_name::<ServerBuilder>("ServerBuilder") | ||||
|         .register_fn( | ||||
|   | ||||
| @@ -5,11 +5,19 @@ let client = new_hetzner_client(HETZNER_API_TOKEN); | ||||
| print("Listing all servers..."); | ||||
| let servers = client.list_servers(); | ||||
| print(servers.show_table()); | ||||
|  | ||||
| // List all SSH keys and print in table | ||||
| print("Listing all SSH keys..."); | ||||
| let ssh_keys = client.list_ssh_keys(); | ||||
| print(ssh_keys.show_table()); | ||||
|  | ||||
| // List all images | ||||
| let params = new_list_images_params_builder() | ||||
|     // .with_type("snapshot") | ||||
|     .with_status("available"); | ||||
| let images = client.list_images(params); | ||||
| print(images.show_table()); | ||||
|  | ||||
| // Get server through ID and print details in table | ||||
| print("Listing details from server with ID 104301883..."); | ||||
| let test_server = client.get_server(104301883); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user