VirtWeb/virtweb_backend/src/libvirt_rest_structures/vm.rs

472 lines
16 KiB
Rust
Raw Normal View History

2023-12-28 18:29:26 +00:00
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;
2024-11-02 17:40:06 +00:00
#[derive(
Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, Hash, Ord, PartialOrd,
)]
2024-11-02 16:44:10 +00:00
pub struct VMGroupId(pub String);
2023-12-28 18:29:26 +00:00
#[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,
}
2024-01-02 17:56:16 +00:00
#[derive(serde::Serialize, serde::Deserialize)]
pub struct NWFilterParam {
name: String,
value: String,
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct NWFilterRef {
name: String,
parameters: Vec<NWFilterParam>,
}
2023-12-28 18:29:26 +00:00
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Network {
#[serde(flatten)]
r#type: NetworkType,
2024-01-02 17:56:16 +00:00
mac: String,
nwfilterref: Option<NWFilterRef>,
2023-12-28 18:29:26 +00:00
}
#[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<XMLUuid>,
pub genid: Option<XMLUuid>,
pub title: Option<String>,
pub description: Option<String>,
2024-11-02 16:31:05 +00:00
/// Group associated with the VM (VirtWeb specific field)
#[serde(skip_serializing_if = "Option::is_none")]
2024-11-02 16:44:10 +00:00
pub group: Option<VMGroupId>,
2023-12-28 18:29:26 +00:00
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<String>,
/// 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<Disk>,
/// Network cards
pub networks: Vec<Network>,
/// Add a TPM v2.0 module
pub tpm_module: bool,
}
impl VMInfo {
/// Turn this VM into a domain
2024-11-02 16:31:05 +00:00
pub fn as_domain(&self) -> anyhow::Result<DomainXML> {
2023-12-28 18:29:26 +00:00
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());
}
}
2024-11-02 16:31:05 +00:00
if let Some(group) = &self.group {
2024-11-02 16:44:10 +00:00
if !regex!("^[a-zA-Z0-9]+$").is_match(&group.0) {
2024-11-02 16:31:05 +00:00
return Err(StructureExtraction("VM group name is invalid!").into());
}
}
2023-12-28 18:29:26 +00:00
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),
};
2024-01-02 17:56:16 +00:00
// 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,
},
})
}
2023-12-28 18:29:26 +00:00
// 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());
}
}
2024-01-02 17:56:16 +00:00
// Apply disks configuration. Starting from now, the function should ideally never fail due to
// bad user input
2023-12-28 18:29:26 +00:00
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(),
2024-11-02 16:31:05 +00:00
metadata: Some(DomainMetadataXML {
virtweb: DomainMetadataVirtWebXML {
ns: "https://virtweb.communiquons.org".to_string(),
2024-11-02 16:44:10 +00:00
group: self.group.clone().map(|g| g.0),
2024-11-02 16:31:05 +00:00
},
}),
2023-12-28 18:29:26 +00:00
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<Self> {
Ok(Self {
name: domain.name,
uuid: domain.uuid,
genid: domain.genid.map(XMLUuid),
title: domain.title,
description: domain.description,
2024-11-02 16:44:10 +00:00
group: domain
.metadata
.clone()
.unwrap_or_default()
.virtweb
.group
.map(VMGroupId),
2023-12-28 18:29:26 +00:00
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}! "
)));
}
},
2024-01-02 17:56:16 +00:00
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(),
}),
2023-12-28 18:29:26 +00:00
})
})
.collect::<Result<Vec<_>, _>>()?,
tpm_module: domain.devices.tpm.is_some(),
})
}
}