@ -0,0 +1,3 @@
|
||||
alter table ip_range
|
||||
add column allocation_mode smallint unsigned not null default 0,
|
||||
add column use_full_range bit(1) not null;
|
@ -247,7 +247,7 @@ pub enum RouterKind {
|
||||
OvhAdditionalIp = 1,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Clone, Debug)]
|
||||
#[derive(FromRow, Clone, Debug, Default)]
|
||||
pub struct IpRange {
|
||||
pub id: u64,
|
||||
pub cidr: String,
|
||||
@ -256,6 +256,22 @@ pub struct IpRange {
|
||||
pub region_id: u64,
|
||||
pub reverse_zone_id: Option<String>,
|
||||
pub access_policy_id: Option<u64>,
|
||||
pub allocation_mode: IpRangeAllocationMode,
|
||||
/// Use all IPs in the range, including first and last
|
||||
pub use_full_range: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, sqlx::Type, Default)]
|
||||
#[repr(u16)]
|
||||
/// How ips are allocated from this range
|
||||
pub enum IpRangeAllocationMode {
|
||||
/// IPs are assigned in a random order
|
||||
Random = 0,
|
||||
#[default]
|
||||
/// IPs are assigned in sequential order
|
||||
Sequential = 1,
|
||||
/// IP(v6) assignment uses SLAAC EUI-64
|
||||
SlaacEui64 = 2,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Clone, Debug)]
|
||||
|
@ -80,13 +80,13 @@ impl LNVpsDb for LNVpsDbMysql {
|
||||
Ok(sqlx::query(
|
||||
"insert into user_ssh_key(name,user_id,key_data) values(?, ?, ?) returning id",
|
||||
)
|
||||
.bind(&new_key.name)
|
||||
.bind(new_key.user_id)
|
||||
.bind(&new_key.key_data)
|
||||
.fetch_one(&self.db)
|
||||
.await
|
||||
.map_err(Error::new)?
|
||||
.try_get(0)?)
|
||||
.bind(&new_key.name)
|
||||
.bind(new_key.user_id)
|
||||
.bind(&new_key.key_data)
|
||||
.fetch_one(&self.db)
|
||||
.await
|
||||
.map_err(Error::new)?
|
||||
.try_get(0)?)
|
||||
}
|
||||
|
||||
async fn get_user_ssh_key(&self, id: u64) -> Result<UserSshKey> {
|
||||
@ -489,9 +489,9 @@ impl LNVpsDb for LNVpsDbMysql {
|
||||
sqlx::query_as(
|
||||
"select * from vm_payment where is_paid = true order by created desc limit 1",
|
||||
)
|
||||
.fetch_optional(&self.db)
|
||||
.await
|
||||
.map_err(Error::new)
|
||||
.fetch_optional(&self.db)
|
||||
.await
|
||||
.map_err(Error::new)
|
||||
}
|
||||
|
||||
async fn list_custom_pricing(&self, region_id: u64) -> Result<Vec<VmCustomPricing>> {
|
||||
|
@ -1,5 +1,4 @@
|
||||
use anyhow::Error;
|
||||
use chrono::Utc;
|
||||
use clap::Parser;
|
||||
use config::{Config, File};
|
||||
use lnvps::api;
|
||||
@ -13,7 +12,7 @@ use lnvps::settings::Settings;
|
||||
use lnvps::status::VmStateCache;
|
||||
use lnvps::worker::{WorkJob, Worker};
|
||||
use lnvps_db::{LNVpsDb, LNVpsDbMysql};
|
||||
use log::{error, LevelFilter};
|
||||
use log::error;
|
||||
use nostr::Keys;
|
||||
use nostr_sdk::Client;
|
||||
use rocket::http::Method;
|
||||
|
@ -5,7 +5,7 @@ use crate::settings::RevolutConfig;
|
||||
use anyhow::{bail, Result};
|
||||
use chrono::{DateTime, Utc};
|
||||
use nostr::Url;
|
||||
use reqwest::header::{HeaderMap, ACCEPT, AUTHORIZATION};
|
||||
use reqwest::header::AUTHORIZATION;
|
||||
use reqwest::{Client, Method, RequestBuilder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::future::Future;
|
||||
|
@ -10,7 +10,7 @@ use anyhow::{anyhow, bail, ensure, Result};
|
||||
use chrono::Utc;
|
||||
use futures::StreamExt;
|
||||
use ipnetwork::IpNetwork;
|
||||
use lnvps_db::{async_trait, DiskType, Vm, VmOsImage};
|
||||
use lnvps_db::{async_trait, DiskType, IpRangeAllocationMode, Vm, VmOsImage};
|
||||
use log::{info, warn};
|
||||
use rand::random;
|
||||
use reqwest::header::{HeaderMap, AUTHORIZATION};
|
||||
@ -414,7 +414,17 @@ impl ProxmoxClient {
|
||||
range_gw.ip()
|
||||
)
|
||||
}
|
||||
IpAddr::V6(addr) => format!("ip6={}", addr),
|
||||
IpAddr::V6(addr) => {
|
||||
let ip_range = value.ranges.iter().find(|r| r.id == ip.ip_range_id)?;
|
||||
if matches!(ip_range.allocation_mode, IpRangeAllocationMode::SlaacEui64)
|
||||
{
|
||||
// just ignore what's in the db and use whatever the host wants
|
||||
// what's in the db is purely informational
|
||||
"ip6=auto".to_string()
|
||||
} else {
|
||||
format!("ip6={}", addr)
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
None
|
||||
@ -422,9 +432,6 @@ impl ProxmoxClient {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// TODO: make this configurable
|
||||
ip_config.push("ip6=auto".to_string());
|
||||
|
||||
let mut net = vec![
|
||||
format!("virtio={}", value.vm.mac_address),
|
||||
format!("bridge={}", self.config.bridge),
|
||||
@ -932,8 +939,7 @@ impl NodeStorage {
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct NodeDisk {
|
||||
}
|
||||
pub struct NodeDisk {}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct DownloadUrlRequest {
|
||||
@ -1111,8 +1117,8 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::{GB, MB, TB};
|
||||
use lnvps_db::{
|
||||
DiskInterface, IpRange, OsDistribution, UserSshKey, VmHost, VmHostDisk, VmIpAssignment,
|
||||
VmTemplate,
|
||||
DiskInterface, IpRange, IpRangeAllocationMode, OsDistribution, UserSshKey, VmHost,
|
||||
VmHostDisk, VmIpAssignment, VmTemplate,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@ -1207,6 +1213,18 @@ mod tests {
|
||||
dns_reverse: None,
|
||||
dns_reverse_ref: None,
|
||||
},
|
||||
VmIpAssignment {
|
||||
id: 3,
|
||||
vm_id: 1,
|
||||
ip_range_id: 3,
|
||||
ip: "fd00::ff:ff:ff:ff:ff".to_string(),
|
||||
deleted: false,
|
||||
arp_ref: None,
|
||||
dns_forward: None,
|
||||
dns_forward_ref: None,
|
||||
dns_reverse: None,
|
||||
dns_reverse_ref: None,
|
||||
},
|
||||
],
|
||||
ranges: vec![
|
||||
IpRange {
|
||||
@ -1215,8 +1233,7 @@ mod tests {
|
||||
gateway: "192.168.1.1/16".to_string(),
|
||||
enabled: true,
|
||||
region_id: 1,
|
||||
reverse_zone_id: None,
|
||||
access_policy_id: None,
|
||||
..Default::default()
|
||||
},
|
||||
IpRange {
|
||||
id: 2,
|
||||
@ -1224,8 +1241,16 @@ mod tests {
|
||||
gateway: "10.10.10.10".to_string(),
|
||||
enabled: true,
|
||||
region_id: 2,
|
||||
reverse_zone_id: None,
|
||||
access_policy_id: None,
|
||||
..Default::default()
|
||||
},
|
||||
IpRange {
|
||||
id: 3,
|
||||
cidr: "fd00::/64".to_string(),
|
||||
gateway: "fd00::1".to_string(),
|
||||
enabled: true,
|
||||
region_id: 1,
|
||||
allocation_mode: IpRangeAllocationMode::SlaacEui64,
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
ssh_key: UserSshKey {
|
||||
|
42
src/mocks.rs
42
src/mocks.rs
@ -1,19 +1,14 @@
|
||||
#![allow(unused)]
|
||||
use crate::dns::{BasicRecord, DnsServer, RecordType};
|
||||
use crate::exchange::{ExchangeRateService, Ticker, TickerRate};
|
||||
use crate::host::{FullVmInfo, TerminalStream, TimeSeries, TimeSeriesData, VmHostClient};
|
||||
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, 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;
|
||||
@ -117,8 +112,19 @@ impl Default for MockDb {
|
||||
gateway: "10.0.0.1/8".to_string(),
|
||||
enabled: true,
|
||||
region_id: 1,
|
||||
reverse_zone_id: None,
|
||||
access_policy_id: None,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
ip_ranges.insert(
|
||||
2,
|
||||
IpRange {
|
||||
id: 2,
|
||||
cidr: "fd00::/64".to_string(),
|
||||
gateway: "fd00::1".to_string(),
|
||||
enabled: true,
|
||||
region_id: 1,
|
||||
allocation_mode: IpRangeAllocationMode::SlaacEui64,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let mut hosts = HashMap::new();
|
||||
@ -324,6 +330,16 @@ impl LNVpsDb for MockDb {
|
||||
Ok(disks.get(&disk_id).ok_or(anyhow!("no disk"))?.clone())
|
||||
}
|
||||
|
||||
async fn update_host_disk(&self, disk: &VmHostDisk) -> anyhow::Result<()> {
|
||||
let mut disks = self.host_disks.lock().await;
|
||||
if let Some(d) = disks.get_mut(&disk.id) {
|
||||
d.size = disk.size;
|
||||
d.kind = disk.kind;
|
||||
d.interface = disk.interface;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_os_image(&self, id: u64) -> anyhow::Result<VmOsImage> {
|
||||
let os_images = self.os_images.lock().await;
|
||||
Ok(os_images.get(&id).ok_or(anyhow!("no image"))?.clone())
|
||||
@ -478,7 +494,7 @@ impl LNVpsDb for MockDb {
|
||||
|
||||
async fn update_vm_ip_assignment(&self, ip_assignment: &VmIpAssignment) -> anyhow::Result<()> {
|
||||
let mut ip_assignments = self.ip_assignments.lock().await;
|
||||
if let Some(i) = ip_assignments.get_mut(&ip_assignment.vm_id) {
|
||||
if let Some(i) = ip_assignments.get_mut(&ip_assignment.id) {
|
||||
i.arp_ref = ip_assignment.arp_ref.clone();
|
||||
i.dns_forward = ip_assignment.dns_forward.clone();
|
||||
i.dns_reverse = ip_assignment.dns_reverse.clone();
|
||||
@ -757,6 +773,10 @@ impl MockVmHost {
|
||||
|
||||
#[async_trait]
|
||||
impl VmHostClient for MockVmHost {
|
||||
async fn get_info(&self) -> anyhow::Result<VmHostInfo> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn download_os_image(&self, image: &VmOsImage) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
@ -876,7 +896,7 @@ impl DnsServer for MockDnsServer {
|
||||
zones.get_mut(zone_id).unwrap()
|
||||
};
|
||||
|
||||
if table.values().any(|v| v.name == record.name) {
|
||||
if table.values().any(|v| v.name == record.name && v.kind == record.kind.to_string()) {
|
||||
bail!("Duplicate record with name {}", record.name);
|
||||
}
|
||||
|
||||
|
@ -354,8 +354,7 @@ mod tests {
|
||||
gateway: "10.0.0.1".to_string(),
|
||||
enabled: true,
|
||||
region_id: 1,
|
||||
reverse_zone_id: None,
|
||||
access_policy_id: None,
|
||||
..Default::default()
|
||||
},
|
||||
usage: 69,
|
||||
}],
|
||||
|
@ -3,22 +3,22 @@ use crate::exchange::{Currency, CurrencyAmount, ExchangeRateService};
|
||||
use crate::fiat::FiatPaymentService;
|
||||
use crate::host::{get_host_client, FullVmInfo};
|
||||
use crate::lightning::{AddInvoiceRequest, LightningNode};
|
||||
use crate::provisioner::{
|
||||
CostResult, HostCapacityService, NetworkProvisioner, PricingEngine, ProvisionerMethod,
|
||||
};
|
||||
use crate::provisioner::{CostResult, HostCapacityService, NetworkProvisioner, PricingEngine};
|
||||
use crate::router::{ArpEntry, MikrotikRouter, OvhDedicatedServerVMacRouter, Router};
|
||||
use crate::settings::{ProvisionerConfig, Settings};
|
||||
use anyhow::{bail, ensure, Context, Result};
|
||||
use chrono::Utc;
|
||||
use ipnetwork::IpNetwork;
|
||||
use isocountry::CountryCode;
|
||||
use lnvps_db::{
|
||||
AccessPolicy, LNVpsDb, NetworkAccessPolicy, PaymentMethod, RouterKind, Vm, VmCustomTemplate,
|
||||
VmHost, VmIpAssignment, VmPayment,
|
||||
AccessPolicy, IpRangeAllocationMode, LNVpsDb, NetworkAccessPolicy, PaymentMethod, RouterKind,
|
||||
Vm, VmCustomTemplate, VmHost, VmIpAssignment, VmPayment,
|
||||
};
|
||||
use log::{info, warn};
|
||||
use nostr::util::hex;
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Add;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
@ -88,8 +88,8 @@ impl LNVpsProvisioner {
|
||||
assignment: &mut VmIpAssignment,
|
||||
policy: &AccessPolicy,
|
||||
) -> Result<()> {
|
||||
// apply network policy
|
||||
if let NetworkAccessPolicy::StaticArp = policy.kind {
|
||||
let ip = IpNetwork::from_str(&assignment.ip)?;
|
||||
if matches!(policy.kind, NetworkAccessPolicy::StaticArp) && ip.is_ipv4() {
|
||||
let router = self
|
||||
.get_router(
|
||||
policy
|
||||
@ -116,8 +116,8 @@ impl LNVpsProvisioner {
|
||||
assignment: &mut VmIpAssignment,
|
||||
policy: &AccessPolicy,
|
||||
) -> Result<()> {
|
||||
// Delete access policy
|
||||
if let NetworkAccessPolicy::StaticArp = &policy.kind {
|
||||
let ip = IpNetwork::from_str(&assignment.ip)?;
|
||||
if matches!(policy.kind, NetworkAccessPolicy::StaticArp) && ip.is_ipv4() {
|
||||
let router = self
|
||||
.get_router(
|
||||
policy
|
||||
@ -292,26 +292,56 @@ impl LNVpsProvisioner {
|
||||
}
|
||||
|
||||
// Use random network provisioner
|
||||
let network = NetworkProvisioner::new(ProvisionerMethod::Random, self.db.clone());
|
||||
let network = NetworkProvisioner::new(self.db.clone());
|
||||
|
||||
let host = self.db.get_host(vm.host_id).await?;
|
||||
let ip = network.pick_ip_for_region(host.region_id).await?;
|
||||
let mut assignment = VmIpAssignment {
|
||||
vm_id,
|
||||
ip_range_id: ip.range_id,
|
||||
ip: ip.ip.to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
let mut assignments = vec![];
|
||||
match ip.ip4 {
|
||||
Some(v4) => {
|
||||
let mut assignment = VmIpAssignment {
|
||||
vm_id,
|
||||
ip_range_id: v4.range_id,
|
||||
ip: v4.ip.ip().to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
//generate mac address from ip assignment
|
||||
let mac = self.get_mac_for_assignment(&host, &vm, &assignment).await?;
|
||||
vm.mac_address = mac.mac_address;
|
||||
assignment.arp_ref = mac.id; // store ref if we got one
|
||||
self.db.update_vm(&vm).await?;
|
||||
//generate mac address from ip assignment
|
||||
let mac = self.get_mac_for_assignment(&host, &vm, &assignment).await?;
|
||||
vm.mac_address = mac.mac_address;
|
||||
assignment.arp_ref = mac.id; // store ref if we got one
|
||||
self.db.update_vm(&vm).await?;
|
||||
|
||||
self.save_ip_assignment(&mut assignment).await?;
|
||||
self.save_ip_assignment(&mut assignment).await?;
|
||||
assignments.push(assignment);
|
||||
}
|
||||
None => bail!("Cannot provision VM without an IPv4 address"),
|
||||
}
|
||||
match ip.ip6 {
|
||||
Some(mut v6) => {
|
||||
match v6.mode {
|
||||
// it's a bit awkward but we need to update the IP AFTER its been picked
|
||||
// simply because sometimes we dont know the MAC of the NIC yet
|
||||
IpRangeAllocationMode::SlaacEui64 => {
|
||||
let mac = NetworkProvisioner::parse_mac(&vm.mac_address)?;
|
||||
let addr = NetworkProvisioner::calculate_eui64(&mac, &v6.ip)?;
|
||||
v6.ip = IpNetwork::new(addr, v6.ip.prefix())?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let mut assignment = VmIpAssignment {
|
||||
vm_id,
|
||||
ip_range_id: v6.range_id,
|
||||
ip: v6.ip.ip().to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
self.save_ip_assignment(&mut assignment).await?;
|
||||
assignments.push(assignment);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
Ok(vec![assignment])
|
||||
Ok(assignments)
|
||||
}
|
||||
|
||||
/// Do any necessary initialization
|
||||
@ -684,28 +714,30 @@ mod tests {
|
||||
println!("{:?}", arp);
|
||||
|
||||
let ips = db.list_vm_ip_assignments(vm.id).await?;
|
||||
assert_eq!(1, ips.len());
|
||||
let ip = ips.first().unwrap();
|
||||
println!("{:?}", ip);
|
||||
assert_eq!(ip.ip, arp.address);
|
||||
assert_eq!(ip.ip_range_id, 1);
|
||||
assert_eq!(ip.vm_id, vm.id);
|
||||
assert!(ip.dns_forward.is_some());
|
||||
assert!(ip.dns_reverse.is_some());
|
||||
assert!(ip.dns_reverse_ref.is_some());
|
||||
assert!(ip.dns_forward_ref.is_some());
|
||||
assert_eq!(ip.dns_reverse, ip.dns_forward);
|
||||
assert_eq!(2, ips.len());
|
||||
|
||||
// lookup v4 ip
|
||||
let v4 = ips.iter().find(|r| r.ip_range_id == 1).unwrap();
|
||||
println!("{:?}", v4);
|
||||
assert_eq!(v4.ip, arp.address);
|
||||
assert_eq!(v4.ip_range_id, 1);
|
||||
assert_eq!(v4.vm_id, vm.id);
|
||||
assert!(v4.dns_forward.is_some());
|
||||
assert!(v4.dns_reverse.is_some());
|
||||
assert!(v4.dns_reverse_ref.is_some());
|
||||
assert!(v4.dns_forward_ref.is_some());
|
||||
assert_eq!(v4.dns_reverse, v4.dns_forward);
|
||||
|
||||
// assert IP address is not CIDR
|
||||
assert!(IpAddr::from_str(&ip.ip).is_ok());
|
||||
assert!(!ip.ip.ends_with("/8"));
|
||||
assert!(!ip.ip.ends_with("/24"));
|
||||
assert!(IpAddr::from_str(&v4.ip).is_ok());
|
||||
assert!(!v4.ip.ends_with("/8"));
|
||||
assert!(!v4.ip.ends_with("/24"));
|
||||
|
||||
// test zones have dns entries
|
||||
{
|
||||
let zones = dns.zones.lock().await;
|
||||
assert_eq!(zones.get("mock-rev-zone-id").unwrap().len(), 1);
|
||||
assert_eq!(zones.get("mock-forward-zone-id").unwrap().len(), 1);
|
||||
assert_eq!(zones.get("mock-forward-zone-id").unwrap().len(), 2);
|
||||
}
|
||||
|
||||
// now expire
|
||||
@ -723,15 +755,16 @@ mod tests {
|
||||
}
|
||||
|
||||
// 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);
|
||||
let ips = db.list_vm_ip_assignments(vm.id).await?;
|
||||
for ip in ips {
|
||||
println!("{:?}", ip);
|
||||
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);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,89 +1,167 @@
|
||||
use anyhow::{bail, Result};
|
||||
use ipnetwork::IpNetwork;
|
||||
use lnvps_db::LNVpsDb;
|
||||
use anyhow::{bail, Context, Result};
|
||||
use clap::builder::TypedValueParser;
|
||||
use ipnetwork::{IpNetwork, Ipv6Network};
|
||||
use lnvps_db::{IpRange, IpRangeAllocationMode, LNVpsDb};
|
||||
use log::warn;
|
||||
use rand::prelude::IteratorRandom;
|
||||
use rocket::form::validate::Contains;
|
||||
use rocket::http::ext::IntoCollection;
|
||||
use std::collections::HashSet;
|
||||
use std::net::IpAddr;
|
||||
use std::net::{IpAddr, Ipv6Addr};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ProvisionerMethod {
|
||||
Sequential,
|
||||
Random,
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AvailableIps {
|
||||
pub ip4: Option<AvailableIp>,
|
||||
pub ip6: Option<AvailableIp>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AvailableIp {
|
||||
pub ip: IpAddr,
|
||||
pub ip: IpNetwork,
|
||||
pub gateway: IpNetwork,
|
||||
pub range_id: u64,
|
||||
pub region_id: u64,
|
||||
pub mode: IpRangeAllocationMode,
|
||||
}
|
||||
|
||||
/// Handles picking available IPs
|
||||
#[derive(Clone)]
|
||||
pub struct NetworkProvisioner {
|
||||
method: ProvisionerMethod,
|
||||
db: Arc<dyn LNVpsDb>,
|
||||
}
|
||||
|
||||
impl NetworkProvisioner {
|
||||
pub fn new(method: ProvisionerMethod, db: Arc<dyn LNVpsDb>) -> Self {
|
||||
Self { method, db }
|
||||
pub fn new(db: Arc<dyn LNVpsDb>) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
/// Pick an IP from one of the available ip ranges
|
||||
/// This method MUST return a free IP which can be used
|
||||
pub async fn pick_ip_for_region(&self, region_id: u64) -> Result<AvailableIp> {
|
||||
pub async fn pick_ip_for_region(&self, region_id: u64) -> Result<AvailableIps> {
|
||||
let ip_ranges = self.db.list_ip_range_in_region(region_id).await?;
|
||||
if ip_ranges.is_empty() {
|
||||
bail!("No ip range found in this region");
|
||||
}
|
||||
|
||||
let mut ret = AvailableIps {
|
||||
ip4: None,
|
||||
ip6: None,
|
||||
};
|
||||
for range in ip_ranges {
|
||||
let range_cidr: IpNetwork = range.cidr.parse()?;
|
||||
let ips = self.db.list_vm_ip_assignments_in_range(range.id).await?;
|
||||
let mut ips: HashSet<IpAddr> = ips.iter().map_while(|i| i.ip.parse().ok()).collect();
|
||||
if ret.ip4.is_none() && range_cidr.is_ipv4() {
|
||||
ret.ip4 = match self.pick_ip_from_range(&range).await {
|
||||
Ok(i) => Some(i),
|
||||
Err(e) => {
|
||||
warn!("Failed to pick ip range: {} {}", range.cidr, e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
if ret.ip6.is_none() && range_cidr.is_ipv6() {
|
||||
ret.ip6 = match self.pick_ip_from_range(&range).await {
|
||||
Ok(i) => Some(i),
|
||||
Err(e) => {
|
||||
warn!("Failed to pick ip range: {} {}", range.cidr, e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ret.ip4.is_none() && ret.ip6.is_none() {
|
||||
bail!("No IPs available in this region");
|
||||
} else {
|
||||
Ok(ret)
|
||||
}
|
||||
}
|
||||
|
||||
let gateway: IpNetwork = range.gateway.parse()?;
|
||||
pub async fn pick_ip_from_range(&self, range: &IpRange) -> Result<AvailableIp> {
|
||||
let range_cidr: IpNetwork = range.cidr.parse()?;
|
||||
let ips = self.db.list_vm_ip_assignments_in_range(range.id).await?;
|
||||
let mut ips: HashSet<IpAddr> = ips.iter().map_while(|i| i.ip.parse().ok()).collect();
|
||||
|
||||
// mark some IPS as always used
|
||||
// Namely:
|
||||
// .0 & .255 of /24 (first and last)
|
||||
// gateway ip of the range
|
||||
let gateway: IpNetwork = range.gateway.parse()?;
|
||||
|
||||
// mark some IPS as always used
|
||||
// Namely:
|
||||
// .0 & .255 of /24 (first and last)
|
||||
// gateway ip of the range
|
||||
if !range.use_full_range && range_cidr.is_ipv4() {
|
||||
ips.insert(range_cidr.iter().next().unwrap());
|
||||
ips.insert(range_cidr.iter().last().unwrap());
|
||||
ips.insert(gateway.ip());
|
||||
}
|
||||
ips.insert(gateway.ip());
|
||||
|
||||
// pick an IP at random
|
||||
let ip_pick = {
|
||||
match self.method {
|
||||
ProvisionerMethod::Sequential => range_cidr.iter().find(|i| !ips.contains(i)),
|
||||
ProvisionerMethod::Random => {
|
||||
let mut rng = rand::rng();
|
||||
loop {
|
||||
if let Some(i) = range_cidr.iter().choose(&mut rng) {
|
||||
if !ips.contains(&i) {
|
||||
break Some(i);
|
||||
}
|
||||
} else {
|
||||
break None;
|
||||
// pick an IP from the range
|
||||
let ip_pick = {
|
||||
match &range.allocation_mode {
|
||||
IpRangeAllocationMode::Sequential => range_cidr
|
||||
.iter()
|
||||
.find(|i| !ips.contains(i))
|
||||
.and_then(|i| IpNetwork::new(i, range_cidr.prefix()).ok()),
|
||||
IpRangeAllocationMode::Random => {
|
||||
let mut rng = rand::rng();
|
||||
loop {
|
||||
if let Some(i) = range_cidr.iter().choose(&mut rng) {
|
||||
if !ips.contains(&i) {
|
||||
break IpNetwork::new(i, range_cidr.prefix()).ok();
|
||||
}
|
||||
} else {
|
||||
break None;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(ip_pick) = ip_pick {
|
||||
return Ok(AvailableIp {
|
||||
range_id: range.id,
|
||||
gateway,
|
||||
ip: ip_pick,
|
||||
region_id,
|
||||
});
|
||||
IpRangeAllocationMode::SlaacEui64 => {
|
||||
if range_cidr.network().is_ipv4() {
|
||||
bail!("Cannot create EUI-64 from IPv4 address")
|
||||
} else {
|
||||
// basically always free ips here
|
||||
Some(range_cidr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
bail!("No IPs available in this region");
|
||||
.context("No ips available in range")?;
|
||||
|
||||
Ok(AvailableIp {
|
||||
range_id: range.id,
|
||||
gateway,
|
||||
ip: ip_pick,
|
||||
region_id: range.region_id,
|
||||
mode: range.allocation_mode.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn calculate_eui64(mac: &[u8; 6], prefix: &IpNetwork) -> Result<IpAddr> {
|
||||
if prefix.is_ipv4() {
|
||||
bail!("Prefix must be IPv6".to_string())
|
||||
}
|
||||
|
||||
let mut eui64 = [0u8; 8];
|
||||
eui64[0] = mac[0] ^ 0x02;
|
||||
eui64[1] = mac[1];
|
||||
eui64[2] = mac[2];
|
||||
eui64[3] = 0xFF;
|
||||
eui64[4] = 0xFE;
|
||||
eui64[5] = mac[3];
|
||||
eui64[6] = mac[4];
|
||||
eui64[7] = mac[5];
|
||||
|
||||
// Combine prefix with EUI-64 interface identifier
|
||||
let mut prefix_bytes = match prefix.network() {
|
||||
IpAddr::V4(_) => bail!("Not supported"),
|
||||
IpAddr::V6(v6) => v6.octets(),
|
||||
};
|
||||
// copy EUI-64 into prefix
|
||||
prefix_bytes[8..16].copy_from_slice(&eui64);
|
||||
|
||||
let ipv6_addr = Ipv6Addr::from(prefix_bytes);
|
||||
Ok(IpAddr::V6(ipv6_addr))
|
||||
}
|
||||
|
||||
pub fn parse_mac(mac: &str) -> Result<[u8; 6]> {
|
||||
Ok(hex::decode(mac.replace(":", ""))?.as_slice().try_into()?)
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,38 +176,44 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn pick_seq_ip_for_region_test() {
|
||||
let db: Arc<dyn LNVpsDb> = Arc::new(MockDb::default());
|
||||
let mgr = NetworkProvisioner::new(ProvisionerMethod::Sequential, db.clone());
|
||||
let mgr = NetworkProvisioner::new(db.clone());
|
||||
|
||||
let mac: [u8; 6] = [0xff, 0xff, 0xff, 0xfa, 0xfb, 0xfc];
|
||||
let gateway = IpNetwork::from_str("10.0.0.1/8").unwrap();
|
||||
let first = IpAddr::from_str("10.0.0.2").unwrap();
|
||||
let second = IpAddr::from_str("10.0.0.3").unwrap();
|
||||
let ip = mgr.pick_ip_for_region(1).await.expect("No ip found in db");
|
||||
assert_eq!(1, ip.region_id);
|
||||
assert_eq!(first, ip.ip);
|
||||
assert_eq!(gateway, ip.gateway);
|
||||
let v4 = ip.ip4.unwrap();
|
||||
assert_eq!(v4.region_id, 1);
|
||||
assert_eq!(first, v4.ip.ip());
|
||||
assert_eq!(gateway, v4.gateway);
|
||||
|
||||
let ip = mgr.pick_ip_for_region(1).await.expect("No ip found in db");
|
||||
assert_eq!(1, ip.region_id);
|
||||
assert_eq!(first, ip.ip);
|
||||
let v4 = ip.ip4.unwrap();
|
||||
assert_eq!(1, v4.region_id);
|
||||
assert_eq!(first, v4.ip.ip());
|
||||
db.insert_vm_ip_assignment(&VmIpAssignment {
|
||||
id: 0,
|
||||
vm_id: 0,
|
||||
ip_range_id: ip.range_id,
|
||||
ip: ip.ip.to_string(),
|
||||
ip_range_id: v4.range_id,
|
||||
ip: v4.ip.ip().to_string(),
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.expect("Could not insert vm ip");
|
||||
let ip = mgr.pick_ip_for_region(1).await.expect("No ip found in db");
|
||||
assert_eq!(second, ip.ip);
|
||||
let v4 = ip.ip4.unwrap();
|
||||
assert_eq!(second, v4.ip.ip());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pick_rng_ip_for_region_test() {
|
||||
let db: Arc<dyn LNVpsDb> = Arc::new(MockDb::default());
|
||||
let mgr = NetworkProvisioner::new(ProvisionerMethod::Random, db);
|
||||
let mgr = NetworkProvisioner::new(db);
|
||||
|
||||
let mac: [u8; 6] = [0xff, 0xff, 0xff, 0xfa, 0xfb, 0xfc];
|
||||
let ip = mgr.pick_ip_for_region(1).await.expect("No ip found in db");
|
||||
assert_eq!(1, ip.region_id);
|
||||
let v4 = ip.ip4.unwrap();
|
||||
assert_eq!(1, v4.region_id);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user