use crate::app_config::AppConfig; use crate::constants; use crate::libvirt_lib_structures::domain::*; use crate::libvirt_lib_structures::XMLUuid; use crate::libvirt_rest_structures::LibVirtStructError; use crate::libvirt_rest_structures::LibVirtStructError::StructureExtraction; use crate::utils::disks_utils::Disk; use crate::utils::files_utils; use crate::utils::files_utils::convert_size_unit_to_mb; use lazy_regex::regex; use num::Integer; #[derive( Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, Hash, Ord, PartialOrd, )] pub struct VMGroupId(pub String); #[derive(serde::Serialize, serde::Deserialize)] pub enum BootType { UEFI, UEFISecureBoot, } #[derive(serde::Serialize, serde::Deserialize)] pub enum VMArchitecture { #[serde(rename = "i686")] I686, #[serde(rename = "x86_64")] X86_64, } #[derive(serde::Serialize, serde::Deserialize)] pub struct NWFilterParam { name: String, value: String, } #[derive(serde::Serialize, serde::Deserialize)] pub struct NWFilterRef { name: String, parameters: Vec, } #[derive(serde::Serialize, serde::Deserialize)] pub struct Network { #[serde(flatten)] r#type: NetworkType, mac: String, nwfilterref: Option, } #[derive(serde::Serialize, serde::Deserialize)] #[serde(tag = "type")] pub enum NetworkType { UserspaceSLIRPStack, DefinedNetwork { network: String }, // TODO : complete network types } #[derive(serde::Serialize, serde::Deserialize)] pub struct VMInfo { /// VM name (alphanumeric characters only) pub name: String, pub uuid: Option, 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 pub memory: usize, /// Number of vCPU for the VM pub number_vcpu: usize, /// Enable VNC access through admin console pub vnc_access: bool, /// Attach ISO file(s) pub iso_files: Vec, /// Storage - https://access.redhat.com/documentation/fr-fr/red_hat_enterprise_linux/6/html/virtualization_administration_guide/sect-virtualization-virtualized_block_devices-adding_storage_devices_to_guests#sect-Virtualization-Adding_storage_devices_to_guests-Adding_file_based_storage_to_a_guest pub disks: Vec, /// Network cards pub networks: Vec, /// Add a TPM v2.0 module pub tpm_module: bool, } impl VMInfo { /// Turn this VM into a domain 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()); } let uuid = if let Some(n) = self.uuid { if !n.is_valid() { return Err(StructureExtraction("VM UUID is invalid!").into()); } n } else { XMLUuid::new_random() }; if let Some(n) = &self.genid { if !n.is_valid() { return Err(StructureExtraction("VM genid is invalid!").into()); } } if let Some(n) = &self.title { if n.contains('\n') { return Err(StructureExtraction("VM title contain newline char!").into()); } } if let Some(group) = &self.group { if !regex!("^[a-zA-Z0-9]+$").is_match(&group.0) { 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()); } if self.number_vcpu == 0 || (self.number_vcpu != 1 && self.number_vcpu.is_odd()) { return Err(StructureExtraction("Invalid number of vCPU specified!").into()); } let mut disks = vec![]; for iso_file in &self.iso_files { if !files_utils::check_file_name(iso_file) { return Err(StructureExtraction("ISO filename is invalid!").into()); } let path = AppConfig::get().iso_storage_path().join(iso_file); if !path.exists() { return Err(StructureExtraction("Specified ISO file does not exists!").into()); } disks.push(DiskXML { r#type: "file".to_string(), device: "cdrom".to_string(), driver: DiskDriverXML { name: "qemu".to_string(), r#type: "raw".to_string(), cache: "none".to_string(), }, source: DiskSourceXML { file: path.to_string_lossy().to_string(), }, target: DiskTargetXML { dev: format!( "hd{}", ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"][disks.len()] ), bus: "sata".to_string(), }, readonly: Some(DiskReadOnlyXML {}), boot: DiskBootXML { order: (disks.len() + 1).to_string(), }, address: None, }) } let (vnc_graphics, vnc_video) = match self.vnc_access { true => ( Some(GraphicsXML { r#type: "vnc".to_string(), socket: AppConfig::get() .vnc_socket_for_domain(&self.name) .to_string_lossy() .to_string(), }), Some(VideoXML { model: VideoModelXML { r#type: "virtio".to_string(), //"qxl".to_string(), }, }), ), false => (None, None), }; // Process network card let mut networks = vec![]; for n in &self.networks { let mac = NetMacAddress { address: n.mac.to_string(), }; let model = Some(NetIntModelXML { r#type: "virtio".to_string(), }); let filterref = if let Some(n) = &n.nwfilterref { if !regex!("^[a-zA-Z0-9\\_\\-]+$").is_match(&n.name) { log::error!("Filter ref name {} is invalid", n.name); return Err(StructureExtraction("Network filter ref name is invalid!").into()); } for p in &n.parameters { if !regex!("^[a-zA-Z0-9_-]+$").is_match(&p.name) { return Err(StructureExtraction( "Network filter ref parameter name is invalid!", ) .into()); } } Some(NetIntfilterRefXML { filter: n.name.to_string(), parameters: n .parameters .iter() .map(|f| NetIntFilterParameterXML { name: f.name.to_string(), value: f.value.to_string(), }) .collect(), }) } else { None }; networks.push(match &n.r#type { NetworkType::UserspaceSLIRPStack => DomainNetInterfaceXML { mac, r#type: "user".to_string(), source: None, model, filterref, }, NetworkType::DefinedNetwork { network } => DomainNetInterfaceXML { mac, r#type: "network".to_string(), source: Some(NetIntSourceXML { network: network.to_string(), }), model, filterref, }, }) } // Check disks name for duplicates for disk in &self.disks { if self.disks.iter().filter(|d| d.name == disk.name).count() > 1 { return Err(StructureExtraction("Two different disks have the same name!").into()); } } // Apply disks configuration. Starting from now, the function should ideally never fail due to // bad user input for disk in &self.disks { disk.check_config()?; disk.apply_config(uuid)?; if disk.delete { continue; } disks.push(DiskXML { r#type: "file".to_string(), device: "disk".to_string(), driver: DiskDriverXML { name: "qemu".to_string(), r#type: "raw".to_string(), cache: "none".to_string(), }, source: DiskSourceXML { file: disk.disk_path(uuid).to_string_lossy().to_string(), }, target: DiskTargetXML { dev: format!( "vd{}", ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"][disks.len()] ), bus: "virtio".to_string(), }, readonly: None, boot: DiskBootXML { order: (disks.len() + 1).to_string(), }, address: None, }) } Ok(DomainXML { r#type: "kvm".to_string(), name: self.name.to_string(), uuid: Some(uuid), genid: self.genid.map(|i| i.0), title: self.title.clone(), description: self.description.clone(), metadata: Some(DomainMetadataXML { virtweb: DomainMetadataVirtWebXML { ns: "https://virtweb.communiquons.org".to_string(), group: self.group.clone().map(|g| g.0), }, }), os: OSXML { r#type: OSTypeXML { arch: match self.architecture { VMArchitecture::I686 => "i686", VMArchitecture::X86_64 => "x86_64", } .to_string(), machine: "q35".to_string(), body: "hvm".to_string(), }, firmware: "efi".to_string(), loader: Some(OSLoaderXML { secure: match self.boot_type { BootType::UEFI => "no".to_string(), BootType::UEFISecureBoot => "yes".to_string(), }, }), }, features: FeaturesXML { acpi: ACPIXML {} }, devices: DevicesXML { graphics: vnc_graphics, video: vnc_video, disks, net_interfaces: networks, inputs: vec![ DomainInputXML { r#type: "mouse".to_string(), }, DomainInputXML { r#type: "keyboard".to_string(), }, DomainInputXML { r#type: "tablet".to_string(), }, ], tpm: match self.tpm_module { true => Some(TPMDeviceXML { model: "tpm-tis".to_string(), backend: TPMBackendXML { r#type: "emulator".to_string(), version: "2.0".to_string(), }, }), false => None, }, }, memory: DomainMemoryXML { unit: "MB".to_string(), memory: self.memory, }, vcpu: DomainVCPUXML { body: self.number_vcpu, }, cpu: DomainCPUXML { mode: "host-passthrough".to_string(), topology: Some(DomainCPUTopology { sockets: 1, cores: match self.number_vcpu { 1 => 1, v => v / 2, }, threads: match self.number_vcpu { 1 => 1, _ => 2, }, }), }, on_poweroff: "destroy".to_string(), on_reboot: "restart".to_string(), on_crash: "destroy".to_string(), }) } /// Turn a domain into a vm pub fn from_domain(domain: DomainXML) -> anyhow::Result { Ok(Self { name: domain.name, uuid: domain.uuid, genid: domain.genid.map(XMLUuid), title: domain.title, description: domain.description, group: domain .metadata .clone() .unwrap_or_default() .virtweb .group .map(VMGroupId), boot_type: match domain.os.loader { None => BootType::UEFI, Some(l) => match l.secure.as_str() { "yes" => BootType::UEFISecureBoot, _ => BootType::UEFI, }, }, architecture: match domain.os.r#type.arch.as_str() { "i686" => VMArchitecture::I686, "x86_64" => VMArchitecture::X86_64, a => { return Err(LibVirtStructError::DomainExtraction(format!( "Unknown architecture: {a}! " )) .into()); } }, number_vcpu: domain.vcpu.body, memory: convert_size_unit_to_mb(&domain.memory.unit, domain.memory.memory)?, vnc_access: domain.devices.graphics.is_some(), iso_files: domain .devices .disks .iter() .filter(|d| d.device == "cdrom") .map(|d| d.source.file.rsplit_once('/').unwrap().1.to_string()) .collect(), disks: domain .devices .disks .iter() .filter(|d| d.device == "disk") .map(|d| Disk::load_from_file(&d.source.file).unwrap()) .collect(), networks: domain .devices .net_interfaces .iter() .map(|d| { Ok(Network { mac: d.mac.address.to_string(), r#type: match d.r#type.as_str() { "user" => NetworkType::UserspaceSLIRPStack, "network" => NetworkType::DefinedNetwork { network: d.source.as_ref().unwrap().network.to_string(), }, a => { return Err(LibVirtStructError::DomainExtraction(format!( "Unknown network interface type: {a}! " ))); } }, nwfilterref: d.filterref.as_ref().map(|f| NWFilterRef { name: f.filter.to_string(), parameters: f .parameters .iter() .map(|p| NWFilterParam { name: p.name.to_string(), value: p.value.to_string(), }) .collect(), }), }) }) .collect::, _>>()?, tpm_module: domain.devices.tpm.is_some(), }) } }