Files
api/src/host/mod.rs
2025-03-24 09:57:30 +00:00

207 lines
5.7 KiB
Rust

use crate::settings::ProvisionerConfig;
use crate::status::VmState;
use anyhow::{bail, Result};
use futures::future::join_all;
use futures::{Sink, Stream};
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::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::sync::mpsc::{Receiver, Sender};
use tokio::sync::Semaphore;
//#[cfg(feature = "libvirt")]
//mod libvirt;
#[cfg(feature = "proxmox")]
mod proxmox;
pub struct TerminalStream {
pub shutdown: Arc<AtomicBool>,
pub rx: Receiver<Vec<u8>>,
pub tx: Sender<Vec<u8>>,
}
/// Generic type for creating VM's
#[async_trait]
pub trait VmHostClient: Send + Sync {
/// 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<()>;
/// 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)]
{
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(
host.ip.parse()?,
&host.name,
&host.api_token,
mac_prefix.clone(),
qemu.clone(),
ssh.clone(),
)),
_ => 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,
/// 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 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,
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,
}