diff --git a/harmony-rs/Cargo.lock b/harmony-rs/Cargo.lock index 4f7478f..79abd3b 100644 --- a/harmony-rs/Cargo.lock +++ b/harmony-rs/Cargo.lock @@ -711,17 +711,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fqm" -version = "0.1.0" -dependencies = [ - "cidr", - "env_logger", - "harmony", - "log", - "tokio", -] - [[package]] name = "funty" version = "2.0.0" @@ -904,6 +893,7 @@ dependencies = [ "serde", "serde_json", "tokio", + "uuid", ] [[package]] @@ -1404,6 +1394,7 @@ dependencies = [ "env_logger", "log", "pretty_assertions", + "rand", "serde", "thiserror", "tokio", @@ -2526,6 +2517,18 @@ dependencies = [ "syn 2.0.90", ] +[[package]] +name = "vbox-opnpsense" +version = "0.1.0" +dependencies = [ + "cidr", + "env_logger", + "harmony", + "harmony_macros", + "log", + "tokio", +] + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/harmony-rs/Cargo.toml b/harmony-rs/Cargo.toml index d965ab6..dbf8814 100644 --- a/harmony-rs/Cargo.toml +++ b/harmony-rs/Cargo.toml @@ -2,6 +2,7 @@ resolver = "2" members = [ "private_repos/*", + "demo/*", "harmony", "opnsense-config", "opnsense-config-xml", "harmony_macros", ] @@ -20,6 +21,16 @@ tokio = { version = "1.40.0", features = ["io-std"] } cidr = "0.2.3" russh = "0.45.0" russh-keys = "0.45.0" +rand = "0.8.5" + +[workspace.dependencies.uuid] +version = "1.11.0" +features = [ + "v4", # Lets you generate random UUIDs + "fast-rng", # Use a faster (but still sufficiently random) RNG + "macro-diagnostics", # Enable better diagnostics for compile-time UUIDs +] + #[workspace.target.x86_64-unknown-linux-gnu] #rustflags = ["-C", "link-arg=-fuse-ld=mold"] diff --git a/harmony-rs/demo/vbox-opnsense/Cargo.toml b/harmony-rs/demo/vbox-opnsense/Cargo.toml new file mode 100644 index 0000000..6a460e1 --- /dev/null +++ b/harmony-rs/demo/vbox-opnsense/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "vbox-opnpsense" +edition = "2024" +version.workspace = true +readme.workspace = true +license.workspace = true + +[dependencies] +harmony = { version = "0.1.0", path = "../../harmony" } +cidr = { workspace = true } +tokio = { workspace = true } +harmony_macros = { version = "1.0.0", path = "../../harmony_macros" } +log = { workspace = true } +env_logger = { workspace = true } diff --git a/harmony-rs/demo/vbox-opnsense/README.md b/harmony-rs/demo/vbox-opnsense/README.md new file mode 100644 index 0000000..596c16b --- /dev/null +++ b/harmony-rs/demo/vbox-opnsense/README.md @@ -0,0 +1,16 @@ +## 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 +cd demo/vbox-opnsense/ +cargo run 192.168.5.229 +``` diff --git a/harmony-rs/demo/vbox-opnsense/src/main.rs b/harmony-rs/demo/vbox-opnsense/src/main.rs new file mode 100644 index 0000000..3a2ee59 --- /dev/null +++ b/harmony-rs/demo/vbox-opnsense/src/main.rs @@ -0,0 +1,91 @@ +use std::{ + net::{IpAddr, Ipv4Addr}, + sync::Arc, +}; + +use cidr::Ipv4Cidr; +use harmony::{ + hardware::{FirewallGroup, HostCategory, Location, PhysicalHost, SwitchGroup}, + infra::opnsense::OPNSenseManagementInterface, + inventory::Inventory, + maestro::Maestro, + modules::okd::{dhcp::OKDBootstrapDhcpScore, dns::OKDBootstrapDnsScore}, + topology::{LogicalHost, UnmanagedRouter}, +}; +use harmony_macros::ip; + +#[tokio::main] +async fn main() { + env_logger::init(); + + let firewall = harmony::topology::LogicalHost { + ip: ip!("192.168.5.229"), + name: String::from("opnsense-1"), + }; + + let firewall = harmony::topology::LogicalHost { + ip: ip!("127.0.0.1"), + name: String::from("opnsense-1"), + }; + let opnsense = Arc::new( + harmony::infra::opnsense::OPNSenseFirewall::new( + firewall, + Some(2222), + "lan", + "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 { + 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(), + dhcp_server: opnsense.clone(), + dns_server: opnsense.clone(), + control_plane: vec![LogicalHost { + ip: ip!("10.100.8.20"), + name: "cp0".to_string(), + }], + workers: vec![], + switch: vec![], + }; + + let inventory = Inventory { + location: Location::new( + "232 des Éperviers, Wendake, Qc, G0A 4V0".to_string(), + "wk".to_string(), + ), + switch: SwitchGroup::from([]), + firewall: FirewallGroup::from([PhysicalHost { + category: HostCategory::Firewall, + network: vec![], + management: Arc::new(OPNSenseManagementInterface::new()), + storage: vec![], + labels: vec![], + }]), + worker_host: vec![], + storage_host: vec![], + control_plane_host: vec![], + }; + + // TODO regroup smaller scores in a larger one such as this + // let okd_boostrap_preparation(); + + // let dhcp_score = OKDBootstrapDhcpScore::new(&topology, &inventory); + // let dns_score = OKDBootstrapDnsScore::new(&topology); + let load_balancer_score = + harmony::modules::okd::load_balancer::OKDLoadBalancerScore::new(&topology); + + let maestro = Maestro::new(inventory, topology); + // maestro.interpret(dns_score).await.unwrap(); + // maestro.interpret(dhcp_score).await.unwrap(); + maestro.interpret(load_balancer_score).await.unwrap(); +} diff --git a/harmony-rs/harmony/Cargo.toml b/harmony-rs/harmony/Cargo.toml index 753fc3b..dd3b6c7 100644 --- a/harmony-rs/harmony/Cargo.toml +++ b/harmony-rs/harmony/Cargo.toml @@ -19,3 +19,4 @@ async-trait = { workspace = true } cidr = { workspace = true } opnsense-config = { path = "../opnsense-config" } opnsense-config-xml = { path = "../opnsense-config-xml" } +uuid = { workspace = true } diff --git a/harmony-rs/harmony/src/domain/interpret/mod.rs b/harmony-rs/harmony/src/domain/interpret/mod.rs index 0b7609b..32f5893 100644 --- a/harmony-rs/harmony/src/domain/interpret/mod.rs +++ b/harmony-rs/harmony/src/domain/interpret/mod.rs @@ -12,7 +12,8 @@ use super::{ pub enum InterpretName { OPNSenseDHCP, - OPNSenseDns + OPNSenseDns, + LoadBalancer, } impl std::fmt::Display for InterpretName { @@ -20,6 +21,7 @@ impl std::fmt::Display for InterpretName { match self { InterpretName::OPNSenseDHCP => f.write_str("OPNSenseDHCP"), InterpretName::OPNSenseDns => f.write_str("OPNSenseDns"), + InterpretName::LoadBalancer => f.write_str("LoadBalancer"), } } } @@ -39,8 +41,8 @@ pub trait Interpret { #[derive(Debug, new)] pub struct Outcome { - status: InterpretStatus, - message: String, + pub status: InterpretStatus, + pub message: String, } impl Outcome { @@ -52,7 +54,7 @@ impl Outcome { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum InterpretStatus { SUCCESS, FAILURE, diff --git a/harmony-rs/harmony/src/domain/topology/load_balancer.rs b/harmony-rs/harmony/src/domain/topology/load_balancer.rs index 9f5226a..7c85529 100644 --- a/harmony-rs/harmony/src/domain/topology/load_balancer.rs +++ b/harmony-rs/harmony/src/domain/topology/load_balancer.rs @@ -1,15 +1,30 @@ -use crate::executors::ExecutorError; -use super::{IpAddress, LogicalHost}; +use std::{net::SocketAddr, str::FromStr}; +use async_trait::async_trait; +use log::debug; + +use super::{IpAddress, LogicalHost}; +use crate::executors::ExecutorError; + +#[async_trait] pub trait LoadBalancer: Send + Sync { - fn add_backend(&mut self, backend: Backend) -> Result<(), ExecutorError>; - fn remove_backend(&mut self, backend_id: &str) -> Result<(), ExecutorError>; - fn add_frontend(&mut self, frontend: Frontend) -> Result<(), ExecutorError>; - fn remove_frontend(&mut self, frontend_id: &str) -> Result<(), ExecutorError>; - fn list_backends(&self) -> Vec; - fn list_frontends(&self) -> Vec; fn get_ip(&self) -> IpAddress; fn get_host(&self) -> LogicalHost; + async fn add_service(&self, service: &LoadBalancerService) -> Result<(), ExecutorError>; + async fn remove_service(&self, service: &LoadBalancerService) -> Result<(), ExecutorError>; + async fn list_services(&self) -> Vec; + async fn ensure_initialized(&self) -> Result<(), ExecutorError>; + async fn commit_config(&self) -> Result<(), ExecutorError>; + async fn ensure_service_exists( + &self, + service: &LoadBalancerService, + ) -> Result<(), ExecutorError> { + debug!("Listing haproxy services {:?}", self.list_services().await); + if !self.list_services().await.contains(service) { + self.add_service(service).await?; + } + Ok(()) + } } impl std::fmt::Debug for dyn LoadBalancer { @@ -17,19 +32,69 @@ impl std::fmt::Debug for dyn LoadBalancer { f.write_fmt(format_args!("LoadBalancer {}", self.get_ip())) } } - -#[derive(Clone, Debug)] -pub struct Backend { - pub id: String, - pub ip: IpAddress, - pub port: u16, - pub weight: u8, +#[derive(Debug, PartialEq)] +pub struct LoadBalancerService { + pub backend_servers: Vec, + pub listening_port: SocketAddr, + pub health_check: Option, } -#[derive(Clone, Debug)] -pub struct Frontend { - pub id: String, - pub ip: IpAddress, +#[derive(Debug, PartialEq)] +pub struct BackendServer { + pub address: String, pub port: u16, - pub backend_ids: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum HttpMethod { + GET, + POST, + PUT, + PATCH, + DELETE, +} + +impl From for HttpMethod { + fn from(value: String) -> Self { + Self::from_str(&value).unwrap() + } +} +impl FromStr for HttpMethod { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.to_uppercase().as_str() { + "GET" => Ok(HttpMethod::GET), + "POST" => Ok(HttpMethod::POST), + "PUT" => Ok(HttpMethod::PUT), + "PATCH" => Ok(HttpMethod::PATCH), + "DELETE" => Ok(HttpMethod::DELETE), + _ => Err(format!("Invalid HTTP method: {}", s)), + } + } +} + +impl std::fmt::Display for HttpMethod { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + HttpMethod::GET => write!(f, "GET"), + HttpMethod::POST => write!(f, "POST"), + HttpMethod::PUT => write!(f, "PUT"), + HttpMethod::PATCH => write!(f, "PATCH"), + HttpMethod::DELETE => write!(f, "DELETE"), + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum HttpStatusCode { + Success2xx, + UserError4xx, + ServerError5xx, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum HealthCheck { + HTTP(String, HttpMethod, HttpStatusCode), + TCP(Option), } diff --git a/harmony-rs/harmony/src/domain/topology/network.rs b/harmony-rs/harmony/src/domain/topology/network.rs index affdcd6..9c78fb0 100644 --- a/harmony-rs/harmony/src/domain/topology/network.rs +++ b/harmony-rs/harmony/src/domain/topology/network.rs @@ -46,6 +46,7 @@ pub trait DhcpServer: Send + Sync { async fn remove_static_mapping(&self, mac: &MacAddress) -> Result<(), ExecutorError>; async fn list_static_mappings(&self) -> Vec<(MacAddress, IpAddress)>; async fn set_next_server(&self, ip: IpAddress) -> Result<(), ExecutorError>; + async fn set_boot_filename(&self, boot_filename: &str) -> Result<(), ExecutorError>; fn get_ip(&self) -> IpAddress; fn get_host(&self) -> LogicalHost; async fn commit_config(&self) -> Result<(), ExecutorError>; diff --git a/harmony-rs/harmony/src/infra/opnsense/haproxy.rs b/harmony-rs/harmony/src/infra/opnsense/haproxy.rs new file mode 100644 index 0000000..8562d80 --- /dev/null +++ b/harmony-rs/harmony/src/infra/opnsense/haproxy.rs @@ -0,0 +1,372 @@ +use log::{debug, info, warn}; +use opnsense_config::Config; +use opnsense_config_xml::{Frontend, HAProxy, HAProxyBackend, HAProxyHealthCheck, HAProxyServer}; +use uuid::Uuid; + +use crate::topology::{ + BackendServer, HealthCheck, HttpMethod, HttpStatusCode, LoadBalancerService, +}; + +pub(crate) fn haproxy_xml_config_to_harmony_loadbalancer( + haproxy: &Option, +) -> Vec { + let haproxy = match haproxy { + Some(haproxy) => haproxy, + None => return vec![], + }; + + haproxy + .frontends + .frontend + .iter() + .map(|frontend| { + let mut backend_servers = vec![]; + let matching_backend = haproxy + .backends + .backends + .iter() + .find(|b| b.uuid == frontend.default_backend); + + let mut health_check = None; + match matching_backend { + Some(backend) => { + backend_servers.append(&mut get_servers_for_backend(backend, haproxy)); + health_check = get_health_check_for_backend(backend, haproxy); + } + None => { + warn!( + "HAProxy config could not find a matching backend for frontend {:?}", + frontend + ); + } + } + + LoadBalancerService { + backend_servers, + listening_port: frontend.bind.parse().expect(&format!( + "HAProxy frontend address should be a valid SocketAddr, got {}", + frontend.bind + )), + health_check, + } + }) + .collect() +} + +pub(crate) fn get_servers_for_backend( + backend: &HAProxyBackend, + haproxy: &HAProxy, +) -> Vec { + let backend_servers: Vec<&str> = match &backend.linked_servers.content { + Some(linked_servers) => linked_servers.split(',').collect(), + None => { + info!("No server defined for HAProxy backend {:?}", backend); + return vec![]; + } + }; + haproxy + .servers + .servers + .iter() + .filter_map(|server| { + if backend_servers.contains(&server.uuid.as_str()) { + return Some(BackendServer { + address: server.address.clone(), + port: server.port, + }); + } + None + }) + .collect() +} + +pub(crate) fn get_health_check_for_backend( + backend: &HAProxyBackend, + haproxy: &HAProxy, +) -> Option { + let health_check_uuid = match &backend.health_check.content { + Some(uuid) => uuid, + None => return None, + }; + + let haproxy_health_check = match haproxy + .healthchecks + .healthchecks + .iter() + .find(|h| &h.uuid == health_check_uuid) + { + Some(health_check) => health_check, + None => return None, + }; + + let binding = haproxy_health_check.health_check_type.to_uppercase(); + let uppercase = binding.as_str(); + match uppercase { + "TCP" => { + if let Some(checkport) = haproxy_health_check.checkport.content.as_ref() { + if checkport.len() > 0 { + return Some(HealthCheck::TCP(Some(checkport.parse().expect(&format!( + "HAProxy check port should be a valid port number, got {checkport}" + ))))); + } + } + return Some(HealthCheck::TCP(None)); + } + "HTTP" => { + let path: String = haproxy_health_check + .http_uri + .content + .clone() + .unwrap_or_default(); + let method: HttpMethod = haproxy_health_check + .http_method + .content + .clone() + .unwrap_or_default() + .into(); + let status_code: HttpStatusCode = HttpStatusCode::Success2xx; + Some(HealthCheck::HTTP(path, method, status_code)) + } + _ => panic!("Received unsupported health check type {}", uppercase), + } +} + +pub(crate) fn harmony_load_balancer_service_to_haproxy_xml( + service: &LoadBalancerService, +) -> ( + Frontend, + HAProxyBackend, + Vec, + Option, +) { + // Here we have to build : + // One frontend + // One backend + // One Option + // Vec of servers + // + // Then merge then with haproxy config individually + // + // We also have to take into account that it is entirely possible that a backe uses a server + // with the same definition as in another backend. So when creating a new backend, we must not + // blindly create new servers because the backend does not exist yet. Even if it is a new + // backend, it may very well reuse existing servers + // + // Also we need to support router integration for port forwarding on WAN as a strategy to + // handle dyndns + // server is standalone + // backend points on server + // backend points to health check + // frontend points to backend + let healthcheck = if let Some(health_check) = &service.health_check { + match health_check { + HealthCheck::HTTP(path, http_method, http_status_code) => { + let haproxy_check = HAProxyHealthCheck { + name: format!("HTTP_{http_method}_{path}"), + uuid: Uuid::new_v4().to_string(), + http_method: http_method.to_string().into(), + health_check_type: "http".to_string(), + http_uri: path.clone().into(), + interval: "2s".to_string(), + ..Default::default() + }; + + Some(haproxy_check) + } + HealthCheck::TCP(port) => { + let (port, port_name) = match port { + Some(port) => (Some(port.to_string()), port.to_string()), + None => (None, "serverport".to_string()), + }; + + let haproxy_check = HAProxyHealthCheck { + name: format!("TCP_{port_name}"), + uuid: Uuid::new_v4().to_string(), + health_check_type: "tcp".to_string(), + checkport: port.into(), + interval: "2s".to_string(), + ..Default::default() + }; + + Some(haproxy_check) + } + } + } else { + None + }; + debug!("Built healthcheck {healthcheck:?}"); + + let servers: Vec = service + .backend_servers + .iter() + .map(server_to_haproxy_server) + .collect(); + debug!("Built servers {servers:?}"); + + let mut backend = HAProxyBackend { + uuid: Uuid::new_v4().to_string(), + enabled: 1, + name: format!("backend_{}", service.listening_port), + algorithm: "roundrobin".to_string(), + random_draws: Some(2), + stickiness_expire: "30m".to_string(), + stickiness_size: "50k".to_string(), + stickiness_conn_rate_period: "10s".to_string(), + stickiness_sess_rate_period: "10s".to_string(), + stickiness_http_req_rate_period: "10s".to_string(), + stickiness_http_err_rate_period: "10s".to_string(), + stickiness_bytes_in_rate_period: "1m".to_string(), + stickiness_bytes_out_rate_period: "1m".to_string(), + mode: "tcp".to_string(), // TODO do not depend on health check here + ..Default::default() + }; + info!("HAPRoxy backend algorithm is currently hardcoded to roundrobin"); + + if let Some(hcheck) = &healthcheck { + backend.health_check_enabled = 1; + backend.health_check = hcheck.uuid.clone().into(); + } + + backend.linked_servers = servers + .iter() + .map(|s| s.uuid.as_str()) + .collect::>() + .join(",") + .into(); + debug!("Built backend {backend:?}"); + + let frontend = Frontend { + uuid: uuid::Uuid::new_v4().to_string(), + enabled: 1, + name: format!("frontend_{}", service.listening_port), + bind: service.listening_port.to_string(), + mode: "tcp".to_string(), // TODO do not depend on health check here + default_backend: backend.uuid.clone(), + ..Default::default() + }; + info!("HAPRoxy frontend and backend mode currently hardcoded to tcp"); + + debug!("Built frontend {frontend:?}"); + (frontend, backend, servers, healthcheck) +} + +fn server_to_haproxy_server(server: &BackendServer) -> HAProxyServer { + HAProxyServer { + uuid: Uuid::new_v4().to_string(), + name: format!("{}_{}", &server.address, &server.port), + enabled: 1, + address: server.address.clone(), + port: server.port, + mode: "active".to_string(), + server_type: "static".to_string(), + ..Default::default() + } +} + +#[cfg(test)] +mod tests { + use opnsense_config_xml::HAProxyServer; + + use super::*; + + #[test] + fn test_get_servers_for_backend_with_linked_servers() { + // Create a backend with linked servers + let mut backend = HAProxyBackend::default(); + backend.linked_servers.content = Some("server1,server2".to_string()); + + // Create an HAProxy instance with servers + let mut haproxy = HAProxy::default(); + let mut server = HAProxyServer::default(); + server.uuid = "server1".to_string(); + server.address = "192.168.1.1".to_string(); + server.port = 80; + + haproxy.servers.servers.push(server); + let mut server = HAProxyServer::default(); + server.uuid = "server3".to_string(); + server.address = "192.168.1.3".to_string(); + server.port = 8080; + + // Call the function + let result = get_servers_for_backend(&backend, &haproxy); + + // Check the result + assert_eq!( + result, + vec![BackendServer { + address: "192.168.1.1".to_string(), + port: 80, + },] + ); + } + #[test] + fn test_get_servers_for_backend_no_linked_servers() { + // Create a backend with no linked servers + let backend = HAProxyBackend::default(); + // Create an HAProxy instance with servers + let mut haproxy = HAProxy::default(); + let mut server = HAProxyServer::default(); + server.uuid = "server1".to_string(); + server.address = "192.168.1.1".to_string(); + server.port = 80; + haproxy.servers.servers.push(server); + // Call the function + let result = get_servers_for_backend(&backend, &haproxy); + // Check the result + assert_eq!(result, vec![]); + } + + #[test] + fn test_get_servers_for_backend_no_matching_servers() { + // Create a backend with linked servers that do not match any in HAProxy + let mut backend = HAProxyBackend::default(); + backend.linked_servers.content = Some("server4,server5".to_string()); + // Create an HAProxy instance with servers + let mut haproxy = HAProxy::default(); + let mut server = HAProxyServer::default(); + server.uuid = "server1".to_string(); + server.address = "192.168.1.1".to_string(); + server.port = 80; + haproxy.servers.servers.push(server); + // Call the function + let result = get_servers_for_backend(&backend, &haproxy); + // Check the result + assert_eq!(result, vec![]); + } + + #[test] + fn test_get_servers_for_backend_multiple_linked_servers() { + // Create a backend with multiple linked servers + let mut backend = HAProxyBackend::default(); + backend.linked_servers.content = Some("server1,server2".to_string()); + // Create an HAProxy instance with matching servers + let mut haproxy = HAProxy::default(); + let mut server = HAProxyServer::default(); + server.uuid = "server1".to_string(); + server.address = "some-hostname.test.mcd".to_string(); + server.port = 80; + haproxy.servers.servers.push(server); + let mut server = HAProxyServer::default(); + server.uuid = "server2".to_string(); + server.address = "192.168.1.2".to_string(); + server.port = 8080; + haproxy.servers.servers.push(server); + // Call the function + let result = get_servers_for_backend(&backend, &haproxy); + // Check the result + assert_eq!( + result, + vec![ + BackendServer { + address: "some-hostname.test.mcd".to_string(), + port: 80, + }, + BackendServer { + address: "192.168.1.2".to_string(), + port: 8080, + }, + ] + ); + } +} diff --git a/harmony-rs/harmony/src/infra/opnsense/mod.rs b/harmony-rs/harmony/src/infra/opnsense/mod.rs index e69801c..7c8c514 100644 --- a/harmony-rs/harmony/src/infra/opnsense/mod.rs +++ b/harmony-rs/harmony/src/infra/opnsense/mod.rs @@ -1,7 +1,11 @@ +mod haproxy; mod management; use std::sync::Arc; use async_trait::async_trait; +use haproxy::{ + haproxy_xml_config_to_harmony_loadbalancer, harmony_load_balancer_service_to_haproxy_xml, +}; use log::debug; pub use management::*; use opnsense_config_xml::Host; @@ -10,8 +14,8 @@ use tokio::sync::RwLock; use crate::{ executors::ExecutorError, topology::{ - Backend, DHCPStaticEntry, DhcpServer, DnsRecord, DnsServer, Firewall, FirewallRule, - Frontend, IpAddress, LoadBalancer, LogicalHost, + DHCPStaticEntry, DhcpServer, DnsRecord, DnsServer, Firewall, FirewallRule, IpAddress, + LoadBalancer, LoadBalancerService, LogicalHost, }, }; @@ -29,18 +33,28 @@ impl OPNSenseFirewall { pub async fn new( host: LogicalHost, + port: Option, cluster_nic_name: &str, username: &str, password: &str, ) -> Self { Self { opnsense_config: Arc::new(RwLock::new( - opnsense_config::Config::from_credentials(host.ip, username, password).await, + opnsense_config::Config::from_credentials(host.ip, port, username, password).await, )), host, cluster_nic_name: cluster_nic_name.into(), } } + + async fn commit_config(&self) -> Result<(), ExecutorError> { + self.opnsense_config + .read() + .await + .apply() + .await + .map_err(|e| ExecutorError::UnexpectedError(e.to_string())) + } } impl Firewall for OPNSenseFirewall { @@ -64,48 +78,68 @@ impl Firewall for OPNSenseFirewall { } } +#[async_trait] impl LoadBalancer for OPNSenseFirewall { - fn add_backend(&mut self, _backend: Backend) -> Result<(), ExecutorError> { - todo!() - } - - fn remove_backend(&mut self, _backend_id: &str) -> Result<(), ExecutorError> { - todo!() - } - - fn add_frontend(&mut self, _frontend: Frontend) -> Result<(), ExecutorError> { - todo!() - } - - fn remove_frontend(&mut self, _frontend_id: &str) -> Result<(), ExecutorError> { - todo!() - } - - fn list_backends(&self) -> Vec { - todo!() - } - - fn list_frontends(&self) -> Vec { - todo!() - } - fn get_ip(&self) -> IpAddress { OPNSenseFirewall::get_ip(self) } fn get_host(&self) -> LogicalHost { self.host.clone() } + + async fn add_service(&self, service: &LoadBalancerService) -> Result<(), ExecutorError> { + let mut config = self.opnsense_config.write().await; + let (frontend, backend, servers, healthcheck) = + harmony_load_balancer_service_to_haproxy_xml(service); + let mut load_balancer = config.load_balancer(); + load_balancer.add_backend(backend); + load_balancer.add_frontend(frontend); + load_balancer.add_servers(servers); + if let Some(healthcheck) = healthcheck { + load_balancer.add_healthcheck(healthcheck); + } + + Ok(()) + } + + async fn remove_service(&self, service: &LoadBalancerService) -> Result<(), ExecutorError> { + todo!() + } + + async fn commit_config(&self) -> Result<(), ExecutorError> { + OPNSenseFirewall::commit_config(self).await?; + todo!("Make sure load balancer is reloaded properly") + } + + async fn ensure_initialized(&self) -> Result<(), ExecutorError> { + let mut config = self.opnsense_config.write().await; + let load_balancer = config.load_balancer(); + if let Some(_) = load_balancer.get_full_config() { + debug!("HAProxy config available in opnsense config, assuming it is already installed"); + return Ok(()); + } + + config.install_package("os-haproxy").await.map_err(|e| { + ExecutorError::UnexpectedError(format!( + "Executor failed when trying to install os-haproxy package with error {e:?}" + )) + })?; + config.load_balancer().enable(true); + Ok(()) + } + + async fn list_services(&self) -> Vec { + let mut config = self.opnsense_config.write().await; + let load_balancer = config.load_balancer(); + let haproxy_xml_config = load_balancer.get_full_config(); + haproxy_xml_config_to_harmony_loadbalancer(haproxy_xml_config) + } } #[async_trait] impl DhcpServer for OPNSenseFirewall { async fn commit_config(&self) -> Result<(), ExecutorError> { - self.opnsense_config - .read() - .await - .apply() - .await - .map_err(|e| ExecutorError::UnexpectedError(e.to_string())) + OPNSenseFirewall::commit_config(self).await } async fn add_static_mapping(&self, entry: &DHCPStaticEntry) -> Result<(), ExecutorError> { @@ -155,6 +189,16 @@ impl DhcpServer for OPNSenseFirewall { Ok(()) } + + async fn set_boot_filename(&self, boot_filename: &str) -> Result<(), ExecutorError> { + { + let mut writable_opnsense = self.opnsense_config.write().await; + writable_opnsense.dhcp().set_boot_filename(boot_filename); + debug!("OPNsense dhcp server set boot filename {boot_filename}"); + } + + Ok(()) + } } #[async_trait] diff --git a/harmony-rs/harmony/src/modules/dhcp.rs b/harmony-rs/harmony/src/modules/dhcp.rs index 09dd87a..18798b4 100644 --- a/harmony-rs/harmony/src/modules/dhcp.rs +++ b/harmony-rs/harmony/src/modules/dhcp.rs @@ -20,6 +20,7 @@ use crate::domain::score::Score; pub struct DhcpScore { host_binding: Vec, next_server: Option, + boot_filename: Option, } impl Score for DhcpScore { @@ -98,22 +99,48 @@ impl DhcpInterpret { )) } - async fn set_next_server( + async fn set_pxe_options( &self, _inventory: &Inventory, topology: &HAClusterTopology, ) -> Result { - match self.score.next_server { + let next_server_outcome = match self.score.next_server { Some(next_server) => { let dhcp_server = Arc::new(topology.dhcp_server.clone()); dhcp_server.set_next_server(next_server).await?; - Ok(Outcome::new( + Outcome::new( InterpretStatus::SUCCESS, format!("Dhcp Interpret Set next boot to {next_server}"), - )) + ) } - None => Ok(Outcome::noop()), + None => Outcome::noop(), + }; + + let boot_filename_outcome = match &self.score.boot_filename { + Some(boot_filename) => { + let dhcp_server = Arc::new(topology.dhcp_server.clone()); + dhcp_server.set_boot_filename(&boot_filename).await?; + Outcome::new( + InterpretStatus::SUCCESS, + format!("Dhcp Interpret Set boot filename to {boot_filename}"), + ) + } + None => Outcome::noop(), + }; + + if next_server_outcome.status == InterpretStatus::NOOP + && boot_filename_outcome.status == InterpretStatus::NOOP + { + return Ok(Outcome::noop()); } + + Ok(Outcome::new( + InterpretStatus::SUCCESS, + format!( + "Dhcp Interpret Set next boot to {:?} and boot_filename to {:?}", + self.score.boot_filename, self.score.boot_filename + ), + )) } } @@ -142,9 +169,9 @@ impl Interpret for DhcpInterpret { ) -> Result { info!("Executing {} on inventory {inventory:?}", self.get_name()); - self.set_next_server(inventory, topology).await?; + self.set_pxe_options(inventory, topology).await?; - // self.add_static_entries(inventory, topology).await?; + self.add_static_entries(inventory, topology).await?; topology.dhcp_server.commit_config().await?; diff --git a/harmony-rs/harmony/src/modules/load_balancer.rs b/harmony-rs/harmony/src/modules/load_balancer.rs new file mode 100644 index 0000000..7e0f19d --- /dev/null +++ b/harmony-rs/harmony/src/modules/load_balancer.rs @@ -0,0 +1,82 @@ +use async_trait::async_trait; +use log::info; + +use crate::{ + data::{Id, Version}, + interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, + inventory::Inventory, + score::Score, + topology::{HAClusterTopology, LoadBalancerService}, +}; + +#[derive(Debug)] +pub struct LoadBalancerScore { + pub public_services: Vec, + pub private_services: Vec, + // TODO public and private services are likely wrong, should be a single list of + // (listen_interface, LoadBalancerService) tuples or something like that + // I am not sure what to use as listen_interface, should it be interface name, ip address, + // uuid? +} + +impl Score for LoadBalancerScore { + type InterpretType = LoadBalancerInterpret; + + fn create_interpret(self) -> Self::InterpretType { + LoadBalancerInterpret::new(self) + } +} + +#[derive(Debug)] +pub struct LoadBalancerInterpret { + version: Version, + status: InterpretStatus, + score: LoadBalancerScore, +} + +impl LoadBalancerInterpret { + pub fn new(score: LoadBalancerScore) -> Self { + Self { + version: Version::from("1.0.0").expect("Version should be valid"), + status: InterpretStatus::QUEUED, + score, + } + } +} + +#[async_trait] +impl Interpret for LoadBalancerInterpret { + async fn execute( + &self, + inventory: &Inventory, + topology: &HAClusterTopology, + ) -> Result { + topology.load_balancer.ensure_initialized().await?; + for service in self.score.public_services.iter() { + info!("Ensuring service exists {service:?}"); + topology.load_balancer.ensure_service_exists(service).await?; + } + + for service in self.score.private_services.iter() { + info!("Ensuring private service exists {service:?}"); + topology.load_balancer.ensure_service_exists(service).await?; + } + + info!("Applying load balancer configuration"); + topology.load_balancer.commit_config().await?; + todo!() + } + fn get_name(&self) -> InterpretName { + InterpretName::LoadBalancer + } + fn get_version(&self) -> Version { + self.version.clone() + } + fn get_status(&self) -> InterpretStatus { + self.status.clone() + } + + fn get_children(&self) -> Vec { + todo!() + } +} diff --git a/harmony-rs/harmony/src/modules/mod.rs b/harmony-rs/harmony/src/modules/mod.rs index e7a05a4..ccaa136 100644 --- a/harmony-rs/harmony/src/modules/mod.rs +++ b/harmony-rs/harmony/src/modules/mod.rs @@ -1,3 +1,4 @@ pub mod dhcp; pub mod dns; pub mod okd; +pub mod load_balancer; diff --git a/harmony-rs/harmony/src/modules/okd/dhcp.rs b/harmony-rs/harmony/src/modules/okd/dhcp.rs index b22efff..b09e9a3 100644 --- a/harmony-rs/harmony/src/modules/okd/dhcp.rs +++ b/harmony-rs/harmony/src/modules/okd/dhcp.rs @@ -31,6 +31,7 @@ impl OKDBootstrapDhcpScore { // 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()), + Some("bootx64.efi".to_string()), ), } } diff --git a/harmony-rs/harmony/src/modules/okd/load_balancer.rs b/harmony-rs/harmony/src/modules/okd/load_balancer.rs new file mode 100644 index 0000000..1f2927e --- /dev/null +++ b/harmony-rs/harmony/src/modules/okd/load_balancer.rs @@ -0,0 +1,88 @@ +use std::net::SocketAddr; + +use crate::{ + modules::load_balancer::LoadBalancerScore, + score::Score, + topology::{ + BackendServer, HAClusterTopology, HealthCheck, HttpMethod, HttpStatusCode, + LoadBalancerService, + }, +}; + +#[derive(Debug)] +pub struct OKDLoadBalancerScore { + load_balancer_score: LoadBalancerScore, +} + +impl OKDLoadBalancerScore { + pub fn new(topology: &HAClusterTopology) -> Self { + let public_ip = topology.router.get_gateway(); + let public_services = vec![ + LoadBalancerService { + backend_servers: Self::control_plane_to_backend_server(topology, 80), + listening_port: SocketAddr::new(public_ip, 80), + health_check: Some(HealthCheck::TCP(None)), + }, + LoadBalancerService { + backend_servers: Self::control_plane_to_backend_server(topology, 443), + listening_port: SocketAddr::new(public_ip, 443), + health_check: Some(HealthCheck::TCP(None)), + }, + ]; + + let private_services = vec![ + LoadBalancerService { + backend_servers: Self::control_plane_to_backend_server(topology, 80), + listening_port: SocketAddr::new(public_ip, 80), + health_check: Some(HealthCheck::TCP(None)), + }, + LoadBalancerService { + backend_servers: Self::control_plane_to_backend_server(topology, 443), + listening_port: SocketAddr::new(public_ip, 443), + health_check: Some(HealthCheck::TCP(None)), + }, + LoadBalancerService { + backend_servers: Self::control_plane_to_backend_server(topology, 22623), + listening_port: SocketAddr::new(public_ip, 22623), + health_check: Some(HealthCheck::TCP(None)), + }, + LoadBalancerService { + backend_servers: Self::control_plane_to_backend_server(topology, 6443), + listening_port: SocketAddr::new(public_ip, 6443), + health_check: Some(HealthCheck::HTTP( + "/readyz".to_string(), + HttpMethod::GET, + HttpStatusCode::Success2xx, + )), + }, + ]; + Self { + load_balancer_score: LoadBalancerScore { + public_services, + private_services, + }, + } + } + + fn control_plane_to_backend_server( + topology: &HAClusterTopology, + port: u16, + ) -> Vec { + topology + .control_plane + .iter() + .map(|cp| BackendServer { + address: cp.ip.to_string(), + port, + }) + .collect() + } +} + +impl Score for OKDLoadBalancerScore { + type InterpretType = ::InterpretType; + + fn create_interpret(self) -> Self::InterpretType { + self.load_balancer_score.create_interpret() + } +} diff --git a/harmony-rs/harmony/src/modules/okd/mod.rs b/harmony-rs/harmony/src/modules/okd/mod.rs index 62802ea..eef476a 100644 --- a/harmony-rs/harmony/src/modules/okd/mod.rs +++ b/harmony-rs/harmony/src/modules/okd/mod.rs @@ -1,3 +1,4 @@ pub mod dhcp; pub mod dns; +pub mod load_balancer; diff --git a/harmony-rs/harmony_macros/src/lib.rs b/harmony-rs/harmony_macros/src/lib.rs index 6a86640..0d01e7e 100644 --- a/harmony-rs/harmony_macros/src/lib.rs +++ b/harmony-rs/harmony_macros/src/lib.rs @@ -18,7 +18,7 @@ pub fn ip(input: TokenStream) -> TokenStream { if let Ok(_) = ip_str.parse::() { let expanded = - quote! { std::net::IpAddr::V4(#ip_str.parse::().unwrap()) }; + quote! { std::net::IpAddr::V6(#ip_str.parse::().unwrap()) }; return TokenStream::from(expanded); } diff --git a/harmony-rs/opnsense-config-xml/Cargo.toml b/harmony-rs/opnsense-config-xml/Cargo.toml index eef282c..ac57fe4 100644 --- a/harmony-rs/opnsense-config-xml/Cargo.toml +++ b/harmony-rs/opnsense-config-xml/Cargo.toml @@ -17,14 +17,8 @@ xml-rs = "0.8" thiserror = "1.0" async-trait = { workspace = true } tokio = { workspace = true } - -[dependencies.uuid] -version = "1.11.0" -features = [ - "v4", # Lets you generate random UUIDs - "fast-rng", # Use a faster (but still sufficiently random) RNG - "macro-diagnostics", # Enable better diagnostics for compile-time UUIDs -] +uuid = { workspace = true } +rand = { workspace = true } [dev-dependencies] pretty_assertions = "1.4.1" diff --git a/harmony-rs/opnsense-config-xml/src/data/dhcpd.rs b/harmony-rs/opnsense-config-xml/src/data/dhcpd.rs index 212758a..c215263 100644 --- a/harmony-rs/opnsense-config-xml/src/data/dhcpd.rs +++ b/harmony-rs/opnsense-config-xml/src/data/dhcpd.rs @@ -18,6 +18,7 @@ pub struct DhcpInterface { pub domain: Option, pub netboot: Option, pub nextserver: Option, + pub filename64: Option, #[yaserde(rename = "ddnsdomainalgorithm")] pub ddns_domain_algorithm: Option, #[yaserde(rename = "numberoptions")] diff --git a/harmony-rs/opnsense-config-xml/src/data/haproxy.rs b/harmony-rs/opnsense-config-xml/src/data/haproxy.rs new file mode 100644 index 0000000..5c1eeab --- /dev/null +++ b/harmony-rs/opnsense-config-xml/src/data/haproxy.rs @@ -0,0 +1,635 @@ +use rand; +use rand::Rng; +use xml::reader::XmlEvent as ReadEvent; +use xml::writer::XmlEvent as WriteEvent; +use yaserde::MaybeString; +use yaserde::{YaDeserialize as YaDeserializeTrait, YaSerialize as YaSerializeTrait}; +use yaserde_derive::{YaDeserialize, YaSerialize}; + +impl YaDeserializeTrait for HAProxyId { + fn deserialize(reader: &mut yaserde::de::Deserializer) -> Result { + let field_name = match reader.peek()? { + ReadEvent::StartElement { + name, attributes, .. + } => { + if attributes.len() > 0 { + return Err(String::from( + "Attributes not currently supported by HAProxyId", + )); + } + + name.local_name.clone() + } + _ => return Err(String::from("Unsupporte ReadEvent type")), + }; + reader.next_event()?; + + let content = match reader.peek()? { + ReadEvent::Characters(content) => content.clone(), + ReadEvent::EndElement { name } => String::new(), + _ => return Err(String::from("Unsupporte ReadEvent type")), + }; + + Ok(Self(content)) + } +} + +impl YaSerializeTrait for HAProxyId { + fn serialize( + &self, + writer: &mut yaserde::ser::Serializer, + ) -> Result<(), String> { + let yaserde_label = writer.get_start_event_name(); + + match yaserde_label { + Some(label) => { + let struct_start_event = xml::writer::XmlEvent::start_element(label.as_ref()); + writer + .write(struct_start_event) + .map_err(|_e| format!("Start element {label:?} write failed"))?; + } + None => return Err("HAPRoxyId must have a label preset in the writer".to_string()), + }; + + writer + .write(WriteEvent::characters(&self.0)) + .expect("Writer failed"); + + writer + .write(WriteEvent::end_element()) + .expect("Writer failed"); + Ok(()) + } + + fn serialize_attributes( + &self, + attributes: Vec, + namespace: xml::namespace::Namespace, + ) -> Result< + ( + Vec, + xml::namespace::Namespace, + ), + String, + > { + todo!() + } +} + +#[derive(PartialEq, Debug)] +pub struct HAProxyId(String); + +impl Default for HAProxyId { + fn default() -> Self { + let mut rng = rand::thread_rng(); + Self(format!("{:x}.{:x}", rng.gen::(), rng.gen::())) + } +} + +#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] +#[yaserde(rename = "HAProxy")] +pub struct HAProxy { + #[yaserde(attribute)] + pub version: String, + pub general: HaProxyGeneral, + pub frontends: HAProxyFrontends, + pub backends: HAProxyBackends, + pub servers: HAProxyServers, + pub healthchecks: HAProxyHealthChecks, + pub acls: MaybeString, + pub actions: MaybeString, + pub luas: MaybeString, + pub fcgis: MaybeString, + pub errorfiles: MaybeString, + pub mapfiles: MaybeString, + pub groups: MaybeString, + pub users: MaybeString, + pub cpus: MaybeString, + pub resolvers: MaybeString, + pub mailers: MaybeString, + pub maintenance: Maintenance, +} + +#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] +pub struct Maintenance { + #[yaserde(rename = "cronjobs")] + pub cronjobs: CronJobs, +} + +#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] +pub struct CronJobs { + #[yaserde(rename = "syncCerts")] + pub sync_certs: u32, + #[yaserde(rename = "syncCertsCron")] + pub sync_certs_cron: MaybeString, + #[yaserde(rename = "updateOcsp")] + pub update_ocsp: u32, + #[yaserde(rename = "updateOcspCron")] + pub update_ocsp_cron: MaybeString, + #[yaserde(rename = "reloadService")] + pub reload_service: u32, + #[yaserde(rename = "reloadServiceCron")] + pub reload_service_cron: MaybeString, + #[yaserde(rename = "restartService")] + pub restart_service: u32, + #[yaserde(rename = "restartServiceCron")] + pub restart_service_cron: MaybeString, +} + +#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] +pub struct HaProxyGeneral { + pub enabled: i32, + #[yaserde(rename = "gracefulStop")] + pub graceful_stop: i32, + #[yaserde(rename = "hardStopAfter")] + pub hard_stop_after: String, + #[yaserde(rename = "closeSpreadTime")] + pub close_spread_time: MaybeString, + #[yaserde(rename = "seamlessReload")] + pub seamless_reload: i32, + #[yaserde(rename = "storeOcsp")] + pub store_ocsp: i32, + #[yaserde(rename = "showIntro")] + pub show_intro: i32, + pub peers: Peers, + pub tuning: Tuning, + pub defaults: HaProxyDefaults, + pub logging: HaProxyLogging, + pub stats: Stats, + pub cache: HaProxyCache, +} + +#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] +pub struct Peers { + pub enabled: i32, + pub name1: MaybeString, + pub listen1: MaybeString, + pub port1: i32, + pub name2: MaybeString, + pub listen2: MaybeString, + pub port2: i32, +} + +#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] +pub struct Tuning { + pub root: i32, + #[yaserde(rename = "maxConnections")] + pub max_connections: MaybeString, + pub nbthread: i32, + #[yaserde(rename = "resolversPrefer")] + pub resolvers_prefer: String, + #[yaserde(rename = "sslServerVerify")] + pub ssl_server_verify: String, + #[yaserde(rename = "maxDHSize")] + pub max_dh_size: i32, + #[yaserde(rename = "bufferSize")] + pub buffer_size: i32, + #[yaserde(rename = "spreadChecks")] + pub spread_checks: i32, + #[yaserde(rename = "bogusProxyEnabled")] + pub bogus_proxy_enabled: i32, + #[yaserde(rename = "luaMaxMem")] + pub lua_max_mem: i32, + #[yaserde(rename = "customOptions")] + pub custom_options: MaybeString, + #[yaserde(rename = "ocspUpdateEnabled")] + pub ocs_update_enabled: MaybeString, + #[yaserde(rename = "ocspUpdateMinDelay")] + pub ocs_update_min_delay: MaybeString, + #[yaserde(rename = "ocspUpdateMaxDelay")] + pub ocs_update_max_delay: MaybeString, + #[yaserde(rename = "ssl_defaultsEnabled")] + pub ssl_defaults_enabled: i32, + #[yaserde(rename = "ssl_bindOptions")] + pub ssl_bind_options: MaybeString, + #[yaserde(rename = "ssl_minVersion")] + pub ssl_min_version: MaybeString, + #[yaserde(rename = "ssl_maxVersion")] + pub ssl_max_version: MaybeString, + #[yaserde(rename = "ssl_cipherList")] + pub ssl_cipher_list: MaybeString, + #[yaserde(rename = "ssl_cipherSuites")] + pub ssl_cipher_suites: MaybeString, + #[yaserde(rename = "h2_initialWindowSize")] + pub h2_initial_window_size: Option, + #[yaserde(rename = "h2_initialWindowSizeOutgoing")] + pub h2_initial_window_size_outgoing: Option, + #[yaserde(rename = "h2_initialWindowSizeIncoming")] + pub h2_initial_window_size_incoming: Option, + #[yaserde(rename = "h2_maxConcurrentStreams")] + pub h2_max_concurrent_streams: Option, + #[yaserde(rename = "h2_maxConcurrentStreamsOutgoing")] + pub h2_max_concurrent_streams_outgoing: Option, + #[yaserde(rename = "h2_maxConcurrentStreamsIncoming")] + pub h2_max_concurrent_streams_incoming: Option, +} + +#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] +pub struct HaProxyDefaults { + #[yaserde(rename = "maxConnections")] + pub max_connections: MaybeString, + #[yaserde(rename = "maxConnectionsServers")] + pub max_connections_servers: MaybeString, + #[yaserde(rename = "timeoutClient")] + pub timeout_client: String, + #[yaserde(rename = "timeoutConnect")] + pub timeout_connect: String, + #[yaserde(rename = "timeoutCheck")] + pub timeout_check: MaybeString, + #[yaserde(rename = "timeoutServer")] + pub timeout_server: String, + pub retries: i32, + pub redispatch: String, + pub init_addr: String, + #[yaserde(rename = "customOptions")] + pub custom_options: MaybeString, +} + +#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] +pub struct HaProxyLogging { + pub host: String, + pub facility: String, + pub level: String, + pub length: MaybeString, +} + +#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] +pub struct Stats { + pub enabled: i32, + pub port: i32, + #[yaserde(rename = "remoteEnabled")] + pub remote_enabled: i32, + #[yaserde(rename = "remoteBind")] + pub remote_bind: MaybeString, + #[yaserde(rename = "authEnabled")] + pub auth_enabled: i32, + #[yaserde(rename = "users")] + pub users: MaybeString, + #[yaserde(rename = "allowedUsers")] + pub allowed_users: MaybeString, + #[yaserde(rename = "allowedGroups")] + pub allowed_groups: MaybeString, + #[yaserde(rename = "customOptions")] + pub custom_options: MaybeString, + pub prometheus_enabled: i32, + pub prometheus_bind: String, + pub prometheus_path: String, +} + +#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] +pub struct HaProxyCache { + pub enabled: i32, + #[yaserde(rename = "totalMaxSize")] + pub total_max_size: i32, + #[yaserde(rename = "maxAge")] + pub max_age: i32, + #[yaserde(rename = "maxObjectSize")] + pub max_object_size: MaybeString, + #[yaserde(rename = "processVary")] + pub process_vary: i32, + #[yaserde(rename = "maxSecondaryEntries")] + pub max_secondary_entries: i32, +} + +#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] +pub struct HAProxyFrontends { + pub frontend: Vec, +} + +#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] +pub struct Frontend { + #[yaserde(attribute)] + pub uuid: String, + pub id: HAProxyId, + pub enabled: i32, + pub name: String, + pub description: MaybeString, + pub bind: String, + #[yaserde(rename = "bindOptions")] + pub bind_options: MaybeString, + pub mode: String, + #[yaserde(rename = "defaultBackend")] + pub default_backend: String, + pub ssl_enabled: i32, + pub ssl_certificates: MaybeString, + pub ssl_default_certificate: MaybeString, + #[yaserde(rename = "ssl_customOptions")] + pub ssl_custom_options: MaybeString, + #[yaserde(rename = "ssl_advancedEnabled")] + pub ssl_advanced_enabled: i32, + #[yaserde(rename = "ssl_bindOptions")] + pub ssl_bind_options: MaybeString, + #[yaserde(rename = "ssl_minVersion")] + pub ssl_min_version: MaybeString, + #[yaserde(rename = "ssl_maxVersion")] + pub ssl_max_version: MaybeString, + #[yaserde(rename = "ssl_cipherList")] + pub ssl_cipher_list: MaybeString, + #[yaserde(rename = "ssl_cipherSuites")] + pub ssl_cipher_suites: MaybeString, + #[yaserde(rename = "ssl_hstsEnabled")] + pub ssl_hsts_enabled: i32, + #[yaserde(rename = "ssl_hstsIncludeSubDomains")] + pub ssl_hsts_include_sub_domains: i32, + #[yaserde(rename = "ssl_hstsPreload")] + pub ssl_hsts_preload: i32, + #[yaserde(rename = "ssl_hstsMaxAge")] + pub ssl_hsts_max_age: i32, + #[yaserde(rename = "ssl_clientAuthEnabled")] + pub ssl_client_auth_enabled: i32, + #[yaserde(rename = "ssl_clientAuthVerify")] + pub ssl_client_auth_verify: MaybeString, + #[yaserde(rename = "ssl_clientAuthCAs")] + pub ssl_client_auth_cas: MaybeString, + #[yaserde(rename = "ssl_clientAuthCRLs")] + pub ssl_client_auth_cr_ls: MaybeString, + #[yaserde(rename = "basicAuthEnabled")] + pub basic_auth_enabled: i32, + #[yaserde(rename = "basicAuthUsers")] + pub basic_auth_users: MaybeString, + #[yaserde(rename = "basicAuthGroups")] + pub basic_auth_groups: MaybeString, + #[yaserde(rename = "tuning_maxConnections")] + pub tuning_max_connections: MaybeString, + #[yaserde(rename = "tuning_timeoutClient")] + pub tuning_timeout_client: MaybeString, + #[yaserde(rename = "tuning_timeoutHttpReq")] + pub tuning_timeout_http_req: MaybeString, + #[yaserde(rename = "tuning_timeoutHttpKeepAlive")] + pub tuning_timeout_http_keep_alive: MaybeString, + #[yaserde(rename = "linkedCpuAffinityRules")] + pub linked_cpu_affinity_rules: MaybeString, + pub tuning_shards: MaybeString, + #[yaserde(rename = "logging_dontLogNull")] + pub logging_dont_log_null: i32, + #[yaserde(rename = "logging_dontLogNormal")] + pub logging_dont_log_normal: i32, + #[yaserde(rename = "logging_logSeparateErrors")] + pub logging_log_separate_errors: i32, + #[yaserde(rename = "logging_detailedLog")] + pub logging_detailed_log: i32, + #[yaserde(rename = "logging_socketStats")] + pub logging_socket_stats: i32, + pub stickiness_pattern: MaybeString, + #[yaserde(rename = "stickiness_dataTypes")] + pub stickiness_data_types: MaybeString, + pub stickiness_expire: MaybeString, + pub stickiness_size: MaybeString, + pub stickiness_counter: i32, + pub stickiness_counter_key: MaybeString, + pub stickiness_length: MaybeString, + #[yaserde(rename = "stickiness_connRatePeriod")] + pub stickiness_conn_rate_period: MaybeString, + #[yaserde(rename = "stickiness_sessRatePeriod")] + pub stickiness_sess_rate_period: MaybeString, + #[yaserde(rename = "stickiness_httpReqRatePeriod")] + pub stickiness_http_req_rate_period: MaybeString, + #[yaserde(rename = "stickiness_httpErrRatePeriod")] + pub stickiness_http_err_rate_period: MaybeString, + #[yaserde(rename = "stickiness_bytesInRatePeriod")] + pub stickiness_bytes_in_rate_period: MaybeString, + #[yaserde(rename = "stickiness_bytesOutRatePeriod")] + pub stickiness_bytes_out_rate_period: MaybeString, + #[yaserde(rename = "http2Enabled")] + pub http2_enabled: i32, + #[yaserde(rename = "http2Enabled_nontls")] + pub http2_enabled_nontls: i32, + pub advertised_protocols: MaybeString, + #[yaserde(rename = "forwardFor")] + pub forward_for: i32, + pub prometheus_enabled: i32, + pub prometheus_path: MaybeString, + #[yaserde(rename = "connectionBehaviour")] + pub connection_behaviour: MaybeString, + #[yaserde(rename = "customOptions")] + pub custom_options: MaybeString, + #[yaserde(rename = "linkedActions")] + pub linked_actions: MaybeString, + #[yaserde(rename = "linkedErrorfiles")] + pub linked_error_files: MaybeString, +} + +#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] +pub struct HAProxyBackends { + #[yaserde(rename = "backend")] + pub backends: Vec, +} + +#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] +pub struct HAProxyBackend { + #[yaserde(attribute, rename = "uuid")] + pub uuid: String, + #[yaserde(rename = "id")] + pub id: HAProxyId, + #[yaserde(rename = "enabled")] + pub enabled: u8, + #[yaserde(rename = "name")] + pub name: String, + #[yaserde(rename = "description")] + pub description: MaybeString, + #[yaserde(rename = "mode")] + pub mode: String, + #[yaserde(rename = "algorithm")] + pub algorithm: String, + #[yaserde(rename = "random_draws")] + pub random_draws: Option, + #[yaserde(rename = "proxyProtocol")] + pub proxy_protocol: MaybeString, + #[yaserde(rename = "linkedServers")] + pub linked_servers: MaybeString, + #[yaserde(rename = "linkedFcgi")] + pub linked_fcgi: MaybeString, + #[yaserde(rename = "linkedResolver")] + pub linked_resolver: MaybeString, + #[yaserde(rename = "resolverOpts")] + pub resolver_opts: MaybeString, + #[yaserde(rename = "resolvePrefer")] + pub resolve_prefer: MaybeString, + #[yaserde(rename = "source")] + pub source: MaybeString, + #[yaserde(rename = "healthCheckEnabled")] + pub health_check_enabled: u8, + #[yaserde(rename = "healthCheck")] + pub health_check: MaybeString, + #[yaserde(rename = "healthCheckLogStatus")] + pub health_check_log_status: u8, + #[yaserde(rename = "checkInterval")] + pub check_interval: MaybeString, + #[yaserde(rename = "checkDownInterval")] + pub check_down_interval: MaybeString, + #[yaserde(rename = "healthCheckFall")] + pub health_check_fall: MaybeString, + #[yaserde(rename = "healthCheckRise")] + pub health_check_rise: MaybeString, + #[yaserde(rename = "linkedMailer")] + pub linked_mailer: MaybeString, + #[yaserde(rename = "http2Enabled")] + pub http2_enabled: u8, + #[yaserde(rename = "http2Enabled_nontls")] + pub http2_enabled_nontls: u8, + #[yaserde(rename = "ba_advertised_protocols")] + pub ba_advertised_protocols: MaybeString, + #[yaserde(rename = "forwardFor")] + pub forward_for: Option, + #[yaserde(rename = "forwardedHeader")] + pub forwarded_header: Option, + #[yaserde(rename = "forwardedHeaderParameters")] + pub forwarded_header_parameters: Option, + pub persistence: MaybeString, + pub persistence_cookiemode: MaybeString, + pub persistence_cookiename: MaybeString, + pub persistence_stripquotes: u8, + pub stickiness_pattern: MaybeString, + #[yaserde(rename = "stickiness_dataTypes")] + pub stickiness_data_types: MaybeString, + pub stickiness_expire: String, + pub stickiness_size: String, + pub stickiness_cookiename: MaybeString, + pub stickiness_cookielength: MaybeString, + #[yaserde(rename = "stickiness_connRatePeriod")] + pub stickiness_conn_rate_period: String, + #[yaserde(rename = "stickiness_sessRatePeriod")] + pub stickiness_sess_rate_period: String, + #[yaserde(rename = "stickiness_httpReqRatePeriod")] + pub stickiness_http_req_rate_period: String, + #[yaserde(rename = "stickiness_httpErrRatePeriod")] + pub stickiness_http_err_rate_period: String, + #[yaserde(rename = "stickiness_bytesInRatePeriod")] + pub stickiness_bytes_in_rate_period: String, + #[yaserde(rename = "stickiness_bytesOutRatePeriod")] + pub stickiness_bytes_out_rate_period: String, + #[yaserde(rename = "basicAuthEnabled")] + pub basic_auth_enabled: u8, + #[yaserde(rename = "basicAuthUsers")] + pub basic_auth_users: MaybeString, + #[yaserde(rename = "basicAuthGroups")] + pub basic_auth_groups: MaybeString, + #[yaserde(rename = "tuning_timeoutConnect")] + pub tuning_timeout_connect: MaybeString, + #[yaserde(rename = "tuning_timeoutCheck")] + pub tuning_timeout_check: MaybeString, + #[yaserde(rename = "tuning_timeoutServer")] + pub tuning_timeout_server: MaybeString, + #[yaserde(rename = "tuning_retries")] + pub tuning_retries: MaybeString, + #[yaserde(rename = "customOptions")] + pub custom_options: MaybeString, + #[yaserde(rename = "tuning_defaultserver")] + pub tuning_defaultserver: MaybeString, + #[yaserde(rename = "tuning_noport")] + pub tuning_noport: u8, + #[yaserde(rename = "tuning_httpreuse")] + pub tuning_httpreuse: MaybeString, + #[yaserde(rename = "tuning_caching")] + pub tuning_caching: u8, + #[yaserde(rename = "linkedActions")] + pub linked_actions: MaybeString, + #[yaserde(rename = "linkedErrorfiles")] + pub linked_errorfiles: MaybeString, +} + +#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] +pub struct HAProxyServers { + #[yaserde(rename = "server")] + pub servers: Vec, +} + +#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] +pub struct HAProxyServer { + #[yaserde(attribute, rename = "uuid")] + pub uuid: String, + pub id: HAProxyId, + pub enabled: u8, + pub name: String, + pub description: MaybeString, + pub address: String, + pub port: u16, + pub checkport: MaybeString, + pub mode: String, + pub multiplexer_protocol: MaybeString, + #[yaserde(rename = "type")] + pub server_type: String, + #[yaserde(rename = "serviceName")] + pub service_name: MaybeString, + pub number: MaybeString, + #[yaserde(rename = "linkedResolver")] + pub linked_resolver: MaybeString, + #[yaserde(rename = "resolverOpts")] + pub resolver_opts: MaybeString, + #[yaserde(rename = "resolvePrefer")] + pub resolve_prefer: MaybeString, + pub ssl: u8, + #[yaserde(rename = "sslSNI")] + pub ssl_sni: MaybeString, + #[yaserde(rename = "sslVerify")] + pub ssl_verify: u8, + #[yaserde(rename = "sslCA")] + pub ssl_ca: MaybeString, + #[yaserde(rename = "sslCRL")] + pub ssl_crl: MaybeString, + #[yaserde(rename = "sslClientCertificate")] + pub ssl_client_certificate: MaybeString, + #[yaserde(rename = "maxConnections")] + pub max_connections: MaybeString, + pub weight: Option, + #[yaserde(rename = "checkInterval")] + pub check_interval: MaybeString, + #[yaserde(rename = "checkDownInterval")] + pub check_down_interval: MaybeString, + pub source: MaybeString, + pub advanced: MaybeString, + #[yaserde(rename = "unix_socket")] + pub unix_socket: MaybeString, +} + +#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] +pub struct HAProxyHealthChecks { + #[yaserde(rename = "healthcheck")] + pub healthchecks: Vec, +} + +#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] +pub struct HAProxyHealthCheck { + #[yaserde(attribute)] + pub uuid: String, + pub name: String, + pub description: MaybeString, + #[yaserde(rename = "type")] + pub health_check_type: String, + pub interval: String, + pub ssl: MaybeString, + #[yaserde(rename = "sslSNI")] + pub ssl_sni: MaybeString, + pub force_ssl: u8, + pub checkport: MaybeString, + pub http_method: MaybeString, + pub http_uri: MaybeString, + pub http_version: MaybeString, + #[yaserde(rename = "http_host")] + pub http_host: MaybeString, + #[yaserde(rename = "http_expressionEnabled")] + pub http_expression_enabled: Option, + pub http_expression: MaybeString, + pub http_negate: MaybeString, + pub http_value: MaybeString, + pub tcp_enabled: MaybeString, + #[yaserde(rename = "tcp_sendValue")] + pub tcp_send_value: MaybeString, + #[yaserde(rename = "tcp_matchType")] + pub tcp_match_type: MaybeString, + pub tcp_negate: MaybeString, + #[yaserde(rename = "tcp_matchValue")] + pub tcp_match_value: MaybeString, + pub agent_port: MaybeString, + pub mysql_user: MaybeString, + pub mysql_post41: MaybeString, + pub pgsql_user: MaybeString, + pub smtp_domain: MaybeString, + pub esmtp_domain: MaybeString, + #[yaserde(rename = "agentPort")] + pub agent_port_uppercase: MaybeString, + #[yaserde(rename = "dbUser")] + pub db_user: MaybeString, + #[yaserde(rename = "smtpDomain")] + pub smtp_domain_uppercase: MaybeString, +} diff --git a/harmony-rs/opnsense-config-xml/src/data/interfaces.rs b/harmony-rs/opnsense-config-xml/src/data/interfaces.rs index e980c73..56b2dea 100644 --- a/harmony-rs/opnsense-config-xml/src/data/interfaces.rs +++ b/harmony-rs/opnsense-config-xml/src/data/interfaces.rs @@ -28,6 +28,8 @@ pub struct Interface { pub r#virtual: Option, pub subnet: Option, pub ipaddrv6: Option, + #[yaserde(rename = "dhcp6-ia-pd-len")] + pub dhcp6_ia_pd_len: Option, pub networks: Option, pub subnetv6: Option, pub media: Option, @@ -38,6 +40,33 @@ pub struct Interface { pub track6_prefix_id: Option, #[yaserde(rename = "dhcprejectfrom")] pub dhcprejectfrom: Option, + pub adv_dhcp6_interface_statement_send_options: Option, + pub adv_dhcp6_interface_statement_request_options: Option, + pub adv_dhcp6_interface_statement_information_only_enable: Option, + pub adv_dhcp6_interface_statement_script: Option, + pub adv_dhcp6_id_assoc_statement_address_enable: Option, + pub adv_dhcp6_id_assoc_statement_address: Option, + pub adv_dhcp6_id_assoc_statement_address_id: Option, + pub adv_dhcp6_id_assoc_statement_address_pltime: Option, + pub adv_dhcp6_id_assoc_statement_address_vltime: Option, + pub adv_dhcp6_id_assoc_statement_prefix_enable: Option, + pub adv_dhcp6_id_assoc_statement_prefix: Option, + pub adv_dhcp6_id_assoc_statement_prefix_id: Option, + pub adv_dhcp6_id_assoc_statement_prefix_pltime: Option, + pub adv_dhcp6_id_assoc_statement_prefix_vltime: Option, + pub adv_dhcp6_prefix_interface_statement_sla_len: Option, + pub adv_dhcp6_authentication_statement_authname: Option, + pub adv_dhcp6_authentication_statement_protocol: Option, + pub adv_dhcp6_authentication_statement_algorithm: Option, + pub adv_dhcp6_authentication_statement_rdm: Option, + pub adv_dhcp6_key_info_statement_keyname: Option, + pub adv_dhcp6_key_info_statement_realm: Option, + pub adv_dhcp6_key_info_statement_keyid: Option, + pub adv_dhcp6_key_info_statement_secret: Option, + pub adv_dhcp6_key_info_statement_expire: Option, + pub adv_dhcp6_config_advanced: Option, + pub adv_dhcp6_config_file_override: Option, + pub adv_dhcp6_config_file_override_path: Option, pub adv_dhcp_pt_timeout: Option, pub adv_dhcp_pt_retry: Option, pub adv_dhcp_pt_select_timeout: Option, diff --git a/harmony-rs/opnsense-config-xml/src/data/mod.rs b/harmony-rs/opnsense-config-xml/src/data/mod.rs index 5d2a4da..af9c448 100644 --- a/harmony-rs/opnsense-config-xml/src/data/mod.rs +++ b/harmony-rs/opnsense-config-xml/src/data/mod.rs @@ -1,6 +1,8 @@ mod opnsense; mod interfaces; mod dhcpd; +mod haproxy; +pub use haproxy::*; pub use opnsense::*; pub use interfaces::*; pub use dhcpd::*; diff --git a/harmony-rs/opnsense-config-xml/src/data/opnsense.rs b/harmony-rs/opnsense-config-xml/src/data/opnsense.rs index 73b9171..a60390b 100644 --- a/harmony-rs/opnsense-config-xml/src/data/opnsense.rs +++ b/harmony-rs/opnsense-config-xml/src/data/opnsense.rs @@ -1,3 +1,4 @@ +use crate::HAProxy; use crate::{data::dhcpd::DhcpInterface, xml_utils::to_xml_str}; use log::error; use uuid::Uuid; @@ -42,6 +43,8 @@ pub struct OPNsense { pub laggs: Laggs, pub wireless: Wireless, pub hasync: Hasync, + #[yaserde(rename = "Pischem")] + pub pischem: Option, pub ifgroups: Ifgroups, } @@ -106,6 +109,7 @@ pub struct Options { pub struct Filters { #[yaserde(rename = "rule")] pub rules: Vec, + pub bypassstaticroutes: Option, } #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] @@ -431,6 +435,9 @@ pub struct OPNsenseXmlSection { #[yaserde(rename = "DHCRelay")] pub dhcrelay: Option, pub trust: Option, + pub tftp: Option, + #[yaserde(rename = "Nginx")] + pub nginx: Option, pub wireguard: Option, #[yaserde(rename = "Swanctl")] pub swanctl: Swanctl, @@ -444,6 +451,19 @@ pub struct OPNsenseXmlSection { pub haproxy: Option, } +#[derive(Debug, YaSerialize, YaDeserialize, PartialEq)] +pub struct Tftp { + general: TftpGeneral, +} + +#[derive(Debug, YaSerialize, YaDeserialize, PartialEq)] +pub struct TftpGeneral { + #[yaserde(attribute)] + version: String, + enabled: u8, + listen: String, +} + #[derive(Debug, YaSerialize, YaDeserialize, PartialEq)] #[yaserde(rename = "IDS")] pub struct IDS { @@ -509,9 +529,9 @@ pub struct IPsec { #[derive(Debug, YaSerialize, YaDeserialize, PartialEq)] pub struct GeneralIpsec { enabled: MaybeString, - preferred_oldsa: MaybeString, - disablevpnrules: MaybeString, - passthrough_networks: MaybeString, + preferred_oldsa: Option, + disablevpnrules: Option, + passthrough_networks: Option, } #[derive(Debug, YaSerialize, YaDeserialize, PartialEq)] @@ -1079,13 +1099,13 @@ pub struct UnboundGeneral { pub port: i32, pub stats: MaybeString, pub active_interface: MaybeString, - pub dnssec: MaybeString, + pub dnssec: Option, pub dns64: MaybeString, pub dns64prefix: MaybeString, pub noarecords: MaybeString, - pub regdhcp: i8, + pub regdhcp: Option, pub regdhcpdomain: MaybeString, - pub regdhcpstatic: i8, + pub regdhcpstatic: Option, pub noreglladdr6: MaybeString, pub noregrecords: MaybeString, pub txtsupport: MaybeString, @@ -1097,22 +1117,22 @@ pub struct UnboundGeneral { #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] pub struct Advanced { - pub hideidentity: i8, - pub hideversion: i8, - pub prefetch: i8, - pub prefetchkey: i8, - pub dnssecstripped: i8, - pub aggressivensec: i8, - pub serveexpired: i8, + pub hideidentity: Option, + pub hideversion: Option, + pub prefetch: Option, + pub prefetchkey: Option, + pub dnssecstripped: Option, + pub aggressivensec: Option, + pub serveexpired: Option, pub serveexpiredreplyttl: MaybeString, pub serveexpiredttl: MaybeString, - pub serveexpiredttlreset: i32, + pub serveexpiredttlreset: Option, pub serveexpiredclienttimeout: MaybeString, - pub qnameminstrict: i32, - pub extendedstatistics: i32, - pub logqueries: i32, - pub logreplies: i32, - pub logtagqueryreply: i32, + pub qnameminstrict: Option, + pub extendedstatistics: Option, + pub logqueries: Option, + pub logreplies: Option, + pub logtagqueryreply: Option, pub logservfail: MaybeString, pub loglocalactions: MaybeString, pub logverbosity: i32, @@ -1127,7 +1147,7 @@ pub struct Advanced { pub numqueriesperthread: MaybeString, pub outgoingrange: MaybeString, pub jostletimeout: MaybeString, - pub discardtimeout: MaybeString, + pub discardtimeout: Option, pub cachemaxttl: MaybeString, pub cachemaxnegativettl: MaybeString, pub cacheminttl: MaybeString, @@ -1141,6 +1161,7 @@ pub struct Advanced { pub struct Acls { #[yaserde(rename = "default_action")] pub default_action: String, + pub acl: Option, } #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] @@ -1154,12 +1175,12 @@ pub struct Dnsbl { pub blocklists: MaybeString, pub wildcards: MaybeString, pub address: MaybeString, - pub nxdomain: i32, + pub nxdomain: Option, } #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] pub struct Forwarding { - pub enabled: i32, + pub enabled: Option, } #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] @@ -1193,7 +1214,7 @@ impl Host { server, mxprio: MaybeString::default(), mx: MaybeString::default(), - description: None + description: None, } } } @@ -1349,553 +1370,6 @@ pub struct ConfigOpenVPN { pub StaticKeys: MaybeString, } -#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] -#[yaserde(rename = "HAProxy")] -pub struct HAProxy { - #[yaserde(attribute)] - pub version: String, - pub general: HaProxyGeneral, - pub frontends: HAProxyFrontends, - pub backends: HAProxyBackends, - pub servers: HAProxyServers, - pub healthchecks: HAProxyHealthChecks, - pub acls: MaybeString, - pub actions: MaybeString, - pub luas: MaybeString, - pub fcgis: MaybeString, - pub errorfiles: MaybeString, - pub mapfiles: MaybeString, - pub groups: MaybeString, - pub users: MaybeString, - pub cpus: MaybeString, - pub resolvers: MaybeString, - pub mailers: MaybeString, - pub maintenance: Maintenance, -} - -#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] -pub struct Maintenance { - #[yaserde(rename = "cronjobs")] - pub cronjobs: CronJobs, -} - -#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] -pub struct CronJobs { - #[yaserde(rename = "syncCerts")] - pub sync_certs: u32, - #[yaserde(rename = "syncCertsCron")] - pub sync_certs_cron: MaybeString, - #[yaserde(rename = "updateOcsp")] - pub update_ocsp: u32, - #[yaserde(rename = "updateOcspCron")] - pub update_ocsp_cron: MaybeString, - #[yaserde(rename = "reloadService")] - pub reload_service: u32, - #[yaserde(rename = "reloadServiceCron")] - pub reload_service_cron: MaybeString, - #[yaserde(rename = "restartService")] - pub restart_service: u32, - #[yaserde(rename = "restartServiceCron")] - pub restart_service_cron: MaybeString, -} - -#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] -pub struct HaProxyGeneral { - pub enabled: i32, - #[yaserde(rename = "gracefulStop")] - pub graceful_stop: i32, - #[yaserde(rename = "hardStopAfter")] - pub hard_stop_after: String, - #[yaserde(rename = "closeSpreadTime")] - pub close_spread_time: MaybeString, - #[yaserde(rename = "seamlessReload")] - pub seamless_reload: i32, - #[yaserde(rename = "storeOcsp")] - pub store_ocsp: i32, - #[yaserde(rename = "showIntro")] - pub show_intro: i32, - pub peers: Peers, - pub tuning: Tuning, - pub defaults: HaProxyDefaults, - pub logging: HaProxyLogging, - pub stats: Stats, - pub cache: HaProxyCache, -} - -#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] -pub struct Peers { - pub enabled: i32, - pub name1: MaybeString, - pub listen1: MaybeString, - pub port1: i32, - pub name2: MaybeString, - pub listen2: MaybeString, - pub port2: i32, -} - -#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] -pub struct Tuning { - pub root: i32, - #[yaserde(rename = "maxConnections")] - pub max_connections: MaybeString, - pub nbthread: i32, - #[yaserde(rename = "resolversPrefer")] - pub resolvers_prefer: String, - #[yaserde(rename = "sslServerVerify")] - pub ssl_server_verify: String, - #[yaserde(rename = "maxDHSize")] - pub max_dh_size: i32, - #[yaserde(rename = "bufferSize")] - pub buffer_size: i32, - #[yaserde(rename = "spreadChecks")] - pub spread_checks: i32, - #[yaserde(rename = "bogusProxyEnabled")] - pub bogus_proxy_enabled: i32, - #[yaserde(rename = "luaMaxMem")] - pub lua_max_mem: i32, - #[yaserde(rename = "customOptions")] - pub custom_options: MaybeString, - #[yaserde(rename = "ocspUpdateEnabled")] - pub ocs_update_enabled: MaybeString, - #[yaserde(rename = "ocspUpdateMinDelay")] - pub ocs_update_min_delay: MaybeString, - #[yaserde(rename = "ocspUpdateMaxDelay")] - pub ocs_update_max_delay: MaybeString, - #[yaserde(rename = "ssl_defaultsEnabled")] - pub ssl_defaults_enabled: i32, - #[yaserde(rename = "ssl_bindOptions")] - pub ssl_bind_options: String, - #[yaserde(rename = "ssl_minVersion")] - pub ssl_min_version: String, - #[yaserde(rename = "ssl_maxVersion")] - pub ssl_max_version: MaybeString, - #[yaserde(rename = "ssl_cipherList")] - pub ssl_cipher_list: String, - #[yaserde(rename = "ssl_cipherSuites")] - pub ssl_cipher_suites: String, - #[yaserde(rename = "h2_initialWindowSize")] - pub h2_initial_window_size: Option, - #[yaserde(rename = "h2_initialWindowSizeOutgoing")] - pub h2_initial_window_size_outgoing: Option, - #[yaserde(rename = "h2_initialWindowSizeIncoming")] - pub h2_initial_window_size_incoming: Option, - #[yaserde(rename = "h2_maxConcurrentStreams")] - pub h2_max_concurrent_streams: Option, - #[yaserde(rename = "h2_maxConcurrentStreamsOutgoing")] - pub h2_max_concurrent_streams_outgoing: Option, - #[yaserde(rename = "h2_maxConcurrentStreamsIncoming")] - pub h2_max_concurrent_streams_incoming: Option, -} - -#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] -pub struct HaProxyDefaults { - #[yaserde(rename = "maxConnections")] - pub max_connections: MaybeString, - #[yaserde(rename = "maxConnectionsServers")] - pub max_connections_servers: MaybeString, - #[yaserde(rename = "timeoutClient")] - pub timeout_client: String, - #[yaserde(rename = "timeoutConnect")] - pub timeout_connect: String, - #[yaserde(rename = "timeoutCheck")] - pub timeout_check: MaybeString, - #[yaserde(rename = "timeoutServer")] - pub timeout_server: String, - pub retries: i32, - pub redispatch: String, - pub init_addr: String, - #[yaserde(rename = "customOptions")] - pub custom_options: MaybeString, -} - -#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] -pub struct HaProxyLogging { - pub host: String, - pub facility: String, - pub level: String, - pub length: MaybeString, -} - -#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] -pub struct Stats { - pub enabled: i32, - pub port: i32, - #[yaserde(rename = "remoteEnabled")] - pub remote_enabled: i32, - #[yaserde(rename = "remoteBind")] - pub remote_bind: MaybeString, - #[yaserde(rename = "authEnabled")] - pub auth_enabled: i32, - #[yaserde(rename = "users")] - pub users: MaybeString, - #[yaserde(rename = "allowedUsers")] - pub allowed_users: MaybeString, - #[yaserde(rename = "allowedGroups")] - pub allowed_groups: MaybeString, - #[yaserde(rename = "customOptions")] - pub custom_options: MaybeString, - pub prometheus_enabled: i32, - pub prometheus_bind: String, - pub prometheus_path: String, -} - -#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] -pub struct HaProxyCache { - pub enabled: i32, - #[yaserde(rename = "totalMaxSize")] - pub total_max_size: i32, - #[yaserde(rename = "maxAge")] - pub max_age: i32, - #[yaserde(rename = "maxObjectSize")] - pub max_object_size: MaybeString, - #[yaserde(rename = "processVary")] - pub process_vary: i32, - #[yaserde(rename = "maxSecondaryEntries")] - pub max_secondary_entries: i32, -} - -#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] -pub struct HAProxyFrontends { - pub frontend: Vec, -} - -#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] -pub struct Frontend { - #[yaserde(attribute)] - pub uuid: String, - pub id: String, - pub enabled: i32, - pub name: String, - pub description: String, - pub bind: String, - #[yaserde(rename = "bindOptions")] - pub bind_options: MaybeString, - pub mode: String, - #[yaserde(rename = "defaultBackend")] - pub default_backend: String, - pub ssl_enabled: i32, - pub ssl_certificates: MaybeString, - pub ssl_default_certificate: MaybeString, - #[yaserde(rename = "ssl_customOptions")] - pub ssl_custom_options: MaybeString, - #[yaserde(rename = "ssl_advancedEnabled")] - pub ssl_advanced_enabled: i32, - #[yaserde(rename = "ssl_bindOptions")] - pub ssl_bind_options: String, - #[yaserde(rename = "ssl_minVersion")] - pub ssl_min_version: String, - #[yaserde(rename = "ssl_maxVersion")] - pub ssl_max_version: MaybeString, - #[yaserde(rename = "ssl_cipherList")] - pub ssl_cipher_list: String, - #[yaserde(rename = "ssl_cipherSuites")] - pub ssl_cipher_suites: String, - #[yaserde(rename = "ssl_hstsEnabled")] - pub ssl_hsts_enabled: i32, - #[yaserde(rename = "ssl_hstsIncludeSubDomains")] - pub ssl_hsts_include_sub_domains: i32, - #[yaserde(rename = "ssl_hstsPreload")] - pub ssl_hsts_preload: i32, - #[yaserde(rename = "ssl_hstsMaxAge")] - pub ssl_hsts_max_age: i32, - #[yaserde(rename = "ssl_clientAuthEnabled")] - pub ssl_client_auth_enabled: i32, - #[yaserde(rename = "ssl_clientAuthVerify")] - pub ssl_client_auth_verify: String, - #[yaserde(rename = "ssl_clientAuthCAs")] - pub ssl_client_auth_cas: MaybeString, - #[yaserde(rename = "ssl_clientAuthCRLs")] - pub ssl_client_auth_cr_ls: MaybeString, - #[yaserde(rename = "basicAuthEnabled")] - pub basic_auth_enabled: i32, - #[yaserde(rename = "basicAuthUsers")] - pub basic_auth_users: MaybeString, - #[yaserde(rename = "basicAuthGroups")] - pub basic_auth_groups: MaybeString, - #[yaserde(rename = "tuning_maxConnections")] - pub tuning_max_connections: MaybeString, - #[yaserde(rename = "tuning_timeoutClient")] - pub tuning_timeout_client: MaybeString, - #[yaserde(rename = "tuning_timeoutHttpReq")] - pub tuning_timeout_http_req: MaybeString, - #[yaserde(rename = "tuning_timeoutHttpKeepAlive")] - pub tuning_timeout_http_keep_alive: MaybeString, - #[yaserde(rename = "linkedCpuAffinityRules")] - pub linked_cpu_affinity_rules: MaybeString, - pub tuning_shards: MaybeString, - #[yaserde(rename = "logging_dontLogNull")] - pub logging_dont_log_null: i32, - #[yaserde(rename = "logging_dontLogNormal")] - pub logging_dont_log_normal: i32, - #[yaserde(rename = "logging_logSeparateErrors")] - pub logging_log_separate_errors: i32, - #[yaserde(rename = "logging_detailedLog")] - pub logging_detailed_log: i32, - #[yaserde(rename = "logging_socketStats")] - pub logging_socket_stats: i32, - pub stickiness_pattern: MaybeString, - #[yaserde(rename = "stickiness_dataTypes")] - pub stickiness_data_types: MaybeString, - pub stickiness_expire: String, - pub stickiness_size: String, - pub stickiness_counter: i32, - pub stickiness_counter_key: String, - pub stickiness_length: MaybeString, - #[yaserde(rename = "stickiness_connRatePeriod")] - pub stickiness_conn_rate_period: String, - #[yaserde(rename = "stickiness_sessRatePeriod")] - pub stickiness_sess_rate_period: String, - #[yaserde(rename = "stickiness_httpReqRatePeriod")] - pub stickiness_http_req_rate_period: String, - #[yaserde(rename = "stickiness_httpErrRatePeriod")] - pub stickiness_http_err_rate_period: String, - #[yaserde(rename = "stickiness_bytesInRatePeriod")] - pub stickiness_bytes_in_rate_period: String, - #[yaserde(rename = "stickiness_bytesOutRatePeriod")] - pub stickiness_bytes_out_rate_period: String, - #[yaserde(rename = "http2Enabled")] - pub http2_enabled: i32, - #[yaserde(rename = "http2Enabled_nontls")] - pub http2_enabled_nontls: i32, - pub advertised_protocols: String, - #[yaserde(rename = "forwardFor")] - pub forward_for: i32, - pub prometheus_enabled: i32, - pub prometheus_path: String, - #[yaserde(rename = "connectionBehaviour")] - pub connection_behaviour: String, - #[yaserde(rename = "customOptions")] - pub custom_options: MaybeString, - #[yaserde(rename = "linkedActions")] - pub linked_actions: MaybeString, - #[yaserde(rename = "linkedErrorfiles")] - pub linked_error_files: MaybeString, -} - -#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] -pub struct HAProxyBackends { - #[yaserde(rename = "backend")] - pub backends: Vec, -} - -#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] -pub struct Backend { - #[yaserde(attribute, rename = "uuid")] - pub uuid: String, - #[yaserde(rename = "id")] - pub id: String, - #[yaserde(rename = "enabled")] - pub enabled: u8, - #[yaserde(rename = "name")] - pub name: String, - #[yaserde(rename = "description")] - pub description: MaybeString, - #[yaserde(rename = "mode")] - pub mode: String, - #[yaserde(rename = "algorithm")] - pub algorithm: String, - #[yaserde(rename = "random_draws")] - pub random_draws: Option, - #[yaserde(rename = "proxyProtocol")] - pub proxy_protocol: MaybeString, - #[yaserde(rename = "linkedServers")] - pub linked_servers: MaybeString, - #[yaserde(rename = "linkedFcgi")] - pub linked_fcgi: MaybeString, - #[yaserde(rename = "linkedResolver")] - pub linked_resolver: MaybeString, - #[yaserde(rename = "resolverOpts")] - pub resolver_opts: MaybeString, - #[yaserde(rename = "resolvePrefer")] - pub resolve_prefer: MaybeString, - #[yaserde(rename = "source")] - pub source: MaybeString, - #[yaserde(rename = "healthCheckEnabled")] - pub health_check_enabled: u8, - #[yaserde(rename = "healthCheck")] - pub health_check: MaybeString, - #[yaserde(rename = "healthCheckLogStatus")] - pub health_check_log_status: u8, - #[yaserde(rename = "checkInterval")] - pub check_interval: MaybeString, - #[yaserde(rename = "checkDownInterval")] - pub check_down_interval: MaybeString, - #[yaserde(rename = "healthCheckFall")] - pub health_check_fall: MaybeString, - #[yaserde(rename = "healthCheckRise")] - pub health_check_rise: MaybeString, - #[yaserde(rename = "linkedMailer")] - pub linked_mailer: MaybeString, - #[yaserde(rename = "http2Enabled")] - pub http2_enabled: u8, - #[yaserde(rename = "http2Enabled_nontls")] - pub http2_enabled_nontls: u8, - #[yaserde(rename = "ba_advertised_protocols")] - pub ba_advertised_protocols: String, - #[yaserde(rename = "forwardFor")] - pub forward_for: Option, - #[yaserde(rename = "forwardedHeader")] - pub forwarded_header: Option, - #[yaserde(rename = "forwardedHeaderParameters")] - pub forwarded_header_parameters: Option, - pub persistence: MaybeString, - pub persistence_cookiemode: String, - pub persistence_cookiename: MaybeString, - pub persistence_stripquotes: u8, - pub stickiness_pattern: MaybeString, - #[yaserde(rename = "stickiness_dataTypes")] - pub stickiness_data_types: MaybeString, - pub stickiness_expire: String, - pub stickiness_size: String, - pub stickiness_cookiename: MaybeString, - pub stickiness_cookielength: MaybeString, - #[yaserde(rename = "stickiness_connRatePeriod")] - pub stickiness_conn_rate_period: String, - #[yaserde(rename = "stickiness_sessRatePeriod")] - pub stickiness_sess_rate_period: String, - #[yaserde(rename = "stickiness_httpReqRatePeriod")] - pub stickiness_http_req_rate_period: String, - #[yaserde(rename = "stickiness_httpErrRatePeriod")] - pub stickiness_http_err_rate_period: String, - #[yaserde(rename = "stickiness_bytesInRatePeriod")] - pub stickiness_bytes_in_rate_period: String, - #[yaserde(rename = "stickiness_bytesOutRatePeriod")] - pub stickiness_bytes_out_rate_period: String, - #[yaserde(rename = "basicAuthEnabled")] - pub basic_auth_enabled: u8, - #[yaserde(rename = "basicAuthUsers")] - pub basic_auth_users: MaybeString, - #[yaserde(rename = "basicAuthGroups")] - pub basic_auth_groups: MaybeString, - #[yaserde(rename = "tuning_timeoutConnect")] - pub tuning_timeout_connect: MaybeString, - #[yaserde(rename = "tuning_timeoutCheck")] - pub tuning_timeout_check: MaybeString, - #[yaserde(rename = "tuning_timeoutServer")] - pub tuning_timeout_server: MaybeString, - #[yaserde(rename = "tuning_retries")] - pub tuning_retries: MaybeString, - #[yaserde(rename = "customOptions")] - pub custom_options: MaybeString, - #[yaserde(rename = "tuning_defaultserver")] - pub tuning_defaultserver: MaybeString, - #[yaserde(rename = "tuning_noport")] - pub tuning_noport: u8, - #[yaserde(rename = "tuning_httpreuse")] - pub tuning_httpreuse: MaybeString, - #[yaserde(rename = "tuning_caching")] - pub tuning_caching: u8, - #[yaserde(rename = "linkedActions")] - pub linked_actions: MaybeString, - #[yaserde(rename = "linkedErrorfiles")] - pub linked_errorfiles: MaybeString, -} - -#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] -pub struct HAProxyServers { - #[yaserde(rename = "server")] - pub servers: Vec, -} - -#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] -pub struct HAProxyServer { - #[yaserde(attribute, rename = "uuid")] - pub uuid: String, - pub id: String, - pub enabled: u8, - pub name: String, - pub description: MaybeString, - pub address: String, - pub port: u16, - pub checkport: MaybeString, - pub mode: String, - pub multiplexer_protocol: String, - #[yaserde(rename = "type")] - pub server_type: String, - #[yaserde(rename = "serviceName")] - pub service_name: MaybeString, - pub number: MaybeString, - #[yaserde(rename = "linkedResolver")] - pub linked_resolver: MaybeString, - #[yaserde(rename = "resolverOpts")] - pub resolver_opts: MaybeString, - #[yaserde(rename = "resolvePrefer")] - pub resolve_prefer: MaybeString, - pub ssl: u8, - #[yaserde(rename = "sslSNI")] - pub ssl_sni: MaybeString, - #[yaserde(rename = "sslVerify")] - pub ssl_verify: u8, - #[yaserde(rename = "sslCA")] - pub ssl_ca: MaybeString, - #[yaserde(rename = "sslCRL")] - pub ssl_crl: MaybeString, - #[yaserde(rename = "sslClientCertificate")] - pub ssl_client_certificate: MaybeString, - #[yaserde(rename = "maxConnections")] - pub max_connections: MaybeString, - pub weight: u32, - #[yaserde(rename = "checkInterval")] - pub check_interval: MaybeString, - #[yaserde(rename = "checkDownInterval")] - pub check_down_interval: MaybeString, - pub source: MaybeString, - pub advanced: MaybeString, - #[yaserde(rename = "unix_socket")] - pub unix_socket: MaybeString, -} - -#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] -pub struct HAProxyHealthChecks { - #[yaserde(rename = "healthcheck")] - pub healthchecks: Vec, -} - -#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] -pub struct HAProxyHealthCheck { - #[yaserde(attribute)] - pub uuid: String, - pub name: String, - pub description: MaybeString, - #[yaserde(rename = "type")] - pub health_check_type: String, - pub interval: String, - pub ssl: String, - #[yaserde(rename = "sslSNI")] - pub ssl_sni: MaybeString, - pub force_ssl: u8, - pub checkport: MaybeString, - pub http_method: String, - pub http_uri: String, - pub http_version: MaybeString, - #[yaserde(rename = "http_host")] - pub http_host: MaybeString, - #[yaserde(rename = "http_expressionEnabled")] - pub http_expression_enabled: Option, - pub http_expression: MaybeString, - pub http_negate: MaybeString, - pub http_value: MaybeString, - pub tcp_enabled: MaybeString, - #[yaserde(rename = "tcp_sendValue")] - pub tcp_send_value: MaybeString, - #[yaserde(rename = "tcp_matchType")] - pub tcp_match_type: MaybeString, - pub tcp_negate: MaybeString, - #[yaserde(rename = "tcp_matchValue")] - pub tcp_match_value: MaybeString, - pub agent_port: MaybeString, - pub mysql_user: MaybeString, - pub mysql_post41: MaybeString, - pub pgsql_user: MaybeString, - pub smtp_domain: MaybeString, - pub esmtp_domain: MaybeString, - #[yaserde(rename = "agentPort")] - pub agent_port_uppercase: MaybeString, - #[yaserde(rename = "dbUser")] - pub db_user: MaybeString, - #[yaserde(rename = "smtpDomain")] - pub smtp_domain_uppercase: MaybeString, -} #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] pub struct StaticRoutes { diff --git a/harmony-rs/opnsense-config/src/config/config.rs b/harmony-rs/opnsense-config/src/config/config.rs index 0336223..fc998ff 100644 --- a/harmony-rs/opnsense-config/src/config/config.rs +++ b/harmony-rs/opnsense-config/src/config/config.rs @@ -1,7 +1,11 @@ -use std::{net::Ipv4Addr, sync::Arc, time::Duration}; +use std::{sync::Arc, time::Duration}; -use crate::{config::{SshConfigManager, SshCredentials, SshOPNSenseShell}, error::Error, modules::{dhcp::DhcpConfig, dns::DnsConfig}}; -use log::trace; +use crate::{ + config::{SshConfigManager, SshCredentials, SshOPNSenseShell}, + error::Error, + modules::{dhcp::DhcpConfig, dns::DnsConfig, load_balancer::LoadBalancerConfig}, +}; +use log::{info, trace}; use opnsense_config_xml::OPNsense; use russh::client; @@ -19,13 +23,8 @@ impl Config { repository: Arc, shell: Arc, ) -> Result { - let xml = repository.load_as_str().await?; - trace!("xml {}", xml); - - let opnsense = OPNsense::from(xml); - Ok(Self { - opnsense, + opnsense: Self::get_opnsense_instance(repository.clone()).await?, repository, shell, }) @@ -39,6 +38,26 @@ impl Config { DnsConfig::new(&mut self.opnsense, self.shell.clone()) } + pub fn load_balancer(&mut self) -> LoadBalancerConfig { + LoadBalancerConfig::new(&mut self.opnsense, self.shell.clone()) + } + + pub async fn install_package(&mut self, package_name: &str) -> Result<(), Error> { + info!("Installing opnsense package {package_name}"); + let output = self.shell + .exec(&format!("pkg install -y {package_name}")) + .await?; + info!("Installation output {output}"); + self.reload_config().await?; + Ok(()) + } + + async fn reload_config(&mut self) -> Result<(), Error> { + info!("Reloading opnsense live config"); + self.opnsense = Self::get_opnsense_instance(self.repository.clone()).await?; + Ok(()) + } + pub async fn restart_dns(&self) -> Result<(), Error> { self.shell.exec("configctl unbound restart").await?; Ok(()) @@ -47,9 +66,7 @@ impl Config { /// Save the config to the repository. This method is meant NOT to reload services, only save /// the config to the live file/database and perhaps take a backup when relevant. pub async fn save(&self) -> Result<(), Error> { - self.repository - .save_config(&self.opnsense.to_xml()) - .await + self.repository.save_config(&self.opnsense.to_xml()).await } /// Save the configuration and reload all services. Be careful with this one as it will cause @@ -60,7 +77,12 @@ impl Config { .await } - pub async fn from_credentials(ipaddr: std::net::IpAddr, username: &str, password: &str) -> Self { + pub async fn from_credentials( + ipaddr: std::net::IpAddr, + port: Option, + username: &str, + password: &str, + ) -> Self { let config = Arc::new(client::Config { inactivity_timeout: Some(Duration::from_secs(5)), ..<_>::default() @@ -71,15 +93,20 @@ impl Config { password: String::from(password), }; - let shell = Arc::new(SshOPNSenseShell::new( - (ipaddr, 22), - credentials, - config, - )); + let port = port.unwrap_or(22); + + let shell = Arc::new(SshOPNSenseShell::new((ipaddr, port), credentials, config)); let manager = Arc::new(SshConfigManager::new(shell.clone())); Config::new(manager, shell).await.unwrap() } + + async fn get_opnsense_instance(repository: Arc) -> Result { + let xml = repository.load_as_str().await?; + trace!("xml {}", xml); + + Ok(OPNsense::from(xml)) + } } #[cfg(test)] diff --git a/harmony-rs/opnsense-config/src/config/shell/ssh.rs b/harmony-rs/opnsense-config/src/config/shell/ssh.rs index 453231c..4b6eb2c 100644 --- a/harmony-rs/opnsense-config/src/config/shell/ssh.rs +++ b/harmony-rs/opnsense-config/src/config/shell/ssh.rs @@ -5,7 +5,7 @@ use std::{ }; use async_trait::async_trait; -use log::debug; +use log::{debug, info}; use russh::{ client::{Config, Handler, Msg}, Channel, @@ -28,6 +28,7 @@ pub struct SshOPNSenseShell { #[async_trait] impl OPNsenseShell for SshOPNSenseShell { async fn exec(&self, command: &str) -> Result { + info!("Executing command on SshOPNSenseShell {command}"); self.run_command(command).await } diff --git a/harmony-rs/opnsense-config/src/lib.rs b/harmony-rs/opnsense-config/src/lib.rs index 59a378e..21c90f0 100644 --- a/harmony-rs/opnsense-config/src/lib.rs +++ b/harmony-rs/opnsense-config/src/lib.rs @@ -33,7 +33,12 @@ mod test { } async fn initialize_config() -> Config { - Config::from_credentials(Ipv4Addr::new(192, 168, 5, 229), "root", "opnsense").await + Config::from_credentials( + std::net::IpAddr::V4(Ipv4Addr::new(192, 168, 5, 229)), + "root", + "opnsense", + ) + .await } async fn get_static_mappings() -> Vec { diff --git a/harmony-rs/opnsense-config/src/modules/dhcp.rs b/harmony-rs/opnsense-config/src/modules/dhcp.rs index deacda3..a53f646 100644 --- a/harmony-rs/opnsense-config/src/modules/dhcp.rs +++ b/harmony-rs/opnsense-config/src/modules/dhcp.rs @@ -202,6 +202,11 @@ impl<'a> DhcpConfig<'a> { self.enable_netboot(); self.get_lan_dhcpd().nextserver = Some(ip.to_string()); } + + pub fn set_boot_filename(&mut self, boot_filename: &str) { + self.enable_netboot(); + self.get_lan_dhcpd().filename64 = Some(boot_filename.to_string()); + } } #[cfg(test)] diff --git a/harmony-rs/opnsense-config/src/modules/dns.rs b/harmony-rs/opnsense-config/src/modules/dns.rs index 05fd748..4f83502 100644 --- a/harmony-rs/opnsense-config/src/modules/dns.rs +++ b/harmony-rs/opnsense-config/src/modules/dns.rs @@ -39,7 +39,7 @@ impl<'a> DnsConfig<'a> { None => todo!("Handle case where unboundplus is not used"), }; - unbound.general.regdhcp = register as i8; - unbound.general.regdhcpstatic = register as i8; + unbound.general.regdhcp = Some(register as i8); + unbound.general.regdhcpstatic = Some(register as i8); } } diff --git a/harmony-rs/opnsense-config/src/modules/load_balancer.rs b/harmony-rs/opnsense-config/src/modules/load_balancer.rs new file mode 100644 index 0000000..dfc4069 --- /dev/null +++ b/harmony-rs/opnsense-config/src/modules/load_balancer.rs @@ -0,0 +1,59 @@ +use std::sync::Arc; + +use log::warn; +use opnsense_config_xml::{ + Frontend, HAProxy, HAProxyBackend, HAProxyHealthCheck, HAProxyServer, OPNsense, +}; + +use crate::config::OPNsenseShell; + +pub struct LoadBalancerConfig<'a> { + opnsense: &'a mut OPNsense, + opnsense_shell: Arc, +} + +impl<'a> LoadBalancerConfig<'a> { + pub fn new(opnsense: &'a mut OPNsense, opnsense_shell: Arc) -> Self { + Self { + opnsense, + opnsense_shell, + } + } + + pub fn get_full_config(&self) -> &Option { + &self.opnsense.opnsense.haproxy + } + + fn with_haproxy(&mut self, f: F) -> R + where + F: FnOnce(&mut HAProxy) -> R, + { + match &mut self.opnsense.opnsense.haproxy.as_mut() { + Some(haproxy) => f(haproxy), + None => unimplemented!( + "Adding a backend is not supported when haproxy config does not exist yet" + ), + } + } + + pub fn enable(&mut self, enabled: bool) { + self.with_haproxy(|haproxy| haproxy.general.enabled = enabled as i32); + } + + pub fn add_backend(&mut self, backend: HAProxyBackend) { + warn!("TODO make sure this new backend does not refer non-existing entities like servers or health checks"); + self.with_haproxy(|haproxy| haproxy.backends.backends.push(backend)); + } + + pub fn add_frontend(&mut self, frontend: Frontend) { + self.with_haproxy(|haproxy| haproxy.frontends.frontend.push(frontend)); + } + + pub fn add_healthcheck(&mut self, healthcheck: HAProxyHealthCheck) { + self.with_haproxy(|haproxy| haproxy.healthchecks.healthchecks.push(healthcheck)); + } + + pub fn add_servers(&mut self, mut servers: Vec) { + self.with_haproxy(|haproxy| haproxy.servers.servers.append(&mut servers)); + } +} diff --git a/harmony-rs/opnsense-config/src/modules/mod.rs b/harmony-rs/opnsense-config/src/modules/mod.rs index 87bd11c..8cd675d 100644 --- a/harmony-rs/opnsense-config/src/modules/mod.rs +++ b/harmony-rs/opnsense-config/src/modules/mod.rs @@ -1,2 +1,3 @@ pub mod dhcp; pub mod dns; +pub mod load_balancer;