Compare commits
3 Commits
fix/add_ro
...
35a459f63c
| Author | SHA1 | Date | |
|---|---|---|---|
| 35a459f63c | |||
| f076d36297 | |||
| 138e414727 |
5
Cargo.lock
generated
5
Cargo.lock
generated
@@ -2285,6 +2285,7 @@ dependencies = [
|
||||
"helm-wrapper-rs",
|
||||
"hex",
|
||||
"http 1.3.1",
|
||||
"inquire",
|
||||
"k3d-rs",
|
||||
"k8s-openapi",
|
||||
"kube",
|
||||
@@ -7049,7 +7050,7 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
||||
[[package]]
|
||||
name = "yaserde"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/jggc/yaserde.git#c94ca32b6505f9c9a668702a1b1f1f88c6374301"
|
||||
source = "git+https://github.com/jggc/yaserde.git#adfdb1c5f4d054f114e5bd0ea7bda9c07a369def"
|
||||
dependencies = [
|
||||
"log",
|
||||
"xml-rs",
|
||||
@@ -7058,7 +7059,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "yaserde_derive"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/jggc/yaserde.git#c94ca32b6505f9c9a668702a1b1f1f88c6374301"
|
||||
source = "git+https://github.com/jggc/yaserde.git#adfdb1c5f4d054f114e5bd0ea7bda9c07a369def"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"log",
|
||||
|
||||
@@ -2,7 +2,7 @@ use harmony::{
|
||||
inventory::Inventory,
|
||||
modules::{
|
||||
dummy::{ErrorScore, PanicScore, SuccessScore},
|
||||
inventory::DiscoverInventoryAgentScore,
|
||||
inventory::LaunchDiscoverInventoryAgentScore,
|
||||
},
|
||||
topology::LocalhostTopology,
|
||||
};
|
||||
@@ -16,7 +16,7 @@ async fn main() {
|
||||
Box::new(SuccessScore {}),
|
||||
Box::new(ErrorScore {}),
|
||||
Box::new(PanicScore {}),
|
||||
Box::new(DiscoverInventoryAgentScore {
|
||||
Box::new(LaunchDiscoverInventoryAgentScore {
|
||||
discovery_timeout: Some(10),
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -5,12 +5,11 @@ use std::{
|
||||
|
||||
use cidr::Ipv4Cidr;
|
||||
use harmony::{
|
||||
hardware::{FirewallGroup, HostCategory, Location, PhysicalHost, SwitchGroup},
|
||||
hardware::{Location, PhysicalHost, SwitchGroup},
|
||||
infra::opnsense::OPNSenseManagementInterface,
|
||||
inventory::Inventory,
|
||||
modules::{
|
||||
http::StaticFilesHttpScore,
|
||||
ipxe::IpxeScore,
|
||||
okd::{
|
||||
bootstrap_dhcp::OKDBootstrapDhcpScore,
|
||||
bootstrap_load_balancer::OKDBootstrapLoadBalancerScore, dhcp::OKDDhcpScore,
|
||||
|
||||
@@ -70,6 +70,7 @@ harmony_inventory_agent = { path = "../harmony_inventory_agent" }
|
||||
harmony_secret_derive = { version = "0.1.0", path = "../harmony_secret_derive" }
|
||||
askama.workspace = true
|
||||
sqlx.workspace = true
|
||||
inquire.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions.workspace = true
|
||||
|
||||
@@ -173,6 +173,10 @@ impl PhysicalHost {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_mac_address(&self) -> Vec<MacAddress> {
|
||||
self.network.iter().map(|nic| nic.mac_address).collect()
|
||||
}
|
||||
|
||||
pub fn label(mut self, name: String, value: String) -> Self {
|
||||
self.labels.push(Label { name, value });
|
||||
self
|
||||
|
||||
@@ -61,3 +61,10 @@ impl Inventory {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum HostRole {
|
||||
Bootstrap,
|
||||
ControlPlane,
|
||||
Worker,
|
||||
Storage,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::hardware::PhysicalHost;
|
||||
use crate::{hardware::PhysicalHost, interpret::InterpretError, inventory::HostRole};
|
||||
|
||||
/// Errors that can occur within the repository layer.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
@@ -15,6 +15,12 @@ pub enum RepoError {
|
||||
ConnectionFailed(String),
|
||||
}
|
||||
|
||||
impl From<RepoError> for InterpretError {
|
||||
fn from(value: RepoError) -> Self {
|
||||
InterpretError::new(format!("Interpret error : {value}"))
|
||||
}
|
||||
}
|
||||
|
||||
// --- Trait and Implementation ---
|
||||
|
||||
/// Defines the contract for inventory persistence.
|
||||
@@ -22,4 +28,10 @@ pub enum RepoError {
|
||||
pub trait InventoryRepository: Send + Sync + 'static {
|
||||
async fn save(&self, host: &PhysicalHost) -> Result<(), RepoError>;
|
||||
async fn get_latest_by_id(&self, host_id: &str) -> Result<Option<PhysicalHost>, RepoError>;
|
||||
async fn get_all_hosts(&self) -> Result<Vec<PhysicalHost>, RepoError>;
|
||||
async fn save_role_mapping(
|
||||
&self,
|
||||
role: &HostRole,
|
||||
host: &PhysicalHost,
|
||||
) -> Result<(), RepoError>;
|
||||
}
|
||||
|
||||
@@ -161,6 +161,14 @@ impl DhcpServer for HAClusterTopology {
|
||||
self.dhcp_server.set_pxe_options(options).await
|
||||
}
|
||||
|
||||
async fn set_dhcp_range(
|
||||
&self,
|
||||
start: &IpAddress,
|
||||
end: &IpAddress,
|
||||
) -> Result<(), ExecutorError> {
|
||||
self.dhcp_server.set_dhcp_range(start, end).await
|
||||
}
|
||||
|
||||
fn get_ip(&self) -> IpAddress {
|
||||
self.dhcp_server.get_ip()
|
||||
}
|
||||
@@ -298,6 +306,13 @@ impl DhcpServer for DummyInfra {
|
||||
async fn set_pxe_options(&self, _options: PxeOptions) -> Result<(), ExecutorError> {
|
||||
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
|
||||
}
|
||||
async fn set_dhcp_range(
|
||||
&self,
|
||||
start: &IpAddress,
|
||||
end: &IpAddress,
|
||||
) -> Result<(), ExecutorError> {
|
||||
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
|
||||
}
|
||||
fn get_ip(&self) -> IpAddress {
|
||||
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
|
||||
}
|
||||
|
||||
@@ -59,6 +59,8 @@ pub trait DhcpServer: Send + Sync + std::fmt::Debug {
|
||||
async fn remove_static_mapping(&self, mac: &MacAddress) -> Result<(), ExecutorError>;
|
||||
async fn list_static_mappings(&self) -> Vec<(MacAddress, IpAddress)>;
|
||||
async fn set_pxe_options(&self, pxe_options: PxeOptions) -> Result<(), ExecutorError>;
|
||||
async fn set_dhcp_range(&self, start: &IpAddress, end: &IpAddress)
|
||||
-> Result<(), ExecutorError>;
|
||||
fn get_ip(&self) -> IpAddress;
|
||||
fn get_host(&self) -> LogicalHost;
|
||||
async fn commit_config(&self) -> Result<(), ExecutorError>;
|
||||
|
||||
@@ -68,4 +68,19 @@ impl DhcpServer for OPNSenseFirewall {
|
||||
ExecutorError::UnexpectedError(format!("Failed to set_pxe_options : {dhcp_error}"))
|
||||
})
|
||||
}
|
||||
|
||||
async fn set_dhcp_range(
|
||||
&self,
|
||||
start: &IpAddress,
|
||||
end: &IpAddress,
|
||||
) -> Result<(), ExecutorError> {
|
||||
let mut writable_opnsense = self.opnsense_config.write().await;
|
||||
writable_opnsense
|
||||
.dhcp()
|
||||
.set_dhcp_range(&start.to_string(), &end.to_string())
|
||||
.await
|
||||
.map_err(|dhcp_error| {
|
||||
ExecutorError::UnexpectedError(format!("Failed to set_dhcp_range : {dhcp_error}"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ pub struct DhcpScore {
|
||||
pub filename: Option<String>,
|
||||
pub filename64: Option<String>,
|
||||
pub filenameipxe: Option<String>,
|
||||
pub dhcp_range: (IpAddress, IpAddress),
|
||||
}
|
||||
|
||||
impl<T: Topology + DhcpServer> Score<T> for DhcpScore {
|
||||
@@ -52,6 +53,104 @@ impl DhcpInterpret {
|
||||
status: InterpretStatus::QUEUED,
|
||||
}
|
||||
}
|
||||
|
||||
async fn set_pxe_options<D: DhcpServer>(
|
||||
&self,
|
||||
_inventory: &Inventory,
|
||||
dhcp_server: &D,
|
||||
) -> Result<Outcome, InterpretError> {
|
||||
let pxe_options = PxeOptions {
|
||||
ipxe_filename: self.score.filenameipxe.clone().unwrap_or_default(),
|
||||
bios_filename: self.score.filename.clone().unwrap_or_default(),
|
||||
efi_filename: self.score.filename64.clone().unwrap_or_default(),
|
||||
tftp_ip: self.score.next_server,
|
||||
};
|
||||
|
||||
dhcp_server.set_pxe_options(pxe_options).await?;
|
||||
|
||||
Ok(Outcome::new(
|
||||
InterpretStatus::SUCCESS,
|
||||
format!(
|
||||
"Dhcp Interpret Set next boot to [{:?}], boot_filename to [{:?}], filename to [{:?}], filename64 to [{:?}], filenameipxe to [:{:?}]",
|
||||
self.score.boot_filename,
|
||||
self.score.boot_filename,
|
||||
self.score.filename,
|
||||
self.score.filename64,
|
||||
self.score.filenameipxe
|
||||
),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: Topology + DhcpServer> Interpret<T> for DhcpInterpret {
|
||||
fn get_name(&self) -> InterpretName {
|
||||
InterpretName::OPNSenseDHCP
|
||||
}
|
||||
|
||||
fn get_version(&self) -> crate::domain::data::Version {
|
||||
self.version.clone()
|
||||
}
|
||||
|
||||
fn get_status(&self) -> InterpretStatus {
|
||||
self.status.clone()
|
||||
}
|
||||
|
||||
fn get_children(&self) -> Vec<Id> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
inventory: &Inventory,
|
||||
topology: &T,
|
||||
) -> Result<Outcome, InterpretError> {
|
||||
info!("Executing DhcpInterpret on inventory {inventory:?}");
|
||||
|
||||
self.set_pxe_options(inventory, topology).await?;
|
||||
topology
|
||||
.set_dhcp_range(&self.score.dhcp_range.0, &self.score.dhcp_range.1)
|
||||
.await?;
|
||||
|
||||
DhcpHostBindingScore {
|
||||
host_binding: self.score.host_binding.clone(),
|
||||
}
|
||||
.interpret(inventory, topology)
|
||||
.await?;
|
||||
|
||||
topology.commit_config().await?;
|
||||
|
||||
Ok(Outcome::new(
|
||||
InterpretStatus::SUCCESS,
|
||||
"Dhcp Interpret execution successful".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, new, Clone, Serialize)]
|
||||
pub struct DhcpHostBindingScore {
|
||||
pub host_binding: Vec<HostBinding>,
|
||||
}
|
||||
|
||||
impl<T: Topology + DhcpServer> Score<T> for DhcpHostBindingScore {
|
||||
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||
Box::new(DhcpHostBindingInterpret {
|
||||
score: self.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn name(&self) -> String {
|
||||
"DhcpHostBindingScore".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.opnsense.org/manual/dhcp.html#advanced-settings
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DhcpHostBindingInterpret {
|
||||
score: DhcpHostBindingScore,
|
||||
}
|
||||
|
||||
impl DhcpHostBindingInterpret {
|
||||
async fn add_static_entries<D: DhcpServer>(
|
||||
&self,
|
||||
_inventory: &Inventory,
|
||||
@@ -94,47 +193,20 @@ impl DhcpInterpret {
|
||||
format!("Dhcp Interpret registered {} entries", number_new_entries),
|
||||
))
|
||||
}
|
||||
|
||||
async fn set_pxe_options<D: DhcpServer>(
|
||||
&self,
|
||||
_inventory: &Inventory,
|
||||
dhcp_server: &D,
|
||||
) -> Result<Outcome, InterpretError> {
|
||||
let pxe_options = PxeOptions {
|
||||
ipxe_filename: self.score.filenameipxe.clone().unwrap_or_default(),
|
||||
bios_filename: self.score.filename.clone().unwrap_or_default(),
|
||||
efi_filename: self.score.filename64.clone().unwrap_or_default(),
|
||||
tftp_ip: self.score.next_server,
|
||||
};
|
||||
|
||||
dhcp_server.set_pxe_options(pxe_options).await?;
|
||||
|
||||
Ok(Outcome::new(
|
||||
InterpretStatus::SUCCESS,
|
||||
format!(
|
||||
"Dhcp Interpret Set next boot to [{:?}], boot_filename to [{:?}], filename to [{:?}], filename64 to [{:?}], filenameipxe to [:{:?}]",
|
||||
self.score.boot_filename,
|
||||
self.score.boot_filename,
|
||||
self.score.filename,
|
||||
self.score.filename64,
|
||||
self.score.filenameipxe
|
||||
),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: DhcpServer> Interpret<T> for DhcpInterpret {
|
||||
impl<T: DhcpServer> Interpret<T> for DhcpHostBindingInterpret {
|
||||
fn get_name(&self) -> InterpretName {
|
||||
InterpretName::OPNSenseDHCP
|
||||
InterpretName::Custom("DhcpHostBindingInterpret")
|
||||
}
|
||||
|
||||
fn get_version(&self) -> crate::domain::data::Version {
|
||||
self.version.clone()
|
||||
Version::from("1.0.0").unwrap()
|
||||
}
|
||||
|
||||
fn get_status(&self) -> InterpretStatus {
|
||||
self.status.clone()
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_children(&self) -> Vec<Id> {
|
||||
@@ -146,9 +218,10 @@ impl<T: DhcpServer> Interpret<T> for DhcpInterpret {
|
||||
inventory: &Inventory,
|
||||
topology: &T,
|
||||
) -> Result<Outcome, InterpretError> {
|
||||
info!("Executing DhcpInterpret on inventory {inventory:?}");
|
||||
|
||||
self.set_pxe_options(inventory, topology).await?;
|
||||
info!(
|
||||
"Executing DhcpHostBindingInterpret on {} bindings",
|
||||
self.score.host_binding.len()
|
||||
);
|
||||
|
||||
self.add_static_entries(inventory, topology).await?;
|
||||
|
||||
@@ -156,7 +229,10 @@ impl<T: DhcpServer> Interpret<T> for DhcpInterpret {
|
||||
|
||||
Ok(Outcome::new(
|
||||
InterpretStatus::SUCCESS,
|
||||
"Dhcp Interpret execution successful".to_string(),
|
||||
format!(
|
||||
"Dhcp Host Binding Interpret execution successful on {} hosts",
|
||||
self.score.host_binding.len()
|
||||
),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,14 @@ use derive_new::new;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
data::{FileContent, Version},
|
||||
data::{FileContent, FilePath, Version},
|
||||
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
||||
inventory::Inventory,
|
||||
score::Score,
|
||||
topology::{HttpServer, Topology},
|
||||
};
|
||||
use harmony_types::id::Id;
|
||||
use harmony_types::net::Url;
|
||||
use harmony_types::{id::Id, net::MacAddress};
|
||||
|
||||
/// Configure an HTTP server that is provided by the Topology
|
||||
///
|
||||
@@ -91,3 +91,33 @@ impl<T: Topology + HttpServer> Interpret<T> for StaticFilesHttpInterpret {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, new, Clone, Serialize)]
|
||||
pub struct IPxeMacBootFileScore {
|
||||
pub content: String,
|
||||
pub mac_address: Vec<MacAddress>,
|
||||
}
|
||||
|
||||
impl<T: Topology + HttpServer> Score<T> for IPxeMacBootFileScore {
|
||||
fn name(&self) -> String {
|
||||
"IPxeMacBootFileScore".to_string()
|
||||
}
|
||||
|
||||
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||
StaticFilesHttpScore {
|
||||
folder_to_serve: None,
|
||||
files: self
|
||||
.mac_address
|
||||
.iter()
|
||||
.map(|mac| FileContent {
|
||||
path: FilePath::Relative(format!(
|
||||
"byMAC/01-{}.ipxe",
|
||||
mac.to_string().replace(":", "-")
|
||||
)),
|
||||
content: self.content.clone(),
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
.create_interpret()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,11 @@ use harmony_types::id::Id;
|
||||
/// This will allow us to register/update hosts running harmony_inventory_agent
|
||||
/// from LAN in the Harmony inventory
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DiscoverInventoryAgentScore {
|
||||
pub struct LaunchDiscoverInventoryAgentScore {
|
||||
pub discovery_timeout: Option<u64>,
|
||||
}
|
||||
|
||||
impl<T: Topology> Score<T> for DiscoverInventoryAgentScore {
|
||||
impl<T: Topology> Score<T> for LaunchDiscoverInventoryAgentScore {
|
||||
fn name(&self) -> String {
|
||||
"DiscoverInventoryAgentScore".to_string()
|
||||
}
|
||||
@@ -36,7 +36,7 @@ impl<T: Topology> Score<T> for DiscoverInventoryAgentScore {
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DiscoverInventoryAgentInterpret {
|
||||
score: DiscoverInventoryAgentScore,
|
||||
score: LaunchDiscoverInventoryAgentScore,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -46,6 +46,13 @@ impl<T: Topology> Interpret<T> for DiscoverInventoryAgentInterpret {
|
||||
_inventory: &Inventory,
|
||||
_topology: &T,
|
||||
) -> Result<Outcome, InterpretError> {
|
||||
match self.score.discovery_timeout {
|
||||
Some(timeout) => info!("Discovery agent will wait for {timeout} seconds"),
|
||||
None => info!(
|
||||
"Discovery agent will wait forever in the background, go on and enjoy this delicious inventory."
|
||||
),
|
||||
};
|
||||
|
||||
harmony_inventory_agent::local_presence::discover_agents(
|
||||
self.score.discovery_timeout,
|
||||
|event: DiscoveryEvent| -> Result<(), String> {
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use derive_new::new;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
data::Version,
|
||||
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
||||
inventory::Inventory,
|
||||
score::Score,
|
||||
topology::Topology,
|
||||
};
|
||||
use harmony_types::id::Id;
|
||||
|
||||
#[derive(Debug, new, Clone, Serialize)]
|
||||
pub struct IpxeScore {
|
||||
//files_to_serve: Url,
|
||||
}
|
||||
|
||||
impl<T: Topology> Score<T> for IpxeScore {
|
||||
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||
Box::new(IpxeInterpret::new(self.clone()))
|
||||
}
|
||||
|
||||
fn name(&self) -> String {
|
||||
"IpxeScore".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, new, Clone)]
|
||||
pub struct IpxeInterpret {
|
||||
_score: IpxeScore,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: Topology> Interpret<T> for IpxeInterpret {
|
||||
async fn execute(
|
||||
&self,
|
||||
_inventory: &Inventory,
|
||||
_topology: &T,
|
||||
) -> Result<Outcome, InterpretError> {
|
||||
/*
|
||||
let http_server = &topology.http_server;
|
||||
http_server.ensure_initialized().await?;
|
||||
Ok(Outcome::success(format!(
|
||||
"Http Server running and serving files from {}",
|
||||
self.score.files_to_serve
|
||||
)))
|
||||
*/
|
||||
todo!();
|
||||
}
|
||||
|
||||
fn get_name(&self) -> InterpretName {
|
||||
InterpretName::Ipxe
|
||||
}
|
||||
|
||||
fn get_version(&self) -> Version {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_status(&self) -> InterpretStatus {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_children(&self) -> Vec<Id> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ pub mod dummy;
|
||||
pub mod helm;
|
||||
pub mod http;
|
||||
pub mod inventory;
|
||||
pub mod ipxe;
|
||||
pub mod k3d;
|
||||
pub mod k8s;
|
||||
pub mod lamp;
|
||||
|
||||
@@ -37,21 +37,23 @@ impl OKDBootstrapDhcpScore {
|
||||
.clone(),
|
||||
});
|
||||
// TODO refactor this so it is not copy pasted from dhcp.rs
|
||||
Self {
|
||||
dhcp_score: DhcpScore::new(
|
||||
host_binding,
|
||||
// TODO : we should add a tftp server to the topology instead of relying on the
|
||||
// router address, this is leaking implementation details
|
||||
Some(topology.router.get_gateway()),
|
||||
None, // To allow UEFI boot we cannot provide a legacy file
|
||||
Some("undionly.kpxe".to_string()),
|
||||
Some("ipxe.efi".to_string()),
|
||||
Some(format!(
|
||||
"http://{}:8080/boot.ipxe",
|
||||
topology.router.get_gateway()
|
||||
)),
|
||||
),
|
||||
}
|
||||
todo!("Add dhcp range")
|
||||
// Self {
|
||||
// dhcp_score: DhcpScore::new(
|
||||
// host_binding,
|
||||
// // TODO : we should add a tftp server to the topology instead of relying on the
|
||||
// // router address, this is leaking implementation details
|
||||
// Some(topology.router.get_gateway()),
|
||||
// None, // To allow UEFI boot we cannot provide a legacy file
|
||||
// Some("undionly.kpxe".to_string()),
|
||||
// Some("ipxe.efi".to_string()),
|
||||
// Some(format!(
|
||||
// "http://{}:8080/boot.ipxe",
|
||||
// topology.router.get_gateway()
|
||||
// )),
|
||||
// (self.),
|
||||
// ),
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
use harmony_types::net::IpAddress;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
@@ -44,6 +47,16 @@ impl OKDDhcpScore {
|
||||
})
|
||||
});
|
||||
|
||||
let dhcp_server_ip = match topology.dhcp_server.get_ip() {
|
||||
std::net::IpAddr::V4(ipv4_addr) => ipv4_addr,
|
||||
std::net::IpAddr::V6(_ipv6_addr) => todo!("Support ipv6 someday"),
|
||||
};
|
||||
|
||||
// TODO this could overflow, we should use proper subnet maths here instead of an ip
|
||||
// address and guessing the subnet size from there
|
||||
let start = Ipv4Addr::from(u32::from(dhcp_server_ip) + 100);
|
||||
let end = Ipv4Addr::from(u32::from(dhcp_server_ip) + 150);
|
||||
|
||||
Self {
|
||||
// TODO : we should add a tftp server to the topology instead of relying on the
|
||||
// router address, this is leaking implementation details
|
||||
@@ -57,6 +70,7 @@ impl OKDDhcpScore {
|
||||
"http://{}:8080/boot.ipxe",
|
||||
topology.router.get_gateway()
|
||||
)),
|
||||
dhcp_range: (IpAddress::from(start), IpAddress::from(end)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,19 +49,25 @@
|
||||
//! - internal_domain: Internal cluster domain (e.g., cluster.local or harmony.mcd).
|
||||
|
||||
use async_trait::async_trait;
|
||||
use chrono::Duration;
|
||||
use derive_new::new;
|
||||
use harmony_macros::ip;
|
||||
use harmony_types::id::Id;
|
||||
use log::info;
|
||||
use log::{error, info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
data::Version,
|
||||
hardware::PhysicalHost,
|
||||
infra::inventory::InventoryRepositoryFactory,
|
||||
instrumentation::{HarmonyEvent, instrument},
|
||||
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
||||
inventory::Inventory,
|
||||
inventory::{HostRole, Inventory},
|
||||
modules::{
|
||||
dhcp::DhcpHostBindingScore, http::IPxeMacBootFileScore,
|
||||
inventory::LaunchDiscoverInventoryAgentScore,
|
||||
},
|
||||
score::Score,
|
||||
topology::{DnsRecord, DnsRecordType, DnsServer, Topology},
|
||||
topology::{HAClusterTopology, HostBinding},
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
@@ -78,8 +84,8 @@ pub struct OKDInstallationScore {
|
||||
pub internal_domain: String,
|
||||
}
|
||||
|
||||
impl<T: Topology + DnsServer + 'static> Score<T> for OKDInstallationScore {
|
||||
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||
impl Score<HAClusterTopology> for OKDInstallationScore {
|
||||
fn create_interpret(&self) -> Box<dyn Interpret<HAClusterTopology>> {
|
||||
Box::new(OKDInstallationInterpret::new(self.clone()))
|
||||
}
|
||||
|
||||
@@ -109,18 +115,12 @@ impl OKDInstallationInterpret {
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_inventory_phase<T: Topology + DnsServer>(
|
||||
async fn run_inventory_phase(
|
||||
&self,
|
||||
inventory: &Inventory,
|
||||
topology: &T,
|
||||
topology: &HAClusterTopology,
|
||||
) -> Result<(), InterpretError> {
|
||||
// 1) Prepare DNS and DHCP lease registration (optional)
|
||||
let dns_score = OKDSetup01InventoryDnsScore::new(
|
||||
self.score.internal_domain.clone(),
|
||||
self.score.public_domain.clone(),
|
||||
Some(true), // register_dhcp_leases
|
||||
);
|
||||
dns_score.interpret(inventory, topology).await?;
|
||||
|
||||
// 2) Serve default iPXE + Kickstart and poll discovery
|
||||
let discovery_score = OKDSetup01InventoryScore::new(self.score.lan_cidr.clone());
|
||||
@@ -129,10 +129,10 @@ impl OKDInstallationInterpret {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_bootstrap_phase<T: Topology + DnsServer>(
|
||||
async fn run_bootstrap_phase(
|
||||
&self,
|
||||
inventory: &Inventory,
|
||||
topology: &T,
|
||||
topology: &HAClusterTopology,
|
||||
) -> Result<(), InterpretError> {
|
||||
// Select and provision bootstrap
|
||||
let bootstrap_score = OKDSetup02BootstrapScore::new(
|
||||
@@ -143,40 +143,40 @@ impl OKDInstallationInterpret {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_control_plane_phase<T: Topology + DnsServer>(
|
||||
async fn run_control_plane_phase(
|
||||
&self,
|
||||
inventory: &Inventory,
|
||||
topology: &T,
|
||||
topology: &HAClusterTopology,
|
||||
) -> Result<(), InterpretError> {
|
||||
let control_plane_score = OKDSetup03ControlPlaneScore::new();
|
||||
control_plane_score.interpret(inventory, topology).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_workers_phase<T: Topology + DnsServer>(
|
||||
async fn run_workers_phase(
|
||||
&self,
|
||||
inventory: &Inventory,
|
||||
topology: &T,
|
||||
topology: &HAClusterTopology,
|
||||
) -> Result<(), InterpretError> {
|
||||
let workers_score = OKDSetup04WorkersScore::new();
|
||||
workers_score.interpret(inventory, topology).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_sanity_phase<T: Topology + DnsServer>(
|
||||
async fn run_sanity_phase(
|
||||
&self,
|
||||
inventory: &Inventory,
|
||||
topology: &T,
|
||||
topology: &HAClusterTopology,
|
||||
) -> Result<(), InterpretError> {
|
||||
let sanity_score = OKDSetup05SanityCheckScore::new();
|
||||
sanity_score.interpret(inventory, topology).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_report_phase<T: Topology + DnsServer>(
|
||||
async fn run_report_phase(
|
||||
&self,
|
||||
inventory: &Inventory,
|
||||
topology: &T,
|
||||
topology: &HAClusterTopology,
|
||||
) -> Result<(), InterpretError> {
|
||||
let report_score = OKDSetup06InstallationReportScore::new(
|
||||
self.score.public_domain.clone(),
|
||||
@@ -188,7 +188,7 @@ impl OKDInstallationInterpret {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: Topology + DnsServer> Interpret<T> for OKDInstallationInterpret {
|
||||
impl Interpret<HAClusterTopology> for OKDInstallationInterpret {
|
||||
fn get_name(&self) -> InterpretName {
|
||||
InterpretName::Custom("OKDInstallationInterpret")
|
||||
}
|
||||
@@ -208,7 +208,7 @@ impl<T: Topology + DnsServer> Interpret<T> for OKDInstallationInterpret {
|
||||
async fn execute(
|
||||
&self,
|
||||
inventory: &Inventory,
|
||||
topology: &T,
|
||||
topology: &HAClusterTopology,
|
||||
) -> Result<Outcome, InterpretError> {
|
||||
instrument(HarmonyEvent::HarmonyStarted).ok();
|
||||
|
||||
@@ -238,111 +238,6 @@ impl<T: Topology + DnsServer> Interpret<T> for OKDInstallationInterpret {
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Step 01: Inventory DNS setup
|
||||
// - Keep DHCP simple; optionally register dynamic leases into DNS.
|
||||
// - Ensure base records for internal/public domains (api/api-int/apps wildcard).
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone, Serialize, new)]
|
||||
struct OKDSetup01InventoryDnsScore {
|
||||
internal_domain: String,
|
||||
public_domain: String,
|
||||
register_dhcp_leases: Option<bool>,
|
||||
}
|
||||
|
||||
impl<T: Topology + DnsServer> Score<T> for OKDSetup01InventoryDnsScore {
|
||||
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||
Box::new(OKDSetup01InventoryDnsInterpret::new(self.clone()))
|
||||
}
|
||||
|
||||
fn name(&self) -> String {
|
||||
"OKDSetup01InventoryDnsScore".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct OKDSetup01InventoryDnsInterpret {
|
||||
score: OKDSetup01InventoryDnsScore,
|
||||
version: Version,
|
||||
status: InterpretStatus,
|
||||
}
|
||||
|
||||
impl OKDSetup01InventoryDnsInterpret {
|
||||
pub fn new(score: OKDSetup01InventoryDnsScore) -> Self {
|
||||
let version = Version::from("1.0.0").unwrap();
|
||||
Self {
|
||||
version,
|
||||
score,
|
||||
status: InterpretStatus::QUEUED,
|
||||
}
|
||||
}
|
||||
|
||||
async fn ensure_dns<T: DnsServer>(&self, dns: &T) -> Result<(), InterpretError> {
|
||||
// Minimal records placeholders; real IPs are set elsewhere in the flow.
|
||||
// We register the names early to ensure resolvability for clients relying on DNS.
|
||||
let mut records: Vec<DnsRecord> = vec![
|
||||
DnsRecord {
|
||||
value: ip!("0.0.0.0"),
|
||||
host: "api".to_string(),
|
||||
domain: self.score.internal_domain.clone(),
|
||||
record_type: DnsRecordType::A,
|
||||
},
|
||||
DnsRecord {
|
||||
value: ip!("0.0.0.0"),
|
||||
host: "api-int".to_string(),
|
||||
domain: self.score.internal_domain.clone(),
|
||||
record_type: DnsRecordType::A,
|
||||
},
|
||||
DnsRecord {
|
||||
value: ip!("0.0.0.0"),
|
||||
host: "*.apps.".to_string(),
|
||||
domain: self.score.internal_domain.clone(),
|
||||
record_type: DnsRecordType::A,
|
||||
},
|
||||
];
|
||||
dns.ensure_hosts_registered(records.drain(..).collect())
|
||||
.await?;
|
||||
if let Some(register) = self.score.register_dhcp_leases {
|
||||
dns.register_dhcp_leases(register).await?;
|
||||
}
|
||||
dns.commit_config().await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: Topology + DnsServer> Interpret<T> for OKDSetup01InventoryDnsInterpret {
|
||||
fn get_name(&self) -> InterpretName {
|
||||
InterpretName::Custom("OKDSetup01InventoryDns")
|
||||
}
|
||||
|
||||
fn get_version(&self) -> Version {
|
||||
self.version.clone()
|
||||
}
|
||||
|
||||
fn get_status(&self) -> InterpretStatus {
|
||||
self.status.clone()
|
||||
}
|
||||
|
||||
fn get_children(&self) -> Vec<Id> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
_inventory: &Inventory,
|
||||
topology: &T,
|
||||
) -> Result<Outcome, InterpretError> {
|
||||
info!("Ensuring base DNS and DHCP lease registration for discovery phase");
|
||||
self.ensure_dns(topology).await?;
|
||||
Ok(Outcome::new(
|
||||
InterpretStatus::SUCCESS,
|
||||
"Inventory DNS prepared".into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Step 01: Inventory (default PXE + Kickstart in RAM + Rust agent)
|
||||
// - This score exposes/ensures the default inventory assets and waits for discoveries.
|
||||
@@ -350,12 +245,10 @@ impl<T: Topology + DnsServer> Interpret<T> for OKDSetup01InventoryDnsInterpret {
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone, Serialize, new)]
|
||||
struct OKDSetup01InventoryScore {
|
||||
lan_cidr: String,
|
||||
}
|
||||
struct OKDSetup01InventoryScore {}
|
||||
|
||||
impl<T: Topology> Score<T> for OKDSetup01InventoryScore {
|
||||
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||
impl Score<HAClusterTopology> for OKDSetup01InventoryScore {
|
||||
fn create_interpret(&self) -> Box<dyn Interpret<HAClusterTopology>> {
|
||||
Box::new(OKDSetup01InventoryInterpret::new(self.clone()))
|
||||
}
|
||||
|
||||
@@ -380,35 +273,10 @@ impl OKDSetup01InventoryInterpret {
|
||||
status: InterpretStatus::QUEUED,
|
||||
}
|
||||
}
|
||||
|
||||
async fn ensure_inventory_assets<T: Topology>(
|
||||
&self,
|
||||
topology: &T,
|
||||
) -> Result<(), InterpretError> {
|
||||
// Placeholder: push or verify iPXE default, Kickstart, and Rust inventory agent are hosted.
|
||||
// Real implementation: publish to the PXE/HTTP server via the topology.
|
||||
info!(
|
||||
"[Inventory] Ensuring default iPXE, Kickstart, and inventory agent are available for LAN {}",
|
||||
self.score.lan_cidr
|
||||
);
|
||||
// topology.publish_http_asset(…) ?
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn discover_nodes(&self) -> Result<usize, InterpretError> {
|
||||
// Placeholder: implement Harmony discovery logic (scan/pull/push mode).
|
||||
// Returns number of newly discovered nodes.
|
||||
info!(
|
||||
"[Inventory] Scanning for inventory agents in {}",
|
||||
self.score.lan_cidr
|
||||
);
|
||||
// In practice, this would query harmony_composer or a local registry store.
|
||||
Ok(3)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: Topology> Interpret<T> for OKDSetup01InventoryInterpret {
|
||||
impl Interpret<HAClusterTopology> for OKDSetup01InventoryInterpret {
|
||||
fn get_name(&self) -> InterpretName {
|
||||
InterpretName::Custom("OKDSetup01Inventory")
|
||||
}
|
||||
@@ -427,16 +295,99 @@ impl<T: Topology> Interpret<T> for OKDSetup01InventoryInterpret {
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
_inventory: &Inventory,
|
||||
topology: &T,
|
||||
inventory: &Inventory,
|
||||
topology: &HAClusterTopology,
|
||||
) -> Result<Outcome, InterpretError> {
|
||||
self.ensure_inventory_assets(topology).await?;
|
||||
let count = self.discover_nodes().await?;
|
||||
info!("[Inventory] Discovered {count} nodes");
|
||||
info!(
|
||||
"Launching discovery agent, make sure that your nodes are successfully PXE booted and running inventory agent. They should answer on `http://<node_ip>:8080/inventory`"
|
||||
);
|
||||
LaunchDiscoverInventoryAgentScore {
|
||||
discovery_timeout: Some(60),
|
||||
}
|
||||
.interpret(inventory, topology)
|
||||
.await?;
|
||||
|
||||
let bootstrap_host: PhysicalHost;
|
||||
let host_repo = InventoryRepositoryFactory::build().await?;
|
||||
|
||||
loop {
|
||||
let all_hosts = host_repo.get_all_hosts().await?;
|
||||
|
||||
if all_hosts.is_empty() {
|
||||
warn!("No discovered hosts found yet. Waiting for hosts to appear...");
|
||||
// Sleep to avoid spamming the user and logs while waiting for nodes.
|
||||
tokio::time::sleep(std::time::Duration::from_secs(5));
|
||||
continue;
|
||||
}
|
||||
|
||||
let ans = inquire::Select::new(
|
||||
"Select the node to be used as the bootstrap node:",
|
||||
all_hosts,
|
||||
)
|
||||
.with_help_message("Press Esc to refresh the list of discovered hosts")
|
||||
.prompt();
|
||||
|
||||
match ans {
|
||||
Ok(choice) => {
|
||||
info!("Selected {} as the bootstrap node.", choice.summary());
|
||||
host_repo
|
||||
.save_role_mapping(&HostRole::Bootstrap, &choice)
|
||||
.await?;
|
||||
bootstrap_host = choice;
|
||||
break;
|
||||
}
|
||||
Err(inquire::InquireError::OperationCanceled) => {
|
||||
info!("Refresh requested. Fetching list of discovered hosts again...");
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to select bootstrap node: {}", e);
|
||||
return Err(InterpretError::new(format!(
|
||||
"Could not select host : {}",
|
||||
e.to_string()
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Outcome::new(
|
||||
InterpretStatus::SUCCESS,
|
||||
format!("Inventory phase complete. Nodes discovered: {count}"),
|
||||
format!(
|
||||
"Found and assigned bootstrap node: {}",
|
||||
bootstrap_host.summary()
|
||||
),
|
||||
))
|
||||
// info!(
|
||||
// "Launching discovery agent, make sure that your nodes are successfully PXE booted and running inventory agent. They should answer on `http://<node_ip>:8080/inventory`"
|
||||
// );
|
||||
// LaunchDiscoverInventoryAgentScore {
|
||||
// discovery_timeout: Some(60),
|
||||
// }
|
||||
// .interpret(inventory, topology)
|
||||
// .await?;
|
||||
//
|
||||
// // TODO write a loop
|
||||
// let bootstrap_host: PhysicalHost;
|
||||
// let mut found_bootstrap_host = false;
|
||||
// let host_repo = InventoryRepositoryFactory::build().await?;
|
||||
// while !found_bootstrap_host {
|
||||
// let all_hosts = host_repo.get_all_hosts().await?;
|
||||
// // TODO use inquire to select among the current hosts, tell the user to cancel if he
|
||||
// // wants to update the list. I believe inquire::Select is the correct option here
|
||||
// //
|
||||
// // The options are all_hosts, all_hosts is of type Vec<PhysicalHost> and PhysicalHost
|
||||
// // has a human friendly `summary() -> String` method that is perfect to have the user
|
||||
// // choose
|
||||
// //
|
||||
// // once the user has chosen one, call host_repo.save_role_mapping(Role::Bootstrap,
|
||||
// // host.id).await?;
|
||||
// bootstrap_host = todo!();
|
||||
// }
|
||||
//
|
||||
// Ok(Outcome::new(
|
||||
// InterpretStatus::SUCCESS,
|
||||
// format!("Found bootstrap node : {}", bootstrap_host.summary()),
|
||||
// ))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,8 +405,8 @@ struct OKDSetup02BootstrapScore {
|
||||
internal_domain: String,
|
||||
}
|
||||
|
||||
impl<T: Topology> Score<T> for OKDSetup02BootstrapScore {
|
||||
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||
impl Score<HAClusterTopology> for OKDSetup02BootstrapScore {
|
||||
fn create_interpret(&self) -> Box<dyn Interpret<HAClusterTopology>> {
|
||||
Box::new(OKDSetup02BootstrapInterpret::new(self.clone()))
|
||||
}
|
||||
|
||||
@@ -481,9 +432,46 @@ impl OKDSetup02BootstrapInterpret {
|
||||
}
|
||||
}
|
||||
|
||||
async fn render_per_mac_pxe(&self) -> Result<(), InterpretError> {
|
||||
fn get_bootstrap_node<'a>(&self, inventory: &'a Inventory) -> &'a PhysicalHost {
|
||||
inventory
|
||||
.worker_host
|
||||
.first()
|
||||
.expect("At least one worker host is required to be used as bootstrap node")
|
||||
}
|
||||
|
||||
async fn configure_host_binding(
|
||||
&self,
|
||||
inventory: &Inventory,
|
||||
topology: &HAClusterTopology,
|
||||
) -> Result<(), InterpretError> {
|
||||
let binding = HostBinding {
|
||||
logical_host: topology.bootstrap_host.clone(),
|
||||
physical_host: self.get_bootstrap_node(inventory).clone(),
|
||||
};
|
||||
info!("Configuring host binding for bootstrap node {binding:?}");
|
||||
|
||||
DhcpHostBindingScore {
|
||||
host_binding: vec![binding],
|
||||
}
|
||||
.interpret(inventory, topology)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn render_per_mac_pxe(
|
||||
&self,
|
||||
inventory: &Inventory,
|
||||
topology: &HAClusterTopology,
|
||||
) -> Result<(), InterpretError> {
|
||||
// Placeholder: use Harmony templates to emit {MAC}.ipxe selecting SCOS live + bootstrap ignition.
|
||||
info!("[Bootstrap] Rendering per-MAC PXE for bootstrap node");
|
||||
let bootstrap_node = self.get_bootstrap_node(inventory);
|
||||
IPxeMacBootFileScore {
|
||||
mac_address: bootstrap_node.get_mac_address(),
|
||||
content: todo!("templace for bootstrap node"),
|
||||
}
|
||||
.interpret(inventory, topology)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -501,7 +489,7 @@ impl OKDSetup02BootstrapInterpret {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: Topology> Interpret<T> for OKDSetup02BootstrapInterpret {
|
||||
impl Interpret<HAClusterTopology> for OKDSetup02BootstrapInterpret {
|
||||
fn get_name(&self) -> InterpretName {
|
||||
InterpretName::Custom("OKDSetup02Bootstrap")
|
||||
}
|
||||
@@ -520,10 +508,11 @@ impl<T: Topology> Interpret<T> for OKDSetup02BootstrapInterpret {
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
_inventory: &Inventory,
|
||||
_topology: &T,
|
||||
inventory: &Inventory,
|
||||
topology: &HAClusterTopology,
|
||||
) -> Result<Outcome, InterpretError> {
|
||||
self.render_per_mac_pxe().await?;
|
||||
self.configure_host_binding(inventory, topology).await?;
|
||||
self.render_per_mac_pxe(inventory, topology).await?;
|
||||
self.reboot_target().await?;
|
||||
self.wait_for_bootstrap_complete().await?;
|
||||
|
||||
@@ -543,8 +532,8 @@ impl<T: Topology> Interpret<T> for OKDSetup02BootstrapInterpret {
|
||||
#[derive(Debug, Clone, Serialize, new)]
|
||||
struct OKDSetup03ControlPlaneScore {}
|
||||
|
||||
impl<T: Topology> Score<T> for OKDSetup03ControlPlaneScore {
|
||||
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||
impl Score<HAClusterTopology> for OKDSetup03ControlPlaneScore {
|
||||
fn create_interpret(&self) -> Box<dyn Interpret<HAClusterTopology>> {
|
||||
Box::new(OKDSetup03ControlPlaneInterpret::new(self.clone()))
|
||||
}
|
||||
|
||||
@@ -583,7 +572,7 @@ impl OKDSetup03ControlPlaneInterpret {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: Topology> Interpret<T> for OKDSetup03ControlPlaneInterpret {
|
||||
impl Interpret<HAClusterTopology> for OKDSetup03ControlPlaneInterpret {
|
||||
fn get_name(&self) -> InterpretName {
|
||||
InterpretName::Custom("OKDSetup03ControlPlane")
|
||||
}
|
||||
@@ -603,7 +592,7 @@ impl<T: Topology> Interpret<T> for OKDSetup03ControlPlaneInterpret {
|
||||
async fn execute(
|
||||
&self,
|
||||
_inventory: &Inventory,
|
||||
_topology: &T,
|
||||
_topology: &HAClusterTopology,
|
||||
) -> Result<Outcome, InterpretError> {
|
||||
self.render_and_reboot().await?;
|
||||
self.persist_network_bond().await?;
|
||||
@@ -623,8 +612,8 @@ impl<T: Topology> Interpret<T> for OKDSetup03ControlPlaneInterpret {
|
||||
#[derive(Debug, Clone, Serialize, new)]
|
||||
struct OKDSetup04WorkersScore {}
|
||||
|
||||
impl<T: Topology> Score<T> for OKDSetup04WorkersScore {
|
||||
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||
impl Score<HAClusterTopology> for OKDSetup04WorkersScore {
|
||||
fn create_interpret(&self) -> Box<dyn Interpret<HAClusterTopology>> {
|
||||
Box::new(OKDSetup04WorkersInterpret::new(self.clone()))
|
||||
}
|
||||
|
||||
@@ -657,7 +646,7 @@ impl OKDSetup04WorkersInterpret {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: Topology> Interpret<T> for OKDSetup04WorkersInterpret {
|
||||
impl Interpret<HAClusterTopology> for OKDSetup04WorkersInterpret {
|
||||
fn get_name(&self) -> InterpretName {
|
||||
InterpretName::Custom("OKDSetup04Workers")
|
||||
}
|
||||
@@ -677,7 +666,7 @@ impl<T: Topology> Interpret<T> for OKDSetup04WorkersInterpret {
|
||||
async fn execute(
|
||||
&self,
|
||||
_inventory: &Inventory,
|
||||
_topology: &T,
|
||||
_topology: &HAClusterTopology,
|
||||
) -> Result<Outcome, InterpretError> {
|
||||
self.render_and_reboot().await?;
|
||||
Ok(Outcome::new(
|
||||
@@ -695,8 +684,8 @@ impl<T: Topology> Interpret<T> for OKDSetup04WorkersInterpret {
|
||||
#[derive(Debug, Clone, Serialize, new)]
|
||||
struct OKDSetup05SanityCheckScore {}
|
||||
|
||||
impl<T: Topology> Score<T> for OKDSetup05SanityCheckScore {
|
||||
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||
impl Score<HAClusterTopology> for OKDSetup05SanityCheckScore {
|
||||
fn create_interpret(&self) -> Box<dyn Interpret<HAClusterTopology>> {
|
||||
Box::new(OKDSetup05SanityCheckInterpret::new(self.clone()))
|
||||
}
|
||||
|
||||
@@ -729,7 +718,7 @@ impl OKDSetup05SanityCheckInterpret {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: Topology> Interpret<T> for OKDSetup05SanityCheckInterpret {
|
||||
impl Interpret<HAClusterTopology> for OKDSetup05SanityCheckInterpret {
|
||||
fn get_name(&self) -> InterpretName {
|
||||
InterpretName::Custom("OKDSetup05SanityCheck")
|
||||
}
|
||||
@@ -749,7 +738,7 @@ impl<T: Topology> Interpret<T> for OKDSetup05SanityCheckInterpret {
|
||||
async fn execute(
|
||||
&self,
|
||||
_inventory: &Inventory,
|
||||
_topology: &T,
|
||||
_topology: &HAClusterTopology,
|
||||
) -> Result<Outcome, InterpretError> {
|
||||
self.run_checks().await?;
|
||||
Ok(Outcome::new(
|
||||
@@ -770,8 +759,8 @@ struct OKDSetup06InstallationReportScore {
|
||||
internal_domain: String,
|
||||
}
|
||||
|
||||
impl<T: Topology> Score<T> for OKDSetup06InstallationReportScore {
|
||||
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||
impl Score<HAClusterTopology> for OKDSetup06InstallationReportScore {
|
||||
fn create_interpret(&self) -> Box<dyn Interpret<HAClusterTopology>> {
|
||||
Box::new(OKDSetup06InstallationReportInterpret::new(self.clone()))
|
||||
}
|
||||
|
||||
@@ -807,7 +796,7 @@ impl OKDSetup06InstallationReportInterpret {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: Topology> Interpret<T> for OKDSetup06InstallationReportInterpret {
|
||||
impl Interpret<HAClusterTopology> for OKDSetup06InstallationReportInterpret {
|
||||
fn get_name(&self) -> InterpretName {
|
||||
InterpretName::Custom("OKDSetup06InstallationReport")
|
||||
}
|
||||
@@ -827,7 +816,7 @@ impl<T: Topology> Interpret<T> for OKDSetup06InstallationReportInterpret {
|
||||
async fn execute(
|
||||
&self,
|
||||
_inventory: &Inventory,
|
||||
_topology: &T,
|
||||
_topology: &HAClusterTopology,
|
||||
) -> Result<Outcome, InterpretError> {
|
||||
self.generate().await?;
|
||||
Ok(Outcome::new(
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use askama::Template;
|
||||
use async_trait::async_trait;
|
||||
use derive_new::new;
|
||||
use harmony_types::net::Url;
|
||||
use harmony_types::net::{IpAddress, Url};
|
||||
use serde::Serialize;
|
||||
use std::net::IpAddr;
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
|
||||
use crate::{
|
||||
data::{FileContent, FilePath, Version},
|
||||
@@ -46,6 +46,16 @@ impl<T: Topology + DhcpServer + TftpServer + HttpServer + Router> Interpret<T> f
|
||||
) -> Result<Outcome, InterpretError> {
|
||||
let gateway_ip = topology.get_gateway();
|
||||
|
||||
let dhcp_server_ip = match DhcpServer::get_ip(topology) {
|
||||
std::net::IpAddr::V4(ipv4_addr) => ipv4_addr,
|
||||
std::net::IpAddr::V6(_ipv6_addr) => todo!("Support ipv6 someday"),
|
||||
};
|
||||
|
||||
// TODO this could overflow, we should use proper subnet maths here instead of an ip
|
||||
// address and guessing the subnet size from there
|
||||
let start = Ipv4Addr::from(u32::from(dhcp_server_ip) + 100);
|
||||
let end = Ipv4Addr::from(u32::from(dhcp_server_ip) + 150);
|
||||
|
||||
let scores: Vec<Box<dyn Score<T>>> = vec![
|
||||
Box::new(DhcpScore {
|
||||
host_binding: vec![],
|
||||
@@ -54,6 +64,7 @@ impl<T: Topology + DhcpServer + TftpServer + HttpServer + Router> Interpret<T> f
|
||||
filename: Some("undionly.kpxe".to_string()),
|
||||
filename64: Some("ipxe.efi".to_string()),
|
||||
filenameipxe: Some(format!("http://{gateway_ip}:8080/boot.ipxe").to_string()),
|
||||
dhcp_range: (IpAddress::from(start), IpAddress::from(end)),
|
||||
}),
|
||||
Box::new(TftpScore {
|
||||
files_to_serve: Url::LocalFolder("./data/pxe/okd/tftpboot/".to_string()),
|
||||
|
||||
@@ -9,6 +9,7 @@ use config::INFISICAL_ENVIRONMENT;
|
||||
use config::INFISICAL_PROJECT_ID;
|
||||
use config::INFISICAL_URL;
|
||||
use config::SECRET_STORE;
|
||||
use log::debug;
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use std::fmt;
|
||||
use store::InfisicalSecretStore;
|
||||
@@ -101,6 +102,7 @@ impl SecretManager {
|
||||
/// Retrieves and deserializes a secret.
|
||||
pub async fn get<T: Secret>() -> Result<T, SecretStoreError> {
|
||||
let manager = get_secret_manager().await;
|
||||
debug!("Getting secret ns {} key {}", &manager.namespace, T::KEY);
|
||||
let raw_value = manager.store.get_raw(&manager.namespace, T::KEY).await?;
|
||||
serde_json::from_slice(&raw_value).map_err(|e| SecretStoreError::Deserialization {
|
||||
key: T::KEY.to_string(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use async_trait::async_trait;
|
||||
use log::info;
|
||||
use log::{debug, info};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::{SecretStore, SecretStoreError};
|
||||
@@ -24,7 +24,7 @@ impl SecretStore for LocalFileSecretStore {
|
||||
.join("secrets");
|
||||
|
||||
let file_path = Self::get_file_path(&data_dir, ns, key);
|
||||
info!(
|
||||
debug!(
|
||||
"LOCAL_STORE: Getting key '{key}' from namespace '{ns}' at {}",
|
||||
file_path.display()
|
||||
);
|
||||
|
||||
@@ -36,6 +36,27 @@ pub struct DnsMasq {
|
||||
pub dhcp_options: Vec<DhcpOptions>,
|
||||
pub dhcp_boot: Vec<DhcpBoot>,
|
||||
pub dhcp_tags: Vec<RawXml>,
|
||||
pub hosts: Vec<DnsmasqHost>,
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize, Clone)]
|
||||
#[yaserde(rename = "hosts")]
|
||||
pub struct DnsmasqHost {
|
||||
#[yaserde(attribute = true)]
|
||||
pub uuid: String,
|
||||
pub host: String,
|
||||
pub domain: MaybeString,
|
||||
pub local: MaybeString,
|
||||
pub ip: MaybeString,
|
||||
pub cnames: MaybeString,
|
||||
pub client_id: MaybeString,
|
||||
pub hwaddr: MaybeString,
|
||||
pub lease_time: MaybeString,
|
||||
pub ignore: Option<u8>,
|
||||
pub set_tag: MaybeString,
|
||||
pub descr: MaybeString,
|
||||
pub comments: MaybeString,
|
||||
pub aliases: MaybeString,
|
||||
}
|
||||
|
||||
// Represents the <dhcp> element and its nested fields.
|
||||
|
||||
@@ -226,6 +226,7 @@ mod tests {
|
||||
"src/tests/data/config-full-ncd0.xml",
|
||||
"src/tests/data/config-full-25.7.xml",
|
||||
"src/tests/data/config-full-25.7-dummy-dnsmasq-options.xml",
|
||||
"src/tests/data/config-25.7-dnsmasq-static-host.xml",
|
||||
] {
|
||||
let mut test_file_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
test_file_path.push(path);
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
// dnsmasq.rs
|
||||
use crate::modules::dhcp::DhcpError;
|
||||
use log::{debug, info};
|
||||
use log::{debug, info, warn};
|
||||
use opnsense_config_xml::dnsmasq::{DhcpRange, DnsMasq, DnsmasqHost}; // Assuming DhcpRange is defined in opnsense_config_xml::dnsmasq
|
||||
use opnsense_config_xml::{MaybeString, StaticMap};
|
||||
use std::collections::HashSet;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use opnsense_config_xml::OPNsense;
|
||||
|
||||
@@ -25,74 +28,156 @@ impl<'a> DhcpConfigDnsMasq<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes a static mapping by its MAC address.
|
||||
/// Static mappings are stored in the <dhcpd> section of the config, shared with the ISC module.
|
||||
pub fn remove_static_mapping(&mut self, mac: &str) {
|
||||
let lan_dhcpd = self.get_lan_dhcpd();
|
||||
lan_dhcpd
|
||||
.staticmaps
|
||||
.retain(|static_entry| static_entry.mac != mac);
|
||||
/// Removes a MAC address from a static mapping.
|
||||
/// If the mapping has no other MAC addresses associated with it, the entire host entry is removed.
|
||||
pub fn remove_static_mapping(&mut self, mac_to_remove: &str) {
|
||||
let dnsmasq = self.get_dnsmasq();
|
||||
|
||||
// Update hwaddr fields for hosts that contain the MAC, removing it from the comma-separated list.
|
||||
for host in dnsmasq.hosts.iter_mut() {
|
||||
let mac = host.hwaddr.content_string();
|
||||
let original_macs: Vec<&str> = mac.split(',').collect();
|
||||
if original_macs
|
||||
.iter()
|
||||
.any(|m| m.eq_ignore_ascii_case(mac_to_remove))
|
||||
{
|
||||
let updated_macs: Vec<&str> = original_macs
|
||||
.into_iter()
|
||||
.filter(|m| !m.eq_ignore_ascii_case(mac_to_remove))
|
||||
.collect();
|
||||
host.hwaddr = updated_macs.join(",").into();
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any host entries that no longer have any MAC addresses.
|
||||
dnsmasq
|
||||
.hosts
|
||||
.retain(|host_entry| !host_entry.hwaddr.content_string().is_empty());
|
||||
}
|
||||
|
||||
/// Retrieves a mutable reference to the LAN interface's DHCP configuration.
|
||||
/// This is located in the shared <dhcpd> section of the config.
|
||||
fn get_lan_dhcpd(&mut self) -> &mut opnsense_config_xml::DhcpInterface {
|
||||
&mut self
|
||||
.opnsense
|
||||
.dhcpd
|
||||
.elements
|
||||
.iter_mut()
|
||||
.find(|(name, _config)| name == "lan")
|
||||
.expect("Interface lan should have dhcpd activated")
|
||||
.1
|
||||
/// Retrieves a mutable reference to the DnsMasq configuration.
|
||||
/// This is located in the <dnsmasq> section of the OPNsense config.
|
||||
fn get_dnsmasq(&mut self) -> &mut DnsMasq {
|
||||
self.opnsense
|
||||
.dnsmasq
|
||||
.as_mut()
|
||||
.expect("Dnsmasq config must be initialized")
|
||||
}
|
||||
|
||||
/// Adds a new static DHCP mapping.
|
||||
/// Validates the MAC address and checks for existing mappings to prevent conflicts.
|
||||
/// Adds or updates a static DHCP mapping.
|
||||
///
|
||||
/// This function implements specific logic to handle existing entries:
|
||||
/// - If no host exists for the given IP or hostname, a new entry is created.
|
||||
/// - If exactly one host exists for the IP and/or hostname, the new MAC is appended to it.
|
||||
/// - It will panic if the IP and hostname exist but point to two different host entries,
|
||||
/// as this represents an unresolvable conflict.
|
||||
/// - It will also panic if multiple entries are found for the IP or hostname, indicating an
|
||||
/// ambiguous state.
|
||||
pub fn add_static_mapping(
|
||||
&mut self,
|
||||
mac: &str,
|
||||
ipaddr: Ipv4Addr,
|
||||
hostname: &str,
|
||||
) -> Result<(), DhcpError> {
|
||||
let mac = mac.to_string();
|
||||
let hostname = hostname.to_string();
|
||||
let lan_dhcpd = self.get_lan_dhcpd();
|
||||
let existing_mappings: &mut Vec<StaticMap> = &mut lan_dhcpd.staticmaps;
|
||||
|
||||
if !Self::is_valid_mac(&mac) {
|
||||
return Err(DhcpError::InvalidMacAddress(mac));
|
||||
if !Self::is_valid_mac(mac) {
|
||||
return Err(DhcpError::InvalidMacAddress(mac.to_string()));
|
||||
}
|
||||
|
||||
// TODO: Validate that the IP address is within a configured DHCP range.
|
||||
let ip_str = ipaddr.to_string();
|
||||
let hosts = &mut self.get_dnsmasq().hosts;
|
||||
|
||||
if existing_mappings
|
||||
let ip_indices: Vec<usize> = hosts
|
||||
.iter()
|
||||
.any(|m| m.ipaddr == ipaddr.to_string() && m.mac == mac)
|
||||
{
|
||||
info!("Mapping already exists for {} [{}], skipping", ipaddr, mac);
|
||||
return Ok(());
|
||||
}
|
||||
.enumerate()
|
||||
.filter(|(_, h)| h.ip.content_string() == ip_str)
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
|
||||
if existing_mappings
|
||||
let hostname_indices: Vec<usize> = hosts
|
||||
.iter()
|
||||
.any(|m| m.ipaddr == ipaddr.to_string())
|
||||
.enumerate()
|
||||
.filter(|(_, h)| h.host == hostname)
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
|
||||
let ip_set: HashSet<usize> = ip_indices.iter().cloned().collect();
|
||||
let hostname_set: HashSet<usize> = hostname_indices.iter().cloned().collect();
|
||||
|
||||
if !ip_indices.is_empty()
|
||||
&& !hostname_indices.is_empty()
|
||||
&& ip_set.intersection(&hostname_set).count() == 0
|
||||
{
|
||||
return Err(DhcpError::IpAddressAlreadyMapped(ipaddr.to_string()));
|
||||
panic!(
|
||||
"Configuration conflict: IP {} and hostname '{}' exist, but in different static host entries.",
|
||||
ipaddr, hostname
|
||||
);
|
||||
}
|
||||
|
||||
if existing_mappings.iter().any(|m| m.mac == mac) {
|
||||
return Err(DhcpError::MacAddressAlreadyMapped(mac));
|
||||
let mut all_indices: Vec<&usize> = ip_set.union(&hostname_set).collect();
|
||||
all_indices.sort();
|
||||
|
||||
match all_indices.len() {
|
||||
0 => {
|
||||
info!(
|
||||
"Creating new static host for {} ({}) with MAC {}",
|
||||
hostname, ipaddr, mac
|
||||
);
|
||||
let new_host = DnsmasqHost {
|
||||
uuid: Uuid::new_v4().to_string(),
|
||||
host: hostname.to_string(),
|
||||
ip: ip_str.into(),
|
||||
hwaddr: mac.to_string().into(),
|
||||
local: MaybeString::from("1"),
|
||||
ignore: Some(0),
|
||||
..Default::default()
|
||||
};
|
||||
hosts.push(new_host);
|
||||
}
|
||||
1 => {
|
||||
let host_index = *all_indices[0];
|
||||
let host_to_modify = &mut hosts[host_index];
|
||||
let host_to_modify_ip = host_to_modify.ip.content_string();
|
||||
if host_to_modify_ip != ip_str {
|
||||
warn!(
|
||||
"Hostname '{}' already exists with a different IP ({}). Appending MAC {}.",
|
||||
hostname, host_to_modify_ip, mac
|
||||
);
|
||||
} else if host_to_modify.host != hostname {
|
||||
warn!(
|
||||
"IP {} already exists with a different hostname ('{}'). Appending MAC {}.",
|
||||
ipaddr, host_to_modify.host, mac
|
||||
);
|
||||
}
|
||||
|
||||
if !host_to_modify
|
||||
.hwaddr
|
||||
.content_string()
|
||||
.split(',')
|
||||
.any(|m| m.eq_ignore_ascii_case(mac))
|
||||
{
|
||||
info!(
|
||||
"Appending MAC {} to existing static host for {} ({})",
|
||||
mac, host_to_modify.host, host_to_modify_ip
|
||||
);
|
||||
let mut updated_macs = host_to_modify.hwaddr.content_string().to_string();
|
||||
updated_macs.push(',');
|
||||
updated_macs.push_str(mac);
|
||||
host_to_modify.hwaddr = updated_macs.into();
|
||||
} else {
|
||||
info!(
|
||||
"MAC {} already present in static host entry for {} ({}). No changes made.",
|
||||
mac, host_to_modify.host, host_to_modify_ip
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
panic!(
|
||||
"Configuration conflict: Found multiple host entries matching IP {} and/or hostname '{}'. Cannot resolve automatically.",
|
||||
ipaddr, hostname
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let static_map = StaticMap {
|
||||
mac,
|
||||
ipaddr: ipaddr.to_string(),
|
||||
hostname: hostname,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
existing_mappings.push(static_map);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -110,6 +195,8 @@ impl<'a> DhcpConfigDnsMasq<'a> {
|
||||
/// Retrieves the list of current static mappings by shelling out to `configctl`.
|
||||
/// This provides the real-time state from the running system.
|
||||
pub async fn get_static_mappings(&self) -> Result<Vec<StaticMap>, Error> {
|
||||
// Note: This command is for the 'dhcpd' service. If dnsmasq uses a different command
|
||||
// or key, this will need to be adjusted.
|
||||
let list_static_output = self
|
||||
.opnsense_shell
|
||||
.exec("configctl dhcpd list static")
|
||||
@@ -117,6 +204,8 @@ impl<'a> DhcpConfigDnsMasq<'a> {
|
||||
|
||||
let value: serde_json::Value = serde_json::from_str(&list_static_output)
|
||||
.unwrap_or_else(|_| panic!("Got invalid json from configctl {list_static_output}"));
|
||||
|
||||
// The JSON output key might be 'dhcpd' even when dnsmasq is the backend.
|
||||
let static_maps = value["dhcpd"]
|
||||
.as_array()
|
||||
.ok_or(Error::Command(format!(
|
||||
@@ -135,6 +224,36 @@ impl<'a> DhcpConfigDnsMasq<'a> {
|
||||
Ok(static_maps)
|
||||
}
|
||||
|
||||
pub async fn set_dhcp_range(&mut self, start: &str, end: &str) -> Result<(), DhcpError> {
|
||||
let dnsmasq = self.get_dnsmasq();
|
||||
let ranges = &mut dnsmasq.dhcp_ranges;
|
||||
|
||||
// Assuming DnsMasq has dhcp_ranges: Vec<DhcpRange>
|
||||
// Find existing range for "lan" interface
|
||||
if let Some(range) = ranges
|
||||
.iter_mut()
|
||||
.find(|r| r.interface == Some("lan".to_string()))
|
||||
{
|
||||
// Update existing range
|
||||
range.start_addr = Some(start.to_string());
|
||||
range.end_addr = Some(end.to_string());
|
||||
} else {
|
||||
// Create new range
|
||||
let new_range = DhcpRange {
|
||||
uuid: Some(Uuid::new_v4().to_string()),
|
||||
interface: Some("lan".to_string()),
|
||||
start_addr: Some(start.to_string()),
|
||||
end_addr: Some(end.to_string()),
|
||||
domain_type: Some("range".to_string()),
|
||||
nosync: Some(0),
|
||||
..Default::default()
|
||||
};
|
||||
ranges.push(new_range);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_pxe_options(
|
||||
&self,
|
||||
tftp_ip: Option<String>,
|
||||
@@ -142,9 +261,9 @@ impl<'a> DhcpConfigDnsMasq<'a> {
|
||||
efi_filename: String,
|
||||
ipxe_filename: String,
|
||||
) -> Result<(), DhcpError> {
|
||||
// As of writing this opnsense does not support negative tags, and the dnsmasq config is a
|
||||
// bit complicated anyways. So we are writing directly a dnsmasq config file to
|
||||
// /usr/local/etc/dnsmasq.conf.d
|
||||
// OPNsense does not support negative tags via its API for dnsmasq, and the required
|
||||
// logic is complex. Therefore, we write a configuration file directly to the
|
||||
// dnsmasq.conf.d directory to achieve the desired PXE boot behavior.
|
||||
let tftp_str = tftp_ip.map_or(String::new(), |i| format!(",{i},{i}"));
|
||||
|
||||
let config = format!(
|
||||
@@ -185,3 +304,274 @@ dhcp-boot=tag:bios,{bios_filename}{tftp_str}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::config::DummyOPNSenseShell;
|
||||
|
||||
use super::*;
|
||||
use opnsense_config_xml::OPNsense;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Helper function to create a DnsmasqHost with minimal boilerplate.
|
||||
fn create_host(uuid: &str, host: &str, ip: &str, hwaddr: &str) -> DnsmasqHost {
|
||||
DnsmasqHost {
|
||||
uuid: uuid.to_string(),
|
||||
host: host.to_string(),
|
||||
ip: ip.into(),
|
||||
hwaddr: hwaddr.into(),
|
||||
local: MaybeString::from("1"),
|
||||
ignore: Some(0),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to set up the test environment with an initial OPNsense configuration.
|
||||
fn setup_test_env(initial_hosts: Vec<DnsmasqHost>) -> DhcpConfigDnsMasq<'static> {
|
||||
let opnsense_config = Box::leak(Box::new(OPNsense {
|
||||
dnsmasq: Some(DnsMasq {
|
||||
hosts: initial_hosts,
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}));
|
||||
|
||||
DhcpConfigDnsMasq::new(opnsense_config, Arc::new(DummyOPNSenseShell {}))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_first_static_mapping() {
|
||||
let mut dhcp_config = setup_test_env(vec![]);
|
||||
let ip = Ipv4Addr::new(192, 168, 1, 10);
|
||||
let mac = "00:11:22:33:44:55";
|
||||
let hostname = "new-host";
|
||||
|
||||
dhcp_config.add_static_mapping(mac, ip, hostname).unwrap();
|
||||
|
||||
let hosts = &dhcp_config.opnsense.dnsmasq.as_ref().unwrap().hosts;
|
||||
assert_eq!(hosts.len(), 1);
|
||||
let host = &hosts[0];
|
||||
assert_eq!(host.host, hostname);
|
||||
assert_eq!(host.ip, ip.to_string().into());
|
||||
assert_eq!(host.hwaddr.content_string(), mac);
|
||||
assert!(Uuid::parse_str(&host.uuid).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_mac_to_existing_host_by_ip_and_hostname() {
|
||||
let initial_host = create_host(
|
||||
"uuid-1",
|
||||
"existing-host",
|
||||
"192.168.1.20",
|
||||
"AA:BB:CC:DD:EE:FF",
|
||||
);
|
||||
let mut dhcp_config = setup_test_env(vec![initial_host]);
|
||||
let ip = Ipv4Addr::new(192, 168, 1, 20);
|
||||
let new_mac = "00:11:22:33:44:55";
|
||||
let hostname = "existing-host";
|
||||
|
||||
dhcp_config
|
||||
.add_static_mapping(new_mac, ip, hostname)
|
||||
.unwrap();
|
||||
|
||||
let hosts = &dhcp_config.opnsense.dnsmasq.as_ref().unwrap().hosts;
|
||||
assert_eq!(hosts.len(), 1);
|
||||
let host = &hosts[0];
|
||||
assert_eq!(
|
||||
host.hwaddr.content_string(),
|
||||
"AA:BB:CC:DD:EE:FF,00:11:22:33:44:55"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_mac_to_existing_host_by_ip_only() {
|
||||
let initial_host = create_host(
|
||||
"uuid-1",
|
||||
"existing-host",
|
||||
"192.168.1.20",
|
||||
"AA:BB:CC:DD:EE:FF",
|
||||
);
|
||||
let mut dhcp_config = setup_test_env(vec![initial_host]);
|
||||
let ip = Ipv4Addr::new(192, 168, 1, 20);
|
||||
let new_mac = "00:11:22:33:44:55";
|
||||
|
||||
// Using a different hostname should still find the host by IP and log a warning.
|
||||
dhcp_config
|
||||
.add_static_mapping(new_mac, ip, "different-host-name")
|
||||
.unwrap();
|
||||
|
||||
let hosts = &dhcp_config.opnsense.dnsmasq.as_ref().unwrap().hosts;
|
||||
assert_eq!(hosts.len(), 1);
|
||||
let host = &hosts[0];
|
||||
assert_eq!(
|
||||
host.hwaddr.content_string(),
|
||||
"AA:BB:CC:DD:EE:FF,00:11:22:33:44:55"
|
||||
);
|
||||
assert_eq!(host.host, "existing-host"); // Original hostname should be preserved.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_mac_to_existing_host_by_hostname_only() {
|
||||
let initial_host = create_host(
|
||||
"uuid-1",
|
||||
"existing-host",
|
||||
"192.168.1.20",
|
||||
"AA:BB:CC:DD:EE:FF",
|
||||
);
|
||||
let mut dhcp_config = setup_test_env(vec![initial_host]);
|
||||
let new_mac = "00:11:22:33:44:55";
|
||||
let hostname = "existing-host";
|
||||
|
||||
// Using a different IP should still find the host by hostname and log a warning.
|
||||
dhcp_config
|
||||
.add_static_mapping(new_mac, Ipv4Addr::new(192, 168, 1, 99), hostname)
|
||||
.unwrap();
|
||||
|
||||
let hosts = &dhcp_config.opnsense.dnsmasq.as_ref().unwrap().hosts;
|
||||
assert_eq!(hosts.len(), 1);
|
||||
let host = &hosts[0];
|
||||
assert_eq!(
|
||||
host.hwaddr.content_string(),
|
||||
"AA:BB:CC:DD:EE:FF,00:11:22:33:44:55"
|
||||
);
|
||||
assert_eq!(host.ip.content_string(), "192.168.1.20"); // Original IP should be preserved.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_duplicate_mac_to_host() {
|
||||
let initial_mac = "AA:BB:CC:DD:EE:FF";
|
||||
let initial_host = create_host("uuid-1", "host-1", "192.168.1.20", initial_mac);
|
||||
let mut dhcp_config = setup_test_env(vec![initial_host]);
|
||||
|
||||
dhcp_config
|
||||
.add_static_mapping(initial_mac, Ipv4Addr::new(192, 168, 1, 20), "host-1")
|
||||
.unwrap();
|
||||
|
||||
let hosts = &dhcp_config.opnsense.dnsmasq.as_ref().unwrap().hosts;
|
||||
assert_eq!(hosts.len(), 1);
|
||||
assert_eq!(hosts[0].hwaddr.content_string(), initial_mac); // No change, no duplication.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_invalid_mac_address() {
|
||||
let mut dhcp_config = setup_test_env(vec![]);
|
||||
let result =
|
||||
dhcp_config.add_static_mapping("invalid-mac", Ipv4Addr::new(10, 0, 0, 1), "host");
|
||||
assert!(matches!(result, Err(DhcpError::InvalidMacAddress(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(
|
||||
expected = "Configuration conflict: IP 192.168.1.10 and hostname 'host-b' exist, but in different static host entries."
|
||||
)]
|
||||
fn test_panic_on_conflicting_ip_and_hostname() {
|
||||
let host_a = create_host("uuid-a", "host-a", "192.168.1.10", "AA:AA:AA:AA:AA:AA");
|
||||
let host_b = create_host("uuid-b", "host-b", "192.168.1.20", "BB:BB:BB:BB:BB:BB");
|
||||
let mut dhcp_config = setup_test_env(vec![host_a, host_b]);
|
||||
|
||||
// This IP belongs to host-a, but the hostname belongs to host-b.
|
||||
dhcp_config
|
||||
.add_static_mapping(
|
||||
"CC:CC:CC:CC:CC:CC",
|
||||
Ipv4Addr::new(192, 168, 1, 10),
|
||||
"host-b",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(
|
||||
expected = "Configuration conflict: Found multiple host entries matching IP 192.168.1.30 and/or hostname 'new-host'."
|
||||
)]
|
||||
fn test_panic_on_multiple_ip_matches() {
|
||||
let host_a = create_host("uuid-a", "host-a", "192.168.1.30", "AA:AA:AA:AA:AA:AA");
|
||||
let host_b = create_host("uuid-b", "host-b", "192.168.1.30", "BB:BB:BB:BB:BB:BB");
|
||||
let mut dhcp_config = setup_test_env(vec![host_a, host_b]);
|
||||
|
||||
// This IP is ambiguous.
|
||||
dhcp_config
|
||||
.add_static_mapping(
|
||||
"CC:CC:CC:CC:CC:CC",
|
||||
Ipv4Addr::new(192, 168, 1, 30),
|
||||
"new-host",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_mac_from_multi_mac_host() {
|
||||
let host = create_host("uuid-1", "host-1", "192.168.1.50", "mac-1,mac-2,mac-3");
|
||||
let mut dhcp_config = setup_test_env(vec![host]);
|
||||
|
||||
dhcp_config.remove_static_mapping("mac-2");
|
||||
|
||||
let hosts = &dhcp_config.opnsense.dnsmasq.as_ref().unwrap().hosts;
|
||||
assert_eq!(hosts.len(), 1);
|
||||
assert_eq!(hosts[0].hwaddr.content_string(), "mac-1,mac-3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_last_mac_from_host() {
|
||||
let host = create_host("uuid-1", "host-1", "192.168.1.50", "mac-1");
|
||||
let mut dhcp_config = setup_test_env(vec![host]);
|
||||
|
||||
dhcp_config.remove_static_mapping("mac-1");
|
||||
|
||||
let hosts = &dhcp_config.opnsense.dnsmasq.as_ref().unwrap().hosts;
|
||||
assert!(hosts.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_non_existent_mac() {
|
||||
let host = create_host("uuid-1", "host-1", "192.168.1.50", "mac-1,mac-2");
|
||||
let mut dhcp_config = setup_test_env(vec![host.clone()]);
|
||||
|
||||
dhcp_config.remove_static_mapping("mac-nonexistent");
|
||||
|
||||
let hosts = &dhcp_config.opnsense.dnsmasq.as_ref().unwrap().hosts;
|
||||
assert_eq!(hosts.len(), 1);
|
||||
assert_eq!(hosts[0], host); // The host should be unchanged.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_mac_case_insensitively() {
|
||||
let host = create_host("uuid-1", "host-1", "192.168.1.50", "AA:BB:CC:DD:EE:FF");
|
||||
let mut dhcp_config = setup_test_env(vec![host]);
|
||||
|
||||
dhcp_config.remove_static_mapping("aa:bb:cc:dd:ee:ff");
|
||||
|
||||
let hosts = &dhcp_config.opnsense.dnsmasq.as_ref().unwrap().hosts;
|
||||
assert!(hosts.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_mac_from_correct_host_only() {
|
||||
let host1 = create_host(
|
||||
"uuid-1",
|
||||
"host-1",
|
||||
"192.168.1.50",
|
||||
"AA:AA:AA:AA:AA:AA,BB:BB:BB:BB:BB:BB",
|
||||
);
|
||||
let host2 = create_host(
|
||||
"uuid-2",
|
||||
"host-2",
|
||||
"192.168.1.51",
|
||||
"CC:CC:CC:CC:CC:CC,DD:DD:DD:DD:DD:DD",
|
||||
);
|
||||
let mut dhcp_config = setup_test_env(vec![host1.clone(), host2.clone()]);
|
||||
|
||||
dhcp_config.remove_static_mapping("AA:AA:AA:AA:AA:AA");
|
||||
|
||||
let hosts = &dhcp_config.opnsense.dnsmasq.as_ref().unwrap().hosts;
|
||||
assert_eq!(hosts.len(), 2);
|
||||
let updated_host1 = hosts.iter().find(|h| h.uuid == "uuid-1").unwrap();
|
||||
let unchanged_host2 = hosts.iter().find(|h| h.uuid == "uuid-2").unwrap();
|
||||
|
||||
assert_eq!(updated_host1.hwaddr.content_string(), "BB:BB:BB:BB:BB:BB");
|
||||
assert_eq!(
|
||||
unchanged_host2.hwaddr.content_string(),
|
||||
"CC:CC:CC:CC:CC:CC,DD:DD:DD:DD:DD:DD"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
1674
opnsense-config/src/tests/data/config-25.7-dnsmasq-static-host.xml
Normal file
1674
opnsense-config/src/tests/data/config-25.7-dnsmasq-static-host.xml
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user