From 97d631ce5da2fcb355ddb426ff240e0357291a16 Mon Sep 17 00:00:00 2001 From: Kieran Date: Mon, 31 Mar 2025 12:19:41 +0100 Subject: [PATCH] feat: automatically delete expired vms on client --- src/host/mod.rs | 3 +++ src/host/proxmox.rs | 28 +++++++++++++++++++++++++++- src/mocks.rs | 24 ++++++++++++++++++++---- src/provisioner/lnvps.rs | 16 +++++++++++++--- 4 files changed, 63 insertions(+), 8 deletions(-) diff --git a/src/host/mod.rs b/src/host/mod.rs index 1b94a1d..0280d04 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -45,6 +45,9 @@ pub trait VmHostClient: Send + Sync { /// 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<()>; diff --git a/src/host/proxmox.rs b/src/host/proxmox.rs index efb0dbf..a6bf5e3 100644 --- a/src/host/proxmox.rs +++ b/src/host/proxmox.rs @@ -611,6 +611,32 @@ impl VmHostClient for ProxmoxClient { Ok(()) } + async fn delete_vm(&self, vm: &Vm) -> Result<()> { + let vm_id: ProxmoxVmId = vm.id.into(); + + // NOT IMPLEMENTED + //let t = self.delete_vm(&self.node, vm_id).await?; + //self.wait_for_task(&t).await?; + + if let Some(ssh) = &self.ssh { + let mut ses = SshClient::new()?; + ses.connect( + (self.api.base().host().unwrap().to_string(), 22), + &ssh.user, + &ssh.key, + ) + .await?; + + let cmd = format!("/usr/sbin/qm destroy {}", vm_id,); + let (code, rsp) = ses.execute(cmd.as_str()).await?; + info!("{}", rsp); + if code != 0 { + bail!("Failed to destroy vm, exit-code {}, {}", code, rsp); + } + } + Ok(()) + } + async fn reinstall_vm(&self, req: &FullVmInfo) -> Result<()> { let vm_id = req.vm.id.into(); @@ -1198,7 +1224,7 @@ mod tests { enabled: true, release_date: Utc::now(), url: "http://localhost.com/ubuntu_server_24.04.img".to_string(), - default_username: None + default_username: None, }, ips: vec![ VmIpAssignment { diff --git a/src/mocks.rs b/src/mocks.rs index f6b3d72..694b461 100644 --- a/src/mocks.rs +++ b/src/mocks.rs @@ -1,14 +1,21 @@ #![allow(unused)] use crate::dns::{BasicRecord, DnsServer, RecordType}; use crate::exchange::{ExchangeRateService, Ticker, TickerRate}; -use crate::host::{FullVmInfo, TerminalStream, TimeSeries, TimeSeriesData, VmHostClient, VmHostInfo}; +use crate::host::{ + FullVmInfo, TerminalStream, TimeSeries, TimeSeriesData, VmHostClient, VmHostInfo, +}; use crate::lightning::{AddInvoiceRequest, AddInvoiceResult, InvoiceUpdate, LightningNode}; use crate::router::{ArpEntry, Router}; use crate::status::{VmRunningState, VmState}; use anyhow::{anyhow, bail, ensure, Context}; use chrono::{DateTime, TimeDelta, Utc}; use fedimint_tonic_lnd::tonic::codegen::tokio_stream::Stream; -use lnvps_db::{async_trait, AccessPolicy, DiskInterface, DiskType, IpRange, IpRangeAllocationMode, LNVpsDb, OsDistribution, User, UserSshKey, Vm, VmCostPlan, VmCostPlanIntervalType, VmCustomPricing, VmCustomPricingDisk, VmCustomTemplate, VmHost, VmHostDisk, VmHostKind, VmHostRegion, VmIpAssignment, VmOsImage, VmPayment, VmTemplate}; +use lnvps_db::{ + async_trait, AccessPolicy, DiskInterface, DiskType, IpRange, IpRangeAllocationMode, LNVpsDb, + OsDistribution, User, UserSshKey, Vm, VmCostPlan, VmCostPlanIntervalType, VmCustomPricing, + VmCustomPricingDisk, VmCustomTemplate, VmHost, VmHostDisk, VmHostKind, VmHostRegion, + VmIpAssignment, VmOsImage, VmPayment, VmTemplate, +}; use std::collections::HashMap; use std::ops::Add; use std::pin::Pin; @@ -174,7 +181,7 @@ impl Default for MockDb { enabled: true, release_date: Utc::now(), url: "https://example.com/debian_12.img".to_string(), - default_username: None + default_username: None, }, ); Self { @@ -828,6 +835,12 @@ impl VmHostClient for MockVmHost { Ok(()) } + async fn delete_vm(&self, vm: &Vm) -> anyhow::Result<()> { + let mut vms = self.vms.lock().await; + vms.remove(&vm.id); + Ok(()) + } + async fn reinstall_vm(&self, cfg: &FullVmInfo) -> anyhow::Result<()> { todo!() } @@ -898,7 +911,10 @@ impl DnsServer for MockDnsServer { zones.get_mut(zone_id).unwrap() }; - if table.values().any(|v| v.name == record.name && v.kind == record.kind.to_string()) { + if table + .values() + .any(|v| v.name == record.name && v.kind == record.kind.to_string()) + { bail!("Duplicate record with name {}", record.name); } diff --git a/src/provisioner/lnvps.rs b/src/provisioner/lnvps.rs index a3f0fdf..9c2fc09 100644 --- a/src/provisioner/lnvps.rs +++ b/src/provisioner/lnvps.rs @@ -593,8 +593,13 @@ impl LNVpsProvisioner { /// Delete a VM and its associated resources pub async fn delete_vm(&self, vm_id: u64) -> Result<()> { - // host client currently doesn't support delete (proxmox) - // VM should already be stopped by [Worker] + let vm = self.db.get_vm(vm_id).await?; + let host = self.db.get_host(vm.host_id).await?; + + let client = get_host_client(&host, &self.provisioner_config)?; + if let Err(e) = client.delete_vm(&vm).await { + warn!("Failed to delete VM: {}", e); + } self.delete_ip_assignments(vm_id).await?; self.db.delete_vm(vm_id).await?; @@ -762,7 +767,12 @@ mod tests { assert_eq!(zones.get("mock-v6-rev-zone-id").unwrap().len(), 1); assert_eq!(zones.get("mock-forward-zone-id").unwrap().len(), 2); - let v6 = zones.get("mock-v6-rev-zone-id").unwrap().iter().next().unwrap(); + let v6 = zones + .get("mock-v6-rev-zone-id") + .unwrap() + .iter() + .next() + .unwrap(); assert_eq!(v6.1.kind, "PTR"); assert!(v6.1.name.ends_with("0.0.d.f.ip6.arpa")); }