diff --git a/examples/ha_cluster/Cargo.toml b/examples/ha_cluster/Cargo.toml new file mode 100644 index 0000000..ed9e65a --- /dev/null +++ b/examples/ha_cluster/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "example-ha-cluster" +edition = "2024" +version.workspace = true +readme.workspace = true +license.workspace = true +publish = false + +[dependencies] +harmony = { path = "../../harmony" } +harmony_tui = { path = "../../harmony_tui" } +harmony_types = { path = "../../harmony_types" } +cidr = { workspace = true } +tokio = { workspace = true } +harmony_macros = { path = "../../harmony_macros" } +log = { workspace = true } +env_logger = { workspace = true } +url = { workspace = true } +harmony_secret = { path = "../../harmony_secret" } +brocade = { path = "../../brocade" } +serde = { workspace = true } diff --git a/examples/ha_cluster/README.md b/examples/ha_cluster/README.md new file mode 100644 index 0000000..a2a7a1a --- /dev/null +++ b/examples/ha_cluster/README.md @@ -0,0 +1,15 @@ +## OPNSense demo + +Download the virtualbox snapshot from {{TODO URL}} + +Start the virtualbox image + +This virtualbox image is configured to use a bridge on the host's physical interface, make sure the bridge is up and the virtual machine can reach internet. + +Credentials are opnsense default (root/opnsense) + +Run the project with the correct ip address on the command line : + +```bash +cargo run -p example-opnsense -- 192.168.5.229 +``` diff --git a/examples/ha_cluster/src/main.rs b/examples/ha_cluster/src/main.rs new file mode 100644 index 0000000..4422f65 --- /dev/null +++ b/examples/ha_cluster/src/main.rs @@ -0,0 +1,141 @@ +use std::{ + net::{IpAddr, Ipv4Addr}, + sync::Arc, +}; + +use brocade::BrocadeOptions; +use cidr::Ipv4Cidr; +use harmony::{ + hardware::{HostCategory, Location, PhysicalHost, SwitchGroup}, + infra::{brocade::BrocadeSwitchClient, opnsense::OPNSenseManagementInterface}, + inventory::Inventory, + modules::{ + dummy::{ErrorScore, PanicScore, SuccessScore}, + http::StaticFilesHttpScore, + okd::{dhcp::OKDDhcpScore, dns::OKDDnsScore, load_balancer::OKDLoadBalancerScore}, + opnsense::OPNsenseShellCommandScore, + tftp::TftpScore, + }, + topology::{LogicalHost, UnmanagedRouter}, +}; +use harmony_macros::{ip, mac_address}; +use harmony_secret::{Secret, SecretManager}; +use harmony_types::net::Url; +use serde::{Deserialize, Serialize}; + +#[tokio::main] +async fn main() { + let firewall = harmony::topology::LogicalHost { + ip: ip!("192.168.5.229"), + name: String::from("opnsense-1"), + }; + + let switch_auth = SecretManager::get_or_prompt::() + .await + .expect("Failed to get credentials"); + + let switches: Vec = vec![ip!("192.168.5.101")]; // TODO: Adjust me + let brocade_options = Some(BrocadeOptions { + dry_run: *harmony::config::DRY_RUN, + ..Default::default() + }); + let switch_client = BrocadeSwitchClient::init( + &switches, + &switch_auth.username, + &switch_auth.password, + brocade_options, + ) + .await + .expect("Failed to connect to switch"); + + let switch_client = Arc::new(switch_client); + + let opnsense = Arc::new( + harmony::infra::opnsense::OPNSenseFirewall::new(firewall, None, "root", "opnsense").await, + ); + let lan_subnet = Ipv4Addr::new(10, 100, 8, 0); + let gateway_ipv4 = Ipv4Addr::new(10, 100, 8, 1); + let gateway_ip = IpAddr::V4(gateway_ipv4); + let topology = harmony::topology::HAClusterTopology { + kubeconfig: None, + domain_name: "demo.harmony.mcd".to_string(), + router: Arc::new(UnmanagedRouter::new( + gateway_ip, + Ipv4Cidr::new(lan_subnet, 24).unwrap(), + )), + load_balancer: opnsense.clone(), + firewall: opnsense.clone(), + tftp_server: opnsense.clone(), + http_server: opnsense.clone(), + dhcp_server: opnsense.clone(), + dns_server: opnsense.clone(), + control_plane: vec![LogicalHost { + ip: ip!("10.100.8.20"), + name: "cp0".to_string(), + }], + bootstrap_host: LogicalHost { + ip: ip!("10.100.8.20"), + name: "cp0".to_string(), + }, + workers: vec![], + switch_client: switch_client.clone(), + }; + + let inventory = Inventory { + location: Location::new( + "232 des Éperviers, Wendake, Qc, G0A 4V0".to_string(), + "wk".to_string(), + ), + switch: SwitchGroup::from([]), + firewall_mgmt: Box::new(OPNSenseManagementInterface::new()), + storage_host: vec![], + worker_host: vec![], + control_plane_host: vec![ + PhysicalHost::empty(HostCategory::Server) + .mac_address(mac_address!("08:00:27:62:EC:C3")), + ], + }; + + // TODO regroup smaller scores in a larger one such as this + // let okd_boostrap_preparation(); + + let dhcp_score = OKDDhcpScore::new(&topology, &inventory); + let dns_score = OKDDnsScore::new(&topology); + let load_balancer_score = OKDLoadBalancerScore::new(&topology); + + let tftp_score = TftpScore::new(Url::LocalFolder("./data/watchguard/tftpboot".to_string())); + let http_score = StaticFilesHttpScore { + folder_to_serve: Some(Url::LocalFolder( + "./data/watchguard/pxe-http-files".to_string(), + )), + files: vec![], + remote_path: None, + }; + + harmony_tui::run( + inventory, + topology, + vec![ + Box::new(dns_score), + Box::new(dhcp_score), + Box::new(load_balancer_score), + Box::new(tftp_score), + Box::new(http_score), + Box::new(OPNsenseShellCommandScore { + opnsense: opnsense.get_opnsense_config(), + command: "touch /tmp/helloharmonytouching".to_string(), + }), + Box::new(SuccessScore {}), + Box::new(ErrorScore {}), + Box::new(PanicScore {}), + ], + ) + .await + .unwrap(); +} + +#[derive(Secret, Serialize, Deserialize, Debug)] +pub struct BrocadeSwitchAuth { + pub username: String, + pub password: String, +} diff --git a/examples/opnsense/Cargo.toml b/examples/opnsense/Cargo.toml index 1574f29..197d6fd 100644 --- a/examples/opnsense/Cargo.toml +++ b/examples/opnsense/Cargo.toml @@ -8,7 +8,7 @@ publish = false [dependencies] harmony = { path = "../../harmony" } -harmony_tui = { path = "../../harmony_tui" } +harmony_cli = { path = "../../harmony_cli" } harmony_types = { path = "../../harmony_types" } cidr = { workspace = true } tokio = { workspace = true } diff --git a/examples/opnsense/env.sh b/examples/opnsense/env.sh new file mode 100644 index 0000000..05f2559 --- /dev/null +++ b/examples/opnsense/env.sh @@ -0,0 +1,3 @@ +export HARMONY_SECRET_NAMESPACE=example-opnsense +export HARMONY_SECRET_STORE=file +export RUST_LOG=info diff --git a/examples/opnsense/src/main.rs b/examples/opnsense/src/main.rs index 4422f65..cb17d0a 100644 --- a/examples/opnsense/src/main.rs +++ b/examples/opnsense/src/main.rs @@ -1,134 +1,70 @@ -use std::{ - net::{IpAddr, Ipv4Addr}, - sync::Arc, -}; - -use brocade::BrocadeOptions; -use cidr::Ipv4Cidr; use harmony::{ - hardware::{HostCategory, Location, PhysicalHost, SwitchGroup}, - infra::{brocade::BrocadeSwitchClient, opnsense::OPNSenseManagementInterface}, + config::secret::OPNSenseFirewallCredentials, + infra::opnsense::OPNSenseFirewall, inventory::Inventory, - modules::{ - dummy::{ErrorScore, PanicScore, SuccessScore}, - http::StaticFilesHttpScore, - okd::{dhcp::OKDDhcpScore, dns::OKDDnsScore, load_balancer::OKDLoadBalancerScore}, - opnsense::OPNsenseShellCommandScore, - tftp::TftpScore, - }, - topology::{LogicalHost, UnmanagedRouter}, + modules::{dhcp::DhcpScore, opnsense::OPNsenseShellCommandScore}, + topology::LogicalHost, }; -use harmony_macros::{ip, mac_address}; +use harmony_macros::{ip, ipv4}; use harmony_secret::{Secret, SecretManager}; -use harmony_types::net::Url; use serde::{Deserialize, Serialize}; #[tokio::main] async fn main() { - let firewall = harmony::topology::LogicalHost { - ip: ip!("192.168.5.229"), + let firewall = LogicalHost { + ip: ip!("192.168.55.1"), name: String::from("opnsense-1"), }; - let switch_auth = SecretManager::get_or_prompt::() + let opnsense_auth = SecretManager::get_or_prompt::() .await .expect("Failed to get credentials"); - let switches: Vec = vec![ip!("192.168.5.101")]; // TODO: Adjust me - let brocade_options = Some(BrocadeOptions { - dry_run: *harmony::config::DRY_RUN, - ..Default::default() - }); - let switch_client = BrocadeSwitchClient::init( - &switches, - &switch_auth.username, - &switch_auth.password, - brocade_options, + let opnsense = OPNSenseFirewall::new( + firewall, + None, + &opnsense_auth.username, + &opnsense_auth.password, ) - .await - .expect("Failed to connect to switch"); + .await; - let switch_client = Arc::new(switch_client); - - let opnsense = Arc::new( - harmony::infra::opnsense::OPNSenseFirewall::new(firewall, None, "root", "opnsense").await, - ); - let lan_subnet = Ipv4Addr::new(10, 100, 8, 0); - let gateway_ipv4 = Ipv4Addr::new(10, 100, 8, 1); - let gateway_ip = IpAddr::V4(gateway_ipv4); - let topology = harmony::topology::HAClusterTopology { - kubeconfig: None, - domain_name: "demo.harmony.mcd".to_string(), - router: Arc::new(UnmanagedRouter::new( - gateway_ip, - Ipv4Cidr::new(lan_subnet, 24).unwrap(), - )), - load_balancer: opnsense.clone(), - firewall: opnsense.clone(), - tftp_server: opnsense.clone(), - http_server: opnsense.clone(), - dhcp_server: opnsense.clone(), - dns_server: opnsense.clone(), - control_plane: vec![LogicalHost { - ip: ip!("10.100.8.20"), - name: "cp0".to_string(), - }], - bootstrap_host: LogicalHost { - ip: ip!("10.100.8.20"), - name: "cp0".to_string(), - }, - workers: vec![], - switch_client: switch_client.clone(), - }; - - let inventory = Inventory { - location: Location::new( - "232 des Éperviers, Wendake, Qc, G0A 4V0".to_string(), - "wk".to_string(), + let dhcp_score = DhcpScore { + dhcp_range: ( + ipv4!("192.168.55.100").into(), + ipv4!("192.168.55.150").into(), ), - switch: SwitchGroup::from([]), - firewall_mgmt: Box::new(OPNSenseManagementInterface::new()), - storage_host: vec![], - worker_host: vec![], - control_plane_host: vec![ - PhysicalHost::empty(HostCategory::Server) - .mac_address(mac_address!("08:00:27:62:EC:C3")), - ], + host_binding: vec![], + next_server: None, + boot_filename: None, + filename: None, + filename64: None, + filenameipxe: Some("filename.ipxe".to_string()), + domain: None, }; + // let dns_score = OKDDnsScore::new(&topology); + // let load_balancer_score = OKDLoadBalancerScore::new(&topology); + // + // let tftp_score = TftpScore::new(Url::LocalFolder("./data/watchguard/tftpboot".to_string())); + // let http_score = StaticFilesHttpScore { + // folder_to_serve: Some(Url::LocalFolder( + // "./data/watchguard/pxe-http-files".to_string(), + // )), + // files: vec![], + // remote_path: None, + // }; + let opnsense_config = opnsense.get_opnsense_config(); - // TODO regroup smaller scores in a larger one such as this - // let okd_boostrap_preparation(); - - let dhcp_score = OKDDhcpScore::new(&topology, &inventory); - let dns_score = OKDDnsScore::new(&topology); - let load_balancer_score = OKDLoadBalancerScore::new(&topology); - - let tftp_score = TftpScore::new(Url::LocalFolder("./data/watchguard/tftpboot".to_string())); - let http_score = StaticFilesHttpScore { - folder_to_serve: Some(Url::LocalFolder( - "./data/watchguard/pxe-http-files".to_string(), - )), - files: vec![], - remote_path: None, - }; - - harmony_tui::run( - inventory, - topology, + harmony_cli::run( + Inventory::autoload(), + opnsense, vec![ - Box::new(dns_score), Box::new(dhcp_score), - Box::new(load_balancer_score), - Box::new(tftp_score), - Box::new(http_score), Box::new(OPNsenseShellCommandScore { - opnsense: opnsense.get_opnsense_config(), - command: "touch /tmp/helloharmonytouching".to_string(), + opnsense: opnsense_config, + command: "touch /tmp/helloharmonytouching_2".to_string(), }), - Box::new(SuccessScore {}), - Box::new(ErrorScore {}), - Box::new(PanicScore {}), ], + None, ) .await .unwrap(); diff --git a/harmony/src/domain/maestro/mod.rs b/harmony/src/domain/maestro/mod.rs index 3469ea3..25210be 100644 --- a/harmony/src/domain/maestro/mod.rs +++ b/harmony/src/domain/maestro/mod.rs @@ -67,16 +67,16 @@ impl Maestro { } } - pub fn register_all(&mut self, mut scores: ScoreVec) { - let mut score_mut = self.scores.write().expect("Should acquire lock"); - score_mut.append(&mut scores); - } - fn is_topology_initialized(&self) -> bool { self.topology_state.status == TopologyStatus::Success || self.topology_state.status == TopologyStatus::Noop } + pub fn register_all(&mut self, mut scores: ScoreVec) { + let mut score_mut = self.scores.write().expect("Should acquire lock"); + score_mut.append(&mut scores); + } + pub async fn interpret(&self, score: Box>) -> Result { if !self.is_topology_initialized() { warn!( diff --git a/harmony/src/domain/topology/mod.rs b/harmony/src/domain/topology/mod.rs index 85e57d7..570021f 100644 --- a/harmony/src/domain/topology/mod.rs +++ b/harmony/src/domain/topology/mod.rs @@ -1,5 +1,6 @@ mod ha_cluster; pub mod ingress; +pub mod opnsense; use harmony_types::net::IpAddress; mod host_binding; mod http; diff --git a/harmony/src/domain/topology/opnsense.rs b/harmony/src/domain/topology/opnsense.rs new file mode 100644 index 0000000..6199446 --- /dev/null +++ b/harmony/src/domain/topology/opnsense.rs @@ -0,0 +1,23 @@ +use async_trait::async_trait; +use log::info; + +use crate::{ + infra::opnsense::OPNSenseFirewall, + topology::{PreparationError, PreparationOutcome, Topology}, +}; + +#[async_trait] +impl Topology for OPNSenseFirewall { + async fn ensure_ready(&self) -> Result { + // FIXME we should be initializing the opnsense config here instead of + // OPNSenseFirewall::new as this causes the config to be loaded too early in + // harmony initialization process + let details = "OPNSenseFirewall topology is ready".to_string(); + info!("{}", details); + Ok(PreparationOutcome::Success { details }) + } + + fn name(&self) -> &str { + "OPNSenseFirewall" + } +} diff --git a/harmony/src/infra/opnsense/mod.rs b/harmony/src/infra/opnsense/mod.rs index 3878cfc..24cb784 100644 --- a/harmony/src/infra/opnsense/mod.rs +++ b/harmony/src/infra/opnsense/mod.rs @@ -25,6 +25,8 @@ impl OPNSenseFirewall { self.host.ip } + /// panics : if the opnsense config file cannot be loaded by the underlying opnsense_config + /// crate pub async fn new(host: LogicalHost, port: Option, username: &str, password: &str) -> Self { Self { opnsense_config: Arc::new(RwLock::new( diff --git a/opnsense-config-xml/src/data/opnsense.rs b/opnsense-config-xml/src/data/opnsense.rs index fa5f985..c471824 100644 --- a/opnsense-config-xml/src/data/opnsense.rs +++ b/opnsense-config-xml/src/data/opnsense.rs @@ -216,7 +216,7 @@ pub struct System { pub maximumfrags: Option, pub aliasesresolveinterval: Option, pub maximumtableentries: Option, - pub language: String, + pub language: Option, pub dnsserver: Option, pub dns1gw: Option, pub dns2gw: Option,