feat: move network config to db
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
closes https://github.com/LNVPS/api/issues/16
This commit is contained in:
23
lnvps_db/migrations/20250325113115_extend_ip_range.sql
Normal file
23
lnvps_db/migrations/20250325113115_extend_ip_range.sql
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
create table router
|
||||||
|
(
|
||||||
|
id integer unsigned not null auto_increment primary key,
|
||||||
|
name varchar(100) not null,
|
||||||
|
enabled bit(1) not null,
|
||||||
|
kind smallint unsigned not null,
|
||||||
|
url varchar(255) not null,
|
||||||
|
token varchar(128) not null
|
||||||
|
);
|
||||||
|
create table access_policy
|
||||||
|
(
|
||||||
|
id integer unsigned not null auto_increment primary key,
|
||||||
|
name varchar(100) not null,
|
||||||
|
kind smallint unsigned not null,
|
||||||
|
router_id integer unsigned,
|
||||||
|
interface varchar(100),
|
||||||
|
constraint fk_access_policy_router foreign key (router_id) references router (id)
|
||||||
|
);
|
||||||
|
alter table ip_range
|
||||||
|
add column reverse_zone_id varchar(255),
|
||||||
|
add column access_policy_id integer unsigned;
|
||||||
|
alter table ip_range
|
||||||
|
add constraint fk_ip_range_access_policy foreign key (access_policy_id) references access_policy (id);
|
@ -163,4 +163,10 @@ pub trait LNVpsDb: Sync + Send {
|
|||||||
|
|
||||||
/// Return the list of disk prices for a given custom pricing model
|
/// Return the list of disk prices for a given custom pricing model
|
||||||
async fn list_custom_pricing_disk(&self, pricing_id: u64) -> Result<Vec<VmCustomPricingDisk>>;
|
async fn list_custom_pricing_disk(&self, pricing_id: u64) -> Result<Vec<VmCustomPricingDisk>>;
|
||||||
|
|
||||||
|
/// Get router config
|
||||||
|
async fn get_router(&self, router_id: u64) -> Result<Router>;
|
||||||
|
|
||||||
|
/// Get access policy
|
||||||
|
async fn get_access_policy(&self, access_policy_id: u64) -> Result<AccessPolicy>;
|
||||||
}
|
}
|
||||||
|
@ -85,7 +85,7 @@ pub struct VmHost {
|
|||||||
/// Memory load factor
|
/// Memory load factor
|
||||||
pub load_memory: f32,
|
pub load_memory: f32,
|
||||||
/// Disk load factor
|
/// Disk load factor
|
||||||
pub load_disk:f32,
|
pub load_disk: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(FromRow, Clone, Debug, Default)]
|
#[derive(FromRow, Clone, Debug, Default)]
|
||||||
@ -207,6 +207,22 @@ impl Display for VmOsImage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(FromRow, Clone, Debug)]
|
||||||
|
pub struct Router {
|
||||||
|
pub id: u64,
|
||||||
|
pub name: String,
|
||||||
|
pub enabled: bool,
|
||||||
|
pub kind: RouterKind,
|
||||||
|
pub url: String,
|
||||||
|
pub token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, sqlx::Type)]
|
||||||
|
#[repr(u16)]
|
||||||
|
pub enum RouterKind {
|
||||||
|
Mikrotik = 0,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(FromRow, Clone, Debug)]
|
#[derive(FromRow, Clone, Debug)]
|
||||||
pub struct IpRange {
|
pub struct IpRange {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
@ -214,6 +230,27 @@ pub struct IpRange {
|
|||||||
pub gateway: String,
|
pub gateway: String,
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub region_id: u64,
|
pub region_id: u64,
|
||||||
|
pub reverse_zone_id: Option<String>,
|
||||||
|
pub access_policy_id: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(FromRow, Clone, Debug)]
|
||||||
|
pub struct AccessPolicy {
|
||||||
|
pub id: u64,
|
||||||
|
pub name: String,
|
||||||
|
pub kind: NetworkAccessPolicy,
|
||||||
|
/// Router used to apply this network access policy
|
||||||
|
pub router_id: Option<u64>,
|
||||||
|
/// Interface name used to apply this policy
|
||||||
|
pub interface: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Policy that determines how packets arrive at the VM
|
||||||
|
#[derive(Debug, Clone, sqlx::Type)]
|
||||||
|
#[repr(u16)]
|
||||||
|
pub enum NetworkAccessPolicy {
|
||||||
|
/// ARP entries are added statically on the access router
|
||||||
|
StaticArp = 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, sqlx::Type)]
|
#[derive(Clone, Debug, sqlx::Type)]
|
||||||
|
@ -1,8 +1,4 @@
|
|||||||
use crate::{
|
use crate::{AccessPolicy, IpRange, LNVpsDb, Router, User, UserSshKey, Vm, VmCostPlan, VmCustomPricing, VmCustomPricingDisk, VmCustomTemplate, VmHost, VmHostDisk, VmHostRegion, VmIpAssignment, VmOsImage, VmPayment, VmTemplate};
|
||||||
IpRange, LNVpsDb, User, UserSshKey, Vm, VmCostPlan, VmCustomPricing, VmCustomPricingDisk,
|
|
||||||
VmCustomTemplate, VmHost, VmHostDisk, VmHostRegion, VmIpAssignment, VmOsImage, VmPayment,
|
|
||||||
VmTemplate,
|
|
||||||
};
|
|
||||||
use anyhow::{bail, Error, Result};
|
use anyhow::{bail, Error, Result};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use sqlx::{Executor, MySqlPool, Row};
|
use sqlx::{Executor, MySqlPool, Row};
|
||||||
@ -526,4 +522,20 @@ impl LNVpsDb for LNVpsDbMysql {
|
|||||||
.await
|
.await
|
||||||
.map_err(Error::new)
|
.map_err(Error::new)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_router(&self, router_id: u64) -> Result<Router> {
|
||||||
|
sqlx::query_as("select * from router where id=?")
|
||||||
|
.bind(router_id)
|
||||||
|
.fetch_one(&self.db)
|
||||||
|
.await
|
||||||
|
.map_err(Error::new)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_access_policy(&self, access_policy_id: u64) -> Result<AccessPolicy> {
|
||||||
|
sqlx::query_as("select * from access_policy where id=?")
|
||||||
|
.bind(access_policy_id)
|
||||||
|
.fetch_one(&self.db)
|
||||||
|
.await
|
||||||
|
.map_err(Error::new)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ use crate::settings::ProvisionerConfig;
|
|||||||
use crate::status::VmState;
|
use crate::status::VmState;
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
use futures::{Sink, Stream};
|
|
||||||
use lnvps_db::{
|
use lnvps_db::{
|
||||||
async_trait, IpRange, LNVpsDb, UserSshKey, Vm, VmCustomTemplate, VmHost, VmHostDisk,
|
async_trait, IpRange, LNVpsDb, UserSshKey, Vm, VmCustomTemplate, VmHost, VmHostDisk,
|
||||||
VmHostKind, VmIpAssignment, VmOsImage, VmTemplate,
|
VmHostKind, VmIpAssignment, VmOsImage, VmTemplate,
|
||||||
@ -10,11 +9,8 @@ use lnvps_db::{
|
|||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::pin::Pin;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::io::{AsyncRead, AsyncWrite};
|
|
||||||
use tokio::sync::mpsc::{Receiver, Sender};
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
use tokio::sync::Semaphore;
|
|
||||||
|
|
||||||
//#[cfg(feature = "libvirt")]
|
//#[cfg(feature = "libvirt")]
|
||||||
//mod libvirt;
|
//mod libvirt;
|
||||||
|
@ -5,31 +5,22 @@ use crate::ssh_client::SshClient;
|
|||||||
use crate::status::{VmRunningState, VmState};
|
use crate::status::{VmRunningState, VmState};
|
||||||
use anyhow::{anyhow, bail, ensure, Result};
|
use anyhow::{anyhow, bail, ensure, Result};
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use futures::{Stream, StreamExt};
|
use futures::StreamExt;
|
||||||
use ipnetwork::IpNetwork;
|
use ipnetwork::IpNetwork;
|
||||||
use lnvps_db::{async_trait, DiskType, Vm, VmOsImage};
|
use lnvps_db::{async_trait, DiskType, Vm, VmOsImage};
|
||||||
use log::{error, info, warn};
|
use log::{info, warn};
|
||||||
use rand::random;
|
use rand::random;
|
||||||
use reqwest::header::{HeaderMap, AUTHORIZATION};
|
use reqwest::header::{HeaderMap, AUTHORIZATION};
|
||||||
use reqwest::{ClientBuilder, Method, Url};
|
use reqwest::{ClientBuilder, Method, Url};
|
||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt::{Debug, Display, Formatter};
|
use std::fmt::{Debug, Display, Formatter};
|
||||||
use std::io::{Read, Write};
|
use std::io::Write;
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
|
||||||
use tokio::sync::mpsc::channel;
|
use tokio::sync::mpsc::channel;
|
||||||
use tokio::sync::Semaphore;
|
|
||||||
use tokio::time;
|
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
use tokio_tungstenite::tungstenite::protocol::Role;
|
|
||||||
use tokio_tungstenite::WebSocketStream;
|
|
||||||
use ws::stream::DuplexStream;
|
|
||||||
|
|
||||||
pub struct ProxmoxClient {
|
pub struct ProxmoxClient {
|
||||||
api: JsonApi,
|
api: JsonApi,
|
||||||
@ -1140,7 +1131,8 @@ mod tests {
|
|||||||
release_date: Utc::now(),
|
release_date: Utc::now(),
|
||||||
url: "http://localhost.com/ubuntu_server_24.04.img".to_string(),
|
url: "http://localhost.com/ubuntu_server_24.04.img".to_string(),
|
||||||
},
|
},
|
||||||
ips: vec![VmIpAssignment {
|
ips: vec![
|
||||||
|
VmIpAssignment {
|
||||||
id: 1,
|
id: 1,
|
||||||
vm_id: 1,
|
vm_id: 1,
|
||||||
ip_range_id: 1,
|
ip_range_id: 1,
|
||||||
@ -1151,14 +1143,40 @@ mod tests {
|
|||||||
dns_forward_ref: None,
|
dns_forward_ref: None,
|
||||||
dns_reverse: None,
|
dns_reverse: None,
|
||||||
dns_reverse_ref: None,
|
dns_reverse_ref: None,
|
||||||
}],
|
},
|
||||||
ranges: vec![IpRange {
|
VmIpAssignment {
|
||||||
|
id: 2,
|
||||||
|
vm_id: 1,
|
||||||
|
ip_range_id: 2,
|
||||||
|
ip: "192.168.2.2".to_string(),
|
||||||
|
deleted: false,
|
||||||
|
arp_ref: None,
|
||||||
|
dns_forward: None,
|
||||||
|
dns_forward_ref: None,
|
||||||
|
dns_reverse: None,
|
||||||
|
dns_reverse_ref: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
ranges: vec![
|
||||||
|
IpRange {
|
||||||
id: 1,
|
id: 1,
|
||||||
cidr: "192.168.1.0/24".to_string(),
|
cidr: "192.168.1.0/24".to_string(),
|
||||||
gateway: "192.168.1.1/16".to_string(),
|
gateway: "192.168.1.1/16".to_string(),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
region_id: 1,
|
region_id: 1,
|
||||||
}],
|
reverse_zone_id: None,
|
||||||
|
access_policy_id: None,
|
||||||
|
},
|
||||||
|
IpRange {
|
||||||
|
id: 2,
|
||||||
|
cidr: "192.168.2.0/24".to_string(),
|
||||||
|
gateway: "10.10.10.10".to_string(),
|
||||||
|
enabled: true,
|
||||||
|
region_id: 2,
|
||||||
|
reverse_zone_id: None,
|
||||||
|
access_policy_id: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
ssh_key: UserSshKey {
|
ssh_key: UserSshKey {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: "test".to_string(),
|
name: "test".to_string(),
|
||||||
@ -1193,7 +1211,10 @@ mod tests {
|
|||||||
assert_eq!(vm.on_boot, Some(true));
|
assert_eq!(vm.on_boot, Some(true));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
vm.ip_config,
|
vm.ip_config,
|
||||||
Some("ip=192.168.1.2/16,gw=192.168.1.1,ip6=auto".to_string())
|
Some(
|
||||||
|
"ip=192.168.1.2/16,gw=192.168.1.1,ip=192.168.2.2/24,gw=10.10.10.10,ip6=auto"
|
||||||
|
.to_string()
|
||||||
|
)
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
30
src/mocks.rs
30
src/mocks.rs
@ -4,15 +4,15 @@ use crate::exchange::{ExchangeRateService, Ticker, TickerRate};
|
|||||||
use crate::host::{FullVmInfo, TerminalStream, TimeSeries, TimeSeriesData, VmHostClient};
|
use crate::host::{FullVmInfo, TerminalStream, TimeSeries, TimeSeriesData, VmHostClient};
|
||||||
use crate::lightning::{AddInvoiceRequest, AddInvoiceResult, InvoiceUpdate, LightningNode};
|
use crate::lightning::{AddInvoiceRequest, AddInvoiceResult, InvoiceUpdate, LightningNode};
|
||||||
use crate::router::{ArpEntry, Router};
|
use crate::router::{ArpEntry, Router};
|
||||||
use crate::settings::NetworkPolicy;
|
|
||||||
use crate::status::{VmRunningState, VmState};
|
use crate::status::{VmRunningState, VmState};
|
||||||
use anyhow::{anyhow, bail, ensure, Context};
|
use anyhow::{anyhow, bail, ensure, Context};
|
||||||
use chrono::{DateTime, TimeDelta, Utc};
|
use chrono::{DateTime, TimeDelta, Utc};
|
||||||
use fedimint_tonic_lnd::tonic::codegen::tokio_stream::Stream;
|
use fedimint_tonic_lnd::tonic::codegen::tokio_stream::Stream;
|
||||||
use lnvps_db::{
|
use lnvps_db::{
|
||||||
async_trait, DiskInterface, DiskType, IpRange, LNVpsDb, OsDistribution, User, UserSshKey, Vm,
|
async_trait, AccessPolicy, DiskInterface, DiskType, IpRange, LNVpsDb, OsDistribution, User,
|
||||||
VmCostPlan, VmCostPlanIntervalType, VmCustomPricing, VmCustomPricingDisk, VmCustomTemplate,
|
UserSshKey, Vm, VmCostPlan, VmCostPlanIntervalType, VmCustomPricing, VmCustomPricingDisk,
|
||||||
VmHost, VmHostDisk, VmHostKind, VmHostRegion, VmIpAssignment, VmOsImage, VmPayment, VmTemplate,
|
VmCustomTemplate, VmHost, VmHostDisk, VmHostKind, VmHostRegion, VmIpAssignment, VmOsImage,
|
||||||
|
VmPayment, VmTemplate,
|
||||||
};
|
};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::ops::Add;
|
use std::ops::Add;
|
||||||
@ -37,6 +37,8 @@ pub struct MockDb {
|
|||||||
pub custom_pricing_disk: Arc<Mutex<HashMap<u64, VmCustomPricingDisk>>>,
|
pub custom_pricing_disk: Arc<Mutex<HashMap<u64, VmCustomPricingDisk>>>,
|
||||||
pub custom_template: Arc<Mutex<HashMap<u64, VmCustomTemplate>>>,
|
pub custom_template: Arc<Mutex<HashMap<u64, VmCustomTemplate>>>,
|
||||||
pub payments: Arc<Mutex<Vec<VmPayment>>>,
|
pub payments: Arc<Mutex<Vec<VmPayment>>>,
|
||||||
|
pub router: Arc<Mutex<HashMap<u64, lnvps_db::Router>>>,
|
||||||
|
pub access_policy: Arc<Mutex<HashMap<u64, AccessPolicy>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MockDb {
|
impl MockDb {
|
||||||
@ -115,6 +117,8 @@ impl Default for MockDb {
|
|||||||
gateway: "10.0.0.1/8".to_string(),
|
gateway: "10.0.0.1/8".to_string(),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
region_id: 1,
|
region_id: 1,
|
||||||
|
reverse_zone_id: None,
|
||||||
|
access_policy_id: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let mut hosts = HashMap::new();
|
let mut hosts = HashMap::new();
|
||||||
@ -181,6 +185,8 @@ impl Default for MockDb {
|
|||||||
user_ssh_keys: Arc::new(Mutex::new(Default::default())),
|
user_ssh_keys: Arc::new(Mutex::new(Default::default())),
|
||||||
custom_template: Arc::new(Default::default()),
|
custom_template: Arc::new(Default::default()),
|
||||||
payments: Arc::new(Default::default()),
|
payments: Arc::new(Default::default()),
|
||||||
|
router: Arc::new(Default::default()),
|
||||||
|
access_policy: Arc::new(Default::default()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -602,21 +608,31 @@ impl LNVpsDb for MockDb {
|
|||||||
.cloned()
|
.cloned()
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_router(&self, router_id: u64) -> anyhow::Result<lnvps_db::Router> {
|
||||||
|
let r = self.router.lock().await;
|
||||||
|
Ok(r.get(&router_id).cloned().context("no router")?)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_access_policy(&self, access_policy_id: u64) -> anyhow::Result<AccessPolicy> {
|
||||||
|
let p = self.access_policy.lock().await;
|
||||||
|
Ok(p.get(&access_policy_id)
|
||||||
|
.cloned()
|
||||||
|
.context("no access policy")?)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct MockRouter {
|
pub struct MockRouter {
|
||||||
pub policy: NetworkPolicy,
|
|
||||||
arp: Arc<Mutex<HashMap<u64, ArpEntry>>>,
|
arp: Arc<Mutex<HashMap<u64, ArpEntry>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MockRouter {
|
impl MockRouter {
|
||||||
pub fn new(policy: NetworkPolicy) -> Self {
|
pub fn new() -> Self {
|
||||||
static LAZY_ARP: LazyLock<Arc<Mutex<HashMap<u64, ArpEntry>>>> =
|
static LAZY_ARP: LazyLock<Arc<Mutex<HashMap<u64, ArpEntry>>>> =
|
||||||
LazyLock::new(|| Arc::new(Mutex::new(HashMap::new())));
|
LazyLock::new(|| Arc::new(Mutex::new(HashMap::new())));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
policy,
|
|
||||||
arp: LAZY_ARP.clone(),
|
arp: LAZY_ARP.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -354,6 +354,8 @@ mod tests {
|
|||||||
gateway: "10.0.0.1".to_string(),
|
gateway: "10.0.0.1".to_string(),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
region_id: 1,
|
region_id: 1,
|
||||||
|
reverse_zone_id: None,
|
||||||
|
access_policy_id: None,
|
||||||
},
|
},
|
||||||
usage: 69,
|
usage: 69,
|
||||||
}],
|
}],
|
||||||
|
@ -6,12 +6,15 @@ use crate::lightning::{AddInvoiceRequest, LightningNode};
|
|||||||
use crate::provisioner::{
|
use crate::provisioner::{
|
||||||
CostResult, HostCapacityService, NetworkProvisioner, PricingEngine, ProvisionerMethod,
|
CostResult, HostCapacityService, NetworkProvisioner, PricingEngine, ProvisionerMethod,
|
||||||
};
|
};
|
||||||
use crate::router::{ArpEntry, Router};
|
use crate::router::{ArpEntry, MikrotikRouter, Router};
|
||||||
use crate::settings::{NetworkAccessPolicy, NetworkPolicy, ProvisionerConfig, Settings};
|
use crate::settings::{ProvisionerConfig, Settings};
|
||||||
use anyhow::{bail, ensure, Context, Result};
|
use anyhow::{bail, ensure, Context, Result};
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use isocountry::CountryCode;
|
use isocountry::CountryCode;
|
||||||
use lnvps_db::{LNVpsDb, PaymentMethod, User, Vm, VmCustomTemplate, VmIpAssignment, VmPayment};
|
use lnvps_db::{
|
||||||
|
AccessPolicy, LNVpsDb, NetworkAccessPolicy, PaymentMethod, RouterKind, Vm, VmCustomTemplate,
|
||||||
|
VmIpAssignment, VmPayment,
|
||||||
|
};
|
||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
use nostr::util::hex;
|
use nostr::util::hex;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@ -30,11 +33,9 @@ pub struct LNVpsProvisioner {
|
|||||||
rates: Arc<dyn ExchangeRateService>,
|
rates: Arc<dyn ExchangeRateService>,
|
||||||
tax_rates: HashMap<CountryCode, f32>,
|
tax_rates: HashMap<CountryCode, f32>,
|
||||||
|
|
||||||
router: Option<Arc<dyn Router>>,
|
|
||||||
dns: Option<Arc<dyn DnsServer>>,
|
dns: Option<Arc<dyn DnsServer>>,
|
||||||
revolut: Option<Arc<dyn FiatPaymentService>>,
|
revolut: Option<Arc<dyn FiatPaymentService>>,
|
||||||
|
|
||||||
network_policy: NetworkPolicy,
|
|
||||||
provisioner_config: ProvisionerConfig,
|
provisioner_config: ProvisionerConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,48 +50,90 @@ impl LNVpsProvisioner {
|
|||||||
db,
|
db,
|
||||||
node,
|
node,
|
||||||
rates,
|
rates,
|
||||||
router: settings.get_router().expect("router config"),
|
|
||||||
dns: settings.get_dns().expect("dns config"),
|
dns: settings.get_dns().expect("dns config"),
|
||||||
revolut: settings.get_revolut().expect("revolut config"),
|
revolut: settings.get_revolut().expect("revolut config"),
|
||||||
tax_rates: settings.tax_rate,
|
tax_rates: settings.tax_rate,
|
||||||
network_policy: settings.network_policy,
|
|
||||||
provisioner_config: settings.provisioner,
|
provisioner_config: settings.provisioner,
|
||||||
read_only: settings.read_only,
|
read_only: settings.read_only,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_router(&self, router_id: u64) -> Result<Arc<dyn Router>> {
|
||||||
|
#[cfg(test)]
|
||||||
|
return Ok(Arc::new(crate::mocks::MockRouter::new()));
|
||||||
|
|
||||||
|
let cfg = self.db.get_router(router_id).await?;
|
||||||
|
match cfg.kind {
|
||||||
|
RouterKind::Mikrotik => {
|
||||||
|
let mut t_split = cfg.token.split(":");
|
||||||
|
let (username, password) = (
|
||||||
|
t_split.next().context("Invalid username:password")?,
|
||||||
|
t_split.next().context("Invalid username:password")?,
|
||||||
|
);
|
||||||
|
Ok(Arc::new(MikrotikRouter::new(&cfg.url, username, password)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Create or Update access policy for a given ip assignment, does not save to database!
|
/// 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<()> {
|
pub async fn update_access_policy(
|
||||||
|
&self,
|
||||||
|
assignment: &mut VmIpAssignment,
|
||||||
|
policy: &AccessPolicy,
|
||||||
|
) -> Result<()> {
|
||||||
// apply network policy
|
// apply network policy
|
||||||
if let NetworkAccessPolicy::StaticArp { interface } = &self.network_policy.access {
|
if let NetworkAccessPolicy::StaticArp = policy.kind {
|
||||||
if let Some(r) = self.router.as_ref() {
|
let router = self
|
||||||
|
.get_router(
|
||||||
|
policy
|
||||||
|
.router_id
|
||||||
|
.context("Cannot apply static arp policy with no router")?,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
let vm = self.db.get_vm(assignment.vm_id).await?;
|
let vm = self.db.get_vm(assignment.vm_id).await?;
|
||||||
let entry = ArpEntry::new(&vm, assignment, Some(interface.clone()))?;
|
let entry = ArpEntry::new(
|
||||||
|
&vm,
|
||||||
|
assignment,
|
||||||
|
Some(
|
||||||
|
policy
|
||||||
|
.interface
|
||||||
|
.as_ref()
|
||||||
|
.context("Cannot apply static arp entry without an interface name")?
|
||||||
|
.clone(),
|
||||||
|
),
|
||||||
|
)?;
|
||||||
let arp = if let Some(_id) = &assignment.arp_ref {
|
let arp = if let Some(_id) = &assignment.arp_ref {
|
||||||
r.update_arp_entry(&entry).await?
|
router.update_arp_entry(&entry).await?
|
||||||
} else {
|
} else {
|
||||||
r.add_arp_entry(&entry).await?
|
router.add_arp_entry(&entry).await?
|
||||||
};
|
};
|
||||||
ensure!(arp.id.is_some(), "ARP id was empty");
|
ensure!(arp.id.is_some(), "ARP id was empty");
|
||||||
assignment.arp_ref = arp.id;
|
assignment.arp_ref = arp.id;
|
||||||
} else {
|
|
||||||
bail!("No router found to apply static arp entry!")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove an access policy for a given ip assignment, does not save to database!
|
/// 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<()> {
|
pub async fn remove_access_policy(
|
||||||
|
&self,
|
||||||
|
assignment: &mut VmIpAssignment,
|
||||||
|
policy: &AccessPolicy,
|
||||||
|
) -> Result<()> {
|
||||||
// Delete access policy
|
// Delete access policy
|
||||||
if let NetworkAccessPolicy::StaticArp { .. } = &self.network_policy.access {
|
if let NetworkAccessPolicy::StaticArp = &policy.kind {
|
||||||
if let Some(r) = self.router.as_ref() {
|
let router = self
|
||||||
|
.get_router(
|
||||||
|
policy
|
||||||
|
.router_id
|
||||||
|
.context("Cannot apply static arp policy with no router")?,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
let id = if let Some(id) = &assignment.arp_ref {
|
let id = if let Some(id) = &assignment.arp_ref {
|
||||||
Some(id.clone())
|
Some(id.clone())
|
||||||
} else {
|
} else {
|
||||||
warn!("ARP REF not found, using arp list");
|
warn!("ARP REF not found, using arp list");
|
||||||
|
|
||||||
let ent = r.list_arp_entry().await?;
|
let ent = router.list_arp_entry().await?;
|
||||||
if let Some(ent) = ent.iter().find(|e| e.address == assignment.ip) {
|
if let Some(ent) = ent.iter().find(|e| e.address == assignment.ip) {
|
||||||
ent.id.clone()
|
ent.id.clone()
|
||||||
} else {
|
} else {
|
||||||
@ -100,13 +143,12 @@ impl LNVpsProvisioner {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Some(id) = id {
|
if let Some(id) = id {
|
||||||
if let Err(e) = r.remove_arp_entry(&id).await {
|
if let Err(e) = router.remove_arp_entry(&id).await {
|
||||||
warn!("Failed to remove arp entry, skipping: {}", e);
|
warn!("Failed to remove arp entry, skipping: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assignment.arp_ref = None;
|
assignment.arp_ref = None;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,8 +211,13 @@ impl LNVpsProvisioner {
|
|||||||
pub async fn delete_ip_assignments(&self, vm_id: u64) -> Result<()> {
|
pub async fn delete_ip_assignments(&self, vm_id: u64) -> Result<()> {
|
||||||
let ips = self.db.list_vm_ip_assignments(vm_id).await?;
|
let ips = self.db.list_vm_ip_assignments(vm_id).await?;
|
||||||
for mut ip in ips {
|
for mut ip in ips {
|
||||||
|
// load range info to check access policy
|
||||||
|
let range = self.db.get_ip_range(ip.ip_range_id).await?;
|
||||||
|
if let Some(ap) = range.access_policy_id {
|
||||||
|
let ap = self.db.get_access_policy(ap).await?;
|
||||||
// remove access policy
|
// remove access policy
|
||||||
self.remove_access_policy(&mut ip).await?;
|
self.remove_access_policy(&mut ip, &ap).await?;
|
||||||
|
}
|
||||||
// remove dns
|
// remove dns
|
||||||
self.remove_ip_dns(&mut ip).await?;
|
self.remove_ip_dns(&mut ip).await?;
|
||||||
// save arp/dns changes
|
// save arp/dns changes
|
||||||
@ -183,8 +230,13 @@ impl LNVpsProvisioner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn save_ip_assignment(&self, assignment: &mut VmIpAssignment) -> Result<()> {
|
async fn save_ip_assignment(&self, assignment: &mut VmIpAssignment) -> Result<()> {
|
||||||
|
// load range info to check access policy
|
||||||
|
let range = self.db.get_ip_range(assignment.ip_range_id).await?;
|
||||||
|
if let Some(ap) = range.access_policy_id {
|
||||||
|
let ap = self.db.get_access_policy(ap).await?;
|
||||||
// apply access policy
|
// apply access policy
|
||||||
self.update_access_policy(assignment).await?;
|
self.update_access_policy(assignment, &ap).await?;
|
||||||
|
}
|
||||||
|
|
||||||
// Add DNS records
|
// Add DNS records
|
||||||
self.update_forward_ip_dns(assignment).await?;
|
self.update_forward_ip_dns(assignment).await?;
|
||||||
@ -498,9 +550,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::exchange::{DefaultRateCache, Ticker};
|
use crate::exchange::{DefaultRateCache, Ticker};
|
||||||
use crate::mocks::{MockDb, MockDnsServer, MockExchangeRate, MockNode, MockRouter};
|
use crate::mocks::{MockDb, MockDnsServer, MockExchangeRate, MockNode, MockRouter};
|
||||||
use crate::settings::{
|
use crate::settings::mock_settings;
|
||||||
mock_settings, DnsServerConfig, LightningConfig, QemuConfig, RouterConfig,
|
|
||||||
};
|
|
||||||
use lnvps_db::{DiskInterface, DiskType, User, UserSshKey, VmTemplate};
|
use lnvps_db::{DiskInterface, DiskType, User, UserSshKey, VmTemplate};
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
@ -509,9 +559,6 @@ mod tests {
|
|||||||
|
|
||||||
pub fn settings() -> Settings {
|
pub fn settings() -> Settings {
|
||||||
let mut settings = mock_settings();
|
let mut settings = mock_settings();
|
||||||
settings.network_policy.access = NetworkAccessPolicy::StaticArp {
|
|
||||||
interface: ROUTER_BRIDGE.to_string(),
|
|
||||||
};
|
|
||||||
settings
|
settings
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -540,7 +587,36 @@ mod tests {
|
|||||||
const MOCK_RATE: f32 = 69_420.0;
|
const MOCK_RATE: f32 = 69_420.0;
|
||||||
rates.set_rate(Ticker::btc_rate("EUR")?, MOCK_RATE).await;
|
rates.set_rate(Ticker::btc_rate("EUR")?, MOCK_RATE).await;
|
||||||
|
|
||||||
let router = MockRouter::new(settings.network_policy.clone());
|
// add static arp policy
|
||||||
|
{
|
||||||
|
let mut r = db.router.lock().await;
|
||||||
|
r.insert(
|
||||||
|
1,
|
||||||
|
lnvps_db::Router {
|
||||||
|
id: 1,
|
||||||
|
name: "mock-router".to_string(),
|
||||||
|
enabled: true,
|
||||||
|
kind: RouterKind::Mikrotik,
|
||||||
|
url: "https://localhost".to_string(),
|
||||||
|
token: "username:password".to_string(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let mut p = db.access_policy.lock().await;
|
||||||
|
p.insert(
|
||||||
|
1,
|
||||||
|
AccessPolicy {
|
||||||
|
id: 1,
|
||||||
|
name: "static-arp".to_string(),
|
||||||
|
kind: NetworkAccessPolicy::StaticArp,
|
||||||
|
router_id: Some(1),
|
||||||
|
interface: Some(ROUTER_BRIDGE.to_string()),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let mut i = db.ip_range.lock().await;
|
||||||
|
let r = i.get_mut(&1).unwrap();
|
||||||
|
r.access_policy_id = Some(1);
|
||||||
|
}
|
||||||
|
|
||||||
let dns = MockDnsServer::new();
|
let dns = MockDnsServer::new();
|
||||||
let provisioner = LNVpsProvisioner::new(settings, db.clone(), node.clone(), rates.clone());
|
let provisioner = LNVpsProvisioner::new(settings, db.clone(), node.clone(), rates.clone());
|
||||||
|
|
||||||
@ -567,6 +643,7 @@ mod tests {
|
|||||||
provisioner.spawn_vm(vm.id).await?;
|
provisioner.spawn_vm(vm.id).await?;
|
||||||
|
|
||||||
// check resources
|
// check resources
|
||||||
|
let router = MockRouter::new();
|
||||||
let arp = router.list_arp_entry().await?;
|
let arp = router.list_arp_entry().await?;
|
||||||
assert_eq!(1, arp.len());
|
assert_eq!(1, arp.len());
|
||||||
let arp = arp.first().unwrap();
|
let arp = arp.first().unwrap();
|
||||||
|
@ -3,7 +3,6 @@ use crate::exchange::ExchangeRateService;
|
|||||||
use crate::fiat::FiatPaymentService;
|
use crate::fiat::FiatPaymentService;
|
||||||
use crate::lightning::LightningNode;
|
use crate::lightning::LightningNode;
|
||||||
use crate::provisioner::LNVpsProvisioner;
|
use crate::provisioner::LNVpsProvisioner;
|
||||||
use crate::router::Router;
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use isocountry::CountryCode;
|
use isocountry::CountryCode;
|
||||||
use lnvps_db::LNVpsDb;
|
use lnvps_db::LNVpsDb;
|
||||||
@ -33,19 +32,12 @@ pub struct Settings {
|
|||||||
/// Provisioning profiles
|
/// Provisioning profiles
|
||||||
pub provisioner: ProvisionerConfig,
|
pub provisioner: ProvisionerConfig,
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
/// Network policy
|
|
||||||
pub network_policy: NetworkPolicy,
|
|
||||||
|
|
||||||
/// Number of days after an expired VM is deleted
|
/// Number of days after an expired VM is deleted
|
||||||
pub delete_after: u16,
|
pub delete_after: u16,
|
||||||
|
|
||||||
/// 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>,
|
|
||||||
|
|
||||||
/// DNS configurations for PTR records
|
/// DNS configurations for PTR records
|
||||||
pub dns: Option<DnsServerConfig>,
|
pub dns: Option<DnsServerConfig>,
|
||||||
|
|
||||||
@ -82,16 +74,6 @@ pub struct NostrConfig {
|
|||||||
pub nsec: String,
|
pub nsec: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
||||||
#[serde(rename_all = "kebab-case")]
|
|
||||||
pub enum RouterConfig {
|
|
||||||
Mikrotik {
|
|
||||||
url: String,
|
|
||||||
username: String,
|
|
||||||
password: String,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub enum DnsServerConfig {
|
pub enum DnsServerConfig {
|
||||||
@ -103,30 +85,6 @@ pub enum DnsServerConfig {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Policy that determines how packets arrive at the VM
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
|
||||||
#[serde(rename_all = "kebab-case")]
|
|
||||||
pub enum NetworkAccessPolicy {
|
|
||||||
/// No special procedure required for packets to arrive
|
|
||||||
#[default]
|
|
||||||
Auto,
|
|
||||||
/// ARP entries are added statically on the access router
|
|
||||||
StaticArp {
|
|
||||||
/// Interface used to add arp entries
|
|
||||||
interface: String,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
|
||||||
#[serde(rename_all = "kebab-case")]
|
|
||||||
pub struct NetworkPolicy {
|
|
||||||
/// Policy that determines how packets arrive at the VM
|
|
||||||
pub access: NetworkAccessPolicy,
|
|
||||||
|
|
||||||
/// Use SLAAC for IPv6 allocation
|
|
||||||
pub ip6_slaac: Option<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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
|
||||||
@ -203,32 +161,6 @@ impl Settings {
|
|||||||
Arc::new(LNVpsProvisioner::new(self.clone(), db, node, exchange))
|
Arc::new(LNVpsProvisioner::new(self.clone(), db, node, exchange))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_router(&self) -> Result<Option<Arc<dyn Router>>> {
|
|
||||||
#[cfg(test)]
|
|
||||||
{
|
|
||||||
if let Some(_router) = &self.router {
|
|
||||||
let router = crate::mocks::MockRouter::new(self.network_policy.clone());
|
|
||||||
Ok(Some(Arc::new(router)))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(not(test))]
|
|
||||||
{
|
|
||||||
match &self.router {
|
|
||||||
#[cfg(feature = "mikrotik")]
|
|
||||||
Some(RouterConfig::Mikrotik {
|
|
||||||
url,
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
}) => Ok(Some(Arc::new(crate::router::MikrotikRouter::new(
|
|
||||||
url, username, password,
|
|
||||||
)))),
|
|
||||||
_ => Ok(None),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_dns(&self) -> Result<Option<Arc<dyn DnsServer>>> {
|
pub fn get_dns(&self) -> Result<Option<Arc<dyn DnsServer>>> {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
{
|
{
|
||||||
@ -285,17 +217,8 @@ pub fn mock_settings() -> Settings {
|
|||||||
ssh: None,
|
ssh: None,
|
||||||
mac_prefix: Some("ff:ff:ff".to_string()),
|
mac_prefix: Some("ff:ff:ff".to_string()),
|
||||||
},
|
},
|
||||||
network_policy: NetworkPolicy {
|
|
||||||
access: NetworkAccessPolicy::Auto,
|
|
||||||
ip6_slaac: None,
|
|
||||||
},
|
|
||||||
delete_after: 0,
|
delete_after: 0,
|
||||||
smtp: None,
|
smtp: None,
|
||||||
router: Some(RouterConfig::Mikrotik {
|
|
||||||
url: "https://localhost".to_string(),
|
|
||||||
username: "admin".to_string(),
|
|
||||||
password: "password123".to_string(),
|
|
||||||
}),
|
|
||||||
dns: Some(DnsServerConfig::Cloudflare {
|
dns: Some(DnsServerConfig::Cloudflare {
|
||||||
token: "abc".to_string(),
|
token: "abc".to_string(),
|
||||||
forward_zone_id: "123".to_string(),
|
forward_zone_id: "123".to_string(),
|
||||||
|
Reference in New Issue
Block a user