feat: libvirt domain xml progress
This commit is contained in:
16
Cargo.lock
generated
16
Cargo.lock
generated
@ -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"
|
||||
|
10
Cargo.toml
10
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"
|
||||
|
13
README.md
13
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"
|
||||
|
@ -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<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]
|
||||
@ -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<String> {
|
||||
Ok("ff:ff:ff:ff:ff:ff".to_string())
|
||||
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<()> {
|
||||
@ -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<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(())
|
||||
}
|
||||
}
|
||||
|
150
src/host/mod.rs
150
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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
@ -1152,153 +1152,13 @@ impl From<RrdDataPoint> 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(
|
||||
|
@ -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()),
|
||||
|
Reference in New Issue
Block a user