Pierre HUBERT
2fadf53dea
All checks were successful
continuous-integration/drone/push Build is passing
472 lines
16 KiB
Rust
472 lines
16 KiB
Rust
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<NWFilterParam>,
|
|
}
|
|
|
|
#[derive(serde::Serialize, serde::Deserialize)]
|
|
pub struct Network {
|
|
#[serde(flatten)]
|
|
r#type: NetworkType,
|
|
mac: String,
|
|
nwfilterref: Option<NWFilterRef>,
|
|
}
|
|
|
|
#[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>,
|
|
/// Group associated with the VM (VirtWeb specific field)
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub group: Option<VMGroupId>,
|
|
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
|
|
pub fn as_domain(&self) -> anyhow::Result<DomainXML> {
|
|
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<Self> {
|
|
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::<Result<Vec<_>, _>>()?,
|
|
|
|
tpm_module: domain.devices.tpm.is_some(),
|
|
})
|
|
}
|
|
}
|