feat: Add OKD DNS score with DNS entries and registering dhcp leases
This commit is contained in:
parent
b098757683
commit
367e96b36a
24
harmony-rs/Cargo.lock
generated
24
harmony-rs/Cargo.lock
generated
@ -896,6 +896,7 @@ dependencies = [
|
||||
"libredfish",
|
||||
"log",
|
||||
"opnsense-config",
|
||||
"opnsense-config-xml",
|
||||
"reqwest",
|
||||
"russh",
|
||||
"rust-ipmi",
|
||||
@ -1406,6 +1407,7 @@ dependencies = [
|
||||
"serde",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"uuid",
|
||||
"xml-rs",
|
||||
"yaserde",
|
||||
"yaserde_derive",
|
||||
@ -2502,6 +2504,28 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"rand",
|
||||
"uuid-macro-internal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uuid-macro-internal"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b91f57fe13a38d0ce9e28a03463d8d3c2468ed03d75375110ec71d93b449a08"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
|
@ -18,3 +18,4 @@ env_logger = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
cidr = { workspace = true }
|
||||
opnsense-config = { path = "../opnsense-config" }
|
||||
opnsense-config-xml = { path = "../opnsense-config-xml" }
|
||||
|
@ -11,6 +11,7 @@ use std::{net::IpAddr, sync::Arc};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HAClusterTopology {
|
||||
pub domain_name: String,
|
||||
pub router: Arc<dyn Router>,
|
||||
pub load_balancer: Arc<dyn LoadBalancer>,
|
||||
pub firewall: Arc<dyn Firewall>,
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::net::Ipv4Addr;
|
||||
use std::{error::Error, net::Ipv4Addr, str::FromStr};
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
@ -60,21 +60,32 @@ impl std::fmt::Debug for dyn DhcpServer {
|
||||
#[async_trait]
|
||||
pub trait DnsServer: Send + Sync {
|
||||
async fn register_dhcp_leases(&self, register: bool) -> Result<(), ExecutorError>;
|
||||
fn add_record(
|
||||
&mut self,
|
||||
name: &str,
|
||||
record_type: DnsRecordType,
|
||||
value: &str,
|
||||
) -> Result<(), ExecutorError>;
|
||||
async fn register_hosts(&self, hosts: Vec<DnsRecord>) -> Result<(), ExecutorError>;
|
||||
fn remove_record(
|
||||
&mut self,
|
||||
name: &str,
|
||||
record_type: DnsRecordType,
|
||||
) -> Result<(), ExecutorError>;
|
||||
fn list_records(&self) -> Vec<DnsRecord>;
|
||||
async fn list_records(&self) -> Vec<DnsRecord>;
|
||||
fn get_ip(&self) -> IpAddress;
|
||||
fn get_host(&self) -> LogicalHost;
|
||||
async fn commit_config(&self) -> Result<(), ExecutorError>;
|
||||
async fn ensure_hosts_registered(&self, hosts: Vec<DnsRecord>) -> Result<(), ExecutorError> {
|
||||
let current_hosts = self.list_records().await;
|
||||
let mut hosts_to_register = vec![];
|
||||
|
||||
for host in hosts {
|
||||
if !current_hosts.iter().any(|h| h == &host) {
|
||||
hosts_to_register.push(host);
|
||||
}
|
||||
}
|
||||
|
||||
if !hosts_to_register.is_empty() {
|
||||
self.register_hosts(hosts_to_register).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for dyn DnsServer {
|
||||
@ -136,7 +147,7 @@ impl std::fmt::Display for MacAddress {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum DnsRecordType {
|
||||
A,
|
||||
AAAA,
|
||||
@ -145,9 +156,170 @@ pub enum DnsRecordType {
|
||||
TXT,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DnsRecord {
|
||||
pub name: String,
|
||||
pub record_type: DnsRecordType,
|
||||
pub value: String,
|
||||
impl std::fmt::Display for DnsRecordType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
DnsRecordType::A => write!(f, "A"),
|
||||
DnsRecordType::AAAA => write!(f, "AAAA"),
|
||||
DnsRecordType::CNAME => write!(f, "CNAME"),
|
||||
DnsRecordType::MX => write!(f, "MX"),
|
||||
DnsRecordType::TXT => write!(f, "TXT"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct DnsRecord {
|
||||
pub host: String,
|
||||
pub domain: String,
|
||||
pub record_type: DnsRecordType,
|
||||
pub value: IpAddress,
|
||||
}
|
||||
|
||||
impl FromStr for DnsRecordType {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"A" => Ok(DnsRecordType::A),
|
||||
"AAAA" => Ok(DnsRecordType::AAAA),
|
||||
"CNAME" => Ok(DnsRecordType::CNAME),
|
||||
"MX" => Ok(DnsRecordType::MX),
|
||||
"TXT" => Ok(DnsRecordType::TXT),
|
||||
_ => Err(format!("Unknown DNSRecordType {s}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::sync::Arc;
|
||||
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ensure_hosts_registered_no_new_hosts() {
|
||||
let server = DummyDnsServer::default();
|
||||
let existing_host = DnsRecord {
|
||||
host: "existing".to_string(),
|
||||
domain: "example.com".to_string(),
|
||||
record_type: DnsRecordType::A,
|
||||
value: IpAddress::V4(Ipv4Addr::new(192, 168, 1, 2)),
|
||||
};
|
||||
|
||||
server
|
||||
.register_hosts(vec![existing_host.clone()])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let new_hosts = vec![
|
||||
existing_host, // already exists
|
||||
];
|
||||
|
||||
server.ensure_hosts_registered(new_hosts).await.unwrap();
|
||||
assert_eq!(server.list_records().await.len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ensure_hosts_registered_with_new_hosts() {
|
||||
let server = DummyDnsServer::default();
|
||||
|
||||
let existing_host = DnsRecord {
|
||||
host: "existing".to_string(),
|
||||
domain: "example.com".to_string(),
|
||||
record_type: DnsRecordType::A,
|
||||
value: IpAddress::V4(Ipv4Addr::new(192, 168, 1, 2)),
|
||||
};
|
||||
|
||||
server
|
||||
.register_hosts(vec![existing_host.clone()])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let new_hosts = vec![
|
||||
existing_host.clone(), // already exists
|
||||
DnsRecord {
|
||||
host: "new".to_string(),
|
||||
domain: "example.com".to_string(),
|
||||
record_type: DnsRecordType::A,
|
||||
value: IpAddress::V4(Ipv4Addr::new(192, 168, 1, 3)),
|
||||
},
|
||||
];
|
||||
|
||||
server.ensure_hosts_registered(new_hosts).await.unwrap();
|
||||
assert_eq!(server.list_records().await.len(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ensure_hosts_registered_no_hosts() {
|
||||
let server = DummyDnsServer::default();
|
||||
|
||||
let new_hosts = vec![];
|
||||
|
||||
server.ensure_hosts_registered(new_hosts).await.unwrap();
|
||||
assert_eq!(server.list_records().await.len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ensure_existing_host_kept_no_new_host() {
|
||||
let server = DummyDnsServer::default();
|
||||
|
||||
let new_hosts = vec![];
|
||||
|
||||
server.ensure_hosts_registered(new_hosts).await.unwrap();
|
||||
assert_eq!(server.list_records().await.len(), 0);
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl DnsServer for DummyDnsServer {
|
||||
async fn register_dhcp_leases(&self, _register: bool) -> Result<(), ExecutorError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn register_hosts(&self, hosts: Vec<DnsRecord>) -> Result<(), ExecutorError> {
|
||||
self.hosts.write().await.extend(hosts);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_record(
|
||||
&mut self,
|
||||
_name: &str,
|
||||
_record_type: DnsRecordType,
|
||||
) -> Result<(), ExecutorError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn list_records(&self) -> Vec<DnsRecord> {
|
||||
self.hosts.read().await.clone()
|
||||
}
|
||||
|
||||
fn get_ip(&self) -> IpAddress {
|
||||
IpAddress::V4(Ipv4Addr::new(192, 168, 0, 1))
|
||||
}
|
||||
|
||||
fn get_host(&self) -> LogicalHost {
|
||||
LogicalHost {
|
||||
ip: self.get_ip(),
|
||||
name: "dummy-host".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn commit_config(&self) -> Result<(), ExecutorError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct DummyDnsServer {
|
||||
hosts: Arc<RwLock<Vec<DnsRecord>>>,
|
||||
}
|
||||
|
||||
impl Default for DummyDnsServer {
|
||||
fn default() -> Self {
|
||||
DummyDnsServer {
|
||||
hosts: Arc::new(RwLock::new(vec![])),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,13 +4,14 @@ use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use log::debug;
|
||||
pub use management::*;
|
||||
use opnsense_config_xml::Host;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::{
|
||||
executors::ExecutorError,
|
||||
topology::{
|
||||
Backend, DHCPStaticEntry, DhcpServer, DnsServer, Firewall, FirewallRule, Frontend,
|
||||
IpAddress, LoadBalancer, LogicalHost,
|
||||
Backend, DHCPStaticEntry, DhcpServer, DnsRecord, DnsServer, Firewall, FirewallRule,
|
||||
Frontend, IpAddress, LoadBalancer, LogicalHost,
|
||||
},
|
||||
};
|
||||
|
||||
@ -158,13 +159,22 @@ impl DhcpServer for OPNSenseFirewall {
|
||||
|
||||
#[async_trait]
|
||||
impl DnsServer for OPNSenseFirewall {
|
||||
fn add_record(
|
||||
&mut self,
|
||||
_name: &str,
|
||||
_record_type: crate::topology::DnsRecordType,
|
||||
_value: &str,
|
||||
) -> Result<(), ExecutorError> {
|
||||
todo!()
|
||||
async fn register_hosts(&self, hosts: Vec<DnsRecord>) -> Result<(), ExecutorError> {
|
||||
let mut writable_opnsense = self.opnsense_config.write().await;
|
||||
let mut dns = writable_opnsense.dns();
|
||||
let hosts = hosts
|
||||
.iter()
|
||||
.map(|h| {
|
||||
Host::new(
|
||||
h.host.clone(),
|
||||
h.domain.clone(),
|
||||
h.record_type.to_string(),
|
||||
h.value.to_string(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
dns.register_hosts(hosts);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_record(
|
||||
@ -175,8 +185,26 @@ impl DnsServer for OPNSenseFirewall {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn list_records(&self) -> Vec<crate::topology::DnsRecord> {
|
||||
todo!()
|
||||
async fn list_records(&self) -> Vec<crate::topology::DnsRecord> {
|
||||
self.opnsense_config
|
||||
.write()
|
||||
.await
|
||||
.dns()
|
||||
.get_hosts()
|
||||
.iter()
|
||||
.map(|h| DnsRecord {
|
||||
host: h.hostname.clone(),
|
||||
domain: h.domain.clone(),
|
||||
record_type: h
|
||||
.rr
|
||||
.parse()
|
||||
.expect("received invalid record type {h.rr} from opnsense"),
|
||||
value: h
|
||||
.server
|
||||
.parse()
|
||||
.expect("received invalid ipv4 record from opnsense {h.server}"),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_ip(&self) -> IpAddress {
|
||||
|
@ -7,11 +7,12 @@ use crate::{
|
||||
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
||||
inventory::Inventory,
|
||||
score::Score,
|
||||
topology::HAClusterTopology,
|
||||
topology::{DnsRecord, HAClusterTopology},
|
||||
};
|
||||
|
||||
#[derive(Debug, new, Clone)]
|
||||
pub struct DnsScore {
|
||||
dns_entries: Vec<DnsRecord>,
|
||||
register_dhcp_leases: Option<bool>,
|
||||
}
|
||||
|
||||
@ -62,6 +63,25 @@ impl DnsInterpret {
|
||||
"DNS Interpret execution successfull".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn ensure_hosts_registered(
|
||||
&self,
|
||||
topology: &HAClusterTopology,
|
||||
) -> Result<Outcome, InterpretError> {
|
||||
let entries = &self.score.dns_entries;
|
||||
topology
|
||||
.dns_server
|
||||
.ensure_hosts_registered(entries.clone())
|
||||
.await?;
|
||||
|
||||
Ok(Outcome::new(
|
||||
InterpretStatus::SUCCESS,
|
||||
format!(
|
||||
"DnsInterpret registered {} hosts successfully",
|
||||
entries.len()
|
||||
),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@ -90,6 +110,7 @@ impl Interpret for DnsInterpret {
|
||||
info!("Executing {} on inventory {inventory:?}", self.get_name());
|
||||
|
||||
self.serve_dhcp_entries(inventory, topology).await?;
|
||||
self.ensure_hosts_registered(&topology).await?;
|
||||
|
||||
topology.dns_server.commit_config().await?;
|
||||
|
||||
|
48
harmony-rs/harmony/src/modules/okd/dns.rs
Normal file
48
harmony-rs/harmony/src/modules/okd/dns.rs
Normal file
@ -0,0 +1,48 @@
|
||||
use crate::{
|
||||
modules::dns::DnsScore,
|
||||
score::Score,
|
||||
topology::{DnsRecord, DnsRecordType, HAClusterTopology},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OKDBootstrapDnsScore {
|
||||
dns_score: DnsScore,
|
||||
}
|
||||
|
||||
impl OKDBootstrapDnsScore {
|
||||
pub fn new(topology: &HAClusterTopology) -> Self {
|
||||
let cluster_domain_name = &topology.domain_name;
|
||||
let dns_entries = vec![
|
||||
DnsRecord {
|
||||
host: "api".to_string(),
|
||||
domain: cluster_domain_name.clone(),
|
||||
record_type: DnsRecordType::A,
|
||||
value: topology.dns_server.get_ip(),
|
||||
},
|
||||
DnsRecord {
|
||||
host: "api-int".to_string(),
|
||||
domain: cluster_domain_name.clone(),
|
||||
record_type: DnsRecordType::A,
|
||||
value: topology.dns_server.get_ip(),
|
||||
},
|
||||
DnsRecord {
|
||||
host: "*".to_string(),
|
||||
domain: format!("apps.{}", cluster_domain_name),
|
||||
record_type: DnsRecordType::A,
|
||||
value: topology.dns_server.get_ip(),
|
||||
},
|
||||
];
|
||||
|
||||
Self {
|
||||
dns_score: DnsScore::new(dns_entries, Some(true)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Score for OKDBootstrapDnsScore {
|
||||
type InterpretType = <DnsScore as Score>::InterpretType;
|
||||
|
||||
fn create_interpret(self) -> Self::InterpretType {
|
||||
self.dns_score.create_interpret()
|
||||
}
|
||||
}
|
@ -1,2 +1,3 @@
|
||||
pub mod dhcp;
|
||||
pub mod dns;
|
||||
|
||||
|
@ -18,6 +18,14 @@ thiserror = "1.0"
|
||||
async-trait = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
|
||||
[dependencies.uuid]
|
||||
version = "1.11.0"
|
||||
features = [
|
||||
"v4", # Lets you generate random UUIDs
|
||||
"fast-rng", # Use a faster (but still sufficiently random) RNG
|
||||
"macro-diagnostics", # Enable better diagnostics for compile-time UUIDs
|
||||
]
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1.4.1"
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
use crate::{data::dhcpd::DhcpInterface, xml_utils::to_xml_str};
|
||||
use log::error;
|
||||
use uuid::Uuid;
|
||||
use yaserde::{MaybeString, NamedList, RawXml};
|
||||
use yaserde_derive::{YaDeserialize, YaSerialize};
|
||||
|
||||
@ -1167,18 +1168,34 @@ pub struct Hosts {
|
||||
pub hosts: Vec<Host>,
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
|
||||
#[derive(Default, Clone, PartialEq, Debug, YaSerialize, YaDeserialize)]
|
||||
pub struct Host {
|
||||
#[yaserde(attribute)]
|
||||
pub uuid: String,
|
||||
pub enabled: i32,
|
||||
pub enabled: i8,
|
||||
pub hostname: String,
|
||||
pub domain: String,
|
||||
pub rr: String,
|
||||
pub mxprio: MaybeString,
|
||||
pub mx: MaybeString,
|
||||
pub server: String,
|
||||
pub description: String,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
impl Host {
|
||||
pub fn new(hostname: String, domain: String, rr: String, server: String) -> Self {
|
||||
Host {
|
||||
uuid: Uuid::new_v4().to_string(),
|
||||
enabled: true as i8,
|
||||
hostname,
|
||||
domain,
|
||||
rr,
|
||||
server,
|
||||
mxprio: MaybeString::default(),
|
||||
mx: MaybeString::default(),
|
||||
description: None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
|
||||
@ -1468,7 +1485,6 @@ pub struct Tuning {
|
||||
pub h2_max_concurrent_streams_outgoing: Option<MaybeString>,
|
||||
#[yaserde(rename = "h2_maxConcurrentStreamsIncoming")]
|
||||
pub h2_max_concurrent_streams_incoming: Option<MaybeString>,
|
||||
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use opnsense_config_xml::OPNsense;
|
||||
use opnsense_config_xml::{Host, OPNsense};
|
||||
|
||||
use crate::config::OPNsenseShell;
|
||||
|
||||
@ -17,6 +17,22 @@ impl<'a> DnsConfig<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_hosts(&mut self, mut hosts: Vec<Host>) {
|
||||
let unbound = match &mut self.opnsense.opnsense.unboundplus {
|
||||
Some(unbound) => unbound,
|
||||
None => todo!("Handle case where unboundplus is not used"),
|
||||
};
|
||||
unbound.hosts.hosts.append(&mut hosts);
|
||||
}
|
||||
|
||||
pub fn get_hosts(&self) -> Vec<Host> {
|
||||
let unbound = match &self.opnsense.opnsense.unboundplus {
|
||||
Some(unbound) => unbound,
|
||||
None => todo!("Handle case where unboundplus is not used"),
|
||||
};
|
||||
unbound.hosts.hosts.clone()
|
||||
}
|
||||
|
||||
pub fn register_dhcp_leases(&mut self, register: bool) {
|
||||
let unbound = match &mut self.opnsense.opnsense.unboundplus {
|
||||
Some(unbound) => unbound,
|
||||
|
Loading…
Reference in New Issue
Block a user