feat: move zone-id configuration
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2025-03-26 16:35:57 +00:00
parent 32fc16dca2
commit c570222e8a
7 changed files with 122 additions and 95 deletions

View File

@ -95,12 +95,13 @@ nostr:
To create PTR records automatically use the following config: To create PTR records automatically use the following config:
```yaml ```yaml
dns: dns:
cloudflare: # The zone where forward (A/AAAA) entries are added (eg. lnvps.cloud zone)
# The zone where forward (A/AAAA) entries are added (eg. lnvps.cloud zone) # We create forward entries with the format vm-<vmid>.lnvps.cloud
# We create forward entries with the format vm-<vmid>.lnvps.cloud forward-zone-id: "my-forward-zone-id"
forward-zone-id: "my-forward-zone-id" api:
# API token to add/remove DNS records to this zone cloudflare:
token: "my-api-token" # API token to add/remove DNS records to this zone
token: "my-api-token"
``` ```
### Taxes ### Taxes

View File

@ -10,12 +10,17 @@ use std::sync::Arc;
pub struct DnsDataMigration { pub struct DnsDataMigration {
db: Arc<dyn LNVpsDb>, db: Arc<dyn LNVpsDb>,
dns: Arc<dyn DnsServer>, dns: Arc<dyn DnsServer>,
forward_zone_id: Option<String>,
} }
impl DnsDataMigration { impl DnsDataMigration {
pub fn new(db: Arc<dyn LNVpsDb>, settings: &Settings) -> Option<Self> { pub fn new(db: Arc<dyn LNVpsDb>, settings: &Settings) -> Option<Self> {
let dns = settings.get_dns().ok().flatten()?; let dns = settings.get_dns().ok().flatten()?;
Some(Self { db, dns }) Some(Self {
db,
dns,
forward_zone_id: settings.dns.as_ref().map(|z| z.forward_zone_id.to_string()),
})
} }
} }
@ -23,7 +28,13 @@ impl DataMigration for DnsDataMigration {
fn migrate(&self) -> Pin<Box<dyn Future<Output = Result<()>> + Send>> { fn migrate(&self) -> Pin<Box<dyn Future<Output = Result<()>> + Send>> {
let db = self.db.clone(); let db = self.db.clone();
let dns = self.dns.clone(); let dns = self.dns.clone();
let forward_zone_id = self.forward_zone_id.clone();
Box::pin(async move { Box::pin(async move {
let zone_id = if let Some(z) = forward_zone_id {
z
} else {
return Ok(());
};
let vms = db.list_vms().await?; let vms = db.list_vms().await?;
for vm in vms { for vm in vms {
@ -32,14 +43,14 @@ impl DataMigration for DnsDataMigration {
let mut did_change = false; let mut did_change = false;
if ip.dns_forward.is_none() { if ip.dns_forward.is_none() {
let rec = BasicRecord::forward(ip)?; let rec = BasicRecord::forward(ip)?;
let r = dns.add_record(&rec).await?; let r = dns.add_record(&zone_id, &rec).await?;
ip.dns_forward = Some(r.name); ip.dns_forward = Some(r.name);
ip.dns_forward_ref = r.id; ip.dns_forward_ref = r.id;
did_change = true; did_change = true;
} }
if ip.dns_reverse.is_none() { if ip.dns_reverse.is_none() {
let rec = BasicRecord::reverse_to_fwd(ip)?; let rec = BasicRecord::reverse_to_fwd(ip)?;
let r = dns.add_record(&rec).await?; let r = dns.add_record(&zone_id, &rec).await?;
ip.dns_reverse = Some(r.value); ip.dns_reverse = Some(r.value);
ip.dns_reverse_ref = r.id; ip.dns_reverse_ref = r.id;
did_change = true; did_change = true;

View File

@ -1,4 +1,4 @@
use crate::dns::{BasicRecord, DnsServer, RecordType}; use crate::dns::{BasicRecord, DnsServer};
use crate::json_api::JsonApi; use crate::json_api::JsonApi;
use anyhow::Context; use anyhow::Context;
use lnvps_db::async_trait; use lnvps_db::async_trait;
@ -7,12 +7,10 @@ use serde::{Deserialize, Serialize};
pub struct Cloudflare { pub struct Cloudflare {
api: JsonApi, api: JsonApi,
reverse_zone_id: String,
forward_zone_id: String,
} }
impl Cloudflare { impl Cloudflare {
pub fn new(token: &str, reverse_zone_id: &str, forward_zone_id: &str) -> Cloudflare { pub fn new(token: &str) -> Cloudflare {
Self { Self {
api: JsonApi::token( api: JsonApi::token(
"https://api.cloudflare.com", "https://api.cloudflare.com",
@ -20,8 +18,6 @@ impl Cloudflare {
false, false,
) )
.unwrap(), .unwrap(),
reverse_zone_id: reverse_zone_id.to_owned(),
forward_zone_id: forward_zone_id.to_owned(),
} }
} }
@ -45,11 +41,7 @@ impl Cloudflare {
#[async_trait] #[async_trait]
impl DnsServer for Cloudflare { impl DnsServer for Cloudflare {
async fn add_record(&self, record: &BasicRecord) -> anyhow::Result<BasicRecord> { async fn add_record(&self, zone_id: &str, record: &BasicRecord) -> anyhow::Result<BasicRecord> {
let zone_id = match &record.kind {
RecordType::PTR => &self.reverse_zone_id,
_ => &self.forward_zone_id,
};
info!( info!(
"Adding record: [{}] {} => {}", "Adding record: [{}] {} => {}",
record.kind, record.name, record.value record.kind, record.name, record.value
@ -75,11 +67,7 @@ impl DnsServer for Cloudflare {
}) })
} }
async fn delete_record(&self, record: &BasicRecord) -> anyhow::Result<()> { async fn delete_record(&self, zone_id: &str, record: &BasicRecord) -> anyhow::Result<()> {
let zone_id = match &record.kind {
RecordType::PTR => &self.reverse_zone_id,
_ => &self.forward_zone_id,
};
let record_id = record.id.as_ref().context("record id missing")?; let record_id = record.id.as_ref().context("record id missing")?;
info!( info!(
"Deleting record: [{}] {} => {}", "Deleting record: [{}] {} => {}",
@ -102,11 +90,11 @@ impl DnsServer for Cloudflare {
Ok(()) Ok(())
} }
async fn update_record(&self, record: &BasicRecord) -> anyhow::Result<BasicRecord> { async fn update_record(
let zone_id = match &record.kind { &self,
RecordType::PTR => &self.reverse_zone_id, zone_id: &str,
_ => &self.forward_zone_id, record: &BasicRecord,
}; ) -> anyhow::Result<BasicRecord> {
info!( info!(
"Updating record: [{}] {} => {}", "Updating record: [{}] {} => {}",
record.kind, record.name, record.value record.kind, record.name, record.value

View File

@ -12,13 +12,13 @@ pub use cloudflare::*;
#[async_trait] #[async_trait]
pub trait DnsServer: Send + Sync { pub trait DnsServer: Send + Sync {
/// Add PTR record to the reverse zone /// Add PTR record to the reverse zone
async fn add_record(&self, record: &BasicRecord) -> Result<BasicRecord>; async fn add_record(&self, zone_id: &str, record: &BasicRecord) -> Result<BasicRecord>;
/// Delete PTR record from the reverse zone /// Delete PTR record from the reverse zone
async fn delete_record(&self, record: &BasicRecord) -> Result<()>; async fn delete_record(&self, zone_id: &str, record: &BasicRecord) -> Result<()>;
/// Update a record /// Update a record
async fn update_record(&self, record: &BasicRecord) -> Result<BasicRecord>; async fn update_record(&self, zone_id: &str, record: &BasicRecord) -> Result<BasicRecord>;
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]

View File

@ -137,7 +137,7 @@ impl Default for MockDb {
load_cpu: 1.5, load_cpu: 1.5,
load_memory: 2.0, load_memory: 2.0,
load_disk: 3.0, load_disk: 3.0,
vlan_id: Some(100) vlan_id: Some(100),
}, },
); );
let mut host_disks = HashMap::new(); let mut host_disks = HashMap::new();
@ -847,8 +847,7 @@ impl VmHostClient for MockVmHost {
} }
pub struct MockDnsServer { pub struct MockDnsServer {
pub forward: Arc<Mutex<HashMap<String, MockDnsEntry>>>, pub zones: Arc<Mutex<HashMap<String, HashMap<String, MockDnsEntry>>>>,
pub reverse: Arc<Mutex<HashMap<String, MockDnsEntry>>>,
} }
pub struct MockDnsEntry { pub struct MockDnsEntry {
@ -859,22 +858,22 @@ pub struct MockDnsEntry {
impl MockDnsServer { impl MockDnsServer {
pub fn new() -> Self { pub fn new() -> Self {
static LAZY_FWD: LazyLock<Arc<Mutex<HashMap<String, MockDnsEntry>>>> = static LAZY_ZONES: LazyLock<Arc<Mutex<HashMap<String, HashMap<String, MockDnsEntry>>>>> =
LazyLock::new(|| Arc::new(Mutex::new(HashMap::new())));
static LAZY_REV: LazyLock<Arc<Mutex<HashMap<String, MockDnsEntry>>>> =
LazyLock::new(|| Arc::new(Mutex::new(HashMap::new()))); LazyLock::new(|| Arc::new(Mutex::new(HashMap::new())));
Self { Self {
forward: LAZY_FWD.clone(), zones: LAZY_ZONES.clone(),
reverse: LAZY_REV.clone(),
} }
} }
} }
#[async_trait] #[async_trait]
impl DnsServer for MockDnsServer { impl DnsServer for MockDnsServer {
async fn add_record(&self, record: &BasicRecord) -> anyhow::Result<BasicRecord> { async fn add_record(&self, zone_id: &str, record: &BasicRecord) -> anyhow::Result<BasicRecord> {
let mut table = match record.kind { let mut zones = self.zones.lock().await;
RecordType::PTR => self.reverse.lock().await, let table = if let Some(t) = zones.get_mut(zone_id) {
_ => self.forward.lock().await, t
} else {
zones.insert(zone_id.to_string(), HashMap::new());
zones.get_mut(zone_id).unwrap()
}; };
if table.values().any(|v| v.name == record.name) { if table.values().any(|v| v.name == record.name) {
@ -902,20 +901,30 @@ impl DnsServer for MockDnsServer {
}) })
} }
async fn delete_record(&self, record: &BasicRecord) -> anyhow::Result<()> { async fn delete_record(&self, zone_id: &str, record: &BasicRecord) -> anyhow::Result<()> {
let mut table = match record.kind { let mut zones = self.zones.lock().await;
RecordType::PTR => self.reverse.lock().await, let table = if let Some(t) = zones.get_mut(zone_id) {
_ => self.forward.lock().await, t
} else {
zones.insert(zone_id.to_string(), HashMap::new());
zones.get_mut(zone_id).unwrap()
}; };
ensure!(record.id.is_some(), "Id is missing"); ensure!(record.id.is_some(), "Id is missing");
table.remove(record.id.as_ref().unwrap()); table.remove(record.id.as_ref().unwrap());
Ok(()) Ok(())
} }
async fn update_record(&self, record: &BasicRecord) -> anyhow::Result<BasicRecord> { async fn update_record(
let mut table = match record.kind { &self,
RecordType::PTR => self.reverse.lock().await, zone_id: &str,
_ => self.forward.lock().await, record: &BasicRecord,
) -> anyhow::Result<BasicRecord> {
let mut zones = self.zones.lock().await;
let table = if let Some(t) = zones.get_mut(zone_id) {
t
} else {
zones.insert(zone_id.to_string(), HashMap::new());
zones.get_mut(zone_id).unwrap()
}; };
ensure!(record.id.is_some(), "Id is missing"); ensure!(record.id.is_some(), "Id is missing");
if let Some(mut r) = table.get_mut(record.id.as_ref().unwrap()) { if let Some(mut r) = table.get_mut(record.id.as_ref().unwrap()) {

View File

@ -36,6 +36,9 @@ pub struct LNVpsProvisioner {
dns: Option<Arc<dyn DnsServer>>, dns: Option<Arc<dyn DnsServer>>,
revolut: Option<Arc<dyn FiatPaymentService>>, revolut: Option<Arc<dyn FiatPaymentService>>,
/// Forward zone ID used for all VM's
/// passed to the DNSServer type
forward_zone_id: Option<String>,
provisioner_config: ProvisionerConfig, provisioner_config: ProvisionerConfig,
} }
@ -55,6 +58,7 @@ impl LNVpsProvisioner {
tax_rates: settings.tax_rate, tax_rates: settings.tax_rate,
provisioner_config: settings.provisioner, provisioner_config: settings.provisioner,
read_only: settings.read_only, read_only: settings.read_only,
forward_zone_id: settings.dns.map(|z| z.forward_zone_id),
} }
} }
@ -149,17 +153,19 @@ impl LNVpsProvisioner {
pub async fn remove_ip_dns(&self, assignment: &mut VmIpAssignment) -> Result<()> { pub async fn remove_ip_dns(&self, assignment: &mut VmIpAssignment) -> Result<()> {
// Delete forward/reverse dns // Delete forward/reverse dns
if let Some(dns) = &self.dns { if let Some(dns) = &self.dns {
if let Some(_r) = &assignment.dns_reverse_ref { let range = self.db.get_ip_range(assignment.ip_range_id).await?;
if let (Some(z), Some(_ref)) = (&range.reverse_zone_id, &assignment.dns_reverse_ref) {
let rev = BasicRecord::reverse(assignment)?; let rev = BasicRecord::reverse(assignment)?;
if let Err(e) = dns.delete_record(&rev).await { if let Err(e) = dns.delete_record(z, &rev).await {
warn!("Failed to delete reverse record: {}", e); warn!("Failed to delete reverse record: {}", e);
} }
assignment.dns_reverse_ref = None; assignment.dns_reverse_ref = None;
assignment.dns_reverse = None; assignment.dns_reverse = None;
} }
if let Some(_r) = &assignment.dns_forward_ref { if let (Some(z), Some(_ref)) = (&self.forward_zone_id, &assignment.dns_forward_ref) {
let rev = BasicRecord::forward(assignment)?; let rev = BasicRecord::forward(assignment)?;
if let Err(e) = dns.delete_record(&rev).await { if let Err(e) = dns.delete_record(z, &rev).await {
warn!("Failed to delete forward record: {}", e); warn!("Failed to delete forward record: {}", e);
} }
assignment.dns_forward_ref = None; assignment.dns_forward_ref = None;
@ -171,12 +177,12 @@ impl LNVpsProvisioner {
/// Update DNS on the dns server, does not save to database! /// Update DNS on the dns server, does not save to database!
pub async fn update_forward_ip_dns(&self, assignment: &mut VmIpAssignment) -> Result<()> { pub async fn update_forward_ip_dns(&self, assignment: &mut VmIpAssignment) -> Result<()> {
if let Some(dns) = &self.dns { if let (Some(z), Some(dns)) = (&self.forward_zone_id, &self.dns) {
let fwd = BasicRecord::forward(assignment)?; let fwd = BasicRecord::forward(assignment)?;
let ret_fwd = if fwd.id.is_some() { let ret_fwd = if fwd.id.is_some() {
dns.update_record(&fwd).await? dns.update_record(z, &fwd).await?
} else { } else {
dns.add_record(&fwd).await? dns.add_record(z, &fwd).await?
}; };
assignment.dns_forward = Some(ret_fwd.name); assignment.dns_forward = Some(ret_fwd.name);
assignment.dns_forward_ref = Some(ret_fwd.id.context("Record id is missing")?); assignment.dns_forward_ref = Some(ret_fwd.id.context("Record id is missing")?);
@ -187,15 +193,18 @@ impl LNVpsProvisioner {
/// Update DNS on the dns server, does not save to database! /// Update DNS on the dns server, does not save to database!
pub async fn update_reverse_ip_dns(&self, assignment: &mut VmIpAssignment) -> Result<()> { pub async fn update_reverse_ip_dns(&self, assignment: &mut VmIpAssignment) -> Result<()> {
if let Some(dns) = &self.dns { if let Some(dns) = &self.dns {
let ret_rev = if assignment.dns_reverse_ref.is_some() { let range = self.db.get_ip_range(assignment.ip_range_id).await?;
dns.update_record(&BasicRecord::reverse(assignment)?) if let Some(z) = &range.reverse_zone_id {
.await? let ret_rev = if assignment.dns_reverse_ref.is_some() {
} else { dns.update_record(z, &BasicRecord::reverse(assignment)?)
dns.add_record(&BasicRecord::reverse_to_fwd(assignment)?) .await?
.await? } else {
}; dns.add_record(z, &BasicRecord::reverse_to_fwd(assignment)?)
assignment.dns_reverse = Some(ret_rev.value); .await?
assignment.dns_reverse_ref = Some(ret_rev.id.context("Record id is missing")?); };
assignment.dns_reverse = Some(ret_rev.value);
assignment.dns_reverse_ref = Some(ret_rev.id.context("Record id is missing")?);
}
} }
Ok(()) Ok(())
} }
@ -636,6 +645,7 @@ mod tests {
let mut i = db.ip_range.lock().await; let mut i = db.ip_range.lock().await;
let r = i.get_mut(&1).unwrap(); let r = i.get_mut(&1).unwrap();
r.access_policy_id = Some(1); r.access_policy_id = Some(1);
r.reverse_zone_id = Some("mock-rev-zone-id".to_string());
} }
let dns = MockDnsServer::new(); let dns = MockDnsServer::new();
@ -691,14 +701,26 @@ mod tests {
assert!(!ip.ip.ends_with("/8")); assert!(!ip.ip.ends_with("/8"));
assert!(!ip.ip.ends_with("/24")); assert!(!ip.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);
}
// now expire // now expire
provisioner.delete_vm(vm.id).await?; provisioner.delete_vm(vm.id).await?;
// test arp/dns is removed // test arp/dns is removed
let arp = router.list_arp_entry().await?; let arp = router.list_arp_entry().await?;
assert!(arp.is_empty()); assert!(arp.is_empty());
assert_eq!(dns.forward.lock().await.len(), 0);
assert_eq!(dns.reverse.lock().await.len(), 0); // test dns entries are deleted
{
let zones = dns.zones.lock().await;
assert_eq!(zones.get("mock-rev-zone-id").unwrap().len(), 0);
assert_eq!(zones.get("mock-forward-zone-id").unwrap().len(), 0);
}
// ensure IPS are deleted // ensure IPS are deleted
let ips = db.ip_assignments.lock().await; let ips = db.ip_assignments.lock().await;

View File

@ -76,29 +76,28 @@ pub struct NostrConfig {
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub enum DnsServerConfig { pub struct DnsServerConfig {
pub forward_zone_id: String,
pub api: DnsServerApi,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum DnsServerApi {
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
Cloudflare { Cloudflare { token: String },
token: String,
forward_zone_id: String,
reverse_zone_id: String,
},
} }
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SmtpConfig { pub struct SmtpConfig {
/// Admin user id, for sending system notifications /// Admin user id, for sending system notifications
pub admin: Option<u64>, pub admin: Option<u64>,
/// Email server host:port /// Email server host:port
pub server: String, pub server: String,
/// From header to use, otherwise empty /// From header to use, otherwise empty
pub from: Option<String>, pub from: Option<String>,
/// Username for SMTP connection /// Username for SMTP connection
pub username: String, pub username: String,
/// Password for SMTP connection /// Password for SMTP connection
pub password: String, pub password: String,
} }
@ -168,16 +167,12 @@ impl Settings {
{ {
match &self.dns { match &self.dns {
None => Ok(None), None => Ok(None),
#[cfg(feature = "cloudflare")] Some(c) => match &c.api {
Some(DnsServerConfig::Cloudflare { #[cfg(feature = "cloudflare")]
token, DnsServerApi::Cloudflare { token } => {
forward_zone_id, Ok(Some(Arc::new(crate::dns::Cloudflare::new(token))))
reverse_zone_id, }
}) => Ok(Some(Arc::new(crate::dns::Cloudflare::new( },
token,
reverse_zone_id,
forward_zone_id,
)))),
} }
} }
} }
@ -216,10 +211,11 @@ pub fn mock_settings() -> Settings {
}, },
delete_after: 0, delete_after: 0,
smtp: None, smtp: None,
dns: Some(DnsServerConfig::Cloudflare { dns: Some(DnsServerConfig {
token: "abc".to_string(), forward_zone_id: "mock-forward-zone-id".to_string(),
forward_zone_id: "123".to_string(), api: DnsServerApi::Cloudflare {
reverse_zone_id: "456".to_string(), token: "abc".to_string(),
},
}), }),
nostr: None, nostr: None,
revolut: None, revolut: None,