From f1339f071183d2a35a8e5cea52c49f9ebce0c093 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Sat, 7 Jun 2025 10:32:39 +0200 Subject: [PATCH] Generate cloud init disk image --- .../src/controllers/vm_controller.rs | 22 ++++++++ virtweb_backend/src/main.rs | 4 ++ virtweb_backend/src/utils/cloud_init_utils.rs | 56 +++++++++++++++++++ virtweb_frontend/src/api/VMApi.ts | 12 ++++ .../src/widgets/tokens/TokenRightsEditor.tsx | 18 +++++- 5 files changed, 111 insertions(+), 1 deletion(-) diff --git a/virtweb_backend/src/controllers/vm_controller.rs b/virtweb_backend/src/controllers/vm_controller.rs index ed18742..121a995 100644 --- a/virtweb_backend/src/controllers/vm_controller.rs +++ b/virtweb_backend/src/controllers/vm_controller.rs @@ -109,6 +109,28 @@ pub async fn get_single_src_def(client: LibVirtReq, id: web::Path) -> HttpResult { + let info = match client.get_single_domain(id.uid).await { + Ok(i) => i, + Err(e) => { + log::error!("Failed to get domain information! {e}"); + return Ok(HttpResponse::InternalServerError().json(e.to_string())); + } + }; + + let vm = VMInfo::from_domain(info)?; + let disk = vm.cloud_init.generate_nocloud_disk()?; + + Ok(HttpResponse::Ok() + .content_type("application/x-iso9660-image") + .insert_header(( + "Content-Disposition", + format!("attachment; filename=\"cloud_init_{}.iso\"", vm.name), + )) + .body(disk)) +} + /// Update a VM information pub async fn update( client: LibVirtReq, diff --git a/virtweb_backend/src/main.rs b/virtweb_backend/src/main.rs index c9b509d..4e5ed94 100644 --- a/virtweb_backend/src/main.rs +++ b/virtweb_backend/src/main.rs @@ -202,6 +202,10 @@ async fn main() -> std::io::Result<()> { "/api/vm/{uid}/src", web::get().to(vm_controller::get_single_src_def), ) + .route( + "/api/vm/{uid}/cloud_init_disk", + web::get().to(vm_controller::get_cloud_init_disk), + ) .route( "/api/vm/{uid}/autostart", web::get().to(vm_controller::get_autostart), diff --git a/virtweb_backend/src/utils/cloud_init_utils.rs b/virtweb_backend/src/utils/cloud_init_utils.rs index 1059845..22c96fd 100644 --- a/virtweb_backend/src/utils/cloud_init_utils.rs +++ b/virtweb_backend/src/utils/cloud_init_utils.rs @@ -1,3 +1,7 @@ +use crate::app_config::AppConfig; +use crate::constants; +use std::process::Command; + /// VM Cloud Init configuration /// /// RedHat documentation: https://docs.redhat.com/fr/documentation/red_hat_enterprise_linux/9/html/configuring_and_managing_cloud-init_for_rhel_9/configuring-cloud-init_cloud-content @@ -17,3 +21,55 @@ pub struct CloudInitConfig { #[serde(skip_serializing_if = "Option::is_none")] network_configuration: Option, } + +impl CloudInitConfig { + /// Generate disk image for nocloud usage + pub fn generate_nocloud_disk(&self) -> anyhow::Result> { + let temp_path = tempfile::tempdir_in(&AppConfig::get().temp_dir)?; + + let mut cmd = Command::new(constants::PROGRAM_CLOUD_LOCALDS); + + // ISO destination path + let temp_iso = temp_path.path().join("disk.iso"); + cmd.arg(&temp_iso); + + // Process network configuration + if let Some(net_conf) = &self.network_configuration { + let net_conf_path = temp_path.path().join("network"); + std::fs::write(&net_conf_path, net_conf)?; + cmd.arg("--network-config").arg(&net_conf_path); + } + + // Process user data + let user_data_path = temp_path.path().join("user-data"); + std::fs::write(&user_data_path, &self.user_data)?; + cmd.arg(user_data_path); + + // Process metadata + let mut metadatas = vec![]; + if let Some(inst_id) = &self.instance_id { + metadatas.push(format!("instance-id: {}", inst_id)); + } + if let Some(local_hostname) = &self.local_hostname { + metadatas.push(format!("local-hostname: {}", local_hostname)); + } + let meta_data_path = temp_path.path().join("meta-data"); + std::fs::write(&meta_data_path, metadatas.join("\n"))?; + cmd.arg(meta_data_path); + + // Execute command + let output = cmd.output()?; + if !output.status.success() { + anyhow::bail!( + "{} exited with status {}!\nStdout: {}\nStderr: {}", + constants::PROGRAM_CLOUD_LOCALDS, + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } + + // Read generated ISO file + Ok(std::fs::read(temp_iso)?) + } +} diff --git a/virtweb_frontend/src/api/VMApi.ts b/virtweb_frontend/src/api/VMApi.ts index 1e44b24..dbc119c 100644 --- a/virtweb_frontend/src/api/VMApi.ts +++ b/virtweb_frontend/src/api/VMApi.ts @@ -82,6 +82,14 @@ export interface VMNetBridge { bridge: string; } +export interface VMCloudInit { + attach_config: boolean; + user_data: string; + instance_id?: string; + local_hostname?: string; + network_configuration?: string; +} + export type VMBootType = "UEFI" | "UEFISecureBoot" | "Legacy"; interface VMInfoInterface { @@ -101,6 +109,7 @@ interface VMInfoInterface { networks: VMNetInterface[]; tpm_module: boolean; oem_strings: string[]; + cloud_init: VMCloudInit; } export class VMInfo implements VMInfoInterface { @@ -120,6 +129,7 @@ export class VMInfo implements VMInfoInterface { networks: VMNetInterface[]; tpm_module: boolean; oem_strings: string[]; + cloud_init: VMCloudInit; constructor(int: VMInfoInterface) { this.name = int.name; @@ -138,6 +148,7 @@ export class VMInfo implements VMInfoInterface { this.networks = int.networks; this.tpm_module = int.tpm_module; this.oem_strings = int.oem_strings; + this.cloud_init = int.cloud_init; } static NewEmpty(): VMInfo { @@ -153,6 +164,7 @@ export class VMInfo implements VMInfoInterface { networks: [], tpm_module: true, oem_strings: [], + cloud_init: { attach_config: false, user_data: "" }, }); } diff --git a/virtweb_frontend/src/widgets/tokens/TokenRightsEditor.tsx b/virtweb_frontend/src/widgets/tokens/TokenRightsEditor.tsx index 5e9f6be..33ef9f7 100644 --- a/virtweb_frontend/src/widgets/tokens/TokenRightsEditor.tsx +++ b/virtweb_frontend/src/widgets/tokens/TokenRightsEditor.tsx @@ -60,6 +60,7 @@ export function TokenRightsEditor(p: { Get XML definition Get autostart Set autostart + Get CloudInit disk Backup disk @@ -84,6 +85,13 @@ export function TokenRightsEditor(p: { {...p} right={{ verb: "PUT", path: "/api/vm/*/autostart" }} /> + {" "} + /> +