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

View File

@ -10,12 +10,17 @@ use std::sync::Arc;
pub struct DnsDataMigration {
db: Arc<dyn LNVpsDb>,
dns: Arc<dyn DnsServer>,
forward_zone_id: Option<String>,
}
impl DnsDataMigration {
pub fn new(db: Arc<dyn LNVpsDb>, settings: &Settings) -> Option<Self> {
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>> {
let db = self.db.clone();
let dns = self.dns.clone();
let forward_zone_id = self.forward_zone_id.clone();
Box::pin(async move {
let zone_id = if let Some(z) = forward_zone_id {
z
} else {
return Ok(());
};
let vms = db.list_vms().await?;
for vm in vms {
@ -32,14 +43,14 @@ impl DataMigration for DnsDataMigration {
let mut did_change = false;
if ip.dns_forward.is_none() {
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_ref = r.id;
did_change = true;
}
if ip.dns_reverse.is_none() {
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_ref = r.id;
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 anyhow::Context;
use lnvps_db::async_trait;
@ -7,12 +7,10 @@ use serde::{Deserialize, Serialize};
pub struct Cloudflare {
api: JsonApi,
reverse_zone_id: String,
forward_zone_id: String,
}
impl Cloudflare {
pub fn new(token: &str, reverse_zone_id: &str, forward_zone_id: &str) -> Cloudflare {
pub fn new(token: &str) -> Cloudflare {
Self {
api: JsonApi::token(
"https://api.cloudflare.com",
@ -20,8 +18,6 @@ impl Cloudflare {
false,
)
.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]
impl DnsServer for Cloudflare {
async fn add_record(&self, record: &BasicRecord) -> anyhow::Result<BasicRecord> {
let zone_id = match &record.kind {
RecordType::PTR => &self.reverse_zone_id,
_ => &self.forward_zone_id,
};
async fn add_record(&self, zone_id: &str, record: &BasicRecord) -> anyhow::Result<BasicRecord> {
info!(
"Adding record: [{}] {} => {}",
record.kind, record.name, record.value
@ -75,11 +67,7 @@ impl DnsServer for Cloudflare {
})
}
async fn delete_record(&self, record: &BasicRecord) -> anyhow::Result<()> {
let zone_id = match &record.kind {
RecordType::PTR => &self.reverse_zone_id,
_ => &self.forward_zone_id,
};
async fn delete_record(&self, zone_id: &str, record: &BasicRecord) -> anyhow::Result<()> {
let record_id = record.id.as_ref().context("record id missing")?;
info!(
"Deleting record: [{}] {} => {}",
@ -102,11 +90,11 @@ impl DnsServer for Cloudflare {
Ok(())
}
async fn update_record(&self, record: &BasicRecord) -> anyhow::Result<BasicRecord> {
let zone_id = match &record.kind {
RecordType::PTR => &self.reverse_zone_id,
_ => &self.forward_zone_id,
};
async fn update_record(
&self,
zone_id: &str,
record: &BasicRecord,
) -> anyhow::Result<BasicRecord> {
info!(
"Updating record: [{}] {} => {}",
record.kind, record.name, record.value

View File

@ -12,13 +12,13 @@ pub use cloudflare::*;
#[async_trait]
pub trait DnsServer: Send + Sync {
/// 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
async fn delete_record(&self, record: &BasicRecord) -> Result<()>;
async fn delete_record(&self, zone_id: &str, record: &BasicRecord) -> Result<()>;
/// 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)]

View File

@ -137,7 +137,7 @@ impl Default for MockDb {
load_cpu: 1.5,
load_memory: 2.0,
load_disk: 3.0,
vlan_id: Some(100)
vlan_id: Some(100),
},
);
let mut host_disks = HashMap::new();
@ -847,8 +847,7 @@ impl VmHostClient for MockVmHost {
}
pub struct MockDnsServer {
pub forward: Arc<Mutex<HashMap<String, MockDnsEntry>>>,
pub reverse: Arc<Mutex<HashMap<String, MockDnsEntry>>>,
pub zones: Arc<Mutex<HashMap<String, HashMap<String, MockDnsEntry>>>>,
}
pub struct MockDnsEntry {
@ -859,22 +858,22 @@ pub struct MockDnsEntry {
impl MockDnsServer {
pub fn new() -> Self {
static LAZY_FWD: LazyLock<Arc<Mutex<HashMap<String, MockDnsEntry>>>> =
LazyLock::new(|| Arc::new(Mutex::new(HashMap::new())));
static LAZY_REV: 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())));
Self {
forward: LAZY_FWD.clone(),
reverse: LAZY_REV.clone(),
zones: LAZY_ZONES.clone(),
}
}
}
#[async_trait]
impl DnsServer for MockDnsServer {
async fn add_record(&self, record: &BasicRecord) -> anyhow::Result<BasicRecord> {
let mut table = match record.kind {
RecordType::PTR => self.reverse.lock().await,
_ => self.forward.lock().await,
async fn add_record(&self, zone_id: &str, 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()
};
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<()> {
let mut table = match record.kind {
RecordType::PTR => self.reverse.lock().await,
_ => self.forward.lock().await,
async fn delete_record(&self, zone_id: &str, record: &BasicRecord) -> anyhow::Result<()> {
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");
table.remove(record.id.as_ref().unwrap());
Ok(())
}
async fn update_record(&self, record: &BasicRecord) -> anyhow::Result<BasicRecord> {
let mut table = match record.kind {
RecordType::PTR => self.reverse.lock().await,
_ => self.forward.lock().await,
async fn update_record(
&self,
zone_id: &str,
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");
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>>,
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,
}
@ -55,6 +58,7 @@ impl LNVpsProvisioner {
tax_rates: settings.tax_rate,
provisioner_config: settings.provisioner,
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<()> {
// Delete forward/reverse 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)?;
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);
}
assignment.dns_reverse_ref = 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)?;
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);
}
assignment.dns_forward_ref = None;
@ -171,12 +177,12 @@ impl LNVpsProvisioner {
/// Update DNS on the dns server, does not save to database!
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 ret_fwd = if fwd.id.is_some() {
dns.update_record(&fwd).await?
dns.update_record(z, &fwd).await?
} else {
dns.add_record(&fwd).await?
dns.add_record(z, &fwd).await?
};
assignment.dns_forward = Some(ret_fwd.name);
assignment.dns_forward_ref = Some(ret_fwd.id.context("Record id is missing")?);
@ -187,16 +193,19 @@ impl LNVpsProvisioner {
/// Update DNS on the dns server, does not save to database!
pub async fn update_reverse_ip_dns(&self, assignment: &mut VmIpAssignment) -> Result<()> {
if let Some(dns) = &self.dns {
let range = self.db.get_ip_range(assignment.ip_range_id).await?;
if let Some(z) = &range.reverse_zone_id {
let ret_rev = if assignment.dns_reverse_ref.is_some() {
dns.update_record(&BasicRecord::reverse(assignment)?)
dns.update_record(z, &BasicRecord::reverse(assignment)?)
.await?
} else {
dns.add_record(&BasicRecord::reverse_to_fwd(assignment)?)
dns.add_record(z, &BasicRecord::reverse_to_fwd(assignment)?)
.await?
};
assignment.dns_reverse = Some(ret_rev.value);
assignment.dns_reverse_ref = Some(ret_rev.id.context("Record id is missing")?);
}
}
Ok(())
}
@ -636,6 +645,7 @@ mod tests {
let mut i = db.ip_range.lock().await;
let r = i.get_mut(&1).unwrap();
r.access_policy_id = Some(1);
r.reverse_zone_id = Some("mock-rev-zone-id".to_string());
}
let dns = MockDnsServer::new();
@ -691,14 +701,26 @@ mod tests {
assert!(!ip.ip.ends_with("/8"));
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
provisioner.delete_vm(vm.id).await?;
// test arp/dns is removed
let arp = router.list_arp_entry().await?;
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
let ips = db.ip_assignments.lock().await;

View File

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