feat: libvirt setup
Some checks failed
continuous-integration/drone/push Build is failing

fix: ip assigment index
feat: default username
This commit is contained in:
2025-03-31 10:40:29 +01:00
parent 6ca8283040
commit 7deed82a7c
10 changed files with 170 additions and 84 deletions

6
Cargo.lock generated
View File

@ -4691,8 +4691,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]] [[package]]
name = "virt" name = "virt"
version = "0.4.2" version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://gitlab.com/libvirt/libvirt-rust.git#70394aad4d9597c9ff87c0ada6711ed4f9528991"
checksum = "77a05f77c836efa9be343b5419663cf829d75203b813579993cdd9c44f51767e"
dependencies = [ dependencies = [
"libc", "libc",
"uuid", "uuid",
@ -4702,8 +4701,7 @@ dependencies = [
[[package]] [[package]]
name = "virt-sys" name = "virt-sys"
version = "0.3.0" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://gitlab.com/libvirt/libvirt-rust.git#70394aad4d9597c9ff87c0ada6711ed4f9528991"
checksum = "c504e459878f09177f41bf2f8bb3e9a8af4fca7a09e73152fee02535d501601c"
dependencies = [ dependencies = [
"libc", "libc",
"pkg-config", "pkg-config",

View File

@ -7,7 +7,17 @@ edition = "2021"
name = "api" name = "api"
[features] [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"] mikrotik = ["dep:reqwest"]
nostr-dm = ["dep:nostr-sdk"] nostr-dm = ["dep:nostr-sdk"]
nostr-dvm = ["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 } reqwest = { version = "0.12.8", optional = true }
#libvirt #libvirt
virt = { version = "0.4.2", optional = true } virt = { git = "https://gitlab.com/libvirt/libvirt-rust.git", optional = true }
#lnd #lnd
fedimint-tonic-lnd = { version = "0.2.0", default-features = false, features = ["invoicesrpc"], optional = true } fedimint-tonic-lnd = { version = "0.2.0", default-features = false, features = ["invoicesrpc"], optional = true }

View File

@ -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);

View File

@ -206,6 +206,7 @@ pub struct VmOsImage {
pub release_date: DateTime<Utc>, pub release_date: DateTime<Utc>,
/// URL location of cloud image /// URL location of cloud image
pub url: String, pub url: String,
pub default_username: Option<String>,
} }
impl VmOsImage { impl VmOsImage {

View File

@ -455,6 +455,7 @@ pub struct ApiVmOsImage {
pub flavour: String, pub flavour: String,
pub version: String, pub version: String,
pub release_date: DateTime<Utc>, pub release_date: DateTime<Utc>,
pub default_username: Option<String>,
} }
impl From<lnvps_db::VmOsImage> for ApiVmOsImage { impl From<lnvps_db::VmOsImage> for ApiVmOsImage {
@ -465,6 +466,7 @@ impl From<lnvps_db::VmOsImage> for ApiVmOsImage {
flavour: image.flavour, flavour: image.flavour,
version: image.version, version: image.version,
release_date: image.release_date, release_date: image.release_date,
default_username: image.default_username,
} }
} }
} }

View File

@ -6,7 +6,7 @@ use anyhow::{bail, Result};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use nostr::Url; use nostr::Url;
use reqwest::header::AUTHORIZATION; use reqwest::header::AUTHORIZATION;
use reqwest::{Client, Method, RequestBuilder}; use reqwest::{Method, RequestBuilder};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::future::Future; use std::future::Future;
use std::pin::Pin; use std::pin::Pin;

View File

@ -1,44 +1,99 @@
use crate::host::{FullVmInfo, TimeSeries, TimeSeriesData, VmHostClient}; use crate::host::{
use crate::status::VmState; 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 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<Self> {
Ok(Self {
connection: Connect::open(Some(url))?,
qemu,
})
}
}
#[async_trait] #[async_trait]
impl VmHostClient for LibVirt { impl VmHostClient for LibVirtHost {
async fn download_os_image(&self, image: &VmOsImage) -> anyhow::Result<()> { 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<()> {
Ok(())
}
async fn generate_mac(&self, vm: &Vm) -> Result<String> {
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!() todo!()
} }
async fn generate_mac(&self, vm: &Vm) -> anyhow::Result<String> { async fn get_vm_state(&self, vm: &Vm) -> Result<VmState> {
todo!() 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<()> { async fn configure_vm(&self, vm: &FullVmInfo) -> 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<VmState> {
todo!()
}
async fn configure_vm(&self, vm: &FullVmInfo) -> anyhow::Result<()> {
todo!() todo!()
} }
@ -46,7 +101,11 @@ impl VmHostClient for LibVirt {
&self, &self,
vm: &Vm, vm: &Vm,
series: TimeSeries, series: TimeSeries,
) -> anyhow::Result<Vec<TimeSeriesData>> { ) -> Result<Vec<TimeSeriesData>> {
todo!()
}
async fn connect_terminal(&self, vm: &Vm) -> Result<TerminalStream> {
todo!() todo!()
} }
} }

View File

@ -12,8 +12,8 @@ use std::collections::HashSet;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::mpsc::{Receiver, Sender}; use tokio::sync::mpsc::{Receiver, Sender};
//#[cfg(feature = "libvirt")] #[cfg(feature = "libvirt")]
//mod libvirt; mod libvirt;
#[cfg(feature = "proxmox")] #[cfg(feature = "proxmox")]
mod proxmox; mod proxmox;
@ -67,31 +67,28 @@ pub trait VmHostClient: Send + Sync {
pub fn get_host_client(host: &VmHost, cfg: &ProvisionerConfig) -> Result<Arc<dyn VmHostClient>> { pub fn get_host_client(host: &VmHost, cfg: &ProvisionerConfig) -> Result<Arc<dyn VmHostClient>> {
#[cfg(test)] #[cfg(test)]
{ return Ok(Arc::new(crate::mocks::MockVmHost::new()));
Ok(Arc::new(crate::mocks::MockVmHost::new()))
} Ok(match host.kind.clone() {
#[cfg(not(test))] #[cfg(feature = "proxmox")]
{ VmHostKind::Proxmox if cfg.proxmox.is_some() => {
Ok(match (host.kind.clone(), &cfg) { let cfg = cfg.proxmox.clone().unwrap();
#[cfg(feature = "proxmox")] Arc::new(proxmox::ProxmoxClient::new(
(
VmHostKind::Proxmox,
ProvisionerConfig::Proxmox {
qemu,
ssh,
mac_prefix,
},
) => Arc::new(proxmox::ProxmoxClient::new(
host.ip.parse()?, host.ip.parse()?,
&host.name, &host.name,
&host.api_token, &host.api_token,
mac_prefix.clone(), cfg.mac_prefix,
qemu.clone(), cfg.qemu,
ssh.clone(), cfg.ssh,
)), ))
_ => bail!("Unknown host config: {}", host.kind), }
}) #[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 /// All VM info necessary to provision a VM and its associated resources

View File

@ -1198,6 +1198,7 @@ mod tests {
enabled: true, enabled: true,
release_date: Utc::now(), release_date: Utc::now(),
url: "http://localhost.com/ubuntu_server_24.04.img".to_string(), url: "http://localhost.com/ubuntu_server_24.04.img".to_string(),
default_username: None
}, },
ips: vec![ ips: vec![
VmIpAssignment { VmIpAssignment {

View File

@ -104,16 +104,27 @@ pub struct SmtpConfig {
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub enum ProvisionerConfig { pub struct ProvisionerConfig {
#[serde(rename_all = "kebab-case")] pub proxmox: Option<ProxmoxConfig>,
Proxmox { pub libvirt: Option<LibVirtConfig>,
/// Generic VM configuration }
qemu: QemuConfig,
/// SSH config for issuing commands via CLI #[derive(Debug, Clone, Deserialize, Serialize)]
ssh: Option<SshConfig>, #[serde(rename_all = "kebab-case")]
/// MAC address prefix for NIC (eg. bc:24:11) pub struct ProxmoxConfig {
mac_prefix: Option<String>, /// Generic VM configuration
}, pub qemu: QemuConfig,
/// SSH config for issuing commands via CLI
pub ssh: Option<SshConfig>,
/// MAC address prefix for NIC (eg. bc:24:11)
pub mac_prefix: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct LibVirtConfig {
/// Generic VM configuration
pub qemu: QemuConfig,
} }
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
@ -198,16 +209,19 @@ pub fn mock_settings() -> Settings {
macaroon: Default::default(), macaroon: Default::default(),
}, },
read_only: false, read_only: false,
provisioner: ProvisionerConfig::Proxmox { provisioner: ProvisionerConfig {
qemu: QemuConfig { proxmox: Some(ProxmoxConfig {
machine: "q35".to_string(), qemu: QemuConfig {
os_type: "l26".to_string(), machine: "q35".to_string(),
bridge: "vmbr1".to_string(), os_type: "l26".to_string(),
cpu: "kvm64".to_string(), bridge: "vmbr1".to_string(),
kvm: false, cpu: "kvm64".to_string(),
}, kvm: false,
ssh: None, },
mac_prefix: Some("ff:ff:ff".to_string()), ssh: None,
mac_prefix: Some("ff:ff:ff".to_string()),
}),
libvirt: None,
}, },
delete_after: 0, delete_after: 0,
smtp: None, smtp: None,