From 55b49699eb772ecd244c0948c912285f3e894d8a Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Sat, 2 Nov 2024 17:31:05 +0100 Subject: [PATCH] Can assign a group to VMs --- .../src/controllers/vm_controller.rs | 6 +- .../src/libvirt_lib_structures/domain.rs | 105 ++++++++++++------ .../src/libvirt_rest_structures/vm.rs | 18 ++- virtweb_frontend/src/routes/VMListRoute.tsx | 2 - 4 files changed, 95 insertions(+), 36 deletions(-) diff --git a/virtweb_backend/src/controllers/vm_controller.rs b/virtweb_backend/src/controllers/vm_controller.rs index e954353..2031767 100644 --- a/virtweb_backend/src/controllers/vm_controller.rs +++ b/virtweb_backend/src/controllers/vm_controller.rs @@ -21,7 +21,7 @@ struct VMUuid { /// Create a new VM pub async fn create(client: LibVirtReq, req: web::Json) -> HttpResult { - let domain = match req.0.as_tomain() { + let domain = match req.0.as_domain() { Ok(d) => d, Err(e) => { log::error!("Failed to extract domain info! {e}"); @@ -83,6 +83,8 @@ pub async fn get_single(client: LibVirtReq, id: web::Path) -> H } }; + log::debug!("INFO={info:#?}"); + let state = client.get_domain_state(id.uid).await?; Ok(HttpResponse::Ok().json(VMInfoAndState { @@ -112,7 +114,7 @@ pub async fn update( id: web::Path, req: web::Json, ) -> HttpResult { - let mut domain = match req.0.as_tomain() { + let mut domain = match req.0.as_domain() { Ok(d) => d, Err(e) => { log::error!("Failed to extract domain info! {e}"); diff --git a/virtweb_backend/src/libvirt_lib_structures/domain.rs b/virtweb_backend/src/libvirt_lib_structures/domain.rs index df80384..f8ec61e 100644 --- a/virtweb_backend/src/libvirt_lib_structures/domain.rs +++ b/virtweb_backend/src/libvirt_lib_structures/domain.rs @@ -1,7 +1,25 @@ use crate::libvirt_lib_structures::XMLUuid; +/// VirtWeb specific metadata +#[derive(serde::Serialize, serde::Deserialize, Default, Debug, Clone)] +#[serde(rename = "virtweb", default)] +pub struct DomainMetadataVirtWebXML { + #[serde(rename = "@xmlns:virtweb", default)] + pub ns: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub group: Option, +} + +/// Domain metadata +#[derive(serde::Serialize, serde::Deserialize, Default, Debug, Clone)] +#[serde(rename = "metadata")] +pub struct DomainMetadataXML { + #[serde(rename = "virtweb:metadata", default)] + pub virtweb: DomainMetadataVirtWebXML, +} + /// OS information -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(rename = "os")] pub struct OSXML { #[serde(rename = "@firmware", default)] @@ -11,7 +29,7 @@ pub struct OSXML { } /// OS Type information -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(rename = "os")] pub struct OSTypeXML { #[serde(rename = "@arch")] @@ -23,7 +41,7 @@ pub struct OSTypeXML { } /// OS Loader information -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(rename = "loader")] pub struct OSLoaderXML { #[serde(rename = "@secure")] @@ -31,39 +49,39 @@ pub struct OSLoaderXML { } /// Hypervisor features -#[derive(serde::Serialize, serde::Deserialize, Default)] +#[derive(serde::Serialize, serde::Deserialize, Default, Debug)] #[serde(rename = "features")] pub struct FeaturesXML { pub acpi: ACPIXML, } /// ACPI feature -#[derive(serde::Serialize, serde::Deserialize, Default)] +#[derive(serde::Serialize, serde::Deserialize, Default, Debug)] #[serde(rename = "acpi")] pub struct ACPIXML {} -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(rename = "mac")] pub struct NetMacAddress { #[serde(rename = "@address")] pub address: String, } -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(rename = "source")] pub struct NetIntSourceXML { #[serde(rename = "@network")] pub network: String, } -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(rename = "model")] pub struct NetIntModelXML { #[serde(rename = "@type")] pub r#type: String, } -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(rename = "filterref")] pub struct NetIntFilterParameterXML { #[serde(rename = "@name")] @@ -72,7 +90,7 @@ pub struct NetIntFilterParameterXML { pub value: String, } -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(rename = "filterref")] pub struct NetIntfilterRefXML { #[serde(rename = "@filter")] @@ -81,7 +99,7 @@ pub struct NetIntfilterRefXML { pub parameters: Vec, } -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(rename = "interface")] pub struct DomainNetInterfaceXML { #[serde(rename = "@type")] @@ -95,14 +113,14 @@ pub struct DomainNetInterfaceXML { pub filterref: Option, } -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(rename = "input")] pub struct DomainInputXML { #[serde(rename = "@type")] pub r#type: String, } -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(rename = "backend")] pub struct TPMBackendXML { #[serde(rename = "@type")] @@ -112,7 +130,7 @@ pub struct TPMBackendXML { pub r#version: String, } -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(rename = "tpm")] pub struct TPMDeviceXML { #[serde(rename = "@model")] @@ -121,7 +139,7 @@ pub struct TPMDeviceXML { } /// Devices information -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(rename = "devices")] pub struct DevicesXML { /// Graphics (used for VNC) @@ -150,7 +168,7 @@ pub struct DevicesXML { } /// Graphics information -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(rename = "graphics")] pub struct GraphicsXML { #[serde(rename = "@type")] @@ -160,14 +178,14 @@ pub struct GraphicsXML { } /// Video device information -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(rename = "video")] pub struct VideoXML { pub model: VideoModelXML, } /// Video model device information -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(rename = "model")] pub struct VideoModelXML { #[serde(rename = "@type")] @@ -175,7 +193,7 @@ pub struct VideoModelXML { } /// Disk information -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(rename = "disk")] pub struct DiskXML { #[serde(rename = "@type")] @@ -193,7 +211,7 @@ pub struct DiskXML { pub address: Option, } -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(rename = "driver")] pub struct DiskDriverXML { #[serde(rename = "@name")] @@ -204,14 +222,14 @@ pub struct DiskDriverXML { pub r#cache: String, } -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(rename = "source")] pub struct DiskSourceXML { #[serde(rename = "@file")] pub file: String, } -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(rename = "target")] pub struct DiskTargetXML { #[serde(rename = "@dev")] @@ -220,18 +238,18 @@ pub struct DiskTargetXML { pub bus: String, } -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(rename = "readonly")] pub struct DiskReadOnlyXML {} -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(rename = "boot")] pub struct DiskBootXML { #[serde(rename = "@order")] pub order: String, } -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(rename = "address")] pub struct DiskAddressXML { #[serde(rename = "@type")] @@ -251,7 +269,7 @@ pub struct DiskAddressXML { } /// Domain RAM information -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(rename = "memory")] pub struct DomainMemoryXML { #[serde(rename = "@unit")] @@ -261,7 +279,7 @@ pub struct DomainMemoryXML { pub memory: usize, } -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(rename = "topology")] pub struct DomainCPUTopology { #[serde(rename = "@sockets")] @@ -272,14 +290,14 @@ pub struct DomainCPUTopology { pub threads: usize, } -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(rename = "cpu")] pub struct DomainVCPUXML { #[serde(rename = "$value")] pub body: usize, } -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(rename = "cpu")] pub struct DomainCPUXML { #[serde(rename = "@mode")] @@ -288,7 +306,7 @@ pub struct DomainCPUXML { } /// Domain information, see https://libvirt.org/formatdomain.html -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(rename = "domain")] pub struct DomainXML { /// Domain type (kvm) @@ -300,6 +318,9 @@ pub struct DomainXML { pub genid: Option, pub title: Option, pub description: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub metadata: Option, + pub os: OSXML, #[serde(default)] pub features: FeaturesXML, @@ -319,10 +340,32 @@ pub struct DomainXML { pub on_crash: String, } +const METADATA_START_MARKER: &str = + ""; +const METADATA_END_MARKER: &str = ""; + impl DomainXML { /// Decode Domain structure from XML definition pub fn parse_xml(xml: &str) -> anyhow::Result { - Ok(quick_xml::de::from_str(xml)?) + let mut res: Self = quick_xml::de::from_str(xml)?; + + // Handle custom metadata parsing issue + // + // https://github.com/tafia/quick-xml/pull/797 + if xml.contains(METADATA_START_MARKER) && xml.contains(METADATA_END_MARKER) { + let s = xml + .split_once(METADATA_START_MARKER) + .unwrap() + .1 + .split_once(METADATA_END_MARKER) + .unwrap() + .0; + let s = format!("{s}"); + let metadata: DomainMetadataVirtWebXML = quick_xml::de::from_str(&s)?; + res.metadata = Some(DomainMetadataXML { virtweb: metadata }); + } + + Ok(res) } /// Turn this domain into its XML definition diff --git a/virtweb_backend/src/libvirt_rest_structures/vm.rs b/virtweb_backend/src/libvirt_rest_structures/vm.rs index 7e93e2c..195b853 100644 --- a/virtweb_backend/src/libvirt_rest_structures/vm.rs +++ b/virtweb_backend/src/libvirt_rest_structures/vm.rs @@ -59,6 +59,9 @@ pub struct VMInfo { pub genid: Option, pub title: Option, pub description: Option, + /// Group associated with the VM (VirtWeb specific field) + #[serde(skip_serializing_if = "Option::is_none")] + pub group: Option, pub boot_type: BootType, pub architecture: VMArchitecture, /// VM allocated memory, in megabytes @@ -79,7 +82,7 @@ pub struct VMInfo { impl VMInfo { /// Turn this VM into a domain - pub fn as_tomain(&self) -> anyhow::Result { + pub fn as_domain(&self) -> anyhow::Result { if !regex!("^[a-zA-Z0-9]+$").is_match(&self.name) { return Err(StructureExtraction("VM name is invalid!").into()); } @@ -105,6 +108,12 @@ impl VMInfo { } } + if let Some(group) = &self.group { + if !regex!("^[a-zA-Z0-9]+$").is_match(group) { + return Err(StructureExtraction("VM group name is invalid!").into()); + } + } + if self.memory < constants::MIN_VM_MEMORY || self.memory > constants::MAX_VM_MEMORY { return Err(StructureExtraction("VM memory is invalid!").into()); } @@ -282,6 +291,12 @@ impl VMInfo { title: self.title.clone(), description: self.description.clone(), + metadata: Some(DomainMetadataXML { + virtweb: DomainMetadataVirtWebXML { + ns: "https://virtweb.communiquons.org".to_string(), + group: self.group.clone(), + }, + }), os: OSXML { r#type: OSTypeXML { arch: match self.architecture { @@ -369,6 +384,7 @@ impl VMInfo { genid: domain.genid.map(XMLUuid), title: domain.title, description: domain.description, + group: domain.metadata.clone().unwrap_or_default().virtweb.group, boot_type: match domain.os.loader { None => BootType::UEFI, Some(l) => match l.secure.as_str() { diff --git a/virtweb_frontend/src/routes/VMListRoute.tsx b/virtweb_frontend/src/routes/VMListRoute.tsx index 320b73d..51fc453 100644 --- a/virtweb_frontend/src/routes/VMListRoute.tsx +++ b/virtweb_frontend/src/routes/VMListRoute.tsx @@ -79,8 +79,6 @@ function VMListWidget(p: { setRunningVMs(new Set([...runningVMs])); }; - console.log(runningVMs); - return (