feat: Add server rebuild example using install_image #1
							
								
								
									
										34
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								README.md
									
									
									
									
									
								
							@@ -30,9 +30,31 @@ cargo run -- examples/01_create_server.rhai
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
The `examples/` directory contains a collection of scripts demonstrating the available functionality. For detailed examples, please see the files in that directory:
 | 
					The `examples/` directory contains a collection of scripts demonstrating the available functionality. For detailed examples, please see the files in that directory:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-   [`examples/01_create_server.rhai`](examples/01_create_server.rhai): Shows how to create a new server with various configuration options using a builder pattern.
 | 
					- [`examples/01_create_server.rhai`](examples/01_create_server.rhai): Shows how to create a new server with various configuration options using a builder pattern.
 | 
				
			||||||
-   [`examples/02_list_servers.rhai`](examples/02_list_servers.rhai): Lists all servers in your project.
 | 
					- [`examples/02_list_servers.rhai`](examples/02_list_servers.rhai): Lists all servers in your project.
 | 
				
			||||||
-   [`examples/03_get_server_details.rhai`](examples/03_get_server_details.rhai): Fetches and displays detailed information for a single server.
 | 
					- [`examples/03_get_server_details.rhai`](examples/03_get_server_details.rhai): Fetches and displays detailed information for a single server.
 | 
				
			||||||
-   [`examples/04_server_actions.rhai`](examples/04_server_actions.rhai): Demonstrates how to reboot, reset, and manage rescue mode for a server.
 | 
					- [`examples/04_server_actions.rhai`](examples/04_server_actions.rhai): Demonstrates how to reboot, reset, and manage rescue mode for a server.
 | 
				
			||||||
-   [`examples/05_list_ssh_keys.rhai`](examples/05_list_ssh_keys.rhai): Lists all SSH keys in your project.
 | 
					- [`examples/05_list_ssh_keys.rhai`](examples/05_list_ssh_keys.rhai): Lists all SSH keys in your project.
 | 
				
			||||||
-   [`examples/06_list_images.rhai`](examples/06_list_images.rhai): Shows how to list system images and snapshots, with examples of filtering and sorting.
 | 
					- [`examples/06_list_images.rhai`](examples/06_list_images.rhai): Shows how to list system images and snapshots, with examples of filtering and sorting.
 | 
				
			||||||
 | 
					- [`examples/07_install_image.rhai`](examples/07_install_image.rhai): Demonstrates server rebuild functionality using install_image (equivalent to traditional installimage).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Features
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Server Management
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- **Create servers** with flexible configuration options
 | 
				
			||||||
 | 
					- **List and inspect** server details and status
 | 
				
			||||||
 | 
					- **Reboot and reset** servers
 | 
				
			||||||
 | 
					- **Rebuild servers** with new images (install_image functionality)
 | 
				
			||||||
 | 
					- **Rescue mode** management
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Image Management
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- **List images** with advanced filtering (by type, status, architecture, etc.)
 | 
				
			||||||
 | 
					- **Support for all image types**: system, backup, snapshot, and app images
 | 
				
			||||||
 | 
					- **Flexible image selection** by name or ID
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### SSH Key Management
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- **List SSH keys** in your project
 | 
				
			||||||
 | 
					- **Automatic SSH key assignment** during server creation
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										161
									
								
								examples/07_install_image.rhai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								examples/07_install_image.rhai
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,161 @@
 | 
				
			|||||||
 | 
					// This script demonstrates how to rebuild a server with a new image using install_image.
 | 
				
			||||||
 | 
					// The install_image function is the Hetzner Cloud equivalent of the traditional installimage command.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Initialize the Hetzner client with your API token.
 | 
				
			||||||
 | 
					let client = new_hetzner_client(get_env("HETZNER_API_TOKEN"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Replace this with the ID of the server you want to rebuild.
 | 
				
			||||||
 | 
					// WARNING: This will DESTROY ALL DATA on the target server!
 | 
				
			||||||
 | 
					let server_id = 1234567; // FIXME: Replace with a real server ID
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// The install_image function rebuilds a server by overwriting its disk with a new image.
 | 
				
			||||||
 | 
					// This is equivalent to the traditional installimage command used on dedicated servers.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Available image types:
 | 
				
			||||||
 | 
					// - system: Official OS images (e.g., "ubuntu-22.04", "debian-12")
 | 
				
			||||||
 | 
					// - backup: Automatic backups of your servers
 | 
				
			||||||
 | 
					// - snapshot: Manual snapshots you've created
 | 
				
			||||||
 | 
					// - app: Application images (if available)
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// The function accepts either an image name or image ID:
 | 
				
			||||||
 | 
					// client.install_image(server_id, "ubuntu-22.04");  // By name
 | 
				
			||||||
 | 
					// client.install_image(server_id, "15512617");      // By ID
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Get current server information before rebuilding.
 | 
				
			||||||
 | 
					print("Getting current server information...");
 | 
				
			||||||
 | 
					let server = client.get_server(server_id);
 | 
				
			||||||
 | 
					print(`Server: ${server.name} (ID: ${server.id})`);
 | 
				
			||||||
 | 
					print(`Current Status: ${server.status}`);
 | 
				
			||||||
 | 
					print(`Current Image: ${server.image.name}`);
 | 
				
			||||||
 | 
					print("");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// List available system images to choose from.
 | 
				
			||||||
 | 
					print("Available system images (first 5):");
 | 
				
			||||||
 | 
					let system_images_params = new_list_images_params_builder()
 | 
				
			||||||
 | 
					    .with_type("system")
 | 
				
			||||||
 | 
					    .with_status("available");
 | 
				
			||||||
 | 
					let system_images = client.list_images(system_images_params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					for i in 0..5 {
 | 
				
			||||||
 | 
					    if i < system_images.len() {
 | 
				
			||||||
 | 
					        let img = system_images[i];
 | 
				
			||||||
 | 
					        print(`  ${img.id}: ${img.name} (${img.type})`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					print("");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// List backup images for this server (if any).
 | 
				
			||||||
 | 
					print("Backup images for this server:");
 | 
				
			||||||
 | 
					let backup_images_params = new_list_images_params_builder()
 | 
				
			||||||
 | 
					    .with_type("backup")
 | 
				
			||||||
 | 
					    .with_bound_to(server_id.to_string());
 | 
				
			||||||
 | 
					let backup_images = client.list_images(backup_images_params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if backup_images.len() > 0 {
 | 
				
			||||||
 | 
					    for i in 0..backup_images.len() {
 | 
				
			||||||
 | 
					        let img = backup_images[i];
 | 
				
			||||||
 | 
					        print(`  ${img.id}: ${img.name} (${img.type})`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					} else {
 | 
				
			||||||
 | 
					    print("  No backup images found for this server");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					print("");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Rebuild Server with System Image ---
 | 
				
			||||||
 | 
					// WARNING: This will DESTROY ALL DATA on the server!
 | 
				
			||||||
 | 
					// The server will be automatically powered off before the rebuild.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Example 1: Rebuild with Ubuntu 22.04 (by name)
 | 
				
			||||||
 | 
					// WARNING: Uncomment the following lines to perform the actual rebuild
 | 
				
			||||||
 | 
					// print("Initiating server rebuild with Ubuntu 22.04...");
 | 
				
			||||||
 | 
					// client.install_image(server_id, "ubuntu-22.04");
 | 
				
			||||||
 | 
					// print("✅ Server rebuild request sent successfully!");
 | 
				
			||||||
 | 
					print("⚠️  install_image call is commented out for safety. Uncomment to execute.");
 | 
				
			||||||
 | 
					print("");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Example 2: Rebuild with specific image ID
 | 
				
			||||||
 | 
					// client.install_image(server_id, "15512617"); // Replace with actual image ID
 | 
				
			||||||
 | 
					// print("Server rebuild initiated with image ID 15512617");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Restore from Backup ---
 | 
				
			||||||
 | 
					// If backup images are available, you can restore from them:
 | 
				
			||||||
 | 
					if backup_images.len() > 0 {
 | 
				
			||||||
 | 
					    let backup_img = backup_images[0];
 | 
				
			||||||
 | 
					    // client.install_image(server_id, backup_img.id.to_string());
 | 
				
			||||||
 | 
					    // print(`Server restore initiated from backup: ${backup_img.name}`);
 | 
				
			||||||
 | 
					    // print(`Backup available: ${backup_img.name} (ID: ${backup_img.id})`);
 | 
				
			||||||
 | 
					} else {
 | 
				
			||||||
 | 
					    // print("No backup images available for this server");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Monitor Rebuild Progress ---
 | 
				
			||||||
 | 
					// The following section demonstrates how to monitor rebuild progress
 | 
				
			||||||
 | 
					// Uncomment when you uncomment the install_image call above
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					print("Monitoring server status during rebuild...");
 | 
				
			||||||
 | 
					print("Note: Server rebuild typically takes 1-3 minutes to complete.");
 | 
				
			||||||
 | 
					print("");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Poll server status every 5 seconds to monitor progress
 | 
				
			||||||
 | 
					let max_attempts = 60; // Maximum 5 minutes of polling
 | 
				
			||||||
 | 
					let attempt = 0;
 | 
				
			||||||
 | 
					let rebuild_seen = false;
 | 
				
			||||||
 | 
					let running_count = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					while attempt < max_attempts {
 | 
				
			||||||
 | 
					    let current_server = client.get_server(server_id);
 | 
				
			||||||
 | 
					    let status = current_server.status; // Use status directly
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    print(`Attempt ${attempt + 1}: Server status is '${status}'`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Check for rebuilding status (case-insensitive)
 | 
				
			||||||
 | 
					    if status == "Rebuilding" || status == "rebuilding" {
 | 
				
			||||||
 | 
					        rebuild_seen = true;
 | 
				
			||||||
 | 
					        print("   → Server is currently being rebuilt...");
 | 
				
			||||||
 | 
					    } else if status == "Off" || status == "off" {
 | 
				
			||||||
 | 
					        print("   → Server is powered off (normal during rebuild)");
 | 
				
			||||||
 | 
					    } else if status == "Running" || status == "running" {
 | 
				
			||||||
 | 
					        if rebuild_seen {
 | 
				
			||||||
 | 
					            print("");
 | 
				
			||||||
 | 
					            print("🎉 Server rebuild completed successfully!");
 | 
				
			||||||
 | 
					            print(`✅ Server is now running with image: ${current_server.image.name}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Verify the image changed
 | 
				
			||||||
 | 
					            if current_server.image.name == "ubuntu-22.04" || current_server.image.name == "Ubuntu 22.04" {
 | 
				
			||||||
 | 
					                print("✅ Image verification: Ubuntu 22.04 installation confirmed!");
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                print(`⚠️  Image verification: Expected 'ubuntu-22.04', got '${current_server.image.name}'`);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            break; // Exit immediately after rebuild completion
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            print("   → Server is running (waiting for rebuild to start...)");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        print(`   → Server status: ${status}`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    attempt = attempt + 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if attempt < max_attempts {
 | 
				
			||||||
 | 
					        print("   Waiting 5 seconds before next check...");
 | 
				
			||||||
 | 
					        sleep(5); // Wait 5 seconds before polling again
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if attempt >= max_attempts {
 | 
				
			||||||
 | 
					    print("");
 | 
				
			||||||
 | 
					    print("⚠️  Timeout: Server rebuild is taking longer than expected.");
 | 
				
			||||||
 | 
					    print("   Check the Hetzner Cloud console for current status.");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					print("");
 | 
				
			||||||
 | 
					print("💡 Tip: You can also monitor rebuild progress in the Hetzner Cloud console.");
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					print("");
 | 
				
			||||||
 | 
					print("To use this example:");
 | 
				
			||||||
 | 
					print("1. Replace server_id with your actual server ID");
 | 
				
			||||||
 | 
					print("2. Uncomment the install_image call and monitoring section");
 | 
				
			||||||
 | 
					print("3. Run the script to rebuild your server");
 | 
				
			||||||
 | 
					print("");
 | 
				
			||||||
 | 
					print("⚠️  Remember: install_image will DESTROY ALL DATA on the target server!");
 | 
				
			||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
use crate::hetzner_api::{
 | 
					use crate::hetzner_api::{
 | 
				
			||||||
    ListImagesParamsBuilder, HetznerClient, ServerBuilder, WrappedCreateServerResponse,
 | 
					    HetznerClient, ListImagesParamsBuilder, ServerBuilder, WrappedCreateServerResponse,
 | 
				
			||||||
    WrappedServer, WrappedSshKey, WrappedImage,
 | 
					    WrappedImage, WrappedServer, WrappedSshKey,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use std::sync::mpsc::{Receiver, Sender};
 | 
					use std::sync::mpsc::{Receiver, Sender};
 | 
				
			||||||
use tokio::runtime::Builder;
 | 
					use tokio::runtime::Builder;
 | 
				
			||||||
@@ -18,6 +18,7 @@ pub enum Request {
 | 
				
			|||||||
    DisableRescueMode(HetznerClient, i64),
 | 
					    DisableRescueMode(HetznerClient, i64),
 | 
				
			||||||
    ListSshKeys(HetznerClient),
 | 
					    ListSshKeys(HetznerClient),
 | 
				
			||||||
    ListImages(HetznerClient, ListImagesParamsBuilder),
 | 
					    ListImages(HetznerClient, ListImagesParamsBuilder),
 | 
				
			||||||
 | 
					    InstallImage(HetznerClient, i64, String),
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub enum Response {
 | 
					pub enum Response {
 | 
				
			||||||
@@ -31,17 +32,15 @@ pub enum Response {
 | 
				
			|||||||
    EnableRescueMode(Result<String, String>),
 | 
					    EnableRescueMode(Result<String, String>),
 | 
				
			||||||
    DisableRescueMode(Result<(), String>),
 | 
					    DisableRescueMode(Result<(), String>),
 | 
				
			||||||
    ListImages(Result<Vec<WrappedImage>, String>),
 | 
					    ListImages(Result<Vec<WrappedImage>, String>),
 | 
				
			||||||
 | 
					    InstallImage(Result<(), String>),
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub fn run_worker(
 | 
					pub fn run_worker(command_rx: Receiver<Request>, reply_tx: Sender<Response>) {
 | 
				
			||||||
    command_rx: Receiver<Request>,
 | 
					 | 
				
			||||||
    reply_tx: Sender<Response>,
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
    std::thread::spawn(move || {
 | 
					    std::thread::spawn(move || {
 | 
				
			||||||
        let rt = Builder::new_current_thread()
 | 
					        let rt = Builder::new_current_thread()
 | 
				
			||||||
            .enable_all()
 | 
					            .enable_all()
 | 
				
			||||||
            .build()
 | 
					            .build()
 | 
				
			||||||
            .unwrap();
 | 
					            .expect("Failed to create async runtime");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        while let Ok(request) = command_rx.recv() {
 | 
					        while let Ok(request) = command_rx.recv() {
 | 
				
			||||||
            let response = match request {
 | 
					            let response = match request {
 | 
				
			||||||
@@ -58,7 +57,9 @@ pub fn run_worker(
 | 
				
			|||||||
                    Response::CreateServer(result)
 | 
					                    Response::CreateServer(result)
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                Request::ListServers(client) => {
 | 
					                Request::ListServers(client) => {
 | 
				
			||||||
                    let result = rt.block_on(client.list_servers()).map_err(|e| e.to_string());
 | 
					                    let result = rt
 | 
				
			||||||
 | 
					                        .block_on(client.list_servers())
 | 
				
			||||||
 | 
					                        .map_err(|e| e.to_string());
 | 
				
			||||||
                    Response::ListServers(result)
 | 
					                    Response::ListServers(result)
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                Request::GetServerStatus(client, server_id) => {
 | 
					                Request::GetServerStatus(client, server_id) => {
 | 
				
			||||||
@@ -68,15 +69,21 @@ pub fn run_worker(
 | 
				
			|||||||
                    Response::GetServerStatus(result)
 | 
					                    Response::GetServerStatus(result)
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                Request::GetServer(client, server_id) => {
 | 
					                Request::GetServer(client, server_id) => {
 | 
				
			||||||
                    let result = rt.block_on(client.get_server(server_id)).map_err(|e| e.to_string());
 | 
					                    let result = rt
 | 
				
			||||||
 | 
					                        .block_on(client.get_server(server_id))
 | 
				
			||||||
 | 
					                        .map_err(|e| e.to_string());
 | 
				
			||||||
                    Response::GetServer(result)
 | 
					                    Response::GetServer(result)
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                Request::RebootServer(client, server_id) => {
 | 
					                Request::RebootServer(client, server_id) => {
 | 
				
			||||||
                    let result = rt.block_on(client.reboot_server(server_id)).map_err(|e| e.to_string());
 | 
					                    let result = rt
 | 
				
			||||||
 | 
					                        .block_on(client.reboot_server(server_id))
 | 
				
			||||||
 | 
					                        .map_err(|e| e.to_string());
 | 
				
			||||||
                    Response::RebootServer(result)
 | 
					                    Response::RebootServer(result)
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                Request::ResetServer(client, server_id) => {
 | 
					                Request::ResetServer(client, server_id) => {
 | 
				
			||||||
                    let result = rt.block_on(client.reset_server(server_id)).map_err(|e| e.to_string());
 | 
					                    let result = rt
 | 
				
			||||||
 | 
					                        .block_on(client.reset_server(server_id))
 | 
				
			||||||
 | 
					                        .map_err(|e| e.to_string());
 | 
				
			||||||
                    Response::ResetServer(result)
 | 
					                    Response::ResetServer(result)
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                Request::EnableRescueMode(client, server_id, ssh_keys) => {
 | 
					                Request::EnableRescueMode(client, server_id, ssh_keys) => {
 | 
				
			||||||
@@ -89,7 +96,8 @@ pub fn run_worker(
 | 
				
			|||||||
                    let result = rt
 | 
					                    let result = rt
 | 
				
			||||||
                        .block_on(async {
 | 
					                        .block_on(async {
 | 
				
			||||||
                            let ssh_keys = client.list_ssh_keys().await?;
 | 
					                            let ssh_keys = client.list_ssh_keys().await?;
 | 
				
			||||||
                            let ssh_key_ids: Vec<i64> = ssh_keys.into_iter().map(|k| k.0.id).collect();
 | 
					                            let ssh_key_ids: Vec<i64> =
 | 
				
			||||||
 | 
					                                ssh_keys.into_iter().map(|k| k.0.id).collect();
 | 
				
			||||||
                            println!("Passing in the following ssh key ids: {:#?}", ssh_key_ids);
 | 
					                            println!("Passing in the following ssh key ids: {:#?}", ssh_key_ids);
 | 
				
			||||||
                            client
 | 
					                            client
 | 
				
			||||||
                                .enable_rescue_mode_for_server(server_id, ssh_key_ids)
 | 
					                                .enable_rescue_mode_for_server(server_id, ssh_key_ids)
 | 
				
			||||||
@@ -99,15 +107,25 @@ pub fn run_worker(
 | 
				
			|||||||
                    Response::EnableRescueMode(result)
 | 
					                    Response::EnableRescueMode(result)
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                Request::DisableRescueMode(client, server_id) => {
 | 
					                Request::DisableRescueMode(client, server_id) => {
 | 
				
			||||||
                    let result = rt.block_on(client.disable_rescue_mode_for_server(server_id)).map_err(|e| e.to_string());
 | 
					                    let result = rt
 | 
				
			||||||
 | 
					                        .block_on(client.disable_rescue_mode_for_server(server_id))
 | 
				
			||||||
 | 
					                        .map_err(|e| e.to_string());
 | 
				
			||||||
                    Response::DisableRescueMode(result)
 | 
					                    Response::DisableRescueMode(result)
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                Request::ListSshKeys(client) => {
 | 
					                Request::ListSshKeys(client) => {
 | 
				
			||||||
                    let result = rt.block_on(client.list_ssh_keys()).map_err(|e| e.to_string());
 | 
					                    let result = rt
 | 
				
			||||||
 | 
					                        .block_on(client.list_ssh_keys())
 | 
				
			||||||
 | 
					                        .map_err(|e| e.to_string());
 | 
				
			||||||
                    Response::ListSshKeys(result)
 | 
					                    Response::ListSshKeys(result)
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					                Request::InstallImage(client, server_id, image) => {
 | 
				
			||||||
 | 
					                    let result = rt
 | 
				
			||||||
 | 
					                        .block_on(client.install_image(server_id, image))
 | 
				
			||||||
 | 
					                        .map_err(|e| e.to_string());
 | 
				
			||||||
 | 
					                    Response::InstallImage(result)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
            reply_tx.send(response).expect("Failed to send response");
 | 
					            reply_tx.send(response).expect("Failed to send response");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,15 +1,15 @@
 | 
				
			|||||||
use hcloud::apis::{
 | 
					use hcloud::apis::{
 | 
				
			||||||
    configuration::Configuration,
 | 
					    configuration::Configuration,
 | 
				
			||||||
    images_api::{self, ListImagesParams},
 | 
					 | 
				
			||||||
    servers_api::{
 | 
					    servers_api::{
 | 
				
			||||||
        self, CreateServerParams, DisableRescueModeForServerParams,
 | 
					        self, CreateServerParams, DisableRescueModeForServerParams,
 | 
				
			||||||
        EnableRescueModeForServerParams, ListServersParams, ResetServerParams,
 | 
					        EnableRescueModeForServerParams, ListServersParams, RebuildServerFromImageParams,
 | 
				
			||||||
 | 
					        ResetServerParams,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    ssh_keys_api::{self, ListSshKeysParams},
 | 
					    ssh_keys_api::{self, ListSshKeysParams},
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use hcloud::models::{
 | 
					use hcloud::models::{
 | 
				
			||||||
    CreateServerRequest, CreateServerResponse, EnableRescueModeForServerRequest, Image, Server,
 | 
					    CreateServerRequest, CreateServerResponse, EnableRescueModeForServerRequest, Image,
 | 
				
			||||||
    SshKey,
 | 
					    RebuildServerFromImageRequest, Server, SshKey,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
use serde_json::Value;
 | 
					use serde_json::Value;
 | 
				
			||||||
@@ -155,21 +155,6 @@ impl ListImagesParamsBuilder {
 | 
				
			|||||||
        self.architecture = Some(architecture);
 | 
					        self.architecture = Some(architecture);
 | 
				
			||||||
        self
 | 
					        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 {
 | 
					impl HetznerClient {
 | 
				
			||||||
@@ -179,7 +164,7 @@ impl HetznerClient {
 | 
				
			|||||||
        let client = reqwest::Client::builder()
 | 
					        let client = reqwest::Client::builder()
 | 
				
			||||||
            .timeout(std::time::Duration::from_secs(5))
 | 
					            .timeout(std::time::Duration::from_secs(5))
 | 
				
			||||||
            .build()
 | 
					            .build()
 | 
				
			||||||
            .unwrap();
 | 
					            .expect("Failed to create HTTP client");
 | 
				
			||||||
        configuration.client = client;
 | 
					        configuration.client = client;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Self { configuration }
 | 
					        Self { configuration }
 | 
				
			||||||
@@ -340,11 +325,11 @@ impl HetznerClient {
 | 
				
			|||||||
        let mut all_images = Vec::new();
 | 
					        let mut all_images = Vec::new();
 | 
				
			||||||
        let mut page = 1;
 | 
					        let mut page = 1;
 | 
				
			||||||
        let per_page = 50;
 | 
					        let per_page = 50;
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
        loop {
 | 
					        loop {
 | 
				
			||||||
            let mut url = "https://api.hetzner.cloud/v1/images".to_string();
 | 
					            let mut url = "https://api.hetzner.cloud/v1/images".to_string();
 | 
				
			||||||
            let mut query_params = Vec::new();
 | 
					            let mut query_params = Vec::new();
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
            if let Some(sort) = &builder.sort {
 | 
					            if let Some(sort) = &builder.sort {
 | 
				
			||||||
                query_params.push(format!("sort={}", sort));
 | 
					                query_params.push(format!("sort={}", sort));
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -369,30 +354,35 @@ impl HetznerClient {
 | 
				
			|||||||
            if let Some(architecture) = &builder.architecture {
 | 
					            if let Some(architecture) = &builder.architecture {
 | 
				
			||||||
                query_params.push(format!("architecture={}", architecture));
 | 
					                query_params.push(format!("architecture={}", architecture));
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
            query_params.push(format!("page={}", page));
 | 
					            query_params.push(format!("page={}", page));
 | 
				
			||||||
            query_params.push(format!("per_page={}", per_page));
 | 
					            query_params.push(format!("per_page={}", per_page));
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
            if !query_params.is_empty() {
 | 
					            if !query_params.is_empty() {
 | 
				
			||||||
                url.push('?');
 | 
					                url.push('?');
 | 
				
			||||||
                url.push_str(&query_params.join("&"));
 | 
					                url.push_str(&query_params.join("&"));
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
            let response: Value = self
 | 
					            let response: Value = self
 | 
				
			||||||
                .configuration
 | 
					                .configuration
 | 
				
			||||||
                .client
 | 
					                .client
 | 
				
			||||||
                .get(&url)
 | 
					                .get(&url)
 | 
				
			||||||
                .bearer_auth(self.configuration.bearer_access_token.as_ref().unwrap())
 | 
					                .bearer_auth(
 | 
				
			||||||
 | 
					                    self.configuration
 | 
				
			||||||
 | 
					                        .bearer_access_token
 | 
				
			||||||
 | 
					                        .as_ref()
 | 
				
			||||||
 | 
					                        .ok_or("API token not configured")?,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
                .send()
 | 
					                .send()
 | 
				
			||||||
                .await?
 | 
					                .await?
 | 
				
			||||||
                .json()
 | 
					                .json()
 | 
				
			||||||
                .await?;
 | 
					                .await?;
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
            if let Some(images_json) = response.get("images").and_then(|i| i.as_array()) {
 | 
					            if let Some(images_json) = response.get("images").and_then(|i| i.as_array()) {
 | 
				
			||||||
                if images_json.is_empty() {
 | 
					                if images_json.is_empty() {
 | 
				
			||||||
                    break;
 | 
					                    break;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
                let images: Vec<WrappedImage> = images_json
 | 
					                let images: Vec<WrappedImage> = images_json
 | 
				
			||||||
                    .iter()
 | 
					                    .iter()
 | 
				
			||||||
                    .filter_map(|image_value| {
 | 
					                    .filter_map(|image_value| {
 | 
				
			||||||
@@ -402,12 +392,12 @@ impl HetznerClient {
 | 
				
			|||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    })
 | 
					                    })
 | 
				
			||||||
                    .collect();
 | 
					                    .collect();
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
                all_images.extend(images);
 | 
					                all_images.extend(images);
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
            if response
 | 
					            if response
 | 
				
			||||||
                .get("meta")
 | 
					                .get("meta")
 | 
				
			||||||
                .and_then(|m| m.get("pagination"))
 | 
					                .and_then(|m| m.get("pagination"))
 | 
				
			||||||
@@ -417,10 +407,24 @@ impl HetznerClient {
 | 
				
			|||||||
            {
 | 
					            {
 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
            page += 1;
 | 
					            page += 1;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
        Ok(all_images)
 | 
					        Ok(all_images)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn install_image(
 | 
				
			||||||
 | 
					        &self,
 | 
				
			||||||
 | 
					        server_id: i64,
 | 
				
			||||||
 | 
					        image: String,
 | 
				
			||||||
 | 
					    ) -> Result<(), Box<dyn std::error::Error>> {
 | 
				
			||||||
 | 
					        let params = RebuildServerFromImageParams {
 | 
				
			||||||
 | 
					            id: server_id,
 | 
				
			||||||
 | 
					            rebuild_server_from_image_request: Some(RebuildServerFromImageRequest::new(image)),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        servers_api::rebuild_server_from_image(&self.configuration, params).await?;
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,7 @@ use crate::hetzner_api::{
 | 
				
			|||||||
    HetznerClient, ListImagesParamsBuilder, ServerBuilder, WrappedCreateServerResponse,
 | 
					    HetznerClient, ListImagesParamsBuilder, ServerBuilder, WrappedCreateServerResponse,
 | 
				
			||||||
    WrappedImage, WrappedServer, WrappedSshKey,
 | 
					    WrappedImage, WrappedServer, WrappedSshKey,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					use hcloud::models::Image;
 | 
				
			||||||
use prettytable::{Cell, Row, Table};
 | 
					use prettytable::{Cell, Row, Table};
 | 
				
			||||||
use rhai::{Engine, EvalAltResult};
 | 
					use rhai::{Engine, EvalAltResult};
 | 
				
			||||||
use std::env;
 | 
					use std::env;
 | 
				
			||||||
@@ -122,7 +123,10 @@ pub fn register_hetzner_api(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    engine
 | 
					    engine
 | 
				
			||||||
        .register_type_with_name::<ListImagesParamsBuilder>("ListImagesParamsBuilder")
 | 
					        .register_type_with_name::<ListImagesParamsBuilder>("ListImagesParamsBuilder")
 | 
				
			||||||
        .register_fn("new_list_images_params_builder", ListImagesParamsBuilder::new)
 | 
					        .register_fn(
 | 
				
			||||||
 | 
					            "new_list_images_params_builder",
 | 
				
			||||||
 | 
					            ListImagesParamsBuilder::new,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        .register_fn("with_sort", ListImagesParamsBuilder::with_sort)
 | 
					        .register_fn("with_sort", ListImagesParamsBuilder::with_sort)
 | 
				
			||||||
        .register_fn("with_type", ListImagesParamsBuilder::with_type)
 | 
					        .register_fn("with_type", ListImagesParamsBuilder::with_type)
 | 
				
			||||||
        .register_fn("with_status", ListImagesParamsBuilder::with_status)
 | 
					        .register_fn("with_status", ListImagesParamsBuilder::with_status)
 | 
				
			||||||
@@ -136,31 +140,53 @@ pub fn register_hetzner_api(
 | 
				
			|||||||
            "with_label_selector",
 | 
					            "with_label_selector",
 | 
				
			||||||
            ListImagesParamsBuilder::with_label_selector,
 | 
					            ListImagesParamsBuilder::with_label_selector,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        .register_fn("with_architecture", ListImagesParamsBuilder::with_architecture);
 | 
					        .register_fn(
 | 
				
			||||||
 | 
					            "with_architecture",
 | 
				
			||||||
 | 
					            ListImagesParamsBuilder::with_architecture,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    engine
 | 
					    engine
 | 
				
			||||||
        .register_fn("list_images", {
 | 
					        .register_fn("list_images", {
 | 
				
			||||||
            let bridge = api_bridge.clone();
 | 
					            let bridge = api_bridge.clone();
 | 
				
			||||||
            move |client: &mut HetznerClient, builder: ListImagesParamsBuilder| {
 | 
					            move |client: &mut HetznerClient, builder: ListImagesParamsBuilder| {
 | 
				
			||||||
                bridge.call(Request::ListImages(client.clone(), builder), |response| {
 | 
					                bridge.call(
 | 
				
			||||||
                    match response {
 | 
					                    Request::ListImages(client.clone(), builder),
 | 
				
			||||||
 | 
					                    |response| match response {
 | 
				
			||||||
                        Response::ListImages(result) => result.map_err(|e| e.into()),
 | 
					                        Response::ListImages(result) => result.map_err(|e| e.into()),
 | 
				
			||||||
                        _ => Err("Unexpected response".into()),
 | 
					                        _ => Err("Unexpected response".into()),
 | 
				
			||||||
                    }
 | 
					                    },
 | 
				
			||||||
                })
 | 
					                )
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .register_fn("install_image", {
 | 
				
			||||||
 | 
					            let bridge = api_bridge.clone();
 | 
				
			||||||
 | 
					            move |client: &mut HetznerClient, server_id: i64, image: &str| {
 | 
				
			||||||
 | 
					                bridge.call(
 | 
				
			||||||
 | 
					                    Request::InstallImage(client.clone(), server_id, image.to_string()),
 | 
				
			||||||
 | 
					                    |response| match response {
 | 
				
			||||||
 | 
					                        Response::InstallImage(result) => result.map_err(|e| e.into()),
 | 
				
			||||||
 | 
					                        _ => Err("Unexpected response".into()),
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
        .register_type_with_name::<WrappedImage>("Image")
 | 
					        .register_type_with_name::<WrappedImage>("Image")
 | 
				
			||||||
        .register_get("id", |image: &mut WrappedImage| image.0.id)
 | 
					        .register_get("id", |image: &mut WrappedImage| image.0.id)
 | 
				
			||||||
        .register_get("name", |image: &mut WrappedImage| image.0.name.clone())
 | 
					        .register_get("name", |image: &mut WrappedImage| {
 | 
				
			||||||
 | 
					            image.0.name.clone().unwrap_or("Unknown".to_string())
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
        .register_get("description", |image: &mut WrappedImage| {
 | 
					        .register_get("description", |image: &mut WrappedImage| {
 | 
				
			||||||
            image.0.description.clone()
 | 
					            image.0.description.clone()
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
        .register_get("status", |image: &mut WrappedImage| {
 | 
					        .register_get("status", |image: &mut WrappedImage| {
 | 
				
			||||||
            format!("{:?}", image.0.status)
 | 
					            format!("{:?}", image.0.status)
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
        .register_get("type", |image: &mut WrappedImage| format!("{:?}", image.0.r#type))
 | 
					        .register_get("type", |image: &mut WrappedImage| {
 | 
				
			||||||
        .register_get("created", |image: &mut WrappedImage| image.0.created.clone())
 | 
					            format!("{:?}", image.0.r#type)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .register_get("created", |image: &mut WrappedImage| {
 | 
				
			||||||
 | 
					            image.0.created.clone()
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
        .register_get("os_flavor", |image: &mut WrappedImage| {
 | 
					        .register_get("os_flavor", |image: &mut WrappedImage| {
 | 
				
			||||||
            format!("{:?}", image.0.os_flavor)
 | 
					            format!("{:?}", image.0.os_flavor)
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
@@ -170,6 +196,10 @@ pub fn register_hetzner_api(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    engine
 | 
					    engine
 | 
				
			||||||
        .register_iterator::<Vec<WrappedImage>>()
 | 
					        .register_iterator::<Vec<WrappedImage>>()
 | 
				
			||||||
 | 
					        .register_fn("len", |list: &mut Vec<WrappedImage>| list.len() as i64)
 | 
				
			||||||
 | 
					        .register_indexer_get(|list: &mut Vec<WrappedImage>, index: i64| {
 | 
				
			||||||
 | 
					            list[index as usize].clone()
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
        .register_fn(
 | 
					        .register_fn(
 | 
				
			||||||
            "show_table",
 | 
					            "show_table",
 | 
				
			||||||
            |images: &mut Vec<WrappedImage>| -> Result<String, Box<EvalAltResult>> {
 | 
					            |images: &mut Vec<WrappedImage>| -> Result<String, Box<EvalAltResult>> {
 | 
				
			||||||
@@ -215,12 +245,7 @@ pub fn register_hetzner_api(
 | 
				
			|||||||
        .register_fn(
 | 
					        .register_fn(
 | 
				
			||||||
            "with_ssh_keys",
 | 
					            "with_ssh_keys",
 | 
				
			||||||
            |builder: ServerBuilder, ssh_keys: rhai::Array| {
 | 
					            |builder: ServerBuilder, ssh_keys: rhai::Array| {
 | 
				
			||||||
                builder.with_ssh_keys(
 | 
					                builder.with_ssh_keys(ssh_keys.into_iter().map(|k| k.as_int().unwrap()).collect())
 | 
				
			||||||
                    ssh_keys
 | 
					 | 
				
			||||||
                        .into_iter()
 | 
					 | 
				
			||||||
                        .map(|k| k.as_int().unwrap())
 | 
					 | 
				
			||||||
                        .collect(),
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -269,6 +294,22 @@ pub fn register_hetzner_api(
 | 
				
			|||||||
        })
 | 
					        })
 | 
				
			||||||
        .register_get("rescue_enabled", |server: &mut WrappedServer| {
 | 
					        .register_get("rescue_enabled", |server: &mut WrappedServer| {
 | 
				
			||||||
            server.0.rescue_enabled
 | 
					            server.0.rescue_enabled
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .register_get("image", |server: &mut WrappedServer| {
 | 
				
			||||||
 | 
					            server
 | 
				
			||||||
 | 
					                .0
 | 
				
			||||||
 | 
					                .image
 | 
				
			||||||
 | 
					                .as_ref()
 | 
				
			||||||
 | 
					                .map(|img| WrappedImage((**img).clone()))
 | 
				
			||||||
 | 
					                .unwrap_or_else(|| {
 | 
				
			||||||
 | 
					                    // Create a dummy image if none exists
 | 
				
			||||||
 | 
					                    WrappedImage(Image {
 | 
				
			||||||
 | 
					                        id: 0,
 | 
				
			||||||
 | 
					                        name: Some("No image".to_string()),
 | 
				
			||||||
 | 
					                        description: "No image associated with this server".to_string(),
 | 
				
			||||||
 | 
					                        ..Default::default()
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    engine
 | 
					    engine
 | 
				
			||||||
@@ -440,6 +481,11 @@ pub fn register_hetzner_api(
 | 
				
			|||||||
        env::var(key).unwrap_or("".to_string())
 | 
					        env::var(key).unwrap_or("".to_string())
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Register sleep function for polling operations
 | 
				
			||||||
 | 
					    engine.register_fn("sleep", |seconds: i64| {
 | 
				
			||||||
 | 
					        std::thread::sleep(std::time::Duration::from_secs(seconds as u64));
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    engine
 | 
					    engine
 | 
				
			||||||
        .register_type_with_name::<WrappedSshKey>("SshKey")
 | 
					        .register_type_with_name::<WrappedSshKey>("SshKey")
 | 
				
			||||||
        .register_get("id", |key: &mut WrappedSshKey| key.0.id)
 | 
					        .register_get("id", |key: &mut WrappedSshKey| key.0.id)
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user