refactor: convert to workspace
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2025-04-02 13:18:18 +01:00
parent 2ae158c31a
commit 9296e571ec
54 changed files with 5494 additions and 97 deletions

View File

@ -0,0 +1,657 @@
use crate::host::{
FullVmInfo, TerminalStream, TimeSeries, TimeSeriesData, VmHostClient, VmHostDiskInfo,
VmHostInfo,
};
use crate::settings::QemuConfig;
use crate::status::{VmRunningState, VmState};
use crate::KB;
use anyhow::{bail, ensure, Context, Result};
use chrono::Utc;
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, VIR_DOMAIN_START_VALIDATE,
};
#[derive(Debug)]
pub struct LibVirtHost {
connection: Connect,
qemu: QemuConfig,
}
impl LibVirtHost {
pub fn new(url: &str, qemu: QemuConfig) -> Result<Self> {
Ok(Self {
connection: Connect::open(Some(url))?,
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<DomainXML> {
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::<Vec<_>>()
.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]
impl VmHostClient for LibVirtHost {
async fn get_info(&self) -> Result<VmHostInfo> {
let info = self.connection.get_node_info()?;
let storage = self
.connection
.list_all_storage_pools(VIR_CONNECT_LIST_STORAGE_POOLS_ACTIVE)?;
Ok(VmHostInfo {
cpu: info.cpus as u16,
memory: info.memory * KB,
disks: storage
.iter()
.filter_map(|p| {
let info = p.get_info().ok()?;
Some(VmHostDiskInfo {
name: p.get_name().context("storage pool name is missing").ok()?,
size: info.capacity,
used: info.allocation,
})
})
.collect(),
})
}
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<String> {
Ok(format!(
"52:54:00:{}:{}:{}",
hex::encode([random::<u8>()]),
hex::encode([random::<u8>()]),
hex::encode([random::<u8>()])
))
}
async fn start_vm(&self, vm: &Vm) -> Result<()> {
Ok(())
}
async fn stop_vm(&self, vm: &Vm) -> Result<()> {
Ok(())
}
async fn reset_vm(&self, vm: &Vm) -> Result<()> {
Ok(())
}
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(())
}
async fn delete_vm(&self, vm: &Vm) -> Result<()> {
todo!()
}
async fn reinstall_vm(&self, cfg: &FullVmInfo) -> Result<()> {
todo!()
}
async fn get_vm_state(&self, vm: &Vm) -> Result<VmState> {
Ok(VmState {
timestamp: Utc::now().timestamp() as u64,
state: VmRunningState::Stopped,
cpu_usage: 0.0,
mem_usage: 0.0,
uptime: 0,
net_in: 0,
net_out: 0,
disk_write: 0,
disk_read: 0,
})
}
async fn configure_vm(&self, vm: &FullVmInfo) -> Result<()> {
todo!()
}
async fn get_time_series_data(
&self,
vm: &Vm,
series: TimeSeries,
) -> Result<Vec<TimeSeriesData>> {
todo!()
}
async fn connect_terminal(&self, vm: &Vm) -> Result<TerminalStream> {
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<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub uuid: Option<Uuid>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
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<DomainDevice>,
}
#[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<DomainOsFirmware>,
#[serde(skip_serializing_if = "Option::is_none")]
pub loader: Option<DomainOsLoader>,
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<DomainOsArch>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "@machine")]
pub machine: Option<DomainOsMachine>,
}
#[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<Self, Self::Err> {
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<Self, Self::Err> {
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<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "@type")]
pub kind: Option<DomainOsLoaderType>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "@secure")]
pub secure: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "@stateless")]
pub stateless: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "@format")]
pub format: Option<DomainOsLoaderFormat>,
}
#[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<NetworkMac>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<NetworkSource>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target: Option<NetworkTarget>,
#[serde(skip_serializing_if = "Option::is_none")]
pub vlan: Option<NetworkVlan>,
}
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
#[serde(rename = "vlan")]
struct NetworkVlan {
#[serde(rename = "tag")]
pub tags: Vec<NetworkVlanTag>,
}
#[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<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename = "target")]
struct NetworkTarget {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "@dev")]
pub dev: Option<String>,
}
#[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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "@dir")]
pub dir: Option<String>,
}
#[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<DiskBus>,
}
#[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 = "<os firmware=\"efi\"><type>hvm</type><boot dev=\"hd\"/></os>";
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 = "<disk type=\"file\" device=\"disk\"><source file=\"/var/lib/libvirt/images/disk.qcow2\"/><target dev=\"vda\" bus=\"virtio\"/></disk>";
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#"<domain type="kvm" id="1"><name>VM1</name><os firmware="efi"><type arch="x86_64" machine="q35">hvm</type><loader secure="true"/><boot dev="hd"/></os><vcpu>2</vcpu><memory>2147483648</memory><devices><disk type="file" device="disk"><source file="default-pool:vm-1-disk0"/><target dev="vda" bus="virtio"/></disk><interface type="bridge"><mac address="ff:ff:ff:ff:ff:fe"/><source bridge="vmbr0"/><vlan><tag id="100"/></vlan></interface></devices></domain>"#;
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(())
}
}

370
lnvps_api/src/host/mod.rs Normal file
View File

@ -0,0 +1,370 @@
use crate::settings::ProvisionerConfig;
use crate::status::VmState;
use anyhow::{bail, Result};
use futures::future::join_all;
use lnvps_db::{
async_trait, IpRange, LNVpsDb, UserSshKey, Vm, VmCustomTemplate, VmHost, VmHostDisk,
VmHostKind, VmIpAssignment, VmOsImage, VmTemplate,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::sync::Arc;
use tokio::sync::mpsc::{Receiver, Sender};
#[cfg(feature = "libvirt")]
mod libvirt;
#[cfg(feature = "proxmox")]
mod proxmox;
pub struct TerminalStream {
pub rx: Receiver<Vec<u8>>,
pub tx: Sender<Vec<u8>>,
}
/// Generic type for creating VM's
#[async_trait]
pub trait VmHostClient: Send + Sync {
async fn get_info(&self) -> Result<VmHostInfo>;
/// Download OS image to the host
async fn download_os_image(&self, image: &VmOsImage) -> Result<()>;
/// Create a random MAC address for the NIC
async fn generate_mac(&self, vm: &Vm) -> Result<String>;
/// Start a VM
async fn start_vm(&self, vm: &Vm) -> Result<()>;
/// Stop a VM
async fn stop_vm(&self, vm: &Vm) -> Result<()>;
/// Reset VM (Hard)
async fn reset_vm(&self, vm: &Vm) -> Result<()>;
/// Spawn a VM
async fn create_vm(&self, cfg: &FullVmInfo) -> Result<()>;
/// Delete a VM
async fn delete_vm(&self, vm: &Vm) -> Result<()>;
/// Re-install a vm OS
async fn reinstall_vm(&self, cfg: &FullVmInfo) -> Result<()>;
/// Get the running status of a VM
async fn get_vm_state(&self, vm: &Vm) -> Result<VmState>;
/// Apply vm configuration (patch)
async fn configure_vm(&self, cfg: &FullVmInfo) -> Result<()>;
/// Get resource usage data
async fn get_time_series_data(
&self,
vm: &Vm,
series: TimeSeries,
) -> Result<Vec<TimeSeriesData>>;
/// Connect to terminal serial port
async fn connect_terminal(&self, vm: &Vm) -> Result<TerminalStream>;
}
pub fn get_host_client(host: &VmHost, cfg: &ProvisionerConfig) -> Result<Arc<dyn VmHostClient>> {
#[cfg(test)]
return Ok(Arc::new(crate::mocks::MockVmHost::new()));
Ok(match host.kind.clone() {
#[cfg(feature = "proxmox")]
VmHostKind::Proxmox if cfg.proxmox.is_some() => {
let cfg = cfg.proxmox.clone().unwrap();
Arc::new(proxmox::ProxmoxClient::new(
host.ip.parse()?,
&host.name,
&host.api_token,
cfg.mac_prefix,
cfg.qemu,
cfg.ssh,
))
}
#[cfg(feature = "libvirt")]
VmHostKind::LibVirt if cfg.libvirt.is_some() => {
let cfg = cfg.libvirt.clone().unwrap();
Arc::new(libvirt::LibVirtHost::new(&host.ip, cfg.qemu)?)
}
_ => bail!("Unknown host config: {}", host.kind),
})
}
/// All VM info necessary to provision a VM and its associated resources
pub struct FullVmInfo {
/// Instance to create
pub vm: Vm,
/// Host where the VM will be spawned
pub host: VmHost,
/// Disk where this VM will be saved on the host
pub disk: VmHostDisk,
/// VM template resources
pub template: Option<VmTemplate>,
/// VM custom template resources
pub custom_template: Option<VmCustomTemplate>,
/// The OS image used to create the VM
pub image: VmOsImage,
/// List of IP resources assigned to this VM
pub ips: Vec<VmIpAssignment>,
/// Ranges associated with [ips]
pub ranges: Vec<IpRange>,
/// SSH key to access the VM
pub ssh_key: UserSshKey,
}
impl FullVmInfo {
pub async fn load(vm_id: u64, db: Arc<dyn LNVpsDb>) -> Result<Self> {
let vm = db.get_vm(vm_id).await?;
let host = db.get_host(vm.host_id).await?;
let image = db.get_os_image(vm.image_id).await?;
let disk = db.get_host_disk(vm.disk_id).await?;
let ssh_key = db.get_user_ssh_key(vm.ssh_key_id).await?;
let ips = db.list_vm_ip_assignments(vm_id).await?;
let ip_range_ids: HashSet<u64> = ips.iter().map(|i| i.ip_range_id).collect();
let ip_ranges: Vec<_> = ip_range_ids.iter().map(|i| db.get_ip_range(*i)).collect();
let ranges: Vec<IpRange> = join_all(ip_ranges)
.await
.into_iter()
.filter_map(Result::ok)
.collect();
let template = if let Some(t) = vm.template_id {
Some(db.get_vm_template(t).await?)
} else {
None
};
let custom_template = if let Some(t) = vm.custom_template_id {
Some(db.get_custom_vm_template(t).await?)
} else {
None
};
// create VM
Ok(FullVmInfo {
vm,
host,
template,
custom_template,
image,
ips,
disk,
ranges,
ssh_key,
})
}
/// CPU cores
pub fn resources(&self) -> Result<VmResources> {
if let Some(t) = &self.template {
Ok(VmResources {
cpu: t.cpu,
memory: t.memory,
disk_size: t.disk_size,
})
} else if let Some(t) = &self.custom_template {
Ok(VmResources {
cpu: t.cpu,
memory: t.memory,
disk_size: t.disk_size,
})
} else {
bail!("Invalid VM config, no template");
}
}
}
#[derive(Clone)]
pub struct VmResources {
pub cpu: u16,
pub memory: u64,
pub disk_size: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct TimeSeriesData {
pub timestamp: u64,
pub cpu: f32,
pub memory: f32,
pub memory_size: u64,
pub net_in: f32,
pub net_out: f32,
pub disk_write: f32,
pub disk_read: f32,
}
#[derive(Debug, Clone)]
pub enum TimeSeries {
Hourly,
Daily,
Weekly,
Monthly,
Yearly,
}
#[derive(Debug, Clone)]
pub struct VmHostInfo {
pub cpu: u16,
pub memory: u64,
pub disks: Vec<VmHostDiskInfo>,
}
#[derive(Debug, Clone)]
pub struct VmHostDiskInfo {
pub name: String,
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(),
},
}
}
}

File diff suppressed because it is too large Load Diff