387 lines
13 KiB
Rust
387 lines
13 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(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 Network {
|
||
|
mac: String,
|
||
|
#[serde(flatten)]
|
||
|
r#type: NetworkType,
|
||
|
}
|
||
|
|
||
|
#[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>,
|
||
|
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_tomain(&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 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),
|
||
|
};
|
||
|
|
||
|
// 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
|
||
|
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,
|
||
|
})
|
||
|
}
|
||
|
|
||
|
let mut networks = vec![];
|
||
|
for n in &self.networks {
|
||
|
networks.push(match &n.r#type {
|
||
|
NetworkType::UserspaceSLIRPStack => DomainNetInterfaceXML {
|
||
|
mac: NetMacAddress {
|
||
|
address: n.mac.to_string(),
|
||
|
},
|
||
|
r#type: "user".to_string(),
|
||
|
source: None,
|
||
|
model: Some(NetIntModelXML {
|
||
|
r#type: "virtio".to_string(),
|
||
|
}),
|
||
|
},
|
||
|
NetworkType::DefinedNetwork { network } => DomainNetInterfaceXML {
|
||
|
mac: NetMacAddress {
|
||
|
address: n.mac.to_string(),
|
||
|
},
|
||
|
r#type: "network".to_string(),
|
||
|
source: Some(NetIntSourceXML {
|
||
|
network: network.to_string(),
|
||
|
}),
|
||
|
model: Some(NetIntModelXML {
|
||
|
r#type: "virtio".to_string(),
|
||
|
}),
|
||
|
},
|
||
|
})
|
||
|
}
|
||
|
|
||
|
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(),
|
||
|
|
||
|
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,
|
||
|
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}! "
|
||
|
)));
|
||
|
}
|
||
|
},
|
||
|
})
|
||
|
})
|
||
|
.collect::<Result<Vec<_>, _>>()?,
|
||
|
|
||
|
tpm_module: domain.devices.tpm.is_some(),
|
||
|
})
|
||
|
}
|
||
|
}
|