added functionality to list different images
This commit is contained in:
parent
6f12a1bf09
commit
45b561fadf
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);
|
||||
|
Loading…
Reference in New Issue
Block a user