From b7d7027eecb070c1a967521d0a14e43cca8752c7 Mon Sep 17 00:00:00 2001 From: Kieran Date: Tue, 1 Apr 2025 11:22:10 +0100 Subject: [PATCH] feat: libvirt domain xml progress --- Cargo.lock | 16 ++ Cargo.toml | 10 +- README.md | 13 +- src/host/libvirt.rs | 552 +++++++++++++++++++++++++++++++++++++++++++- src/host/mod.rs | 150 ++++++++++++ src/host/proxmox.rs | 149 +----------- src/settings.rs | 3 + 7 files changed, 738 insertions(+), 155 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7d3dc4e..f237b7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2058,6 +2058,7 @@ dependencies = [ "native-tls", "nostr", "nostr-sdk", + "quick-xml", "rand 0.9.0", "reqwest", "rocket", @@ -2073,6 +2074,7 @@ dependencies = [ "tokio-stream", "tokio-tungstenite 0.21.0", "urlencoding", + "uuid", "virt", ] @@ -2867,6 +2869,16 @@ dependencies = [ "cc", ] +[[package]] +name = "quick-xml" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf763ab1c7a3aa408be466efc86efe35ed1bd3dd74173ed39d6b0d0a6f0ba148" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "quote" version = "1.0.40" @@ -4669,6 +4681,10 @@ name = "uuid" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +dependencies = [ + "getrandom 0.3.2", + "serde", +] [[package]] name = "valuable" diff --git a/Cargo.toml b/Cargo.toml index d1636a7..3dd7b6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,14 +15,13 @@ default = [ "lnd", "cloudflare", "revolut", - "bitvora", - "libvirt" + "bitvora" ] mikrotik = ["dep:reqwest"] nostr-dm = ["dep:nostr-sdk"] nostr-dvm = ["dep:nostr-sdk"] proxmox = ["dep:reqwest", "dep:ssh2", "dep:tokio-tungstenite"] -libvirt = ["dep:virt"] +libvirt = ["dep:virt", "dep:uuid", "dep:quick-xml"] lnd = ["dep:fedimint-tonic-lnd"] bitvora = ["dep:reqwest", "dep:tokio-stream"] cloudflare = ["dep:reqwest"] @@ -34,6 +33,7 @@ tokio = { version = "1.37.0", features = ["rt", "rt-multi-thread", "macros", "sy anyhow = "1.0.83" config = { version = "0.15.8", features = ["yaml"] } log = "0.4.21" +env_logger = "0.11.7" serde = { version = "1.0.213", features = ["derive"] } serde_json = "1.0.132" rocket = { version = "0.5.1", features = ["json"] } @@ -64,6 +64,9 @@ reqwest = { version = "0.12.8", optional = true } #libvirt virt = { git = "https://gitlab.com/libvirt/libvirt-rust.git", optional = true } +#virtxml = {git = "https://gitlab.com/libvirt/libvirt-rust-xml.git", optional = true} +uuid = { version = "1.16.0", features = ["v4", "serde"], optional = true } +quick-xml = { version = "0.37.3", features = ["serde", "serialize"], optional = true } #lnd fedimint-tonic-lnd = { version = "0.2.0", default-features = false, features = ["invoicesrpc"], optional = true } @@ -74,4 +77,3 @@ tokio-stream = { version = "0.1.17", features = ["sync"], optional = true } #revolut sha2 = { version = "0.10.8", optional = true } hmac = { version = "0.12.1", optional = true } -env_logger = "0.11.7" diff --git a/README.md b/README.md index 4a2eba5..4168f46 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ A bitcoin powered VPS system. - [RevolutPay](https://www.revolut.com/business/revolut-pay/) - VM Backend: - Proxmox + - LibVirt (WIP) - Network Resources: - Mikrotik JSON-API - OVH API (dedicated server virtual mac) @@ -42,10 +43,18 @@ delete-after: 3 read-only: false # Provisioner is the main process which handles creating/deleting VM's -# Currently supports: Proxmox provisioner: proxmox: - # Proxmox (QEMU) settings used for spawning VM's + # QEMU settings used for spawning VM's + qemu: + bios: "ovmf" + machine: "q35" + os-type: "l26" + bridge: "vmbr0" + cpu: "kvm64" + kvm: false + libvirt: + # QEMU settings used for spawning VM's qemu: bios: "ovmf" machine: "q35" diff --git a/src/host/libvirt.rs b/src/host/libvirt.rs index 19cbc3d..c5215a4 100644 --- a/src/host/libvirt.rs +++ b/src/host/libvirt.rs @@ -5,12 +5,20 @@ use crate::host::{ use crate::settings::QemuConfig; use crate::status::{VmRunningState, VmState}; use crate::KB; -use anyhow::{Context, Result}; +use anyhow::{bail, ensure, Context, Result}; use chrono::Utc; -use lnvps_db::{async_trait, Vm, VmOsImage}; +use lnvps_db::{async_trait, LNVpsDb, Vm, VmOsImage}; +use log::info; +use rand::random; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; +use std::sync::Arc; +use uuid::Uuid; use virt::connect::Connect; use virt::domain::Domain; -use virt::sys::{virDomainCreate, VIR_CONNECT_LIST_STORAGE_POOLS_ACTIVE}; +use virt::sys::{ + virDomainCreate, VIR_CONNECT_LIST_STORAGE_POOLS_ACTIVE, VIR_DOMAIN_START_VALIDATE, +}; #[derive(Debug)] pub struct LibVirtHost { @@ -25,6 +33,94 @@ impl LibVirtHost { qemu, }) } + + pub fn import_disk_image(&self, vm: &Vm, image: &VmOsImage) -> Result<()> { + // https://libvirt.org/html/libvirt-libvirt-storage.html#virStorageVolUpload + // https://libvirt.org/html/libvirt-libvirt-storage.html#virStorageVolResize + Ok(()) + } + + pub fn create_domain_xml(&self, cfg: &FullVmInfo) -> Result { + let storage = self + .connection + .list_all_storage_pools(VIR_CONNECT_LIST_STORAGE_POOLS_ACTIVE)?; + + // check the storage disk exists, we don't need anything else from it for now + let _storage_disk = if let Some(d) = storage + .iter() + .find(|s| s.get_name().map(|n| n == cfg.disk.name).unwrap_or(false)) + { + d + } else { + bail!( + "Disk \"{}\" not found on host! Available pools: {}", + cfg.disk.name, + storage + .iter() + .filter_map(|s| s.get_name().ok()) + .collect::>() + .join(",") + ); + }; + + let resources = cfg.resources()?; + let mut devices = vec![]; + // primary disk + devices.push(DomainDevice::Disk(Disk { + kind: DiskType::File, + device: DiskDevice::Disk, + source: DiskSource { + file: Some(format!("{}:vm-{}-disk0", cfg.disk.name, cfg.vm.id)), + ..Default::default() + }, + target: DiskTarget { + dev: "vda".to_string(), + bus: Some(DiskBus::VirtIO), + }, + })); + devices.push(DomainDevice::Interface(NetworkInterface { + kind: NetworkKind::Bridge, + mac: Some(NetworkMac { + address: cfg.vm.mac_address.clone(), + }), + source: Some(NetworkSource { + bridge: Some(self.qemu.bridge.clone()), + }), + target: None, + vlan: cfg.host.vlan_id.map(|v| NetworkVlan { + tags: vec![NetworkVlanTag { id: v as u32 }], + }), + })); + Ok(DomainXML { + kind: DomainType::KVM, + id: Some(cfg.vm.id), + name: Some(format!("VM{}", cfg.vm.id)), + uuid: None, + title: None, + description: None, + os: DomainOs { + kind: DomainOsType { + kind: DomainOsTypeKind::Hvm, + arch: Some(DomainOsArch::from_str(&self.qemu.arch)?), + machine: Some(DomainOsMachine::from_str(&self.qemu.machine)?), + }, + firmware: Some(DomainOsFirmware::EFI), + loader: Some(DomainOsLoader { + read_only: None, + kind: None, + secure: Some(true), + stateless: None, + format: None, + }), + boot: DomainOsBoot { + dev: DomainOsBootDev::HardDrive, + }, + }, + vcpu: resources.cpu, + memory: resources.memory, + devices: DomainDevices { contents: devices }, + }) + } } #[async_trait] @@ -52,11 +148,17 @@ impl VmHostClient for LibVirtHost { } async fn download_os_image(&self, image: &VmOsImage) -> Result<()> { + // TODO: download ISO images to host (somehow, ssh?) Ok(()) } - async fn generate_mac(&self, vm: &Vm) -> Result { - Ok("ff:ff:ff:ff:ff:ff".to_string()) + async fn generate_mac(&self, _vm: &Vm) -> Result { + Ok(format!( + "52:54:00:{}:{}:{}", + hex::encode([random::()]), + hex::encode([random::()]), + hex::encode([random::()]) + )) } async fn start_vm(&self, vm: &Vm) -> Result<()> { @@ -72,6 +174,10 @@ impl VmHostClient for LibVirtHost { } async fn create_vm(&self, cfg: &FullVmInfo) -> Result<()> { + let domain = self.create_domain_xml(cfg)?; + let xml = quick_xml::se::to_string(&domain)?; + let domain = Domain::create_xml(&self.connection, &xml, VIR_DOMAIN_START_VALIDATE)?; + Ok(()) } @@ -113,3 +219,439 @@ impl VmHostClient for LibVirtHost { todo!() } } + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +#[serde(rename = "domain")] +struct DomainXML { + #[serde(rename = "@type")] + pub kind: DomainType, + + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "@id")] + pub id: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub uuid: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + pub os: DomainOs, + pub vcpu: u16, + pub memory: u64, + pub devices: DomainDevices, +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +#[serde(rename = "devices")] +struct DomainDevices { + #[serde(rename = "$value")] + pub contents: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +#[serde(rename_all = "lowercase")] +enum DomainType { + #[default] + KVM, + XEN, + HVF, + QEMU, + LXC, +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +#[serde(rename = "os")] +struct DomainOs { + #[serde(rename = "type")] + pub kind: DomainOsType, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "@firmware")] + pub firmware: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub loader: Option, + pub boot: DomainOsBoot, +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +#[serde(rename_all = "lowercase")] +enum DomainOsFirmware { + #[default] + EFI, + BIOS, +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +struct DomainOsType { + #[serde(rename = "$text")] + pub kind: DomainOsTypeKind, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "@arch")] + pub arch: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "@machine")] + pub machine: Option, +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +#[serde(rename_all = "lowercase")] +enum DomainOsTypeKind { + #[default] + Hvm, + Xen, + Linux, + XenPvh, + Exe, +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +#[serde(rename_all = "lowercase")] +enum DomainOsMachine { + #[default] + Q35, + PC, +} + +impl FromStr for DomainOsMachine { + type Err = anyhow::Error; + + fn from_str(s: &str) -> std::result::Result { + match s.to_lowercase().as_str() { + "q35" => Ok(DomainOsMachine::Q35), + "pc" => Ok(DomainOsMachine::PC), + v => bail!("Unknown machine type {}", v), + } + } +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +#[serde(rename_all = "lowercase")] +enum DomainOsArch { + #[default] + X86_64, + I686, +} + +impl FromStr for DomainOsArch { + type Err = anyhow::Error; + + fn from_str(s: &str) -> std::result::Result { + match s.to_lowercase().as_str() { + "x86_64" => Ok(Self::X86_64), + "i686" => Ok(Self::I686), + v => bail!("unsupported arch {}", v), + } + } +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +#[serde(rename = "loader")] +struct DomainOsLoader { + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "@readonly")] + pub read_only: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "@type")] + pub kind: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "@secure")] + pub secure: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "@stateless")] + pub stateless: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "@format")] + pub format: Option, +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +#[serde(rename_all = "lowercase")] +enum DomainOsLoaderType { + #[default] + ROM, + PFlash, +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +#[serde(rename_all = "lowercase")] +enum DomainOsLoaderFormat { + Raw, + #[default] + QCow2, +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +struct DomainOsBoot { + #[serde(rename = "@dev")] + pub dev: DomainOsBootDev, +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +#[serde(rename_all = "lowercase")] +enum DomainOsBootDev { + #[serde(rename = "fd")] + Floppy, + #[serde(rename = "hd")] + #[default] + HardDrive, + CdRom, + Network, +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +#[serde(rename = "vcpu")] +struct DomainVCPU { + #[serde(rename = "$text")] + pub count: u32, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +enum DomainDevice { + #[serde(rename = "disk")] + Disk(Disk), + #[serde(rename = "interface")] + Interface(NetworkInterface), + #[serde(other)] + Other, +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +#[serde(rename = "interface")] +struct NetworkInterface { + #[serde(rename = "@type")] + pub kind: NetworkKind, + #[serde(skip_serializing_if = "Option::is_none")] + pub mac: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub target: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub vlan: Option, +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +#[serde(rename = "vlan")] +struct NetworkVlan { + #[serde(rename = "tag")] + pub tags: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +#[serde(rename = "tag")] +struct NetworkVlanTag { + #[serde(rename = "@id")] + pub id: u32, +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +#[serde(rename_all = "lowercase")] +enum NetworkKind { + Network, + #[default] + Bridge, + User, + Ethernet, + Direct, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename = "mac")] +struct NetworkMac { + #[serde(rename = "@address")] + pub address: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename = "source")] +struct NetworkSource { + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "@bridge")] + pub bridge: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename = "target")] +struct NetworkTarget { + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "@dev")] + pub dev: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename = "disk")] +struct Disk { + #[serde(rename = "@type")] + pub kind: DiskType, + #[serde(rename = "@device")] + pub device: DiskDevice, + pub source: DiskSource, + pub target: DiskTarget, +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +#[serde(rename_all = "lowercase")] +enum DiskType { + #[default] + File, + Block, + Dir, + Network, + Volume, + Nvme, + VHostUser, + VHostVdpa, +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +#[serde(rename_all = "lowercase")] +enum DiskDevice { + Floppy, + #[default] + Disk, + CdRom, + Lun, +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +#[serde(rename = "source")] +struct DiskSource { + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "@file")] + pub file: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "@dir")] + pub dir: Option, +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +#[serde(rename = "target")] +struct DiskTarget { + /// Device name (hint) + #[serde(rename = "@dev")] + pub dev: String, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "@bus")] + pub bus: Option, +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +#[serde(rename_all = "lowercase")] +enum DiskBus { + #[default] + IDE, + SCSI, + VirtIO, + XEN, + USB, + SATA, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::host::tests::mock_full_vm; + + fn cfg() -> FullVmInfo { + let mut cfg = mock_full_vm(); + // adjust mock data for libvirt test driver + cfg.disk.name = "default-pool".to_string(); + + cfg + } + + #[test] + fn test_xml_os() -> Result<()> { + let tag = "hvm"; + + let test = DomainOs { + kind: DomainOsType { + kind: DomainOsTypeKind::Hvm, + arch: None, + machine: None, + }, + firmware: Some(DomainOsFirmware::EFI), + loader: None, + boot: DomainOsBoot { + dev: DomainOsBootDev::HardDrive, + }, + }; + + let xml = quick_xml::se::to_string(&test)?; + assert_eq!(tag, xml); + Ok(()) + } + + #[test] + fn text_xml_disk() -> Result<()> { + let tag = ""; + + let test = Disk { + kind: DiskType::File, + device: DiskDevice::Disk, + source: DiskSource { + file: Some("/var/lib/libvirt/images/disk.qcow2".to_string()), + ..Default::default() + }, + target: DiskTarget { + dev: "vda".to_string(), + bus: Some(DiskBus::VirtIO), + }, + }; + let xml = quick_xml::se::to_string(&test)?; + assert_eq!(tag, xml); + Ok(()) + } + + #[test] + fn text_config_to_domain() -> Result<()> { + let cfg = cfg(); + let template = cfg.template.clone().unwrap(); + + let q_cfg = QemuConfig { + machine: "q35".to_string(), + os_type: "l26".to_string(), + bridge: "vmbr0".to_string(), + cpu: "kvm64".to_string(), + kvm: true, + arch: "x86_64".to_string(), + }; + let host = LibVirtHost::new("test:///default", q_cfg)?; + let xml = host.create_domain_xml(&cfg)?; + + let res = cfg.resources()?; + assert_eq!(xml.vcpu, res.cpu); + assert_eq!(xml.memory, res.memory); + + let xml = quick_xml::se::to_string(&xml)?; + println!("{}", xml); + + let output = r#"VM1hvm22147483648"#; + assert_eq!(xml, output); + + Ok(()) + } + + #[ignore] + #[tokio::test] + async fn text_vm_lifecycle() -> Result<()> { + let cfg = cfg(); + let template = cfg.template.clone().unwrap(); + + let q_cfg = QemuConfig { + machine: "q35".to_string(), + os_type: "l26".to_string(), + bridge: "vmbr0".to_string(), + cpu: "kvm64".to_string(), + kvm: true, + arch: "x86_64".to_string(), + }; + let host = LibVirtHost::new("test:///default", q_cfg)?; + println!("{:?}", host.get_info().await?); + host.create_vm(&cfg).await?; + + Ok(()) + } +} diff --git a/src/host/mod.rs b/src/host/mod.rs index 0280d04..df45327 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -218,3 +218,153 @@ pub struct VmHostDiskInfo { pub size: u64, pub used: u64, } + +#[cfg(test)] +mod tests { + use chrono::Utc; + use lnvps_db::{DiskInterface, DiskType, IpRange, IpRangeAllocationMode, OsDistribution, UserSshKey, Vm, VmHost, VmHostDisk, VmIpAssignment, VmOsImage, VmTemplate}; + use crate::{GB, TB}; + use crate::host::FullVmInfo; + + pub fn mock_full_vm() -> FullVmInfo { + let template = VmTemplate { + id: 1, + name: "example".to_string(), + enabled: true, + created: Default::default(), + expires: None, + cpu: 2, + memory: 2 * GB, + disk_size: 100 * GB, + disk_type: DiskType::SSD, + disk_interface: DiskInterface::PCIe, + cost_plan_id: 1, + region_id: 1, + }; + FullVmInfo { + vm: Vm { + id: 1, + host_id: 1, + user_id: 1, + image_id: 1, + template_id: Some(template.id), + custom_template_id: None, + ssh_key_id: 1, + created: Default::default(), + expires: Default::default(), + disk_id: 1, + mac_address: "ff:ff:ff:ff:ff:fe".to_string(), + deleted: false, + ref_code: None, + }, + host: VmHost { + id: 1, + kind: Default::default(), + region_id: 1, + name: "mock".to_string(), + ip: "https://localhost:8006".to_string(), + cpu: 20, + memory: 128 * GB, + enabled: true, + api_token: "mock".to_string(), + load_cpu: 1.0, + load_memory: 1.0, + load_disk: 1.0, + vlan_id: Some(100), + }, + disk: VmHostDisk { + id: 1, + host_id: 1, + name: "ssd".to_string(), + size: TB * 20, + kind: DiskType::SSD, + interface: DiskInterface::PCIe, + enabled: true, + }, + template: Some(template.clone()), + custom_template: None, + image: VmOsImage { + id: 1, + distribution: OsDistribution::Ubuntu, + flavour: "Server".to_string(), + version: "24.04.03".to_string(), + enabled: true, + release_date: Utc::now(), + url: "http://localhost.com/ubuntu_server_24.04.img".to_string(), + default_username: None, + }, + ips: vec![ + VmIpAssignment { + id: 1, + vm_id: 1, + ip_range_id: 1, + ip: "192.168.1.2".to_string(), + deleted: false, + arp_ref: None, + dns_forward: None, + dns_forward_ref: None, + dns_reverse: None, + dns_reverse_ref: None, + }, + VmIpAssignment { + id: 2, + vm_id: 1, + ip_range_id: 2, + ip: "192.168.2.2".to_string(), + deleted: false, + arp_ref: None, + dns_forward: None, + dns_forward_ref: None, + dns_reverse: None, + dns_reverse_ref: None, + }, + VmIpAssignment { + id: 3, + vm_id: 1, + ip_range_id: 3, + ip: "fd00::ff:ff:ff:ff:ff".to_string(), + deleted: false, + arp_ref: None, + dns_forward: None, + dns_forward_ref: None, + dns_reverse: None, + dns_reverse_ref: None, + }, + ], + ranges: vec![ + IpRange { + id: 1, + cidr: "192.168.1.0/24".to_string(), + gateway: "192.168.1.1/16".to_string(), + enabled: true, + region_id: 1, + ..Default::default() + }, + IpRange { + id: 2, + cidr: "192.168.2.0/24".to_string(), + gateway: "10.10.10.10".to_string(), + enabled: true, + region_id: 2, + ..Default::default() + }, + IpRange { + id: 3, + cidr: "fd00::/64".to_string(), + gateway: "fd00::1".to_string(), + enabled: true, + region_id: 1, + allocation_mode: IpRangeAllocationMode::SlaacEui64, + ..Default::default() + }, + ], + ssh_key: UserSshKey { + id: 1, + name: "test".to_string(), + user_id: 1, + created: Default::default(), + key_data: "ssh-ed25519 AAA=".to_string(), + }, + } + } +} \ No newline at end of file diff --git a/src/host/proxmox.rs b/src/host/proxmox.rs index a6bf5e3..308416a 100644 --- a/src/host/proxmox.rs +++ b/src/host/proxmox.rs @@ -1152,153 +1152,13 @@ impl From for TimeSeriesData { #[cfg(test)] mod tests { use super::*; - use crate::{GB, MB, TB}; - use lnvps_db::{ - DiskInterface, IpRange, IpRangeAllocationMode, OsDistribution, UserSshKey, VmHost, - VmHostDisk, VmIpAssignment, VmTemplate, - }; + use crate::host::tests::mock_full_vm; + use crate::MB; #[test] fn test_config() -> Result<()> { - let template = VmTemplate { - id: 1, - name: "example".to_string(), - enabled: true, - created: Default::default(), - expires: None, - cpu: 2, - memory: 2 * GB, - disk_size: 100 * GB, - disk_type: DiskType::SSD, - disk_interface: DiskInterface::PCIe, - cost_plan_id: 1, - region_id: 1, - }; - let cfg = FullVmInfo { - vm: Vm { - id: 1, - host_id: 1, - user_id: 1, - image_id: 1, - template_id: Some(template.id), - custom_template_id: None, - ssh_key_id: 1, - created: Default::default(), - expires: Default::default(), - disk_id: 1, - mac_address: "ff:ff:ff:ff:ff:fe".to_string(), - deleted: false, - ref_code: None, - }, - host: VmHost { - id: 1, - kind: Default::default(), - region_id: 1, - name: "mock".to_string(), - ip: "https://localhost:8006".to_string(), - cpu: 20, - memory: 128 * GB, - enabled: true, - api_token: "mock".to_string(), - load_cpu: 1.0, - load_memory: 1.0, - load_disk: 1.0, - vlan_id: Some(100), - }, - disk: VmHostDisk { - id: 1, - host_id: 1, - name: "ssd".to_string(), - size: TB * 20, - kind: DiskType::SSD, - interface: DiskInterface::PCIe, - enabled: true, - }, - template: Some(template.clone()), - custom_template: None, - image: VmOsImage { - id: 1, - distribution: OsDistribution::Ubuntu, - flavour: "Server".to_string(), - version: "24.04.03".to_string(), - enabled: true, - release_date: Utc::now(), - url: "http://localhost.com/ubuntu_server_24.04.img".to_string(), - default_username: None, - }, - ips: vec![ - VmIpAssignment { - id: 1, - vm_id: 1, - ip_range_id: 1, - ip: "192.168.1.2".to_string(), - deleted: false, - arp_ref: None, - dns_forward: None, - dns_forward_ref: None, - dns_reverse: None, - dns_reverse_ref: None, - }, - VmIpAssignment { - id: 2, - vm_id: 1, - ip_range_id: 2, - ip: "192.168.2.2".to_string(), - deleted: false, - arp_ref: None, - dns_forward: None, - dns_forward_ref: None, - dns_reverse: None, - dns_reverse_ref: None, - }, - VmIpAssignment { - id: 3, - vm_id: 1, - ip_range_id: 3, - ip: "fd00::ff:ff:ff:ff:ff".to_string(), - deleted: false, - arp_ref: None, - dns_forward: None, - dns_forward_ref: None, - dns_reverse: None, - dns_reverse_ref: None, - }, - ], - ranges: vec![ - IpRange { - id: 1, - cidr: "192.168.1.0/24".to_string(), - gateway: "192.168.1.1/16".to_string(), - enabled: true, - region_id: 1, - ..Default::default() - }, - IpRange { - id: 2, - cidr: "192.168.2.0/24".to_string(), - gateway: "10.10.10.10".to_string(), - enabled: true, - region_id: 2, - ..Default::default() - }, - IpRange { - id: 3, - cidr: "fd00::/64".to_string(), - gateway: "fd00::1".to_string(), - enabled: true, - region_id: 1, - allocation_mode: IpRangeAllocationMode::SlaacEui64, - ..Default::default() - }, - ], - ssh_key: UserSshKey { - id: 1, - name: "test".to_string(), - user_id: 1, - created: Default::default(), - key_data: "ssh-ed25519 AAA=".to_string(), - }, - }; + let cfg = mock_full_vm(); + let template = cfg.template.clone().unwrap(); let q_cfg = QemuConfig { machine: "q35".to_string(), @@ -1306,6 +1166,7 @@ mod tests { bridge: "vmbr1".to_string(), cpu: "kvm64".to_string(), kvm: true, + arch: "x86_64".to_string(), }; let p = ProxmoxClient::new( diff --git a/src/settings.rs b/src/settings.rs index 4e90bee..4aa9e89 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -148,6 +148,8 @@ pub struct QemuConfig { pub cpu: String, /// Enable virtualization inside VM pub kvm: bool, + /// CPU architecture + pub arch: String, } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -217,6 +219,7 @@ pub fn mock_settings() -> Settings { bridge: "vmbr1".to_string(), cpu: "kvm64".to_string(), kvm: false, + arch: "x86_64".to_string(), }, ssh: None, mac_prefix: Some("ff:ff:ff".to_string()),