diff --git a/Cargo.lock b/Cargo.lock index 844651a..9232ffb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2285,6 +2285,7 @@ dependencies = [ "helm-wrapper-rs", "hex", "http 1.3.1", + "inquire", "k3d-rs", "k8s-openapi", "kube", diff --git a/examples/cli/src/main.rs b/examples/cli/src/main.rs index 524d69c..a8bc901 100644 --- a/examples/cli/src/main.rs +++ b/examples/cli/src/main.rs @@ -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), }), ], diff --git a/examples/nanodc/src/main.rs b/examples/nanodc/src/main.rs index a6bb8e4..f66bac9 100644 --- a/examples/nanodc/src/main.rs +++ b/examples/nanodc/src/main.rs @@ -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, diff --git a/harmony/Cargo.toml b/harmony/Cargo.toml index 07a2480..ce789a6 100644 --- a/harmony/Cargo.toml +++ b/harmony/Cargo.toml @@ -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 diff --git a/harmony/src/domain/inventory/mod.rs b/harmony/src/domain/inventory/mod.rs index ae8589d..894d0f2 100644 --- a/harmony/src/domain/inventory/mod.rs +++ b/harmony/src/domain/inventory/mod.rs @@ -61,3 +61,10 @@ impl Inventory { } } } + +pub enum HostRole { + Bootstrap, + ControlPlane, + Worker, + Storage, +} diff --git a/harmony/src/domain/inventory/repository.rs b/harmony/src/domain/inventory/repository.rs index e4e02a9..f638b18 100644 --- a/harmony/src/domain/inventory/repository.rs +++ b/harmony/src/domain/inventory/repository.rs @@ -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 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, RepoError>; + async fn get_all_hosts(&self) -> Result, RepoError>; + async fn save_role_mapping( + &self, + role: &HostRole, + host: &PhysicalHost, + ) -> Result<(), RepoError>; } diff --git a/harmony/src/domain/topology/ha_cluster.rs b/harmony/src/domain/topology/ha_cluster.rs index 707081a..4fac60b 100644 --- a/harmony/src/domain/topology/ha_cluster.rs +++ b/harmony/src/domain/topology/ha_cluster.rs @@ -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) } diff --git a/harmony/src/domain/topology/network.rs b/harmony/src/domain/topology/network.rs index 7773ae1..b5baf8a 100644 --- a/harmony/src/domain/topology/network.rs +++ b/harmony/src/domain/topology/network.rs @@ -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>; diff --git a/harmony/src/infra/opnsense/dhcp.rs b/harmony/src/infra/opnsense/dhcp.rs index 272ffc2..bf8a93a 100644 --- a/harmony/src/infra/opnsense/dhcp.rs +++ b/harmony/src/infra/opnsense/dhcp.rs @@ -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}")) + }) + } } diff --git a/harmony/src/modules/dhcp.rs b/harmony/src/modules/dhcp.rs index b48f8e1..b5c540c 100644 --- a/harmony/src/modules/dhcp.rs +++ b/harmony/src/modules/dhcp.rs @@ -22,6 +22,7 @@ pub struct DhcpScore { pub filename: Option, pub filename64: Option, pub filenameipxe: Option, + pub dhcp_range: (IpAddress, IpAddress), } impl Score for DhcpScore { @@ -58,9 +59,6 @@ impl DhcpInterpret { _inventory: &Inventory, dhcp_server: &D, ) -> Result { - todo!( - "I don't think this set_pxe_options function still works since the major dnsmasq refactoring. It certainly is not far off, but we use the dedicated okd ipxe score now. They should work together, this needs refactoring." - ); let pxe_options = PxeOptions { ipxe_filename: self.score.filenameipxe.clone().unwrap_or_default(), bios_filename: self.score.filename.clone().unwrap_or_default(), @@ -110,6 +108,9 @@ impl Interpret for DhcpInterpret { 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(), @@ -146,7 +147,7 @@ impl Score for DhcpHostBindingScore { // https://docs.opnsense.org/manual/dhcp.html#advanced-settings #[derive(Debug, Clone)] pub struct DhcpHostBindingInterpret { - score: DhcpScore, + score: DhcpHostBindingScore, } impl DhcpHostBindingInterpret { diff --git a/harmony/src/modules/inventory/mod.rs b/harmony/src/modules/inventory/mod.rs index 67d7489..85e0853 100644 --- a/harmony/src/modules/inventory/mod.rs +++ b/harmony/src/modules/inventory/mod.rs @@ -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, } -impl Score for DiscoverInventoryAgentScore { +impl Score for LaunchDiscoverInventoryAgentScore { fn name(&self) -> String { "DiscoverInventoryAgentScore".to_string() } @@ -36,7 +36,7 @@ impl Score for DiscoverInventoryAgentScore { #[derive(Debug)] struct DiscoverInventoryAgentInterpret { - score: DiscoverInventoryAgentScore, + score: LaunchDiscoverInventoryAgentScore, } #[async_trait] @@ -46,6 +46,13 @@ impl Interpret for DiscoverInventoryAgentInterpret { _inventory: &Inventory, _topology: &T, ) -> Result { + 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> { diff --git a/harmony/src/modules/okd/bootstrap_dhcp.rs b/harmony/src/modules/okd/bootstrap_dhcp.rs index c7ffe7d..c8f323d 100644 --- a/harmony/src/modules/okd/bootstrap_dhcp.rs +++ b/harmony/src/modules/okd/bootstrap_dhcp.rs @@ -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.), + // ), + // } } } diff --git a/harmony/src/modules/okd/dhcp.rs b/harmony/src/modules/okd/dhcp.rs index 3386592..e6f0f04 100644 --- a/harmony/src/modules/okd/dhcp.rs +++ b/harmony/src/modules/okd/dhcp.rs @@ -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)), }, } } diff --git a/harmony/src/modules/okd/installation.rs b/harmony/src/modules/okd/installation.rs index b7f676e..94d5e01 100644 --- a/harmony/src/modules/okd/installation.rs +++ b/harmony/src/modules/okd/installation.rs @@ -49,18 +49,23 @@ //! - 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_types::id::Id; -use log::{error, 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, - modules::{dhcp::DhcpHostBindingScore, http::IPxeMacBootFileScore}, + inventory::{HostRole, Inventory}, + modules::{ + dhcp::DhcpHostBindingScore, http::IPxeMacBootFileScore, + inventory::LaunchDiscoverInventoryAgentScore, + }, score::Score, topology::{HAClusterTopology, HostBinding}, }; @@ -116,12 +121,6 @@ impl OKDInstallationInterpret { 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()); @@ -239,79 +238,6 @@ impl Interpret 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, -} - -impl Score for OKDSetup01InventoryDnsScore { - fn create_interpret(&self) -> Box> { - 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_trait] -impl Interpret 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 { - vec![] - } - - async fn execute( - &self, - _inventory: &Inventory, - topology: &HAClusterTopology, - ) -> Result { - info!("Ensuring base DNS and DHCP lease registration for discovery phase"); - error!("TODO setup ipxe score here and launch inventory agent"); - 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. @@ -319,9 +245,7 @@ impl Interpret for OKDSetup01InventoryDnsInterpret { // ------------------------------------------------------------------------------------------------- #[derive(Debug, Clone, Serialize, new)] -struct OKDSetup01InventoryScore { - lan_cidr: String, -} +struct OKDSetup01InventoryScore {} impl Score for OKDSetup01InventoryScore { fn create_interpret(&self) -> Box> { @@ -349,31 +273,6 @@ impl OKDSetup01InventoryInterpret { status: InterpretStatus::QUEUED, } } - - async fn ensure_inventory_assets( - &self, - topology: &HAClusterTopology, - ) -> 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 { - // 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] @@ -396,16 +295,99 @@ impl Interpret for OKDSetup01InventoryInterpret { async fn execute( &self, - _inventory: &Inventory, + inventory: &Inventory, topology: &HAClusterTopology, ) -> Result { - 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://: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://: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 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()), + // )) } } diff --git a/harmony/src/modules/okd/ipxe.rs b/harmony/src/modules/okd/ipxe.rs index 38de035..d5b5bdb 100644 --- a/harmony/src/modules/okd/ipxe.rs +++ b/harmony/src/modules/okd/ipxe.rs @@ -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 Interpret f ) -> Result { 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>> = vec![ Box::new(DhcpScore { host_binding: vec![], @@ -54,6 +64,7 @@ impl Interpret 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()), diff --git a/harmony_secret/src/lib.rs b/harmony_secret/src/lib.rs index 4f54d95..4603e4c 100644 --- a/harmony_secret/src/lib.rs +++ b/harmony_secret/src/lib.rs @@ -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() -> Result { 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(), diff --git a/harmony_secret/src/store/local_file.rs b/harmony_secret/src/store/local_file.rs index ed81c65..c277335 100644 --- a/harmony_secret/src/store/local_file.rs +++ b/harmony_secret/src/store/local_file.rs @@ -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() ); diff --git a/opnsense-config/src/modules/dnsmasq.rs b/opnsense-config/src/modules/dnsmasq.rs index c6018cc..e6417a8 100644 --- a/opnsense-config/src/modules/dnsmasq.rs +++ b/opnsense-config/src/modules/dnsmasq.rs @@ -1,7 +1,7 @@ // dnsmasq.rs use crate::modules::dhcp::DhcpError; use log::{debug, info, warn}; -use opnsense_config_xml::dnsmasq::{DnsMasq, DnsmasqHost}; +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; @@ -224,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 + // 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, diff --git a/opnsense-config/src/tests/data/config-25.7-dnsmasq-static-host.xml b/opnsense-config/src/tests/data/config-25.7-dnsmasq-static-host.xml new file mode 100644 index 0000000..eddff8c --- /dev/null +++ b/opnsense-config/src/tests/data/config-25.7-dnsmasq-static-host.xml @@ -0,0 +1,1674 @@ + + + opnsense + + + 115200 + serial + normal + OPNsense + testpxe.harmony.mcd + + admins + System Administrators + system + 1999 + 0 + page-all + + + + root + System Administrator + system + $2y$10$YRVoF4SgskIsrXOvOQjGieB9XqHPRra9R7d80B3BZdbY/j21TwBfS + + 0 + 0 + + + + + + + + + + + + + Etc/UTC + 0.opnsense.pool.ntp.org 1.opnsense.pool.ntp.org 2.opnsense.pool.ntp.org 3.opnsense.pool.ntp.org + + https + 68a72b6f7f776 + + + + + + 1 + yes + 1 + 1 + 1 + 1 + 1 + 1 + hadp + hadp + hadp + + monthly + + 1 + 1 + + admins + 1 + + + + + + enabled + 1 + + 1 + + + -1 + -1 + + + + os-caddy,os-haproxy,os-tftp + + + 0 + + en_US + + 1 + + + + + vtnet0 + + 1 + + + dhcp + + 1 + 1 + + dhcp6 + 0 + + + + + + vtnet1 + 1 + 192.168.1.1 + 24 + + + + + + + + + 1 + lo0 + Loopback + 1 + 127.0.0.1 + none + 1 + 8 + ::1 + 128 + + + + + + + public + + + + + automatic + + + + + pass + lan + inet + Default allow LAN to any rule + + lan + + + + + + + pass + lan + inet6 + Default allow LAN IPv6 to any rule + + lan + + + + + + + + + + + 0.opnsense.pool.ntp.org + + + root@192.168.1.5 + /api/dnsmasq/settings/set made changes + + + + + + + + + + + + + + + v9 + + + + 0 + + 1800 + 15 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 0 + wan + 192.168.0.0/16,10.0.0.0/8,172.16.0.0/12 + + + W0D23 + 4 + + + + + + + 0 + 0 + 0 + + + + 0 + 0 + + + + 0 + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + + 16 + 32 + 4 + 1000 + 1 + 0 + 0 + 0 + + + + + + + + 1 + 0 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + + + + + + + + + + 0 + + + + + + + 0 + 0 + + + ipsec + 0 + 1 + + + + + + + + + + + + + 0 + 0 + + 4000 + 1 + raw + + + 0 + + 2 + + + + + + + + 0 + 127.0.0.1 + 8000 + + + + + 0 + 0 + + 4000 + 1 + + + 0 + + 2 + + + + + + + + + + 0 + 120 + 120 + 127.0.0.1 + 25 + + + 0 + auto + 1 + + + + + 0 + root + + 2812 + + + 5 + 1 + + + 0 + root@localhost.local + 0 + + + + + + + 1 + $HOST + + system + + + + 300 + 30 +
+ + + + cfed35dc-f74b-417d-9ed9-682c5de96495,f961277a-07f1-49a4-90ee-bb15738d9ebb,30b2cce2-f650-4e44-a3e2-ee53886cda3f,3c86136f-35a4-4126-865b-82732c6542d9 + + + + + 1 + RootFs + + filesystem + + + / + 300 + 30 +
+ + + + fbb8dfe2-b9ad-4730-a0f3-41d7ecda6289 + + + + + 0 + carp_status_change + + custom + + + /usr/local/opnsense/scripts/OPNsense/Monit/carp_status + 300 + 30 +
+ + + + 11ceca8a-dff8-45e0-9dc5-ed80dc4b3947 + + + + + 0 + gateway_alert + + custom + + + /usr/local/opnsense/scripts/OPNsense/Monit/gateway_alert + 300 + 30 +
+ + + + fad1f465-4a92-4b93-be66-59d7059b8779 + + + + + Ping + NetworkPing + failed ping + alert + + + + NetworkLink + NetworkInterface + failed link + alert + + + + NetworkSaturation + NetworkInterface + saturation is greater than 75% + alert + + + + MemoryUsage + SystemResource + memory usage is greater than 75% + alert + + + + CPUUsage + SystemResource + cpu usage is greater than 75% + alert + + + + LoadAvg1 + SystemResource + loadavg (1min) is greater than 4 + alert + + + + LoadAvg5 + SystemResource + loadavg (5min) is greater than 3 + alert + + + + LoadAvg15 + SystemResource + loadavg (15min) is greater than 2 + alert + + + + SpaceUsage + SpaceUsage + space usage is greater than 75% + alert + + + + ChangedStatus + ProgramStatus + changed status + alert + + + + NonZeroStatus + ProgramStatus + status != 0 + alert + + + + + + + + + 1 + 1 + 31 + + + + + + + + + + + + 1 + 53 + 0 + + 0 + 0 + + 0 + 0 + + 0 + 0 + 0 + 0 + 0 + transparent + + 0 + + + 0 + 0 + 0 + 0 + 0 + 1 + 0 + + + 0 + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 1 + 0 + + 0.0.0.0/8,10.0.0.0/8,100.64.0.0/10,169.254.0.0/16,172.16.0.0/12,192.0.2.0/24,192.168.0.0/16,198.18.0.0/15,198.51.100.0/24,203.0.113.0/24,233.252.0.0/24,::1/128,2001:db8::/32,fc00::/8,fd00::/8,fe80::/10 + + + + + + + + + + + + + + 0 + + + + + allow + + + 0 + 0 + + + + + +
+ 0 + + + 0 + + + + + + + + + 0 + 0 + 0 + 1 + 0 + + + + + + + + + + + 1 + 192.168.1.1 + + + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 0 + 60s + + 0 + 0 + 1 + + 0 + + + 1024 + + + 1024 + + + 0 + + 1 + ipv4 + ignore + 2048 + 16384 + 2 + 0 + 0 + + 0 + 300 + 3600 + 0 + prefer-client-ciphers + TLSv1.2 + + ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256 + TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 + + + + + + + + + + + 30s + 30s + + 30s + 3 + x-1 + last,libc + + + + 127.0.0.1 + local0 + info + + + + 0 + 8822 + 0 + + 0 + + + + + 0 + *:8404 + /metrics + + + 0 + 4 + 60 + + 0 + 10 + + + + + 9fdfbc10cd927a0e.4bc71f5a + 1 + frontend_192.168.1.1:80 + + 192.168.1.1:80 + + tcp + 5ac12c5f-70c9-4beb-85ca-a76060170ce0 + 0 + + + + 0 + + + + + + 0 + 0 + 0 + 0 + 0 + + + + 0 + + + + + + + + + 0 + 0 + 0 + 0 + 0 + + + + + 0 + + + + + + + + + 0 + 0 + + 0 + 0 + + + + + + + + 4e26f04f7ce919a9.9309067c + 1 + frontend_192.168.1.1:443 + + 192.168.1.1:443 + + tcp + 62e22d3f-58e4-4a58-bb31-88b55337d41c + 0 + + + + 0 + + + + + + 0 + 0 + 0 + 0 + 0 + + + + 0 + + + + + + + + + 0 + 0 + 0 + 0 + 0 + + + + + 0 + + + + + + + + + 0 + 0 + + 0 + 0 + + + + + + + + 9a98ae460a9aafb7.422509b9 + 1 + frontend_192.168.1.1:22623 + + 192.168.1.1:22623 + + tcp + 7aa31ee2-86f3-4ee2-b661-98ca6eb76bc9 + 0 + + + + 0 + + + + + + 0 + 0 + 0 + 0 + 0 + + + + 0 + + + + + + + + + 0 + 0 + 0 + 0 + 0 + + + + + 0 + + + + + + + + + 0 + 0 + + 0 + 0 + + + + + + + + 7832147fac80fc37.486ed3ac + 1 + frontend_192.168.1.1:6443 + + 192.168.1.1:6443 + + tcp + 61ef67ba-68e5-46fb-89ed-ede779bfcfc0 + 0 + + + + 0 + + + + + + 0 + 0 + 0 + 0 + 0 + + + + 0 + + + + + + + + + 0 + 0 + 0 + 0 + 0 + + + + + 0 + + + + + + + + + 0 + 0 + + 0 + 0 + + + + + + + + + + 6a54c1779007c844.4f855343 + 1 + backend_192.168.1.1:80 + + tcp + roundrobin + 2 + + 07f4ebd1-5f2e-48c5-9418-a719109b2928,cfd90752-fc3e-42c5-8a7c-e49486e8de38 + + + + + + 1 + 24321590-fe6e-4ada-980a-1c9e1bb6a990 + 0 + + + + + + 0 + 0 + + + + + 0 + + + 30m + 50k + + + 10s + 10s + 10s + 10s + 1m + 1m + 0 + + + + + + + + + 0 + + 0 + + + + + 17919f61aad1fdcb.6e03e25e + 1 + backend_192.168.1.1:443 + + tcp + roundrobin + 2 + + 2dfb75a8-1713-4452-a5f4-83c9759729ca,ff876fa2-0409-4b51-bbaf-406c53e74a71 + + + + + + 1 + bb246c2b-6180-428c-a168-a2875b3d1b0a + 0 + + + + + + 0 + 0 + + + + + 0 + + + 30m + 50k + + + 10s + 10s + 10s + 10s + 1m + 1m + 0 + + + + + + + + + 0 + + 0 + + + + + 9dadf56d866ff29b.690416dd + 1 + backend_192.168.1.1:22623 + + tcp + roundrobin + 2 + + c8653027-8497-4bba-a5ae-6034011cf7c7,89ffae6e-79a4-413a-89ea-6e9e52783243 + + + + + + 1 + 46542083-998b-4bb9-a47c-a586aac8bc0d + 0 + + + + + + 0 + 0 + + + + + 0 + + + 30m + 50k + + + 10s + 10s + 10s + 10s + 1m + 1m + 0 + + + + + + + + + 0 + + 0 + + + + + fd857b0343b2e697.21f0f89e + 1 + backend_192.168.1.1:6443 + + tcp + roundrobin + 2 + + b85e244f-fbb8-4d5d-93be-ff9fb9d828fc,c94bcbd2-2fe2-49a5-b611-034d1fc54119 + + + + + + 1 + 52dc61b7-d23e-4e47-ba22-9edfd914bbcb + 0 + + + + + + 0 + 0 + + + + + 0 + + + 30m + 50k + + + 10s + 10s + 10s + 10s + 1m + 1m + 0 + + + + + + + + + 0 + + 0 + + + + + + + f33ae26833f881f7.d1ec1e06 + 1 + 10.100.8.20_80 + +
10.100.8.20
+ 80 + + active + + static + + + + + + 0 + + 0 + + + + + + + + + +
+ + fd47b7f46d7d69d8.65fb15f6 + 1 + 10.100.8.20_443 + +
10.100.8.20
+ 443 + + active + + static + + + + + + 0 + + 0 + + + + + + + + + +
+ + 3e3974dbcbc95c6d.cc0c066d + 1 + 10.100.8.20_22623 + +
10.100.8.20
+ 22623 + + active + + static + + + + + + 0 + + 0 + + + + + + + + + +
+ + 2817f4f2b67b9a5f.1960d7f6 + 1 + 10.100.8.20_6443 + +
10.100.8.20
+ 6443 + + active + + static + + + + + + 0 + + 0 + + + + + + + + + +
+
+ + + TCP_serverport + + tcp + 2s + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + HTTP_GET_/readyz + + http + 2s + + + 0 + + GET + /readyz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + 0 + + 0 + + 0 + + + +
+ + + + + 68a72b6f7f776 + Web GUI TLS certificate + LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUhFakNDQlBxZ0F3SUJBZ0lVUkZqWUQ0Z1U0bzRNZGdiN2pIc29KNU9GVGFnd0RRWUpLb1pJaHZjTkFRRUwKQlFBd2dZWXhHakFZQmdOVkJBTU1FVTlRVG5ObGJuTmxMbWx1ZEdWeWJtRnNNUXN3Q1FZRFZRUUdFd0pPVERFVgpNQk1HQTFVRUNBd01XblZwWkMxSWIyeHNZVzVrTVJVd0V3WURWUVFIREF4TmFXUmtaV3hvWVhKdWFYTXhMVEFyCkJnTlZCQW9NSkU5UVRuTmxibk5sSUhObGJHWXRjMmxuYm1Wa0lIZGxZaUJqWlhKMGFXWnBZMkYwWlRBZUZ3MHkKTlRBNE1qRXhOREl4TXpaYUZ3MHlOakE1TWpJeE5ESXhNelphTUlHR01Sb3dHQVlEVlFRRERCRlBVRTV6Wlc1egpaUzVwYm5SbGNtNWhiREVMTUFrR0ExVUVCaE1DVGt3eEZUQVRCZ05WQkFnTURGcDFhV1F0U0c5c2JHRnVaREVWCk1CTUdBMVVFQnd3TVRXbGtaR1ZzYUdGeWJtbHpNUzB3S3dZRFZRUUtEQ1JQVUU1elpXNXpaU0J6Wld4bUxYTnAKWjI1bFpDQjNaV0lnWTJWeWRHbG1hV05oZEdVd2dnSWlNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUNEd0F3Z2dJSwpBb0lDQVFDbENkeFJ3ZWJQQkxvYlVORnYvL2t3TEdKWExweDl6OFFHV2lyWTNpamVDeUxDQ0FwczBLaE1adTNRClhkczMranppbDRnSE96L0hvUEo5Z0xxMy9FYnR4cE9ENWkvQzZwbXc3SGM1M2tTQ3JCK2tlWUFnVWZ1aDU3MzAKZyt3cGc5RDQzaHFBNzF1L3F0ZC95eitnTVJnTWdZMndEK3ZWQWRrdGxVSWlmN2piTmR1RDRGMmdkL0gwbzljWApEUm5zMzNNQVptTkZwajN4QWFwQi9RWnhKV1JMZ1J5K1A5MWcyZEZFNzhNaWY4ZTRNSCtrU29ndzIwVG1JbmpzCitKdEVTc0xQZmx2eUZUa0lkTVdFbURWOG1HUk5hNXVoYXlEbVNEUU9xV0NUTlZHV3ZVWjZTRnJRZ1Q1MDBEdXgKWnRtYlhGdEVqRzlIaGd5SW5QT0dKbWYzTWVzS3dYclVNMW1BenVCRVBFR0lwOTc3UTY2SitpTDYzWTUvTTB3aAphMGVVNGppNTVRQnJOQjlaWjJsa080bGU2TXdmZm50c29JakMrVDh5RW5tbW5nQTlTdWNPRW9CcFFhd3cvRGhOCmtSNGk4TUptR1JNdmpLazlHVzZ3Z2VNVThJVDhKZDRjTmJOVzdFSGpzV08xN1luTVhIMEUxOVZqa2d1R3dIODAKZ3ZROGtzTmV4WVA3WWo0b0VycnRKbWVhWU8wbFVkV0tGektNdS8va0UvNG5HK0h4emlRUnA5QmdyTURNYks4ZgpkM29mY2tqZFZTTW9Vc1FJaWlmdTFMK1I4V1Y3K3hsTzdTWS80dGk3Y28zcjNXRTYyVlE4Vk9QMVphcStWRFpvClNIMVRCa0lTSU5paVJFRzhZSDQvRHJwNWZ2dHBPcERBRGN1TGdDNDJHcExmS1pwVEtRSURBUUFCbzRJQmREQ0MKQVhBd0NRWURWUjBUQkFJd0FEQVJCZ2xnaGtnQmh2aENBUUVFQkFNQ0JrQXdOQVlKWUlaSUFZYjRRZ0VOQkNjVwpKVTlRVG5ObGJuTmxJRWRsYm1WeVlYUmxaQ0JUWlhKMlpYSWdRMlZ5ZEdsbWFXTmhkR1V3SFFZRFZSME9CQllFCkZIdUVQK05yYlorZWdMdWZVSUFKaUo2M1c4SDFNSUd3QmdOVkhTTUVnYWd3Z2FXaGdZeWtnWWt3Z1lZeEdqQVkKQmdOVkJBTU1FVTlRVG5ObGJuTmxMbWx1ZEdWeWJtRnNNUXN3Q1FZRFZRUUdFd0pPVERFVk1CTUdBMVVFQ0F3TQpXblZwWkMxSWIyeHNZVzVrTVJVd0V3WURWUVFIREF4TmFXUmtaV3hvWVhKdWFYTXhMVEFyQmdOVkJBb01KRTlRClRuTmxibk5sSUhObGJHWXRjMmxuYm1Wa0lIZGxZaUJqWlhKMGFXWnBZMkYwWllJVVJGallENGdVNG80TWRnYjcKakhzb0o1T0ZUYWd3SFFZRFZSMGxCQll3RkFZSUt3WUJCUVVIQXdFR0NDc0dBUVVGQ0FJQ01Bc0dBMVVkRHdRRQpBd0lGb0RBY0JnTlZIUkVFRlRBVGdoRlBVRTV6Wlc1elpTNXBiblJsY201aGJEQU5CZ2txaGtpRzl3MEJBUXNGCkFBT0NBZ0VBV2JzM2MwSXYwcEd3Y0wvUmRlbnBiZVJHQ3FsODY0V1ZITEtMZzJIR3BkKytJdmRFcHJEZkZ3SCsKdHdOd2VrZTlXUEtYa20vUkZDWE5DQmVLNjkxeURVWCtCNUJOMjMvSks5N1lzRVdtMURIV3FvSDE1WmdqelZ0QQp2d2JmbnRQdlhCWU1wV2ZQY0Zua0hjN3pxUjI3RzBEZHFUeGg2TjhFenV1S3JRWXFtaWhJUXFkNU9HRVhteW9ZCmdPVjdoZ0lWSUR6a1Z0QkRiS3dFV3VFN2pKYzViMXR4Mk1FUFRsVklEZGo0Zm5vdURWemdkczA2RER4aFM4eXAKbXJOSXhxb045ekEzYXVtTnRNZ2haSHVZRHdjbm5GSnBNZHlJSEdHZ1dlNnZZNHFtdEFSVDd3a0x6MTZnUG9LMAo5bFhVU0RmV3YwUDJGUXFHZTJjaXQ3VVE2ZGtsUWsrVGVtUEFwNnhEV09HR3oxRkdmUUoxN040b3AvOGtlOUo2Cm96RVp3QTh1aDVYTUl2N3loM2dobjV1d1R6RDUyZ1BBZFdaekEyaHVWV3p5cVM0WVc0N3ZkaGV6TTFTUndabVEKUmYzNDk0UVFydWd0bzdycWdMUlRTSXN4WEtkU21MaHZjT0hsSlhISW1XNTRzeFlXNm9NUStpRExFT29ZVVdpcgp1aUJvT1RsNEJaOG5Xcm9pV0JtWlFLaVRPYlFRczVWTkIwYnVybmRISTJVdmtIRTE3QTM0bFYySjY5Q0dNNzJ2CjQ5aE9TN3B2Tzg4cEVKZm90d01YYlRhdkR2WTBHazZxbERFMVBud1U2Wm8ySEprcFdUTUxOSzh1alZ1RkhlMGkKR2JvZi9va08vZW4rUi9PUXNyd1JYbzFwVTRiWnlyWGVQeUdqSSsrdFYzemhjd0IwWjNJPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + + + LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUpRUUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQ1Nzd2dna25BZ0VBQW9JQ0FRQ2xDZHhSd2ViUEJMb2IKVU5Gdi8va3dMR0pYTHB4OXo4UUdXaXJZM2lqZUN5TENDQXBzMEtoTVp1M1FYZHMzK2p6aWw0Z0hPei9Ib1BKOQpnTHEzL0VidHhwT0Q1aS9DNnBtdzdIYzUza1NDckIra2VZQWdVZnVoNTczMGcrd3BnOUQ0M2hxQTcxdS9xdGQvCnl6K2dNUmdNZ1kyd0QrdlZBZGt0bFVJaWY3amJOZHVENEYyZ2QvSDBvOWNYRFJuczMzTUFabU5GcGozeEFhcEIKL1FaeEpXUkxnUnkrUDkxZzJkRkU3OE1pZjhlNE1IK2tTb2d3MjBUbUluanMrSnRFU3NMUGZsdnlGVGtJZE1XRQptRFY4bUdSTmE1dWhheURtU0RRT3FXQ1ROVkdXdlVaNlNGclFnVDUwMER1eFp0bWJYRnRFakc5SGhneUluUE9HCkptZjNNZXNLd1hyVU0xbUF6dUJFUEVHSXA5NzdRNjZKK2lMNjNZNS9NMHdoYTBlVTRqaTU1UUJyTkI5WloybGsKTzRsZTZNd2ZmbnRzb0lqQytUOHlFbm1tbmdBOVN1Y09Fb0JwUWF3dy9EaE5rUjRpOE1KbUdSTXZqS2s5R1c2dwpnZU1VOElUOEpkNGNOYk5XN0VIanNXTzE3WW5NWEgwRTE5VmprZ3VHd0g4MGd2UThrc05leFlQN1lqNG9FcnJ0CkptZWFZTzBsVWRXS0Z6S011Ly9rRS80bkcrSHh6aVFScDlCZ3JNRE1iSzhmZDNvZmNramRWU01vVXNRSWlpZnUKMUwrUjhXVjcreGxPN1NZLzR0aTdjbzNyM1dFNjJWUThWT1AxWmFxK1ZEWm9TSDFUQmtJU0lOaWlSRUc4WUg0LwpEcnA1ZnZ0cE9wREFEY3VMZ0M0MkdwTGZLWnBUS1FJREFRQUJBb0lDQUFTSHc4Tit4aDR5ckFVcDc4WGFTZlhYCmtnK0FtUTBmRWV0MnVDeGgxTTlia09Xd29OQ2gzYXpUT24zNHhaYkF5TUVUbGNsVkNBZ3IwOXc4RjJRTGljcm4KSTQrQVZ4bExwVkprKzFUY1ZCY2VNSFFzWGFjRmVSblZxYkkzbU5qKzVGS2dqaXV4NWx2WmpiYlZWbmJJUWplOQpxcTBGa3R5ekEwb3NDYmUydDlWVW9pVDVtTGhaOG90Ym9BRGkvQzR6YUEyL3djUGNyMkNaUWhvem51U21PUjJWCmVydUNOMHA4VURGTFA1a0gxdXlvY0NpTFh6ZXdIVEVRQ3krK0YwMEZuRmxqeDVSYW5za3JvMnhqWFR5QlZtZUYKcDYwRHF0Q0hkTjVlS2VlQWxDL0dIRlFvL2swdzd3ejMxbHVsVGgza3FDQzJsaXRwYzVpZ2JsTGxaUDgxSUpXTQp0bkhlczNsTXk1RGNDWUx3L3huZFdmVDZFMTB4WlhFNWI0QTdxYjF4Yjhsd1FoNHFJckhDZ2p1NDVPYXNCMERJClBYZ3E2eWkwL2FKWXV6SU5kcjRTeFRibExGUkp6MXlQaGZTZDVGbjdWQVBYU1JNTDlDbzJrL0M1SDlwdG1HMjYKZHBLQVNib1ZMcStrbXg3anVKYXc0a1JNNHZmYndHZGNMZEhqMXByZ08xNkd1ckpQOVRRQ0x5YzhaR0xOekcvaApIMzBpU2FlclJOUmtDRlhmeTEzWWJJZTZHTE12KzVmODlYSENGNmZrZ1JkZjVrbTA3cEc3SCtMZytmZFdtd2lZCm0waExNSFVZeHJ3WkFma2tvZjhlSllscEVQVmQ3ZytCVjd2eTZhYW0yQituUTdHYk84WUprSnlJME04amlSaDEKeGdjRmFZaGZlT21RZGtVbi9BcUJBb0lCQVFEU1JZbDl0SnJyQk5UOXlZN0twWTJiOGVURFJqdDByU1NQRUJvNgppeWoyVWR5S1ZSbHFFdzRma2IrejV3WWt2bnBpMW1uS3NjNFlLZmoyaDVSdXVDbzVzTUNLbmpDUXBpbll4bWRFCk45Z3l6SWRYMmlzRUh6dXNSZkZiajBVSWU1dHc0TE9pL3cyVzFYWGJUc3liOFdhTmhkbVQ4TGxDNjQ5WkNNUWQKeDZkeTdOWS9uYnVWVVQ0KzM3WmV0VlR1eDg1ekl5OTdnMWp4dFZhaXZrd2hQVWtLcWpXWDYzaUZidjFuY1FVdgpiQURrWkFoOXRWYWV2UGZ2NURDeDZITldiVFlObjVRZWd3OTRyVndoSjhYb1V5ZDRqWFB0VmdXU2VkN0tWd2U5CmNkNW9CZWFBOVhDdnJxdkNIRjI4QXg2OUI2YWQrQlk1S0dVcGU2LythQnlKdlQwUkFvSUJBUURJN2c3c0dMc3AKVWJ4dGhJQm9yRzF5MDRKWWlnaE5VMlF4YjdzSkxocnRTc2NtRkxSZU5DYy8zOTBCV3ZrbVFIazFnZkpxV3hDLwp2R0VMT0Iwd3U5VFBBRWFsS25IZ2RhNkFTMURuM3NTWTltcFBRRjYvbEY2cm00cDlNUU1TTFo1V3ZsL0ZNRklHCjUvaXVSVjJaOVdkeTV4QVFWNG5uZmdMOWJDNzhPa2k3VnFPTDJDZk0vcEJEMHdzRUZmOGZiejFSZXo4dEFRZ2QKVXY4cEpFTWdDTCtQeEdkdG5DYUcxYm5obXhEUUxiWmQ4TTFOQkpKOWZLZFgzVWtLcTlDdmFJVXBIZlduSFBWVAprVWNwMUVTYnEzOFVhTzFSS1NBNUtQd1ZiNkVPVGJBSGtlaEN4ZVhpN2F3YkZwYXlTelpIaWl4Y05QQjk1YUtSCkpJQ0J5ekFwQTVTWkFvSUJBRlZKYXlrWGxqWjVNVUwyKy9ucUNIUVdPeW1SVlJCUUlpSDg4QWFLNTBSeGs3aHcKSit6RWFkZ1lMOTl5ZHlWME5RUGQzKzhkQzNEMXBVdXBWbVZLUWFaQXNQZ0lqYjQrQjM4cmlqczdRMi9uVVlZcQpzWVBzZnpHeTlPQ2tUZVhRN1ExdHRxOElNS1RiVkFCdUI4UEF1RTN5Mm51TkNqZkFmOVluSGhUT0pIY1M1UnZNCmlJZForcHRaOWdpWUdDajUxaDBSU25NWXBYejBobjFnSGxUbEhMazhySnhBSUJSUEhtMVVoRHZsM0w3R2JFTkEKeUM5K2lqbzlIaHNySTQwTW92NEhtZlorUmtvMlZzWUQ4ZHYzem15eFF6SWkwQVBIZHJ3dmJLNUVmMmRGN1dhbApKdDI3UldOb1NnUzJaME5ZMVJZQnlGSEt0cTJLdzZtMjVNeGhlMkVDZ2dFQVhSNFdSRXhoMEpCVXB0eVZOZTFTCis3Z1IzRDU4QW5uM0lRSUt5QUpaOEVhTGJKYUQwSFNUREFNUFJTV0grYlkvZGhDMjY1c3djK3MxZmlHUFJacUcKMFRmcmhYZmFOby9UUXhta2NSRElRNnRQTVZNL2xjR0k3amF6UTdtSEZ0R1ZZOVh1UkZCVWMyYmwxTDNJMXlUbgp3RlJkR1hXNEwxUXl4b2R3YnV3RDhPNEI5VGxEbUxrUTJwM2ZxUkVZbnRUS3NneFFCdWRIZjIrTFdPRzVTZ3RECjI3akZ4Z0pyeUdrY0wvWFJJT2xPYnRLK0VrZGdMRStzcmdlYlpocWlKK2hrYmQyNGpxM1k4OVdNQ1ZLYVNScDkKVmxRYVIxYXIzRkdtSWJrT0JyYnlNVS9wTjZqSEZSZllmdVZGQ1hQWnYrWEZFU1pubmJEaVdpbDBkTEpacTJoQgpZUUtDQVFBOVlTcE1wS3dhVGwrTmhTZlovMXU0NjZiMkpZcmJPYlRJM2VCZUowMnFwNXdQTjZYUHJ5aVZaZ1FXClh5cG04a3M5MEJIblBPNUczNFJnKzhLRFlONU1Ed1hBclJubDdSeTEySG5oV3lSaHNKYmdZOEh1c2d4SEROMU8KMEcwSFBpVWtIbTYydlRNYll6bkhPeE5sS1hFdFhBcTlZM3dQYkFjb01rRXZ0MzEwdEdLSUNtdTdEWkpXRlVvTAp1Y3RVS3Boc0V5VWdHbHIwRjJKekVoQWdMRXplczB0S1JpRWdlaFdnbXdlMEhlTEhCdW5oRFBTMmFJY2lCME1pCjY2SGc3cVZyMDlneXpFeGxrY3RLRzhsSm9WbU8vdlhucWQrWDB5M21YTUVZbkFIWHpIeG1Pd2JCNnF3Y3VWTlEKZytqRXliUWF3d3A2OC9id0JncFREQUhORGxrRQotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tCg== + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + 0 + 0 + + 1400 + + + + + + 1 + 0 + 8080 + 8443 + + + + + 0 + + + + + + 0 + 10 + h1,h2 + + + + + 0 + 0 + 10 + + + + + + + 0 + + + + 0 + + + + + + + + + + 1 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + + lan + 0 + + + + + 0 + 0 + + + 1 + + 1 + + 0 + 1 + + 0 + 0 + + 1 + + teststatichost + + 1 + 192.168.1.20 + + + 01:c4:f3:f4:8a:15,01:c4:f3:f4:8a:16 + + 0 + + description + controlled by someone comments + + + + ipxe + + + pxeEfi + + + pxeBios + + + match + + + + + 8d190cf3-8d2d-47db-ab9b-fa21016b533e + iPXE + 0 + + + +