added functionality to list different images

This commit is contained in:
Maxime Van Hees 2025-07-17 13:38:16 +02:00
parent 6f12a1bf09
commit 45b561fadf
6 changed files with 275 additions and 5 deletions

1
Cargo.lock generated
View File

@ -467,6 +467,7 @@ dependencies = [
"reqwest",
"rhai",
"serde",
"serde_json",
"tokio",
]

View File

@ -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"

View File

@ -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))

View File

@ -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)
}
}

View File

@ -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(

View File

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