feat: delete ip resources

This commit is contained in:
2025-03-05 10:03:29 +00:00
parent fc735590d6
commit 8838853957
6 changed files with 200 additions and 99 deletions

View File

@ -104,7 +104,7 @@ pub trait LNVpsDb: Sync + Send {
/// List VM ip assignments
async fn insert_vm_ip_assignment(&self, ip_assignment: &VmIpAssignment) -> Result<u64>;
/// Update VM ip assignments
/// Update VM ip assignments (arp/dns refs)
async fn update_vm_ip_assignment(&self, ip_assignment: &VmIpAssignment) -> Result<()>;
/// List VM ip assignments

View File

@ -232,6 +232,7 @@ async fn v1_patch_vm(
for mut ip in ips.iter_mut() {
ip.dns_reverse = Some(ptr.to_string());
provisioner.update_reverse_ip_dns(&mut ip).await?;
db.update_vm_ip_assignment(&ip).await?;
}
}

View File

@ -5,7 +5,7 @@ use crate::lightning::{AddInvoiceRequest, AddInvoiceResult, InvoiceUpdate, Light
use crate::router::{ArpEntry, Router};
use crate::settings::NetworkPolicy;
use crate::status::{VmRunningState, VmState};
use anyhow::{anyhow, bail};
use anyhow::{anyhow, bail, ensure};
use chrono::{DateTime, Utc};
use fedimint_tonic_lnd::tonic::codegen::tokio_stream::Stream;
use lnvps_db::{
@ -14,7 +14,6 @@ use lnvps_db::{
VmIpAssignment, VmOsImage, VmPayment, VmTemplate,
};
use std::collections::HashMap;
use std::net::IpAddr;
use std::pin::Pin;
use std::sync::{Arc, LazyLock};
use tokio::sync::Mutex;
@ -510,24 +509,15 @@ impl Router for MockRouter {
Ok(arp.values().cloned().collect())
}
async fn add_arp_entry(
&self,
ip: IpAddr,
mac: &str,
interface: &str,
comment: Option<&str>,
) -> anyhow::Result<ArpEntry> {
async fn add_arp_entry(&self, entry: &ArpEntry) -> anyhow::Result<ArpEntry> {
let mut arp = self.arp.lock().await;
if arp.iter().any(|(k, v)| v.address == ip.to_string()) {
if arp.iter().any(|(k, v)| v.address == entry.address) {
bail!("Address is already in use");
}
let max_id = *arp.keys().max().unwrap_or(&0);
let e = ArpEntry {
id: (max_id + 1).to_string(),
address: ip.to_string(),
mac_address: mac.to_string(),
interface: Some(interface.to_string()),
comment: comment.map(|s| s.to_string()),
id: Some((max_id + 1).to_string()),
..entry.clone()
};
arp.insert(max_id + 1, e.clone());
Ok(e)
@ -538,6 +528,18 @@ impl Router for MockRouter {
arp.remove(&id.parse::<u64>()?);
Ok(())
}
async fn update_arp_entry(&self, entry: &ArpEntry) -> anyhow::Result<ArpEntry> {
ensure!(entry.id.is_some(), "id is missing");
let mut arp = self.arp.lock().await;
if let Some(mut a) = arp.get_mut(&entry.id.as_ref().unwrap().parse::<u64>()?) {
a.mac_address = entry.mac_address.clone();
a.address = entry.address.clone();
a.interface = entry.interface.clone();
a.comment = entry.comment.clone();
}
Ok(entry.clone())
}
}
#[derive(Clone, Debug, Default)]
@ -728,10 +730,26 @@ impl DnsServer for MockDnsServer {
}
async fn delete_record(&self, record: &BasicRecord) -> anyhow::Result<()> {
todo!()
let mut table = match record.kind {
RecordType::PTR => self.reverse.lock().await,
_ => self.forward.lock().await,
};
ensure!(record.id.is_some(), "Id is missing");
table.remove(record.id.as_ref().unwrap());
Ok(())
}
async fn update_record(&self, name: &BasicRecord) -> anyhow::Result<BasicRecord> {
todo!()
async fn update_record(&self, record: &BasicRecord) -> anyhow::Result<BasicRecord> {
let mut table = match record.kind {
RecordType::PTR => self.reverse.lock().await,
_ => self.forward.lock().await,
};
ensure!(record.id.is_some(), "Id is missing");
if let Some(mut r) = table.get_mut(record.id.as_ref().unwrap()) {
r.name = record.name.clone();
r.value = record.value.clone();
r.kind = record.kind.to_string();
}
Ok(record.clone())
}
}

View File

@ -3,9 +3,9 @@ use crate::exchange::{ExchangeRateService, Ticker};
use crate::host::{get_host_client, FullVmInfo};
use crate::lightning::{AddInvoiceRequest, LightningNode};
use crate::provisioner::{NetworkProvisioner, ProvisionerMethod};
use crate::router::Router;
use crate::router::{ArpEntry, Router};
use crate::settings::{NetworkAccessPolicy, NetworkPolicy, ProvisionerConfig, Settings};
use anyhow::{bail, Context, Result};
use anyhow::{bail, ensure, Context, Result};
use chrono::{Days, Months, Utc};
use futures::future::join_all;
use lnvps_db::{IpRange, LNVpsDb, Vm, VmCostPlanIntervalType, VmIpAssignment, VmPayment};
@ -55,24 +55,78 @@ impl LNVpsProvisioner {
}
}
pub async fn delete_ip_assignment(&self, vm: &Vm) -> Result<()> {
// Delete access policy
if let NetworkAccessPolicy::StaticArp { .. } = &self.network_policy.access {
/// Create or Update access policy for a given ip assignment, does not save to database!
pub async fn update_access_policy(&self, assignment: &mut VmIpAssignment) -> Result<()> {
// apply network policy
if let NetworkAccessPolicy::StaticArp { interface } = &self.network_policy.access {
if let Some(r) = self.router.as_ref() {
let ent = r.list_arp_entry().await?;
if let Some(ent) = ent
.iter()
.find(|e| e.mac_address.eq_ignore_ascii_case(&vm.mac_address))
{
r.remove_arp_entry(&ent.id).await?;
let vm = self.db.get_vm(assignment.vm_id).await?;
let entry = ArpEntry::new(&vm, &assignment, Some(interface.clone()))?;
let arp = if let Some(_id) = &assignment.arp_ref {
r.update_arp_entry(&entry).await?
} else {
warn!("ARP entry not found, skipping")
}
r.add_arp_entry(&entry).await?
};
ensure!(arp.id.is_some(), "ARP id was empty");
assignment.arp_ref = arp.id;
} else {
bail!("No router found to apply static arp entry!")
}
}
Ok(())
}
/// Remove an access policy for a given ip assignment, does not save to database!
pub async fn remove_access_policy(&self, assignment: &mut VmIpAssignment) -> Result<()> {
// Delete access policy
if let NetworkAccessPolicy::StaticArp { .. } = &self.network_policy.access {
if let Some(r) = self.router.as_ref() {
let id = if let Some(id) = &assignment.arp_ref {
Some(id.clone())
} else {
warn!("ARP REF not found, using arp list");
let ent = r.list_arp_entry().await?;
if let Some(ent) = ent.iter().find(|e| e.address == assignment.ip) {
ent.id.clone()
} else {
warn!("ARP entry not found, skipping");
None
}
};
if let Some(id) = id {
if let Err(e) = r.remove_arp_entry(&id).await {
warn!("Failed to remove arp entry, skipping: {}", e);
}
}
assignment.arp_ref = None;
}
}
Ok(())
}
/// Delete DNS on the dns server, does not save to database!
pub async fn remove_ip_dns(&self, assignment: &mut VmIpAssignment) -> Result<()> {
// Delete forward/reverse dns
if let Some(dns) = &self.dns {
if let Some(_r) = &assignment.dns_reverse_ref {
let rev = BasicRecord::reverse(assignment)?;
dns.delete_record(&rev).await?;
assignment.dns_reverse_ref = None;
assignment.dns_reverse = None;
}
if let Some(_r) = &assignment.dns_forward_ref {
let rev = BasicRecord::forward(assignment)?;
dns.delete_record(&rev).await?;
assignment.dns_forward_ref = None;
assignment.dns_forward = None;
}
}
Ok(())
}
/// Update DNS on the dns server, does not save to database!
pub async fn update_forward_ip_dns(&self, assignment: &mut VmIpAssignment) -> Result<()> {
if let Some(dns) = &self.dns {
let fwd = BasicRecord::forward(assignment)?;
@ -83,15 +137,11 @@ impl LNVpsProvisioner {
};
assignment.dns_forward = Some(ret_fwd.name);
assignment.dns_forward_ref = Some(ret_fwd.id.context("Record id is missing")?);
// save to db
if assignment.id != 0 {
self.db.update_vm_ip_assignment(assignment).await?;
}
}
Ok(())
}
/// Update DNS on the dns server, does not save to database!
pub async fn update_reverse_ip_dns(&self, assignment: &mut VmIpAssignment) -> Result<()> {
if let Some(dns) = &self.dns {
let ret_rev = if assignment.dns_reverse_ref.is_some() {
@ -103,32 +153,30 @@ impl LNVpsProvisioner {
};
assignment.dns_reverse = Some(ret_rev.value);
assignment.dns_reverse_ref = Some(ret_rev.id.context("Record id is missing")?);
// save to db
if assignment.id != 0 {
self.db.update_vm_ip_assignment(assignment).await?;
}
}
Ok(())
}
async fn save_ip_assignment(&self, vm: &Vm, assignment: &mut VmIpAssignment) -> Result<()> {
let ip = IpAddr::from_str(&assignment.ip)?;
// apply network policy
if let NetworkAccessPolicy::StaticArp { interface } = &self.network_policy.access {
if let Some(r) = self.router.as_ref() {
r.add_arp_entry(
ip.clone(),
&vm.mac_address,
interface,
Some(&format!("VM{}", vm.id)),
)
.await?;
} else {
bail!("No router found to apply static arp entry!")
}
/// Delete all ip assignments for a given vm
pub async fn delete_ip_assignments(&self, vm_id: u64) -> Result<()> {
let ips = self.db.list_vm_ip_assignments(vm_id).await?;
for mut ip in ips {
// remove access policy
self.remove_access_policy(&mut ip).await?;
// remove dns
self.remove_ip_dns(&mut ip).await?;
// save arp/dns changes
self.db.update_vm_ip_assignment(&ip).await?;
}
// mark as deleted
self.db.delete_vm_ip_assignment(vm_id).await?;
Ok(())
}
async fn save_ip_assignment(&self, assignment: &mut VmIpAssignment) -> Result<()> {
// apply access policy
self.update_access_policy(assignment).await?;
// Add DNS records
self.update_forward_ip_dns(assignment).await?;
@ -164,7 +212,7 @@ impl LNVpsProvisioner {
dns_reverse_ref: None,
};
self.save_ip_assignment(&vm, &mut assignment).await?;
self.save_ip_assignment(&mut assignment).await?;
Ok(vec![assignment])
}
@ -324,14 +372,11 @@ impl LNVpsProvisioner {
/// Delete a VM and its associated resources
pub async fn delete_vm(&self, vm_id: u64) -> Result<()> {
let vm = self.db.get_vm(vm_id).await?;
// host client currently doesn't support delete (proxmox)
// VM should already be stopped by [Worker]
self.delete_ip_assignment(&vm).await?;
self.db.delete_vm_ip_assignment(vm.id).await?;
self.db.delete_vm(vm.id).await?;
self.delete_ip_assignments(vm_id).await?;
self.db.delete_vm(vm_id).await?;
Ok(())
}
@ -351,7 +396,7 @@ impl LNVpsProvisioner {
mod tests {
use super::*;
use crate::exchange::DefaultRateCache;
use crate::mocks::{MockDb, MockNode};
use crate::mocks::{MockDb, MockDnsServer, MockNode, MockRouter};
use crate::settings::{DnsServerConfig, LightningConfig, QemuConfig, RouterConfig};
use lnvps_db::UserSshKey;
@ -403,7 +448,8 @@ mod tests {
let db = Arc::new(MockDb::default());
let node = Arc::new(MockNode::default());
let rates = Arc::new(DefaultRateCache::default());
let router = settings.get_router().expect("router").unwrap();
let router = MockRouter::new(settings.network_policy.clone());
let dns = MockDnsServer::new();
let provisioner = LNVpsProvisioner::new(settings, db.clone(), node.clone(), rates.clone());
let pubkey: [u8; 32] = random();
@ -448,6 +494,26 @@ mod tests {
assert!(!ip.ip.ends_with("/8"));
assert!(!ip.ip.ends_with("/24"));
// now expire
provisioner.delete_vm(vm.id).await?;
// test arp/dns is removed
let arp = router.list_arp_entry().await?;
assert!(arp.is_empty());
assert_eq!(dns.forward.lock().await.len(), 0);
assert_eq!(dns.reverse.lock().await.len(), 0);
// ensure IPS are deleted
let ips = db.ip_assignments.lock().await;
let ip = ips.values().next().unwrap();
assert!(ip.arp_ref.is_none());
assert!(ip.dns_forward.is_none());
assert!(ip.dns_reverse.is_none());
assert!(ip.dns_reverse_ref.is_none());
assert!(ip.dns_forward_ref.is_none());
assert!(ip.deleted);
println!("{:?}", ip);
Ok(())
}
}

View File

@ -1,6 +1,6 @@
use crate::json_api::JsonApi;
use crate::router::{ArpEntry, Router};
use anyhow::Result;
use anyhow::{ensure, Result};
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use log::debug;
@ -32,27 +32,9 @@ impl Router for MikrotikRouter {
Ok(rsp.into_iter().map(|e| e.into()).collect())
}
async fn add_arp_entry(
&self,
ip: IpAddr,
mac: &str,
arp_interface: &str,
comment: Option<&str>,
) -> Result<ArpEntry> {
let rsp: MikrotikArpEntry = self
.api
.req(
Method::PUT,
"/rest/ip/arp",
MikrotikArpEntry {
id: None,
address: ip.to_string(),
mac_address: Some(mac.to_string()),
interface: arp_interface.to_string(),
comment: comment.map(|c| c.to_string()),
},
)
.await?;
async fn add_arp_entry(&self, entry: &ArpEntry) -> Result<ArpEntry> {
let req: MikrotikArpEntry = entry.clone().into();
let rsp: MikrotikArpEntry = self.api.req(Method::PUT, "/rest/ip/arp", req).await?;
debug!("{:?}", rsp);
Ok(rsp.into())
}
@ -60,11 +42,26 @@ impl Router for MikrotikRouter {
async fn remove_arp_entry(&self, id: &str) -> Result<()> {
let rsp: MikrotikArpEntry = self
.api
.req(Method::DELETE, &format!("/rest/ip/arp/{id}"), ())
.req(Method::DELETE, &format!("/rest/ip/arp/{}", id), ())
.await?;
debug!("{:?}", rsp);
Ok(())
}
async fn update_arp_entry(&self, entry: &ArpEntry) -> Result<ArpEntry> {
ensure!(entry.id.is_some(), "Cannot update an arp entry without ID");
let req: MikrotikArpEntry = entry.clone().into();
let rsp: MikrotikArpEntry = self
.api
.req(
Method::PATCH,
&format!("/rest/ip/arp/{}", entry.id.as_ref().unwrap()),
req,
)
.await?;
debug!("{:?}", rsp);
Ok(rsp.into())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -84,7 +81,7 @@ pub struct MikrotikArpEntry {
impl Into<ArpEntry> for MikrotikArpEntry {
fn into(self) -> ArpEntry {
ArpEntry {
id: self.id.unwrap(),
id: self.id,
address: self.address,
mac_address: self.mac_address.unwrap(),
interface: Some(self.interface),
@ -92,3 +89,15 @@ impl Into<ArpEntry> for MikrotikArpEntry {
}
}
}
impl Into<MikrotikArpEntry> for ArpEntry {
fn into(self) -> MikrotikArpEntry {
MikrotikArpEntry {
id: self.id,
address: self.address,
mac_address: Some(self.mac_address),
interface: self.interface.unwrap(),
comment: self.comment,
}
}
}

View File

@ -1,6 +1,6 @@
use anyhow::Result;
use lnvps_db::{Vm, VmIpAssignment};
use rocket::async_trait;
use std::net::IpAddr;
/// Router defines a network device used to access the hosts
///
@ -12,25 +12,32 @@ use std::net::IpAddr;
#[async_trait]
pub trait Router: Send + Sync {
async fn list_arp_entry(&self) -> Result<Vec<ArpEntry>>;
async fn add_arp_entry(
&self,
ip: IpAddr,
mac: &str,
interface: &str,
comment: Option<&str>,
) -> Result<ArpEntry>;
async fn add_arp_entry(&self, entry: &ArpEntry) -> Result<ArpEntry>;
async fn remove_arp_entry(&self, id: &str) -> Result<()>;
async fn update_arp_entry(&self, entry: &ArpEntry) -> Result<ArpEntry>;
}
#[derive(Debug, Clone)]
pub struct ArpEntry {
pub id: String,
pub id: Option<String>,
pub address: String,
pub mac_address: String,
pub interface: Option<String>,
pub comment: Option<String>,
}
impl ArpEntry {
pub fn new(vm: &Vm, ip: &VmIpAssignment, interface: Option<String>) -> Result<Self> {
Ok(Self {
id: ip.arp_ref.clone(),
address: ip.ip.clone(),
mac_address: vm.mac_address.clone(),
interface,
comment: Some(format!("VM{}", vm.id)),
})
}
}
#[cfg(feature = "mikrotik")]
mod mikrotik;
#[cfg(feature = "mikrotik")]