feat: ipv6 allocation

closes #9
This commit is contained in:
2025-03-28 10:46:16 +00:00
parent f4b8f88772
commit d18f32e897
10 changed files with 322 additions and 143 deletions

View File

@ -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;

View File

@ -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)]

View File

@ -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>> {

View File

@ -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;

View File

@ -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;

View File

@ -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 {

View File

@ -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);
}

View File

@ -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,
}],

View File

@ -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(())
}

View File

@ -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);
}
}