diff --git a/Cargo.lock b/Cargo.lock index 35b5a47..7d3dc4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4691,8 +4691,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "virt" version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a05f77c836efa9be343b5419663cf829d75203b813579993cdd9c44f51767e" +source = "git+https://gitlab.com/libvirt/libvirt-rust.git#70394aad4d9597c9ff87c0ada6711ed4f9528991" dependencies = [ "libc", "uuid", @@ -4702,8 +4701,7 @@ dependencies = [ [[package]] name = "virt-sys" version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c504e459878f09177f41bf2f8bb3e9a8af4fca7a09e73152fee02535d501601c" +source = "git+https://gitlab.com/libvirt/libvirt-rust.git#70394aad4d9597c9ff87c0ada6711ed4f9528991" dependencies = [ "libc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index 146c6dc..d1636a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,17 @@ edition = "2021" name = "api" [features] -default = ["mikrotik", "nostr-dm", "nostr-dvm", "proxmox", "lnd", "cloudflare", "revolut", "bitvora"] +default = [ + "mikrotik", + "nostr-dm", + "nostr-dvm", + "proxmox", + "lnd", + "cloudflare", + "revolut", + "bitvora", + "libvirt" +] mikrotik = ["dep:reqwest"] nostr-dm = ["dep:nostr-sdk"] nostr-dvm = ["dep:nostr-sdk"] @@ -53,7 +63,7 @@ ssh2 = { version = "0.9.4", optional = true } reqwest = { version = "0.12.8", optional = true } #libvirt -virt = { version = "0.4.2", optional = true } +virt = { git = "https://gitlab.com/libvirt/libvirt-rust.git", optional = true } #lnd fedimint-tonic-lnd = { version = "0.2.0", default-features = false, features = ["invoicesrpc"], optional = true } diff --git a/lnvps_db/migrations/20250328220956_fixes.sql b/lnvps_db/migrations/20250328220956_fixes.sql new file mode 100644 index 0000000..e558387 --- /dev/null +++ b/lnvps_db/migrations/20250328220956_fixes.sql @@ -0,0 +1,4 @@ +-- Add migration script here +ALTER TABLE vm_ip_assignment DROP KEY ix_vm_ip_assignment_ip; +alter table vm_os_image + add column default_username varchar(50); \ No newline at end of file diff --git a/lnvps_db/src/model.rs b/lnvps_db/src/model.rs index a633ce1..9a0016b 100644 --- a/lnvps_db/src/model.rs +++ b/lnvps_db/src/model.rs @@ -206,6 +206,7 @@ pub struct VmOsImage { pub release_date: DateTime, /// URL location of cloud image pub url: String, + pub default_username: Option, } impl VmOsImage { diff --git a/src/api/model.rs b/src/api/model.rs index f86fe06..1c49523 100644 --- a/src/api/model.rs +++ b/src/api/model.rs @@ -455,6 +455,7 @@ pub struct ApiVmOsImage { pub flavour: String, pub version: String, pub release_date: DateTime, + pub default_username: Option, } impl From for ApiVmOsImage { @@ -465,6 +466,7 @@ impl From for ApiVmOsImage { flavour: image.flavour, version: image.version, release_date: image.release_date, + default_username: image.default_username, } } } diff --git a/src/fiat/revolut.rs b/src/fiat/revolut.rs index f18e855..3354cd6 100644 --- a/src/fiat/revolut.rs +++ b/src/fiat/revolut.rs @@ -6,7 +6,7 @@ use anyhow::{bail, Result}; use chrono::{DateTime, Utc}; use nostr::Url; use reqwest::header::AUTHORIZATION; -use reqwest::{Client, Method, RequestBuilder}; +use reqwest::{Method, RequestBuilder}; use serde::{Deserialize, Serialize}; use std::future::Future; use std::pin::Pin; diff --git a/src/host/libvirt.rs b/src/host/libvirt.rs index 0453dbd..3e64365 100644 --- a/src/host/libvirt.rs +++ b/src/host/libvirt.rs @@ -1,44 +1,99 @@ -use crate::host::{FullVmInfo, TimeSeries, TimeSeriesData, VmHostClient}; -use crate::status::VmState; +use crate::host::{ + FullVmInfo, TerminalStream, TimeSeries, TimeSeriesData, VmHostClient, VmHostDiskInfo, + VmHostInfo, +}; +use crate::settings::QemuConfig; +use crate::status::{VmRunningState, VmState}; +use crate::KB; +use anyhow::{Context, Result}; +use chrono::Utc; use lnvps_db::{async_trait, Vm, VmOsImage}; +use virt::connect::Connect; +use virt::domain::Domain; +use virt::sys::{virDomainCreate, VIR_CONNECT_LIST_STORAGE_POOLS_ACTIVE}; -pub struct LibVirt {} +#[derive(Debug)] +pub struct LibVirtHost { + connection: Connect, + qemu: QemuConfig, +} + +impl LibVirtHost { + pub fn new(url: &str, qemu: QemuConfig) -> Result { + Ok(Self { + connection: Connect::open(Some(url))?, + qemu, + }) + } +} #[async_trait] -impl VmHostClient for LibVirt { - async fn download_os_image(&self, image: &VmOsImage) -> anyhow::Result<()> { +impl VmHostClient for LibVirtHost { + async fn get_info(&self) -> Result { + 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<()> { + Ok(()) + } + + async fn generate_mac(&self, vm: &Vm) -> Result { + Ok("ff:ff:ff:ff:ff:ff".to_string()) + } + + 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<()> { + Ok(()) + } + + async fn reinstall_vm(&self, cfg: &FullVmInfo) -> Result<()> { todo!() } - async fn generate_mac(&self, vm: &Vm) -> anyhow::Result { - todo!() + async fn get_vm_state(&self, vm: &Vm) -> Result { + 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 start_vm(&self, vm: &Vm) -> anyhow::Result<()> { - todo!() - } - - async fn stop_vm(&self, vm: &Vm) -> anyhow::Result<()> { - todo!() - } - - async fn reset_vm(&self, vm: &Vm) -> anyhow::Result<()> { - todo!() - } - - async fn create_vm(&self, cfg: &FullVmInfo) -> anyhow::Result<()> { - todo!() - } - - async fn reinstall_vm(&self, cfg: &FullVmInfo) -> anyhow::Result<()> { - todo!() - } - - async fn get_vm_state(&self, vm: &Vm) -> anyhow::Result { - todo!() - } - - async fn configure_vm(&self, vm: &FullVmInfo) -> anyhow::Result<()> { + async fn configure_vm(&self, vm: &FullVmInfo) -> Result<()> { todo!() } @@ -46,7 +101,11 @@ impl VmHostClient for LibVirt { &self, vm: &Vm, series: TimeSeries, - ) -> anyhow::Result> { + ) -> Result> { + todo!() + } + + async fn connect_terminal(&self, vm: &Vm) -> Result { todo!() } } diff --git a/src/host/mod.rs b/src/host/mod.rs index b26d367..1b94a1d 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -12,8 +12,8 @@ use std::collections::HashSet; use std::sync::Arc; use tokio::sync::mpsc::{Receiver, Sender}; -//#[cfg(feature = "libvirt")] -//mod libvirt; +#[cfg(feature = "libvirt")] +mod libvirt; #[cfg(feature = "proxmox")] mod proxmox; @@ -67,31 +67,28 @@ pub trait VmHostClient: Send + Sync { pub fn get_host_client(host: &VmHost, cfg: &ProvisionerConfig) -> Result> { #[cfg(test)] - { - Ok(Arc::new(crate::mocks::MockVmHost::new())) - } - #[cfg(not(test))] - { - Ok(match (host.kind.clone(), &cfg) { - #[cfg(feature = "proxmox")] - ( - VmHostKind::Proxmox, - ProvisionerConfig::Proxmox { - qemu, - ssh, - mac_prefix, - }, - ) => Arc::new(proxmox::ProxmoxClient::new( + 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, - mac_prefix.clone(), - qemu.clone(), - ssh.clone(), - )), - _ => bail!("Unknown host config: {}", host.kind), - }) - } + 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 diff --git a/src/host/proxmox.rs b/src/host/proxmox.rs index a3cc5bf..efb0dbf 100644 --- a/src/host/proxmox.rs +++ b/src/host/proxmox.rs @@ -1198,6 +1198,7 @@ mod tests { enabled: true, release_date: Utc::now(), url: "http://localhost.com/ubuntu_server_24.04.img".to_string(), + default_username: None }, ips: vec![ VmIpAssignment { diff --git a/src/settings.rs b/src/settings.rs index a6e66f9..4e90bee 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -104,16 +104,27 @@ pub struct SmtpConfig { #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] -pub enum ProvisionerConfig { - #[serde(rename_all = "kebab-case")] - Proxmox { - /// Generic VM configuration - qemu: QemuConfig, - /// SSH config for issuing commands via CLI - ssh: Option, - /// MAC address prefix for NIC (eg. bc:24:11) - mac_prefix: Option, - }, +pub struct ProvisionerConfig { + pub proxmox: Option, + pub libvirt: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct ProxmoxConfig { + /// Generic VM configuration + pub qemu: QemuConfig, + /// SSH config for issuing commands via CLI + pub ssh: Option, + /// MAC address prefix for NIC (eg. bc:24:11) + pub mac_prefix: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct LibVirtConfig { + /// Generic VM configuration + pub qemu: QemuConfig, } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -198,16 +209,19 @@ pub fn mock_settings() -> Settings { macaroon: Default::default(), }, read_only: false, - provisioner: ProvisionerConfig::Proxmox { - qemu: QemuConfig { - machine: "q35".to_string(), - os_type: "l26".to_string(), - bridge: "vmbr1".to_string(), - cpu: "kvm64".to_string(), - kvm: false, - }, - ssh: None, - mac_prefix: Some("ff:ff:ff".to_string()), + provisioner: ProvisionerConfig { + proxmox: Some(ProxmoxConfig { + qemu: QemuConfig { + machine: "q35".to_string(), + os_type: "l26".to_string(), + bridge: "vmbr1".to_string(), + cpu: "kvm64".to_string(), + kvm: false, + }, + ssh: None, + mac_prefix: Some("ff:ff:ff".to_string()), + }), + libvirt: None, }, delete_after: 0, smtp: None,