feat: router arp entry

This commit is contained in:
kieran 2024-12-05 14:13:39 +00:00
parent 7ffff1e698
commit 81b233a047
No known key found for this signature in database
GPG Key ID: DE71CEB3925BE941
9 changed files with 197 additions and 30 deletions

View File

@ -6,6 +6,9 @@ edition = "2021"
[[bin]] [[bin]]
name = "api" name = "api"
[features]
default = ["mikrotik"]
mikrotik = []
[dependencies] [dependencies]
lnvps_db = { path = "lnvps_db" } lnvps_db = { path = "lnvps_db" }
@ -20,7 +23,7 @@ serde_json = "1.0.132"
rocket = { version = "0.5.1", features = ["json"] } rocket = { version = "0.5.1", features = ["json"] }
chrono = { version = "0.4.38", features = ["serde"] } chrono = { version = "0.4.38", features = ["serde"] }
nostr = { version = "0.37.0", default-features = false, features = ["std"] } nostr = { version = "0.37.0", default-features = false, features = ["std"] }
base64 = "0.22.1" base64 = { version = "0.22.1", features = ["alloc"] }
urlencoding = "2.1.3" urlencoding = "2.1.3"
fedimint-tonic-lnd = { version = "0.2.0", default-features = false, features = ["invoicesrpc"] } fedimint-tonic-lnd = { version = "0.2.0", default-features = false, features = ["invoicesrpc"] }
ipnetwork = "0.20.0" ipnetwork = "0.20.0"

View File

@ -50,11 +50,12 @@ async fn main() -> Result<(), Error> {
db.execute(setup_script).await?; db.execute(setup_script).await?;
} }
let router = settings.router.as_ref().map(|r| r.get_router());
let status = VmStateCache::new(); let status = VmStateCache::new();
let worker_provisioner = let worker_provisioner =
settings settings
.provisioner .provisioner
.get_provisioner(db.clone(), lnd.clone(), exchange.clone()); .get_provisioner(db.clone(), router, lnd.clone(), exchange.clone());
worker_provisioner.init().await?; worker_provisioner.init().await?;
let mut worker = Worker::new(db.clone(), worker_provisioner, &settings, status.clone()); let mut worker = Worker::new(db.clone(), worker_provisioner, &settings, status.clone());
@ -110,10 +111,11 @@ async fn main() -> Result<(), Error> {
} }
}); });
let router = settings.router.as_ref().map(|r| r.get_router());
let provisioner = let provisioner =
settings settings
.provisioner .provisioner
.get_provisioner(db.clone(), lnd.clone(), exchange.clone()); .get_provisioner(db.clone(), router, lnd.clone(), exchange.clone());
let db: Box<dyn LNVpsDb> = Box::new(db.clone()); let db: Box<dyn LNVpsDb> = Box::new(db.clone());
let pv: Box<dyn Provisioner> = Box::new(provisioner); let pv: Box<dyn Provisioner> = Box::new(provisioner);

View File

@ -282,10 +282,10 @@ impl ProxmoxClient {
body: R, body: R,
) -> Result<T> { ) -> Result<T> {
let body = serde_json::to_string(&body)?; let body = serde_json::to_string(&body)?;
debug!("{}", &body); debug!(">> {} {}: {}", method.clone(), path, &body);
let rsp = self let rsp = self
.client .client
.request(method, self.base.join(path)?) .request(method.clone(), self.base.join(path)?)
.header("Authorization", format!("PVEAPIToken={}", self.token)) .header("Authorization", format!("PVEAPIToken={}", self.token))
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.header("Accept", "application/json") .header("Accept", "application/json")
@ -299,7 +299,7 @@ impl ProxmoxClient {
if status.is_success() { if status.is_success() {
Ok(serde_json::from_str(&text)?) Ok(serde_json::from_str(&text)?)
} else { } else {
bail!("{}", status); bail!("{} {}: {}", method, path, status);
} }
} }
} }
@ -579,5 +579,5 @@ pub struct VmConfig {
pub kvm: Option<bool>, pub kvm: Option<bool>,
#[serde(rename = "serial0")] #[serde(rename = "serial0")]
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub serial_0: Option<String> pub serial_0: Option<String>,
} }

View File

@ -1,6 +1,6 @@
use base64::prelude::BASE64_STANDARD; use base64::prelude::BASE64_STANDARD;
use base64::Engine; use base64::Engine;
use log::{debug, info}; use log::debug;
use nostr::{Event, JsonUtil, Kind, Timestamp}; use nostr::{Event, JsonUtil, Kind, Timestamp};
use rocket::http::uri::{Absolute, Uri}; use rocket::http::uri::{Absolute, Uri};
use rocket::http::Status; use rocket::http::Status;

View File

@ -5,6 +5,7 @@ use crate::host::proxmox::{
VmConfig, VmConfig,
}; };
use crate::provisioner::Provisioner; use crate::provisioner::Provisioner;
use crate::router::Router;
use crate::settings::{QemuConfig, SshConfig}; use crate::settings::{QemuConfig, SshConfig};
use crate::ssh_client::SshClient; use crate::ssh_client::SshClient;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
@ -15,7 +16,7 @@ use fedimint_tonic_lnd::Client;
use ipnetwork::IpNetwork; use ipnetwork::IpNetwork;
use lnvps_db::hydrate::Hydrate; use lnvps_db::hydrate::Hydrate;
use lnvps_db::{IpRange, LNVpsDb, Vm, VmCostPlanIntervalType, VmIpAssignment, VmPayment}; use lnvps_db::{IpRange, LNVpsDb, Vm, VmCostPlanIntervalType, VmIpAssignment, VmPayment};
use log::info; use log::{info, warn};
use nostr::util::hex; use nostr::util::hex;
use rand::random; use rand::random;
use rand::seq::IteratorRandom; use rand::seq::IteratorRandom;
@ -27,6 +28,7 @@ use std::time::Duration;
pub struct LNVpsProvisioner { pub struct LNVpsProvisioner {
db: Box<dyn LNVpsDb>, db: Box<dyn LNVpsDb>,
router: Option<Box<dyn Router>>,
lnd: Client, lnd: Client,
rates: ExchangeRateCache, rates: ExchangeRateCache,
read_only: bool, read_only: bool,
@ -40,11 +42,13 @@ impl LNVpsProvisioner {
config: QemuConfig, config: QemuConfig,
ssh: Option<SshConfig>, ssh: Option<SshConfig>,
db: impl LNVpsDb + 'static, db: impl LNVpsDb + 'static,
router: Option<impl Router + 'static>,
lnd: Client, lnd: Client,
rates: ExchangeRateCache, rates: ExchangeRateCache,
) -> Self { ) -> Self {
Self { Self {
db: Box::new(db), db: Box::new(db),
router: router.map(|r| Box::new(r) as Box<dyn Router>),
lnd, lnd,
rates, rates,
config, config,
@ -265,6 +269,12 @@ impl Provisioner for LNVpsProvisioner {
ip: ip_net.to_string(), ip: ip_net.to_string(),
..Default::default() ..Default::default()
}; };
// add arp entry for router
if let Some(r) = self.router.as_ref() {
r.add_arp_entry(ip, &vm.mac_address, Some(&format!("VM{}", vm.id)))
.await?;
}
let id = self.db.insert_vm_ip_assignment(&assignment).await?; let id = self.db.insert_vm_ip_assignment(&assignment).await?;
assignment.id = id; assignment.id = id;
@ -274,6 +284,10 @@ impl Provisioner for LNVpsProvisioner {
} }
} }
if ret.is_empty() {
bail!("No ip ranges found in this region");
}
Ok(ret) Ok(ret)
} }
@ -451,6 +465,21 @@ impl Provisioner for LNVpsProvisioner {
let j_stop = client.stop_vm(&host.name, vm.id + 100).await?; let j_stop = client.stop_vm(&host.name, vm.id + 100).await?;
client.wait_for_task(&j_stop).await?; client.wait_for_task(&j_stop).await?;
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
.as_ref()
.map(|m| m.eq_ignore_ascii_case(&vm.mac_address))
.unwrap_or(false)
}) {
r.remove_arp_entry(ent.id.as_ref().unwrap().as_str())
.await?;
} else {
warn!("ARP entry not found, skipping")
}
}
self.db.delete_vm_ip_assignment(vm.id).await?; self.db.delete_vm_ip_assignment(vm.id).await?;
self.db.delete_vm(vm.id).await?; self.db.delete_vm(vm.id).await?;

View File

@ -1,29 +1,101 @@
use crate::router::Router; use crate::router::{ArpEntry, Router};
use anyhow::{bail, Result};
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use log::debug;
use reqwest::{Client, Method, Url};
use rocket::async_trait; use rocket::async_trait;
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::net::IpAddr; use std::net::IpAddr;
pub struct MikrotikRouter { pub struct MikrotikRouter {
url: String, url: Url,
token: String, username: String,
password: String,
client: Client,
arp_interface: String,
} }
impl MikrotikRouter { impl MikrotikRouter {
pub fn new(url: &str, token: &str) -> Self { pub fn new(url: &str, username: &str, password: &str, arp_interface: &str) -> Self {
Self { Self {
url: url.to_string(), url: url.parse().unwrap(),
token: token.to_string(), username: username.to_string(),
password: password.to_string(),
client: Client::builder()
.danger_accept_invalid_certs(true)
.build()
.unwrap(),
arp_interface: arp_interface.to_string(),
}
}
async fn req<T: DeserializeOwned, R: Serialize>(
&self,
method: Method,
path: &str,
body: R,
) -> Result<T> {
let body = serde_json::to_string(&body)?;
debug!(">> {} {}: {}", method.clone(), path, &body);
let rsp = self
.client
.request(method.clone(), self.url.join(path)?)
.header(
"Authorization",
format!(
"Basic {}",
STANDARD.encode(format!("{}:{}", self.username, self.password))
),
)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.body(body)
.send()
.await?;
let status = rsp.status();
let text = rsp.text().await?;
#[cfg(debug_assertions)]
debug!("<< {}", text);
if status.is_success() {
Ok(serde_json::from_str(&text)?)
} else {
bail!("{} {}: {}", method, path, status);
} }
} }
} }
#[async_trait] #[async_trait]
impl Router for MikrotikRouter { impl Router for MikrotikRouter {
async fn add_arp_entry( async fn list_arp_entry(&self) -> Result<Vec<ArpEntry>> {
&self, let rsp: Vec<ArpEntry> = self.req(Method::GET, "/rest/ip/arp", ()).await?;
ip: IpAddr, Ok(rsp)
mac: &[u8; 6], }
comment: Option<&str>,
) -> anyhow::Result<()> { async fn add_arp_entry(&self, ip: IpAddr, mac: &str, comment: Option<&str>) -> Result<()> {
todo!() let _rsp: ArpEntry = self
.req(
Method::PUT,
"/rest/ip/arp",
ArpEntry {
address: ip.to_string(),
mac_address: Some(mac.to_string()),
interface: self.arp_interface.to_string(),
comment: comment.map(|c| c.to_string()),
..Default::default()
},
)
.await?;
Ok(())
}
async fn remove_arp_entry(&self, id: &str) -> Result<()> {
let _rsp: ArpEntry = self
.req(Method::DELETE, &format!("/rest/ip/arp/{id}"), ())
.await?;
Ok(())
} }
} }

View File

@ -1,5 +1,6 @@
use anyhow::Result; use anyhow::Result;
use rocket::async_trait; use rocket::async_trait;
use rocket::serde::{Deserialize, Serialize};
use std::net::IpAddr; use std::net::IpAddr;
/// Router defines a network device used to access the hosts /// Router defines a network device used to access the hosts
@ -10,9 +11,27 @@ use std::net::IpAddr;
/// ///
/// It also prevents people from re-assigning their IP to another in the range, /// It also prevents people from re-assigning their IP to another in the range,
#[async_trait] #[async_trait]
pub trait Router { pub trait Router: Send + Sync {
async fn add_arp_entry(&self, ip: IpAddr, mac: &[u8; 6], comment: Option<&str>) -> Result<()>; async fn list_arp_entry(&self) -> Result<Vec<ArpEntry>>;
async fn add_arp_entry(&self, ip: IpAddr, mac: &str, comment: Option<&str>) -> Result<()>;
async fn remove_arp_entry(&self, id: &str) -> Result<()>;
} }
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ArpEntry {
#[serde(rename = ".id")]
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub address: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "mac-address")]
pub mac_address: Option<String>,
pub interface: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
}
#[cfg(feature = "mikrotik")]
mod mikrotik; mod mikrotik;
#[cfg(feature = "mikrotik")]
pub use mikrotik::*; pub use mikrotik::*;

View File

@ -1,6 +1,7 @@
use crate::exchange::ExchangeRateCache; use crate::exchange::ExchangeRateCache;
use crate::provisioner::lnvps::LNVpsProvisioner; use crate::provisioner::lnvps::LNVpsProvisioner;
use crate::provisioner::Provisioner; use crate::provisioner::Provisioner;
use crate::router::{MikrotikRouter, Router};
use fedimint_tonic_lnd::Client; use fedimint_tonic_lnd::Client;
use lnvps_db::LNVpsDb; use lnvps_db::LNVpsDb;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -20,6 +21,9 @@ pub struct Settings {
/// SMTP settings for sending emails /// SMTP settings for sending emails
pub smtp: Option<SmtpConfig>, pub smtp: Option<SmtpConfig>,
/// Network router config
pub router: Option<RouterConfig>,
} }
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
@ -29,6 +33,18 @@ pub struct LndConfig {
pub macaroon: PathBuf, pub macaroon: PathBuf,
} }
#[derive(Debug, Clone, Deserialize, Serialize)]
pub enum RouterConfig {
Mikrotik {
url: String,
username: String,
password: String,
/// Interface used to add arp entries
arp_interface: String,
},
}
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SmtpConfig { pub struct SmtpConfig {
/// Admin user id, for sending system notifications /// Admin user id, for sending system notifications
@ -92,6 +108,7 @@ impl ProvisionerConfig {
pub fn get_provisioner( pub fn get_provisioner(
&self, &self,
db: impl LNVpsDb + 'static, db: impl LNVpsDb + 'static,
router: Option<impl Router + 'static>,
lnd: Client, lnd: Client,
exchange: ExchangeRateCache, exchange: ExchangeRateCache,
) -> impl Provisioner + 'static { ) -> impl Provisioner + 'static {
@ -100,7 +117,28 @@ impl ProvisionerConfig {
qemu, qemu,
ssh, ssh,
read_only, read_only,
} => LNVpsProvisioner::new(*read_only, qemu.clone(), ssh.clone(), db, lnd, exchange), } => LNVpsProvisioner::new(
*read_only,
qemu.clone(),
ssh.clone(),
db,
router,
lnd,
exchange,
),
}
}
}
impl RouterConfig {
pub fn get_router(&self) -> impl Router + 'static {
match self {
RouterConfig::Mikrotik {
url,
username,
password,
arp_interface,
} => MikrotikRouter::new(&url, &username, &password, &arp_interface),
} }
} }
} }

View File

@ -4,12 +4,11 @@ use crate::provisioner::Provisioner;
use crate::settings::{Settings, SmtpConfig}; use crate::settings::{Settings, SmtpConfig};
use crate::status::{VmRunningState, VmState, VmStateCache}; use crate::status::{VmRunningState, VmState, VmStateCache};
use anyhow::Result; use anyhow::Result;
use chrono::{DateTime, Days, Utc}; use chrono::{Days, Utc};
use lettre::message::MessageBuilder; use lettre::message::MessageBuilder;
use lettre::transport::smtp::authentication::Credentials; use lettre::transport::smtp::authentication::Credentials;
use lettre::transport::smtp::SmtpTransportBuilder;
use lettre::AsyncTransport; use lettre::AsyncTransport;
use lettre::{AsyncSmtpTransport, SmtpTransport, Tokio1Executor, Transport}; use lettre::{AsyncSmtpTransport, Tokio1Executor, Transport};
use lnvps_db::LNVpsDb; use lnvps_db::LNVpsDb;
use log::{debug, error, info}; use log::{debug, error, info};
use rocket::futures::SinkExt; use rocket::futures::SinkExt;
@ -118,11 +117,16 @@ impl Worker {
{ {
info!("Deleting expired VM {}", db_vm.id); info!("Deleting expired VM {}", db_vm.id);
self.provisioner.delete_vm(db_vm.id).await?; self.provisioner.delete_vm(db_vm.id).await?;
let title = Some(format!("[VM{}] Deleted", db_vm.id));
self.tx.send(WorkJob::SendNotification { self.tx.send(WorkJob::SendNotification {
user_id: db_vm.user_id, user_id: db_vm.user_id,
title: Some(format!("[VM{}] Deleted", db_vm.id)), title: title.clone(),
message: format!("Your VM #{} has been deleted!", db_vm.id), message: format!("Your VM #{} has been deleted!", db_vm.id),
})?; })?;
self.queue_admin_notification(
format!("VM{} is ready for deletion", db_vm.id),
title,
)?;
} }
} }
@ -222,7 +226,7 @@ impl Worker {
} }
let msg = b.body(message)?; let msg = b.body(message)?;
let mut sender = AsyncSmtpTransport::<Tokio1Executor>::relay(&smtp.server)? let sender = AsyncSmtpTransport::<Tokio1Executor>::relay(&smtp.server)?
.credentials(Credentials::new( .credentials(Credentials::new(
smtp.username.to_string(), smtp.username.to_string(),
smtp.password.to_string(), smtp.password.to_string(),