diff --git a/Cargo.lock b/Cargo.lock index d825f9c..f9c279f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1158,6 +1158,7 @@ dependencies = [ "rust-ipmi", "semver", "serde", + "serde-value", "serde_json", "serde_yaml", "tokio", @@ -1195,6 +1196,9 @@ dependencies = [ [[package]] name = "harmony_types" version = "0.1.0" +dependencies = [ + "serde", +] [[package]] name = "hashbrown" diff --git a/Cargo.toml b/Cargo.toml index 8c7afdd..c36cd27 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ url = "2.5.4" kube = "0.98.0" k8s-openapi = { version = "0.24.0", features = [ "v1_30" ] } serde_yaml = "0.9.34" +serde-value = "0.7.0" http = "1.2.0" [workspace.dependencies.uuid] diff --git a/examples/lamp/src/main.rs b/examples/lamp/src/main.rs index 1251b8c..7277aa5 100644 --- a/examples/lamp/src/main.rs +++ b/examples/lamp/src/main.rs @@ -2,8 +2,7 @@ use harmony::{ data::Version, maestro::Maestro, modules::lamp::{LAMPConfig, LAMPScore}, - score::Score, - topology::{HAClusterTopology, Topology, Url}, + topology::{HAClusterTopology, Url}, }; #[tokio::main] @@ -23,7 +22,3 @@ async fn main() { .await .unwrap(); } - -fn clone_score + Clone + 'static>(score: S) -> Box { - Box::new(score.clone()) -} diff --git a/examples/nanodc/src/main.rs b/examples/nanodc/src/main.rs index 66a5de5..8aad09a 100644 --- a/examples/nanodc/src/main.rs +++ b/examples/nanodc/src/main.rs @@ -1,7 +1,10 @@ use harmony::{ inventory::Inventory, maestro::Maestro, - modules::{dummy::{ErrorScore, PanicScore, SuccessScore}, k8s::deployment::K8sDeploymentScore}, + modules::{ + dummy::{ErrorScore, PanicScore, SuccessScore}, + k8s::deployment::K8sDeploymentScore, + }, topology::HAClusterTopology, }; diff --git a/harmony/Cargo.toml b/harmony/Cargo.toml index f084af0..c5348d9 100644 --- a/harmony/Cargo.toml +++ b/harmony/Cargo.toml @@ -29,3 +29,4 @@ kube = { workspace = true } k8s-openapi = { workspace = true } serde_yaml = { workspace = true } http = { workspace = true } +serde-value = { workspace = true } diff --git a/harmony/src/domain/hardware/mod.rs b/harmony/src/domain/hardware/mod.rs index 47d7a33..8e24768 100644 --- a/harmony/src/domain/hardware/mod.rs +++ b/harmony/src/domain/hardware/mod.rs @@ -2,6 +2,8 @@ use std::sync::Arc; use derive_new::new; use harmony_types::net::MacAddress; +use serde::{Serialize, Serializer, ser::SerializeStruct}; +use serde_value::Value; pub type HostGroup = Vec; pub type SwitchGroup = Vec; @@ -75,10 +77,7 @@ impl PhysicalHost { } pub fn label(mut self, name: String, value: String) -> Self { - self.labels.push(Label { - _name: name, - _value: value, - }); + self.labels.push(Label { name, value }); self } @@ -88,7 +87,49 @@ impl PhysicalHost { } } -#[derive(new)] +// Custom Serialize implementation for PhysicalHost +impl Serialize for PhysicalHost { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // Determine the number of fields + let mut num_fields = 5; // category, network, storage, labels, management + if self.memory_size.is_some() { + num_fields += 1; + } + if self.cpu_count.is_some() { + num_fields += 1; + } + + // Create a serialization structure + let mut state = serializer.serialize_struct("PhysicalHost", num_fields)?; + + // Serialize the standard fields + state.serialize_field("category", &self.category)?; + state.serialize_field("network", &self.network)?; + state.serialize_field("storage", &self.storage)?; + state.serialize_field("labels", &self.labels)?; + + // Serialize optional fields + if let Some(memory) = self.memory_size { + state.serialize_field("memory_size", &memory)?; + } + if let Some(cpu) = self.cpu_count { + state.serialize_field("cpu_count", &cpu)?; + } + + let mgmt_data = self.management.serialize_management(); + // pub management: Arc, + + // Handle management interface - either as a field or flattened + state.serialize_field("management", &mgmt_data)?; + + state.end() + } +} + +#[derive(new, Serialize)] pub struct ManualManagementInterface; impl ManagementInterface for ManualManagementInterface { @@ -101,7 +142,7 @@ impl ManagementInterface for ManualManagementInterface { } } -pub trait ManagementInterface: Send + Sync { +pub trait ManagementInterface: Send + Sync + SerializableManagement { fn boot_to_pxe(&self); fn get_supported_protocol_names(&self) -> String; } @@ -115,21 +156,49 @@ impl std::fmt::Debug for dyn ManagementInterface { } } -#[derive(Debug, Clone)] +// Define a trait for serializing management interfaces +pub trait SerializableManagement { + fn serialize_management(&self) -> Value; +} + +// Provide a blanket implementation for all types that implement both ManagementInterface and Serialize +impl SerializableManagement for T +where + T: ManagementInterface + Serialize, +{ + fn serialize_management(&self) -> Value { + serde_value::to_value(self).expect("ManagementInterface should serialize successfully") + } +} + +#[derive(Debug, Clone, Serialize)] pub enum HostCategory { Server, Firewall, Switch, } -#[derive(Debug, new, Clone)] +#[derive(Debug, new, Clone, Serialize)] pub struct NetworkInterface { pub name: Option, pub mac_address: MacAddress, pub speed: Option, } -#[derive(Debug, new, Clone)] +#[cfg(test)] +use harmony_macros::mac_address; +#[cfg(test)] +impl NetworkInterface { + pub fn dummy() -> Self { + Self { + name: Some(String::new()), + mac_address: mac_address!("00:00:00:00:00:00"), + speed: Some(0), + } + } +} + +#[derive(Debug, new, Clone, Serialize)] pub enum StorageConnectionType { Sata3g, Sata6g, @@ -137,13 +206,13 @@ pub enum StorageConnectionType { Sas12g, PCIE, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub enum StorageKind { SSD, NVME, HDD, } -#[derive(Debug, new, Clone)] +#[derive(Debug, new, Clone, Serialize)] pub struct Storage { pub connection: StorageConnectionType, pub kind: StorageKind, @@ -151,20 +220,33 @@ pub struct Storage { pub serial: String, } -#[derive(Debug, Clone)] +#[cfg(test)] +impl Storage { + pub fn dummy() -> Self { + Self { + connection: StorageConnectionType::Sata3g, + kind: StorageKind::SSD, + size: 0, + serial: String::new(), + } + } +} + +#[derive(Debug, Clone, Serialize)] pub struct Switch { _interface: Vec, _management_interface: NetworkInterface, } -#[derive(Debug, new, Clone)] +#[derive(Debug, new, Clone, Serialize)] pub struct Label { - _name: String, - _value: String, + pub name: String, + pub value: String, } + pub type Address = String; -#[derive(new, Debug)] +#[derive(new, Debug, Serialize)] pub struct Location { pub address: Address, pub name: String, @@ -178,3 +260,158 @@ impl Location { } } } + +#[cfg(test)] +mod tests { + use super::*; + use serde::{Deserialize, Serialize}; + use std::sync::Arc; + + // Mock implementation of ManagementInterface + #[derive(Debug, Clone, Serialize, Deserialize)] + struct MockHPIlo { + ip: String, + username: String, + password: String, + firmware_version: String, + } + + impl ManagementInterface for MockHPIlo { + fn boot_to_pxe(&self) {} + + fn get_supported_protocol_names(&self) -> String { + String::new() + } + } + + // Another mock implementation + #[derive(Debug, Clone, Serialize, Deserialize)] + struct MockDellIdrac { + hostname: String, + port: u16, + api_token: String, + } + + impl ManagementInterface for MockDellIdrac { + fn boot_to_pxe(&self) {} + + fn get_supported_protocol_names(&self) -> String { + String::new() + } + } + + #[test] + fn test_serialize_physical_host_with_hp_ilo() { + // Create a PhysicalHost with HP iLO management + let host = PhysicalHost { + category: HostCategory::Server, + network: vec![NetworkInterface::dummy()], + management: Arc::new(MockHPIlo { + ip: "192.168.1.100".to_string(), + username: "admin".to_string(), + password: "password123".to_string(), + firmware_version: "2.5.0".to_string(), + }), + storage: vec![Storage::dummy()], + labels: vec![Label::new("datacenter".to_string(), "us-east".to_string())], + memory_size: Some(64_000_000), + cpu_count: Some(16), + }; + + // Serialize to JSON + let json = serde_json::to_string(&host).expect("Failed to serialize host"); + + // Check that the serialized JSON contains the HP iLO details + assert!(json.contains("192.168.1.100")); + assert!(json.contains("admin")); + assert!(json.contains("password123")); + assert!(json.contains("firmware_version")); + assert!(json.contains("2.5.0")); + + // Parse back to verify structure (not the exact management interface) + let parsed: serde_json::Value = serde_json::from_str(&json).expect("Failed to parse JSON"); + + // Verify basic structure + assert_eq!(parsed["cpu_count"], 16); + assert_eq!(parsed["memory_size"], 64_000_000); + assert_eq!(parsed["network"][0]["name"], ""); + } + + #[test] + fn test_serialize_physical_host_with_dell_idrac() { + // Create a PhysicalHost with Dell iDRAC management + let host = PhysicalHost { + category: HostCategory::Server, + network: vec![NetworkInterface::dummy()], + management: Arc::new(MockDellIdrac { + hostname: "idrac-server01".to_string(), + port: 443, + api_token: "abcdef123456".to_string(), + }), + storage: vec![Storage::dummy()], + labels: vec![Label::new("env".to_string(), "production".to_string())], + memory_size: Some(128_000_000), + cpu_count: Some(32), + }; + + // Serialize to JSON + let json = serde_json::to_string(&host).expect("Failed to serialize host"); + + // Check that the serialized JSON contains the Dell iDRAC details + assert!(json.contains("idrac-server01")); + assert!(json.contains("443")); + assert!(json.contains("abcdef123456")); + + // Parse back to verify structure + let parsed: serde_json::Value = serde_json::from_str(&json).expect("Failed to parse JSON"); + + // Verify basic structure + assert_eq!(parsed["cpu_count"], 32); + assert_eq!(parsed["memory_size"], 128_000_000); + assert_eq!(parsed["storage"][0]["path"], serde_json::Value::Null); + } + + #[test] + fn test_different_management_implementations_produce_valid_json() { + // Create hosts with different management implementations + let host1 = PhysicalHost { + category: HostCategory::Server, + network: vec![], + management: Arc::new(MockHPIlo { + ip: "10.0.0.1".to_string(), + username: "root".to_string(), + password: "secret".to_string(), + firmware_version: "3.0.0".to_string(), + }), + storage: vec![], + labels: vec![], + memory_size: None, + cpu_count: None, + }; + + let host2 = PhysicalHost { + category: HostCategory::Server, + network: vec![], + management: Arc::new(MockDellIdrac { + hostname: "server02-idrac".to_string(), + port: 8443, + api_token: "token123".to_string(), + }), + storage: vec![], + labels: vec![], + memory_size: None, + cpu_count: None, + }; + + // Both should serialize successfully + let json1 = serde_json::to_string(&host1).expect("Failed to serialize host1"); + let json2 = serde_json::to_string(&host2).expect("Failed to serialize host2"); + + // Both JSONs should be valid and parseable + let _: serde_json::Value = serde_json::from_str(&json1).expect("Invalid JSON for host1"); + let _: serde_json::Value = serde_json::from_str(&json2).expect("Invalid JSON for host2"); + + // The JSONs should be different because they contain different management interfaces + assert_ne!(json1, json2); + } +} diff --git a/harmony/src/domain/interpret/mod.rs b/harmony/src/domain/interpret/mod.rs index 0268ffd..9cec988 100644 --- a/harmony/src/domain/interpret/mod.rs +++ b/harmony/src/domain/interpret/mod.rs @@ -37,7 +37,7 @@ impl std::fmt::Display for InterpretName { } #[async_trait] -pub trait Interpret: std::fmt::Debug + Send { +pub trait Interpret: std::fmt::Debug + Send { async fn execute(&self, inventory: &Inventory, topology: &T) -> Result; fn get_name(&self) -> InterpretName; diff --git a/harmony/src/domain/score.rs b/harmony/src/domain/score.rs index f1b9378..dbe7aa5 100644 --- a/harmony/src/domain/score.rs +++ b/harmony/src/domain/score.rs @@ -1,15 +1,35 @@ +use serde::Serialize; +use serde_value::Value; + use super::{interpret::Interpret, topology::Topology}; -pub trait Score: std::fmt::Debug + Send + Sync + CloneBoxScore { +pub trait Score: + std::fmt::Debug + Send + Sync + CloneBoxScore + SerializeScore +{ fn create_interpret(&self) -> Box>; fn name(&self) -> String; } +pub trait SerializeScore { + fn serialize(&self) -> Value; +} + +impl<'de, S, T> SerializeScore for S +where + T: Topology, + S: Score + Serialize, +{ + fn serialize(&self) -> Value { + // TODO not sure if this is the right place to handle the error or it should bubble + // up? + serde_value::to_value(&self).expect("Score should serialize successfully") + } +} + pub trait CloneBoxScore { fn clone_box(&self) -> Box>; } - impl CloneBoxScore for S where T: Topology, @@ -19,5 +39,3 @@ where Box::new(self.clone()) } } - -pub trait FrontendScore: Score + std::fmt::Display {} diff --git a/harmony/src/domain/topology/ha_cluster.rs b/harmony/src/domain/topology/ha_cluster.rs index b6c1c9a..0e9230b 100644 --- a/harmony/src/domain/topology/ha_cluster.rs +++ b/harmony/src/domain/topology/ha_cluster.rs @@ -334,7 +334,6 @@ impl TftpServer for DummyInfra { #[async_trait] impl HttpServer for DummyInfra { async fn serve_files(&self, _url: &Url) -> Result<(), ExecutorError> { - unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) } fn get_ip(&self) -> IpAddress { diff --git a/harmony/src/domain/topology/host_binding.rs b/harmony/src/domain/topology/host_binding.rs index c01869a..371f159 100644 --- a/harmony/src/domain/topology/host_binding.rs +++ b/harmony/src/domain/topology/host_binding.rs @@ -1,4 +1,5 @@ use derive_new::new; +use serde::Serialize; use crate::hardware::PhysicalHost; @@ -8,7 +9,7 @@ use super::LogicalHost; /// /// This is the only construct that directly maps a logical host to a physical host. /// It serves as a bridge between the logical cluster structure and the physical infrastructure. -#[derive(Debug, new, Clone)] +#[derive(Debug, new, Clone, Serialize)] pub struct HostBinding { /// Reference to the LogicalHost pub logical_host: LogicalHost, diff --git a/harmony/src/domain/topology/load_balancer.rs b/harmony/src/domain/topology/load_balancer.rs index 4ebcba8..afb9092 100644 --- a/harmony/src/domain/topology/load_balancer.rs +++ b/harmony/src/domain/topology/load_balancer.rs @@ -2,6 +2,7 @@ use std::{net::SocketAddr, str::FromStr}; use async_trait::async_trait; use log::debug; +use serde::Serialize; use super::{IpAddress, LogicalHost}; use crate::executors::ExecutorError; @@ -36,20 +37,20 @@ impl std::fmt::Debug for dyn LoadBalancer { f.write_fmt(format_args!("LoadBalancer {}", self.get_ip())) } } -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Clone, Serialize)] pub struct LoadBalancerService { pub backend_servers: Vec, pub listening_port: SocketAddr, pub health_check: Option, } -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Clone, Serialize)] pub struct BackendServer { pub address: String, pub port: u16, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize)] pub enum HttpMethod { GET, POST, @@ -91,14 +92,14 @@ impl std::fmt::Display for HttpMethod { } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize)] pub enum HttpStatusCode { Success2xx, UserError4xx, ServerError5xx, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize)] pub enum HealthCheck { HTTP(String, HttpMethod, HttpStatusCode), TCP(Option), diff --git a/harmony/src/domain/topology/mod.rs b/harmony/src/domain/topology/mod.rs index e1c7f7c..fa0b7fe 100644 --- a/harmony/src/domain/topology/mod.rs +++ b/harmony/src/domain/topology/mod.rs @@ -12,6 +12,7 @@ mod network; pub use host_binding::*; pub use http::*; pub use network::*; +use serde::Serialize; pub use tftp::*; use std::net::IpAddr; @@ -20,8 +21,6 @@ pub trait Topology { fn name(&self) -> &str; } -pub trait Capability {} - pub type IpAddress = IpAddr; #[derive(Debug, Clone)] @@ -30,6 +29,18 @@ pub enum Url { Url(url::Url), } +impl Serialize for Url { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Url::LocalFolder(path) => serializer.serialize_str(path), + Url::Url(url) => serializer.serialize_str(&url.as_str()), + } + } +} + impl std::fmt::Display for Url { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -48,7 +59,7 @@ impl std::fmt::Display for Url { /// - A control plane node /// /// This abstraction focuses on the logical role and services, independent of the physical hardware. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct LogicalHost { /// The IP address of this logical host. pub ip: IpAddress, @@ -130,3 +141,23 @@ fn increment_ip(ip: IpAddress, increment: u32) -> Option { } } } + +#[cfg(test)] +mod tests { + use super::*; + use serde_json; + + #[test] + fn test_serialize_local_folder() { + let url = Url::LocalFolder("path/to/folder".to_string()); + let serialized = serde_json::to_string(&url).unwrap(); + assert_eq!(serialized, "\"path/to/folder\""); + } + + #[test] + fn test_serialize_url() { + let url = Url::Url(url::Url::parse("https://example.com").unwrap()); + let serialized = serde_json::to_string(&url).unwrap(); + assert_eq!(serialized, "\"https://example.com/\""); + } +} diff --git a/harmony/src/domain/topology/network.rs b/harmony/src/domain/topology/network.rs index 6aba5f3..13d1902 100644 --- a/harmony/src/domain/topology/network.rs +++ b/harmony/src/domain/topology/network.rs @@ -2,10 +2,11 @@ use std::{net::Ipv4Addr, str::FromStr, sync::Arc}; use async_trait::async_trait; use harmony_types::net::MacAddress; +use serde::Serialize; use crate::executors::ExecutorError; -use super::{openshift::OpenshiftClient, IpAddress, LogicalHost}; +use super::{IpAddress, LogicalHost, openshift::OpenshiftClient}; #[derive(Debug)] pub struct DHCPStaticEntry { @@ -41,11 +42,10 @@ pub struct NetworkDomain { pub name: String, } #[async_trait] -pub trait OcK8sclient: Send + Sync + std::fmt::Debug { +pub trait OcK8sclient: Send + Sync + std::fmt::Debug { async fn oc_client(&self) -> Result, kube::Error>; } - #[async_trait] pub trait DhcpServer: Send + Sync + std::fmt::Debug { async fn add_static_mapping(&self, entry: &DHCPStaticEntry) -> Result<(), ExecutorError>; @@ -62,11 +62,7 @@ pub trait DhcpServer: Send + Sync + std::fmt::Debug { pub trait DnsServer: Send + Sync { async fn register_dhcp_leases(&self, register: bool) -> Result<(), ExecutorError>; async fn register_hosts(&self, hosts: Vec) -> Result<(), ExecutorError>; - fn remove_record( - &self, - name: &str, - record_type: DnsRecordType, - ) -> Result<(), ExecutorError>; + fn remove_record(&self, name: &str, record_type: DnsRecordType) -> Result<(), ExecutorError>; async fn list_records(&self) -> Vec; fn get_ip(&self) -> IpAddress; fn get_host(&self) -> LogicalHost; @@ -117,7 +113,7 @@ pub enum Action { Deny, } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] pub enum DnsRecordType { A, AAAA, @@ -138,7 +134,7 @@ impl std::fmt::Display for DnsRecordType { } } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] pub struct DnsRecord { pub host: String, pub domain: String, diff --git a/harmony/src/infra/hp_ilo/mod.rs b/harmony/src/infra/hp_ilo/mod.rs index 051f175..ff0e313 100644 --- a/harmony/src/infra/hp_ilo/mod.rs +++ b/harmony/src/infra/hp_ilo/mod.rs @@ -3,8 +3,9 @@ use crate::topology::IpAddress; use derive_new::new; use harmony_types::net::MacAddress; use log::info; +use serde::Serialize; -#[derive(new)] +#[derive(new, Serialize)] pub struct HPIlo { ip_address: Option, mac_address: Option, diff --git a/harmony/src/infra/intel_amt/mod.rs b/harmony/src/infra/intel_amt/mod.rs index 088afd5..7251729 100644 --- a/harmony/src/infra/intel_amt/mod.rs +++ b/harmony/src/infra/intel_amt/mod.rs @@ -2,8 +2,9 @@ use crate::hardware::ManagementInterface; use derive_new::new; use harmony_types::net::MacAddress; use log::info; +use serde::Serialize; -#[derive(new)] +#[derive(new, Serialize)] pub struct IntelAmtManagement { mac_address: MacAddress, } diff --git a/harmony/src/infra/opnsense/management.rs b/harmony/src/infra/opnsense/management.rs index db2bef4..a8dce18 100644 --- a/harmony/src/infra/opnsense/management.rs +++ b/harmony/src/infra/opnsense/management.rs @@ -1,7 +1,8 @@ use crate::hardware::ManagementInterface; use derive_new::new; +use serde::Serialize; -#[derive(new)] +#[derive(new, Serialize)] pub struct OPNSenseManagementInterface {} impl ManagementInterface for OPNSenseManagementInterface { diff --git a/harmony/src/modules/dhcp.rs b/harmony/src/modules/dhcp.rs index e145e03..6ef0c3d 100644 --- a/harmony/src/modules/dhcp.rs +++ b/harmony/src/modules/dhcp.rs @@ -1,7 +1,7 @@ - use async_trait::async_trait; use derive_new::new; use log::info; +use serde::Serialize; use crate::{ domain::{data::Version, interpret::InterpretStatus}, @@ -12,7 +12,7 @@ use crate::{ use crate::domain::score::Score; -#[derive(Debug, new, Clone)] +#[derive(Debug, new, Clone, Serialize)] pub struct DhcpScore { pub host_binding: Vec, pub next_server: Option, @@ -134,7 +134,7 @@ impl DhcpInterpret { } #[async_trait] -impl Interpret for DhcpInterpret { +impl Interpret for DhcpInterpret { fn get_name(&self) -> InterpretName { InterpretName::OPNSenseDHCP } diff --git a/harmony/src/modules/dns.rs b/harmony/src/modules/dns.rs index c96d08e..e52e2f9 100644 --- a/harmony/src/modules/dns.rs +++ b/harmony/src/modules/dns.rs @@ -1,6 +1,7 @@ use async_trait::async_trait; use derive_new::new; use log::info; +use serde::Serialize; use crate::{ data::Version, @@ -10,7 +11,7 @@ use crate::{ topology::{DnsRecord, DnsServer, Topology}, }; -#[derive(Debug, new, Clone)] +#[derive(Debug, new, Clone, Serialize)] pub struct DnsScore { dns_entries: Vec, register_dhcp_leases: Option, diff --git a/harmony/src/modules/dummy.rs b/harmony/src/modules/dummy.rs index e9bd540..2e63797 100644 --- a/harmony/src/modules/dummy.rs +++ b/harmony/src/modules/dummy.rs @@ -1,4 +1,5 @@ use async_trait::async_trait; +use serde::Serialize; use crate::{ data::Version, @@ -10,7 +11,7 @@ use crate::{ /// Score that always errors. This is only useful for development/testing purposes. It does nothing /// except returning Err(InterpretError) when interpreted. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct ErrorScore; impl Score for ErrorScore { @@ -28,7 +29,7 @@ impl Score for ErrorScore { /// Score that always succeeds. This is only useful for development/testing purposes. It does nothing /// except returning Ok(Outcome::success) when interpreted. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct SuccessScore; impl Score for SuccessScore { @@ -81,7 +82,7 @@ impl Interpret for DummyInterpret { /// Score that always panics. This is only useful for development/testing purposes. It does nothing /// except panic! with an error message when interpreted -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct PanicScore; impl Score for PanicScore { diff --git a/harmony/src/modules/http.rs b/harmony/src/modules/http.rs index 419bd89..c98ff8f 100644 --- a/harmony/src/modules/http.rs +++ b/harmony/src/modules/http.rs @@ -1,5 +1,6 @@ use async_trait::async_trait; use derive_new::new; +use serde::Serialize; use crate::{ data::{Id, Version}, @@ -9,7 +10,7 @@ use crate::{ topology::{HttpServer, Topology, Url}, }; -#[derive(Debug, new, Clone)] +#[derive(Debug, new, Clone, Serialize)] pub struct HttpScore { files_to_serve: Url, } diff --git a/harmony/src/modules/k8s/deployment.rs b/harmony/src/modules/k8s/deployment.rs index 4528ed6..cd2ad90 100644 --- a/harmony/src/modules/k8s/deployment.rs +++ b/harmony/src/modules/k8s/deployment.rs @@ -1,17 +1,22 @@ use k8s_openapi::api::apps::v1::Deployment; +use serde::Serialize; use serde_json::json; -use crate::{interpret::Interpret, score::Score, topology::{OcK8sclient, Topology}}; +use crate::{ + interpret::Interpret, + score::Score, + topology::{OcK8sclient, Topology}, +}; use super::resource::{K8sResourceInterpret, K8sResourceScore}; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct K8sDeploymentScore { pub name: String, pub image: String, } -impl Score for K8sDeploymentScore { +impl Score for K8sDeploymentScore { fn create_interpret(&self) -> Box> { let deployment: Deployment = serde_json::from_value(json!( { diff --git a/harmony/src/modules/k8s/resource.rs b/harmony/src/modules/k8s/resource.rs index e33cd7a..505c4a4 100644 --- a/harmony/src/modules/k8s/resource.rs +++ b/harmony/src/modules/k8s/resource.rs @@ -1,7 +1,7 @@ use async_trait::async_trait; use k8s_openapi::NamespaceResourceScope; use kube::Resource; -use serde::de::DeserializeOwned; +use serde::{Serialize, de::DeserializeOwned}; use crate::{ data::{Id, Version}, @@ -11,7 +11,7 @@ use crate::{ topology::{OcK8sclient, Topology}, }; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct K8sResourceScore { pub resource: Vec, } diff --git a/harmony/src/modules/lamp.rs b/harmony/src/modules/lamp.rs index 90f36da..ef7227c 100644 --- a/harmony/src/modules/lamp.rs +++ b/harmony/src/modules/lamp.rs @@ -1,6 +1,7 @@ use std::path::{Path, PathBuf}; use async_trait::async_trait; +use serde::Serialize; use crate::{ data::{Id, Version}, @@ -11,7 +12,7 @@ use crate::{ topology::{OcK8sclient, Topology, Url}, }; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct LAMPScore { pub name: String, pub domain: Url, @@ -19,7 +20,7 @@ pub struct LAMPScore { pub php_version: Version, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct LAMPConfig { pub project_root: PathBuf, pub ssl_enabled: bool, @@ -34,7 +35,7 @@ impl Default for LAMPConfig { } } -impl Score for LAMPScore { +impl Score for LAMPScore { fn create_interpret(&self) -> Box> { todo!() } @@ -50,7 +51,7 @@ pub struct LAMPInterpret { } #[async_trait] -impl Interpret for LAMPInterpret { +impl Interpret for LAMPInterpret { async fn execute( &self, inventory: &Inventory, diff --git a/harmony/src/modules/load_balancer.rs b/harmony/src/modules/load_balancer.rs index 91ff16b..cd78f84 100644 --- a/harmony/src/modules/load_balancer.rs +++ b/harmony/src/modules/load_balancer.rs @@ -1,15 +1,16 @@ use async_trait::async_trait; use log::info; +use serde::Serialize; use crate::{ data::{Id, Version}, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, - score::{FrontendScore, Score}, + score::Score, topology::{LoadBalancer, LoadBalancerService, Topology}, }; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct LoadBalancerScore { pub public_services: Vec, pub private_services: Vec, @@ -19,8 +20,6 @@ pub struct LoadBalancerScore { // uuid? } -impl FrontendScore for LoadBalancerScore {} - impl Score for LoadBalancerScore { fn create_interpret(&self) -> Box> { Box::new(LoadBalancerInterpret::new(self.clone())) diff --git a/harmony/src/modules/mod.rs b/harmony/src/modules/mod.rs index 51e164f..8456867 100644 --- a/harmony/src/modules/mod.rs +++ b/harmony/src/modules/mod.rs @@ -3,8 +3,8 @@ pub mod dns; pub mod dummy; pub mod http; pub mod k8s; +pub mod lamp; pub mod load_balancer; pub mod okd; pub mod opnsense; pub mod tftp; -pub mod lamp; diff --git a/harmony/src/modules/okd/bootstrap_dhcp.rs b/harmony/src/modules/okd/bootstrap_dhcp.rs index ebb0a9d..2e3dd6f 100644 --- a/harmony/src/modules/okd/bootstrap_dhcp.rs +++ b/harmony/src/modules/okd/bootstrap_dhcp.rs @@ -1,3 +1,5 @@ +use serde::Serialize; + use crate::{ interpret::Interpret, inventory::Inventory, @@ -6,7 +8,7 @@ use crate::{ topology::{DhcpServer, HAClusterTopology, HostBinding, Topology}, }; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct OKDBootstrapDhcpScore { dhcp_score: DhcpScore, } diff --git a/harmony/src/modules/okd/bootstrap_load_balancer.rs b/harmony/src/modules/okd/bootstrap_load_balancer.rs index d983eed..d6cd2f3 100644 --- a/harmony/src/modules/okd/bootstrap_load_balancer.rs +++ b/harmony/src/modules/okd/bootstrap_load_balancer.rs @@ -1,15 +1,18 @@ use std::net::SocketAddr; +use serde::Serialize; + use crate::{ interpret::Interpret, modules::load_balancer::LoadBalancerScore, score::Score, topology::{ - BackendServer, HAClusterTopology, HealthCheck, HttpMethod, HttpStatusCode, LoadBalancer, LoadBalancerService, Topology + BackendServer, HAClusterTopology, HealthCheck, HttpMethod, HttpStatusCode, LoadBalancer, + LoadBalancerService, Topology, }, }; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct OKDBootstrapLoadBalancerScore { load_balancer_score: LoadBalancerScore, } diff --git a/harmony/src/modules/okd/dhcp.rs b/harmony/src/modules/okd/dhcp.rs index e3e0e09..a060b31 100644 --- a/harmony/src/modules/okd/dhcp.rs +++ b/harmony/src/modules/okd/dhcp.rs @@ -1,3 +1,5 @@ +use serde::Serialize; + use crate::{ interpret::Interpret, inventory::Inventory, @@ -6,7 +8,7 @@ use crate::{ topology::{DhcpServer, HAClusterTopology, HostBinding, Topology}, }; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct OKDDhcpScore { dhcp_score: DhcpScore, } diff --git a/harmony/src/modules/okd/dns.rs b/harmony/src/modules/okd/dns.rs index 34c2dc2..6f99cb8 100644 --- a/harmony/src/modules/okd/dns.rs +++ b/harmony/src/modules/okd/dns.rs @@ -1,3 +1,5 @@ +use serde::Serialize; + use crate::{ interpret::Interpret, modules::dns::DnsScore, @@ -5,7 +7,7 @@ use crate::{ topology::{DnsRecord, DnsRecordType, DnsServer, HAClusterTopology, Topology}, }; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct OKDDnsScore { dns_score: DnsScore, } diff --git a/harmony/src/modules/okd/load_balancer.rs b/harmony/src/modules/okd/load_balancer.rs index 47185e1..0345d46 100644 --- a/harmony/src/modules/okd/load_balancer.rs +++ b/harmony/src/modules/okd/load_balancer.rs @@ -1,11 +1,14 @@ use std::net::SocketAddr; +use serde::Serialize; + use crate::{ interpret::Interpret, modules::load_balancer::LoadBalancerScore, - score::{FrontendScore, Score}, + score::Score, topology::{ - BackendServer, HAClusterTopology, HealthCheck, HttpMethod, HttpStatusCode, LoadBalancer, LoadBalancerService, Topology + BackendServer, HAClusterTopology, HealthCheck, HttpMethod, HttpStatusCode, LoadBalancer, + LoadBalancerService, Topology, }, }; @@ -15,9 +18,7 @@ impl std::fmt::Display for OKDLoadBalancerScore { } } -impl FrontendScore for OKDLoadBalancerScore {} - -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct OKDLoadBalancerScore { load_balancer_score: LoadBalancerScore, } diff --git a/harmony/src/modules/opnsense/mod.rs b/harmony/src/modules/opnsense/mod.rs index 763195d..28b52cf 100644 --- a/harmony/src/modules/opnsense/mod.rs +++ b/harmony/src/modules/opnsense/mod.rs @@ -1,6 +1,4 @@ - mod shell; mod upgrade; pub use shell::*; pub use upgrade::*; - diff --git a/harmony/src/modules/opnsense/shell.rs b/harmony/src/modules/opnsense/shell.rs index 225e2ad..a35a43c 100644 --- a/harmony/src/modules/opnsense/shell.rs +++ b/harmony/src/modules/opnsense/shell.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use async_trait::async_trait; +use serde::Serialize; use tokio::sync::RwLock; use crate::{ @@ -13,10 +14,27 @@ use crate::{ #[derive(Debug, Clone)] pub struct OPNsenseShellCommandScore { + // TODO I am pretty sure we should not hold a direct reference to the + // opnsense_config::Config here. + // This causes a problem with serialization but also could cause many more problems as this + // is mixing concerns of configuration (which is the Responsibility of Scores to define) + // and state/execution which is the responsibility of interprets via topologies to manage + // + // I feel like a better solution would be for this Score/Interpret to require + // Topology + OPNSenseShell trait bindings pub opnsense: Arc>, pub command: String, } +impl Serialize for OPNsenseShellCommandScore { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + todo!("See comment about moving opnsense_config::Config outside the score") + } +} + impl Score for OPNsenseShellCommandScore { fn create_interpret(&self) -> Box> { Box::new(OPNsenseShellInterpret { @@ -37,7 +55,7 @@ pub struct OPNsenseShellInterpret { } #[async_trait] -impl Interpret for OPNsenseShellInterpret { +impl Interpret for OPNsenseShellInterpret { async fn execute( &self, _inventory: &Inventory, diff --git a/harmony/src/modules/opnsense/upgrade.rs b/harmony/src/modules/opnsense/upgrade.rs index 06d373f..45adf12 100644 --- a/harmony/src/modules/opnsense/upgrade.rs +++ b/harmony/src/modules/opnsense/upgrade.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use serde::Serialize; use tokio::sync::RwLock; use crate::{ @@ -15,6 +16,15 @@ pub struct OPNSenseLaunchUpgrade { pub opnsense: Arc>, } +impl Serialize for OPNSenseLaunchUpgrade { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + todo!("See comment in OPNSenseShellCommandScore and apply the same idea here") + } +} + impl Score for OPNSenseLaunchUpgrade { fn create_interpret(&self) -> Box> { let score = OPNsenseShellCommandScore { diff --git a/harmony/src/modules/tftp.rs b/harmony/src/modules/tftp.rs index 3e90fc0..357e480 100644 --- a/harmony/src/modules/tftp.rs +++ b/harmony/src/modules/tftp.rs @@ -1,5 +1,6 @@ use async_trait::async_trait; use derive_new::new; +use serde::Serialize; use crate::{ data::{Id, Version}, @@ -9,12 +10,12 @@ use crate::{ topology::{Router, TftpServer, Topology, Url}, }; -#[derive(Debug, new, Clone)] +#[derive(Debug, new, Clone, Serialize)] pub struct TftpScore { files_to_serve: Url, } -impl Score for TftpScore { +impl Score for TftpScore { fn create_interpret(&self) -> Box> { Box::new(TftpInterpret::new(self.clone())) } @@ -30,7 +31,7 @@ pub struct TftpInterpret { } #[async_trait] -impl Interpret for TftpInterpret { +impl Interpret for TftpInterpret { async fn execute( &self, _inventory: &Inventory, diff --git a/harmony_tui/src/widget/score.rs b/harmony_tui/src/widget/score.rs index ea62249..b0d2c27 100644 --- a/harmony_tui/src/widget/score.rs +++ b/harmony_tui/src/widget/score.rs @@ -1,10 +1,13 @@ use std::sync::{Arc, RwLock}; use crossterm::event::{Event, KeyCode, KeyEventKind}; -use harmony::{modules::okd::load_balancer::OKDLoadBalancerScore, score::Score, topology::{LoadBalancer, Topology}}; +use harmony::{score::Score, topology::Topology}; use log::{info, warn}; use ratatui::{ - layout::Rect, style::{Style, Stylize}, widgets::{List, ListItem, ListState, StatefulWidget, Widget}, Frame + Frame, + layout::Rect, + style::{Style, Stylize}, + widgets::{List, ListItem, ListState, StatefulWidget, Widget}, }; use tokio::sync::mpsc; @@ -23,19 +26,20 @@ struct Execution { score: Box>, } -impl FrontendScore for OKDLoadBalancerScore {} - #[derive(Debug)] pub(crate) struct ScoreListWidget { list_state: Arc>, - scores: Vec>>, + scores: Vec>>, execution: Option>, execution_history: Vec>, sender: mpsc::Sender>, } impl ScoreListWidget { - pub(crate) fn new(scores: Vec>>, sender: mpsc::Sender>) -> Self { + pub(crate) fn new( + scores: Vec>>, + sender: mpsc::Sender>, + ) -> Self { let mut list_state = ListState::default(); list_state.select_first(); let list_state = Arc::new(RwLock::new(list_state)); @@ -141,4 +145,3 @@ impl Widget for &ScoreListWidget { fn score_to_list_item<'a, T: Topology>(score: &'a Box>) -> ListItem<'a> { ListItem::new(score.name()) } - diff --git a/harmony_types/Cargo.toml b/harmony_types/Cargo.toml index 9b2f97a..0b8c068 100644 --- a/harmony_types/Cargo.toml +++ b/harmony_types/Cargo.toml @@ -4,3 +4,6 @@ edition = "2024" version.workspace = true readme.workspace = true license.workspace = true + +[dependencies] +serde = { version = "1.0.209", features = ["derive"] } diff --git a/harmony_types/src/lib.rs b/harmony_types/src/lib.rs index 71dbbaf..9f4930d 100644 --- a/harmony_types/src/lib.rs +++ b/harmony_types/src/lib.rs @@ -1,5 +1,7 @@ pub mod net { - #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] + use serde::Serialize; + + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize)] pub struct MacAddress(pub [u8; 6]); impl MacAddress { diff --git a/opnsense-config/src/config/config.rs b/opnsense-config/src/config/config.rs index 3f71d55..10dab61 100644 --- a/opnsense-config/src/config/config.rs +++ b/opnsense-config/src/config/config.rs @@ -11,6 +11,7 @@ use crate::{ use log::{debug, info, trace, warn}; use opnsense_config_xml::OPNsense; use russh::client; +use serde::Serialize; use super::{ConfigManager, OPNsenseShell}; @@ -21,6 +22,15 @@ pub struct Config { shell: Arc, } +impl Serialize for Config { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + todo!() + } +} + impl Config { pub async fn new( repository: Arc,