feat: router arp entry
This commit is contained in:
parent
7ffff1e698
commit
81b233a047
@ -6,6 +6,9 @@ edition = "2021"
|
||||
[[bin]]
|
||||
name = "api"
|
||||
|
||||
[features]
|
||||
default = ["mikrotik"]
|
||||
mikrotik = []
|
||||
|
||||
[dependencies]
|
||||
lnvps_db = { path = "lnvps_db" }
|
||||
@ -20,7 +23,7 @@ serde_json = "1.0.132"
|
||||
rocket = { version = "0.5.1", features = ["json"] }
|
||||
chrono = { version = "0.4.38", features = ["serde"] }
|
||||
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"
|
||||
fedimint-tonic-lnd = { version = "0.2.0", default-features = false, features = ["invoicesrpc"] }
|
||||
ipnetwork = "0.20.0"
|
||||
|
@ -50,11 +50,12 @@ async fn main() -> Result<(), Error> {
|
||||
db.execute(setup_script).await?;
|
||||
}
|
||||
|
||||
let router = settings.router.as_ref().map(|r| r.get_router());
|
||||
let status = VmStateCache::new();
|
||||
let worker_provisioner =
|
||||
settings
|
||||
.provisioner
|
||||
.get_provisioner(db.clone(), lnd.clone(), exchange.clone());
|
||||
.get_provisioner(db.clone(), router, lnd.clone(), exchange.clone());
|
||||
worker_provisioner.init().await?;
|
||||
|
||||
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 =
|
||||
settings
|
||||
.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 pv: Box<dyn Provisioner> = Box::new(provisioner);
|
||||
|
@ -282,10 +282,10 @@ impl ProxmoxClient {
|
||||
body: R,
|
||||
) -> Result<T> {
|
||||
let body = serde_json::to_string(&body)?;
|
||||
debug!("{}", &body);
|
||||
debug!(">> {} {}: {}", method.clone(), path, &body);
|
||||
let rsp = self
|
||||
.client
|
||||
.request(method, self.base.join(path)?)
|
||||
.request(method.clone(), self.base.join(path)?)
|
||||
.header("Authorization", format!("PVEAPIToken={}", self.token))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "application/json")
|
||||
@ -299,7 +299,7 @@ impl ProxmoxClient {
|
||||
if status.is_success() {
|
||||
Ok(serde_json::from_str(&text)?)
|
||||
} else {
|
||||
bail!("{}", status);
|
||||
bail!("{} {}: {}", method, path, status);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -579,5 +579,5 @@ pub struct VmConfig {
|
||||
pub kvm: Option<bool>,
|
||||
#[serde(rename = "serial0")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub serial_0: Option<String>
|
||||
pub serial_0: Option<String>,
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
use base64::Engine;
|
||||
use log::{debug, info};
|
||||
use log::debug;
|
||||
use nostr::{Event, JsonUtil, Kind, Timestamp};
|
||||
use rocket::http::uri::{Absolute, Uri};
|
||||
use rocket::http::Status;
|
||||
|
@ -5,6 +5,7 @@ use crate::host::proxmox::{
|
||||
VmConfig,
|
||||
};
|
||||
use crate::provisioner::Provisioner;
|
||||
use crate::router::Router;
|
||||
use crate::settings::{QemuConfig, SshConfig};
|
||||
use crate::ssh_client::SshClient;
|
||||
use anyhow::{bail, Result};
|
||||
@ -15,7 +16,7 @@ use fedimint_tonic_lnd::Client;
|
||||
use ipnetwork::IpNetwork;
|
||||
use lnvps_db::hydrate::Hydrate;
|
||||
use lnvps_db::{IpRange, LNVpsDb, Vm, VmCostPlanIntervalType, VmIpAssignment, VmPayment};
|
||||
use log::info;
|
||||
use log::{info, warn};
|
||||
use nostr::util::hex;
|
||||
use rand::random;
|
||||
use rand::seq::IteratorRandom;
|
||||
@ -27,6 +28,7 @@ use std::time::Duration;
|
||||
|
||||
pub struct LNVpsProvisioner {
|
||||
db: Box<dyn LNVpsDb>,
|
||||
router: Option<Box<dyn Router>>,
|
||||
lnd: Client,
|
||||
rates: ExchangeRateCache,
|
||||
read_only: bool,
|
||||
@ -40,11 +42,13 @@ impl LNVpsProvisioner {
|
||||
config: QemuConfig,
|
||||
ssh: Option<SshConfig>,
|
||||
db: impl LNVpsDb + 'static,
|
||||
router: Option<impl Router + 'static>,
|
||||
lnd: Client,
|
||||
rates: ExchangeRateCache,
|
||||
) -> Self {
|
||||
Self {
|
||||
db: Box::new(db),
|
||||
router: router.map(|r| Box::new(r) as Box<dyn Router>),
|
||||
lnd,
|
||||
rates,
|
||||
config,
|
||||
@ -265,6 +269,12 @@ impl Provisioner for LNVpsProvisioner {
|
||||
ip: ip_net.to_string(),
|
||||
..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?;
|
||||
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)
|
||||
}
|
||||
|
||||
@ -451,6 +465,21 @@ impl Provisioner for LNVpsProvisioner {
|
||||
let j_stop = client.stop_vm(&host.name, vm.id + 100).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(vm.id).await?;
|
||||
|
||||
|
@ -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 serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use std::net::IpAddr;
|
||||
|
||||
pub struct MikrotikRouter {
|
||||
url: String,
|
||||
token: String,
|
||||
url: Url,
|
||||
username: String,
|
||||
password: String,
|
||||
client: Client,
|
||||
arp_interface: String,
|
||||
}
|
||||
|
||||
impl MikrotikRouter {
|
||||
pub fn new(url: &str, token: &str) -> Self {
|
||||
pub fn new(url: &str, username: &str, password: &str, arp_interface: &str) -> Self {
|
||||
Self {
|
||||
url: url.to_string(),
|
||||
token: token.to_string(),
|
||||
url: url.parse().unwrap(),
|
||||
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]
|
||||
impl Router for MikrotikRouter {
|
||||
async fn add_arp_entry(
|
||||
&self,
|
||||
ip: IpAddr,
|
||||
mac: &[u8; 6],
|
||||
comment: Option<&str>,
|
||||
) -> anyhow::Result<()> {
|
||||
todo!()
|
||||
async fn list_arp_entry(&self) -> Result<Vec<ArpEntry>> {
|
||||
let rsp: Vec<ArpEntry> = self.req(Method::GET, "/rest/ip/arp", ()).await?;
|
||||
Ok(rsp)
|
||||
}
|
||||
|
||||
async fn add_arp_entry(&self, ip: IpAddr, mac: &str, comment: Option<&str>) -> Result<()> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
use anyhow::Result;
|
||||
use rocket::async_trait;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use std::net::IpAddr;
|
||||
|
||||
/// 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,
|
||||
#[async_trait]
|
||||
pub trait Router {
|
||||
async fn add_arp_entry(&self, ip: IpAddr, mac: &[u8; 6], comment: Option<&str>) -> Result<()>;
|
||||
pub trait Router: Send + Sync {
|
||||
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;
|
||||
#[cfg(feature = "mikrotik")]
|
||||
pub use mikrotik::*;
|
||||
|
@ -1,6 +1,7 @@
|
||||
use crate::exchange::ExchangeRateCache;
|
||||
use crate::provisioner::lnvps::LNVpsProvisioner;
|
||||
use crate::provisioner::Provisioner;
|
||||
use crate::router::{MikrotikRouter, Router};
|
||||
use fedimint_tonic_lnd::Client;
|
||||
use lnvps_db::LNVpsDb;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -20,6 +21,9 @@ pub struct Settings {
|
||||
|
||||
/// SMTP settings for sending emails
|
||||
pub smtp: Option<SmtpConfig>,
|
||||
|
||||
/// Network router config
|
||||
pub router: Option<RouterConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
@ -29,6 +33,18 @@ pub struct LndConfig {
|
||||
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)]
|
||||
pub struct SmtpConfig {
|
||||
/// Admin user id, for sending system notifications
|
||||
@ -92,6 +108,7 @@ impl ProvisionerConfig {
|
||||
pub fn get_provisioner(
|
||||
&self,
|
||||
db: impl LNVpsDb + 'static,
|
||||
router: Option<impl Router + 'static>,
|
||||
lnd: Client,
|
||||
exchange: ExchangeRateCache,
|
||||
) -> impl Provisioner + 'static {
|
||||
@ -100,7 +117,28 @@ impl ProvisionerConfig {
|
||||
qemu,
|
||||
ssh,
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,12 +4,11 @@ use crate::provisioner::Provisioner;
|
||||
use crate::settings::{Settings, SmtpConfig};
|
||||
use crate::status::{VmRunningState, VmState, VmStateCache};
|
||||
use anyhow::Result;
|
||||
use chrono::{DateTime, Days, Utc};
|
||||
use chrono::{Days, Utc};
|
||||
use lettre::message::MessageBuilder;
|
||||
use lettre::transport::smtp::authentication::Credentials;
|
||||
use lettre::transport::smtp::SmtpTransportBuilder;
|
||||
use lettre::AsyncTransport;
|
||||
use lettre::{AsyncSmtpTransport, SmtpTransport, Tokio1Executor, Transport};
|
||||
use lettre::{AsyncSmtpTransport, Tokio1Executor, Transport};
|
||||
use lnvps_db::LNVpsDb;
|
||||
use log::{debug, error, info};
|
||||
use rocket::futures::SinkExt;
|
||||
@ -118,11 +117,16 @@ impl Worker {
|
||||
{
|
||||
info!("Deleting expired VM {}", db_vm.id);
|
||||
self.provisioner.delete_vm(db_vm.id).await?;
|
||||
let title = Some(format!("[VM{}] Deleted", db_vm.id));
|
||||
self.tx.send(WorkJob::SendNotification {
|
||||
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),
|
||||
})?;
|
||||
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 mut sender = AsyncSmtpTransport::<Tokio1Executor>::relay(&smtp.server)?
|
||||
let sender = AsyncSmtpTransport::<Tokio1Executor>::relay(&smtp.server)?
|
||||
.credentials(Credentials::new(
|
||||
smtp.username.to_string(),
|
||||
smtp.password.to_string(),
|
||||
|
Loading…
x
Reference in New Issue
Block a user