feat: add serde derive to Score types

This commit adds `serde` dependency and derives `Serialize` trait for `Score` types. This is necessary for serialization and deserialization of these types, which is required to display Scores to various user interfaces

- Added `serde` dependency to `harmony_types/Cargo.toml`.
- Added `serde::Serialize` derive macro to `MacAddress` in `harmony_types/src/lib.rs`.
- Added `serde::Serialize` derive macro to `Config` in `opnsense-config/src/config/config.rs`.
- Added `serde::Serialize` derive macro to `Score` in `harmony_types/src/lib.rs`.
- Added `serde::Serialize` derive macro to `Config` and `Score` in relevant modules.
- Added placeholder `todo!()` implementations for `serialize` methods. These will be implemented in future commits.
This commit is contained in:
Jean-Gabriel Gill-Couture 2025-04-05 14:33:39 -04:00
parent ab9b7476a4
commit b4cc5cff4f
38 changed files with 450 additions and 97 deletions

4
Cargo.lock generated
View File

@ -1158,6 +1158,7 @@ dependencies = [
"rust-ipmi", "rust-ipmi",
"semver", "semver",
"serde", "serde",
"serde-value",
"serde_json", "serde_json",
"serde_yaml", "serde_yaml",
"tokio", "tokio",
@ -1195,6 +1196,9 @@ dependencies = [
[[package]] [[package]]
name = "harmony_types" name = "harmony_types"
version = "0.1.0" version = "0.1.0"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"

View File

@ -30,6 +30,7 @@ url = "2.5.4"
kube = "0.98.0" kube = "0.98.0"
k8s-openapi = { version = "0.24.0", features = [ "v1_30" ] } k8s-openapi = { version = "0.24.0", features = [ "v1_30" ] }
serde_yaml = "0.9.34" serde_yaml = "0.9.34"
serde-value = "0.7.0"
http = "1.2.0" http = "1.2.0"
[workspace.dependencies.uuid] [workspace.dependencies.uuid]

View File

@ -2,8 +2,7 @@ use harmony::{
data::Version, data::Version,
maestro::Maestro, maestro::Maestro,
modules::lamp::{LAMPConfig, LAMPScore}, modules::lamp::{LAMPConfig, LAMPScore},
score::Score, topology::{HAClusterTopology, Url},
topology::{HAClusterTopology, Topology, Url},
}; };
#[tokio::main] #[tokio::main]
@ -23,7 +22,3 @@ async fn main() {
.await .await
.unwrap(); .unwrap();
} }
fn clone_score<T: Topology, S: Score<T> + Clone + 'static>(score: S) -> Box<S> {
Box::new(score.clone())
}

View File

@ -1,7 +1,10 @@
use harmony::{ use harmony::{
inventory::Inventory, inventory::Inventory,
maestro::Maestro, maestro::Maestro,
modules::{dummy::{ErrorScore, PanicScore, SuccessScore}, k8s::deployment::K8sDeploymentScore}, modules::{
dummy::{ErrorScore, PanicScore, SuccessScore},
k8s::deployment::K8sDeploymentScore,
},
topology::HAClusterTopology, topology::HAClusterTopology,
}; };

View File

@ -29,3 +29,4 @@ kube = { workspace = true }
k8s-openapi = { workspace = true } k8s-openapi = { workspace = true }
serde_yaml = { workspace = true } serde_yaml = { workspace = true }
http = { workspace = true } http = { workspace = true }
serde-value = { workspace = true }

View File

@ -2,6 +2,8 @@ use std::sync::Arc;
use derive_new::new; use derive_new::new;
use harmony_types::net::MacAddress; use harmony_types::net::MacAddress;
use serde::{Serialize, Serializer, ser::SerializeStruct};
use serde_value::Value;
pub type HostGroup = Vec<PhysicalHost>; pub type HostGroup = Vec<PhysicalHost>;
pub type SwitchGroup = Vec<Switch>; pub type SwitchGroup = Vec<Switch>;
@ -75,10 +77,7 @@ impl PhysicalHost {
} }
pub fn label(mut self, name: String, value: String) -> Self { pub fn label(mut self, name: String, value: String) -> Self {
self.labels.push(Label { self.labels.push(Label { name, value });
_name: name,
_value: value,
});
self self
} }
@ -88,7 +87,49 @@ impl PhysicalHost {
} }
} }
#[derive(new)] // Custom Serialize implementation for PhysicalHost
impl Serialize for PhysicalHost {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<dyn ManagementInterface>,
// Handle management interface - either as a field or flattened
state.serialize_field("management", &mgmt_data)?;
state.end()
}
}
#[derive(new, Serialize)]
pub struct ManualManagementInterface; pub struct ManualManagementInterface;
impl ManagementInterface for 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 boot_to_pxe(&self);
fn get_supported_protocol_names(&self) -> String; 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<T> 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 { pub enum HostCategory {
Server, Server,
Firewall, Firewall,
Switch, Switch,
} }
#[derive(Debug, new, Clone)] #[derive(Debug, new, Clone, Serialize)]
pub struct NetworkInterface { pub struct NetworkInterface {
pub name: Option<String>, pub name: Option<String>,
pub mac_address: MacAddress, pub mac_address: MacAddress,
pub speed: Option<u64>, pub speed: Option<u64>,
} }
#[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 { pub enum StorageConnectionType {
Sata3g, Sata3g,
Sata6g, Sata6g,
@ -137,13 +206,13 @@ pub enum StorageConnectionType {
Sas12g, Sas12g,
PCIE, PCIE,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize)]
pub enum StorageKind { pub enum StorageKind {
SSD, SSD,
NVME, NVME,
HDD, HDD,
} }
#[derive(Debug, new, Clone)] #[derive(Debug, new, Clone, Serialize)]
pub struct Storage { pub struct Storage {
pub connection: StorageConnectionType, pub connection: StorageConnectionType,
pub kind: StorageKind, pub kind: StorageKind,
@ -151,20 +220,33 @@ pub struct Storage {
pub serial: String, 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 { pub struct Switch {
_interface: Vec<NetworkInterface>, _interface: Vec<NetworkInterface>,
_management_interface: NetworkInterface, _management_interface: NetworkInterface,
} }
#[derive(Debug, new, Clone)] #[derive(Debug, new, Clone, Serialize)]
pub struct Label { pub struct Label {
_name: String, pub name: String,
_value: String, pub value: String,
} }
pub type Address = String; pub type Address = String;
#[derive(new, Debug)] #[derive(new, Debug, Serialize)]
pub struct Location { pub struct Location {
pub address: Address, pub address: Address,
pub name: String, 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);
}
}

View File

@ -37,7 +37,7 @@ impl std::fmt::Display for InterpretName {
} }
#[async_trait] #[async_trait]
pub trait Interpret<T: Topology>: std::fmt::Debug + Send { pub trait Interpret<T>: std::fmt::Debug + Send {
async fn execute(&self, inventory: &Inventory, topology: &T) async fn execute(&self, inventory: &Inventory, topology: &T)
-> Result<Outcome, InterpretError>; -> Result<Outcome, InterpretError>;
fn get_name(&self) -> InterpretName; fn get_name(&self) -> InterpretName;

View File

@ -1,15 +1,35 @@
use serde::Serialize;
use serde_value::Value;
use super::{interpret::Interpret, topology::Topology}; use super::{interpret::Interpret, topology::Topology};
pub trait Score<T: Topology>: std::fmt::Debug + Send + Sync + CloneBoxScore<T> { pub trait Score<T: Topology>:
std::fmt::Debug + Send + Sync + CloneBoxScore<T> + SerializeScore<T>
{
fn create_interpret(&self) -> Box<dyn Interpret<T>>; fn create_interpret(&self) -> Box<dyn Interpret<T>>;
fn name(&self) -> String; fn name(&self) -> String;
} }
pub trait SerializeScore<T: Topology> {
fn serialize(&self) -> Value;
}
impl<'de, S, T> SerializeScore<T> for S
where
T: Topology,
S: Score<T> + 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<T: Topology> { pub trait CloneBoxScore<T: Topology> {
fn clone_box(&self) -> Box<dyn Score<T>>; fn clone_box(&self) -> Box<dyn Score<T>>;
} }
impl<S, T> CloneBoxScore<T> for S impl<S, T> CloneBoxScore<T> for S
where where
T: Topology, T: Topology,
@ -19,5 +39,3 @@ where
Box::new(self.clone()) Box::new(self.clone())
} }
} }
pub trait FrontendScore<T: Topology>: Score<T> + std::fmt::Display {}

View File

@ -334,7 +334,6 @@ impl TftpServer for DummyInfra {
#[async_trait] #[async_trait]
impl HttpServer for DummyInfra { impl HttpServer for DummyInfra {
async fn serve_files(&self, _url: &Url) -> Result<(), ExecutorError> { async fn serve_files(&self, _url: &Url) -> Result<(), ExecutorError> {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
} }
fn get_ip(&self) -> IpAddress { fn get_ip(&self) -> IpAddress {

View File

@ -1,4 +1,5 @@
use derive_new::new; use derive_new::new;
use serde::Serialize;
use crate::hardware::PhysicalHost; 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. /// 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. /// 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 { pub struct HostBinding {
/// Reference to the LogicalHost /// Reference to the LogicalHost
pub logical_host: LogicalHost, pub logical_host: LogicalHost,

View File

@ -2,6 +2,7 @@ use std::{net::SocketAddr, str::FromStr};
use async_trait::async_trait; use async_trait::async_trait;
use log::debug; use log::debug;
use serde::Serialize;
use super::{IpAddress, LogicalHost}; use super::{IpAddress, LogicalHost};
use crate::executors::ExecutorError; use crate::executors::ExecutorError;
@ -36,20 +37,20 @@ impl std::fmt::Debug for dyn LoadBalancer {
f.write_fmt(format_args!("LoadBalancer {}", self.get_ip())) f.write_fmt(format_args!("LoadBalancer {}", self.get_ip()))
} }
} }
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone, Serialize)]
pub struct LoadBalancerService { pub struct LoadBalancerService {
pub backend_servers: Vec<BackendServer>, pub backend_servers: Vec<BackendServer>,
pub listening_port: SocketAddr, pub listening_port: SocketAddr,
pub health_check: Option<HealthCheck>, pub health_check: Option<HealthCheck>,
} }
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone, Serialize)]
pub struct BackendServer { pub struct BackendServer {
pub address: String, pub address: String,
pub port: u16, pub port: u16,
} }
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, Serialize)]
pub enum HttpMethod { pub enum HttpMethod {
GET, GET,
POST, POST,
@ -91,14 +92,14 @@ impl std::fmt::Display for HttpMethod {
} }
} }
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, Serialize)]
pub enum HttpStatusCode { pub enum HttpStatusCode {
Success2xx, Success2xx,
UserError4xx, UserError4xx,
ServerError5xx, ServerError5xx,
} }
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, Serialize)]
pub enum HealthCheck { pub enum HealthCheck {
HTTP(String, HttpMethod, HttpStatusCode), HTTP(String, HttpMethod, HttpStatusCode),
TCP(Option<u16>), TCP(Option<u16>),

View File

@ -12,6 +12,7 @@ mod network;
pub use host_binding::*; pub use host_binding::*;
pub use http::*; pub use http::*;
pub use network::*; pub use network::*;
use serde::Serialize;
pub use tftp::*; pub use tftp::*;
use std::net::IpAddr; use std::net::IpAddr;
@ -20,8 +21,6 @@ pub trait Topology {
fn name(&self) -> &str; fn name(&self) -> &str;
} }
pub trait Capability {}
pub type IpAddress = IpAddr; pub type IpAddress = IpAddr;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -30,6 +29,18 @@ pub enum Url {
Url(url::Url), Url(url::Url),
} }
impl Serialize for Url {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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 { impl std::fmt::Display for Url {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
@ -48,7 +59,7 @@ impl std::fmt::Display for Url {
/// - A control plane node /// - A control plane node
/// ///
/// This abstraction focuses on the logical role and services, independent of the physical hardware. /// This abstraction focuses on the logical role and services, independent of the physical hardware.
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize)]
pub struct LogicalHost { pub struct LogicalHost {
/// The IP address of this logical host. /// The IP address of this logical host.
pub ip: IpAddress, pub ip: IpAddress,
@ -130,3 +141,23 @@ fn increment_ip(ip: IpAddress, increment: u32) -> Option<IpAddress> {
} }
} }
} }
#[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/\"");
}
}

View File

@ -2,10 +2,11 @@ use std::{net::Ipv4Addr, str::FromStr, sync::Arc};
use async_trait::async_trait; use async_trait::async_trait;
use harmony_types::net::MacAddress; use harmony_types::net::MacAddress;
use serde::Serialize;
use crate::executors::ExecutorError; use crate::executors::ExecutorError;
use super::{openshift::OpenshiftClient, IpAddress, LogicalHost}; use super::{IpAddress, LogicalHost, openshift::OpenshiftClient};
#[derive(Debug)] #[derive(Debug)]
pub struct DHCPStaticEntry { pub struct DHCPStaticEntry {
@ -45,7 +46,6 @@ pub trait OcK8sclient: Send + Sync + std::fmt::Debug {
async fn oc_client(&self) -> Result<Arc<OpenshiftClient>, kube::Error>; async fn oc_client(&self) -> Result<Arc<OpenshiftClient>, kube::Error>;
} }
#[async_trait] #[async_trait]
pub trait DhcpServer: Send + Sync + std::fmt::Debug { pub trait DhcpServer: Send + Sync + std::fmt::Debug {
async fn add_static_mapping(&self, entry: &DHCPStaticEntry) -> Result<(), ExecutorError>; 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 { pub trait DnsServer: Send + Sync {
async fn register_dhcp_leases(&self, register: bool) -> Result<(), ExecutorError>; async fn register_dhcp_leases(&self, register: bool) -> Result<(), ExecutorError>;
async fn register_hosts(&self, hosts: Vec<DnsRecord>) -> Result<(), ExecutorError>; async fn register_hosts(&self, hosts: Vec<DnsRecord>) -> Result<(), ExecutorError>;
fn remove_record( fn remove_record(&self, name: &str, record_type: DnsRecordType) -> Result<(), ExecutorError>;
&self,
name: &str,
record_type: DnsRecordType,
) -> Result<(), ExecutorError>;
async fn list_records(&self) -> Vec<DnsRecord>; async fn list_records(&self) -> Vec<DnsRecord>;
fn get_ip(&self) -> IpAddress; fn get_ip(&self) -> IpAddress;
fn get_host(&self) -> LogicalHost; fn get_host(&self) -> LogicalHost;
@ -117,7 +113,7 @@ pub enum Action {
Deny, Deny,
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub enum DnsRecordType { pub enum DnsRecordType {
A, A,
AAAA, 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 struct DnsRecord {
pub host: String, pub host: String,
pub domain: String, pub domain: String,

View File

@ -3,8 +3,9 @@ use crate::topology::IpAddress;
use derive_new::new; use derive_new::new;
use harmony_types::net::MacAddress; use harmony_types::net::MacAddress;
use log::info; use log::info;
use serde::Serialize;
#[derive(new)] #[derive(new, Serialize)]
pub struct HPIlo { pub struct HPIlo {
ip_address: Option<IpAddress>, ip_address: Option<IpAddress>,
mac_address: Option<MacAddress>, mac_address: Option<MacAddress>,

View File

@ -2,8 +2,9 @@ use crate::hardware::ManagementInterface;
use derive_new::new; use derive_new::new;
use harmony_types::net::MacAddress; use harmony_types::net::MacAddress;
use log::info; use log::info;
use serde::Serialize;
#[derive(new)] #[derive(new, Serialize)]
pub struct IntelAmtManagement { pub struct IntelAmtManagement {
mac_address: MacAddress, mac_address: MacAddress,
} }

View File

@ -1,7 +1,8 @@
use crate::hardware::ManagementInterface; use crate::hardware::ManagementInterface;
use derive_new::new; use derive_new::new;
use serde::Serialize;
#[derive(new)] #[derive(new, Serialize)]
pub struct OPNSenseManagementInterface {} pub struct OPNSenseManagementInterface {}
impl ManagementInterface for OPNSenseManagementInterface { impl ManagementInterface for OPNSenseManagementInterface {

View File

@ -1,7 +1,7 @@
use async_trait::async_trait; use async_trait::async_trait;
use derive_new::new; use derive_new::new;
use log::info; use log::info;
use serde::Serialize;
use crate::{ use crate::{
domain::{data::Version, interpret::InterpretStatus}, domain::{data::Version, interpret::InterpretStatus},
@ -12,7 +12,7 @@ use crate::{
use crate::domain::score::Score; use crate::domain::score::Score;
#[derive(Debug, new, Clone)] #[derive(Debug, new, Clone, Serialize)]
pub struct DhcpScore { pub struct DhcpScore {
pub host_binding: Vec<HostBinding>, pub host_binding: Vec<HostBinding>,
pub next_server: Option<IpAddress>, pub next_server: Option<IpAddress>,
@ -134,7 +134,7 @@ impl DhcpInterpret {
} }
#[async_trait] #[async_trait]
impl<T: Topology + DhcpServer> Interpret<T> for DhcpInterpret { impl<T: DhcpServer> Interpret<T> for DhcpInterpret {
fn get_name(&self) -> InterpretName { fn get_name(&self) -> InterpretName {
InterpretName::OPNSenseDHCP InterpretName::OPNSenseDHCP
} }

View File

@ -1,6 +1,7 @@
use async_trait::async_trait; use async_trait::async_trait;
use derive_new::new; use derive_new::new;
use log::info; use log::info;
use serde::Serialize;
use crate::{ use crate::{
data::Version, data::Version,
@ -10,7 +11,7 @@ use crate::{
topology::{DnsRecord, DnsServer, Topology}, topology::{DnsRecord, DnsServer, Topology},
}; };
#[derive(Debug, new, Clone)] #[derive(Debug, new, Clone, Serialize)]
pub struct DnsScore { pub struct DnsScore {
dns_entries: Vec<DnsRecord>, dns_entries: Vec<DnsRecord>,
register_dhcp_leases: Option<bool>, register_dhcp_leases: Option<bool>,

View File

@ -1,4 +1,5 @@
use async_trait::async_trait; use async_trait::async_trait;
use serde::Serialize;
use crate::{ use crate::{
data::Version, data::Version,
@ -10,7 +11,7 @@ use crate::{
/// Score that always errors. This is only useful for development/testing purposes. It does nothing /// Score that always errors. This is only useful for development/testing purposes. It does nothing
/// except returning Err(InterpretError) when interpreted. /// except returning Err(InterpretError) when interpreted.
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize)]
pub struct ErrorScore; pub struct ErrorScore;
impl<T: Topology> Score<T> for ErrorScore { impl<T: Topology> Score<T> for ErrorScore {
@ -28,7 +29,7 @@ impl<T: Topology> Score<T> for ErrorScore {
/// Score that always succeeds. This is only useful for development/testing purposes. It does nothing /// Score that always succeeds. This is only useful for development/testing purposes. It does nothing
/// except returning Ok(Outcome::success) when interpreted. /// except returning Ok(Outcome::success) when interpreted.
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize)]
pub struct SuccessScore; pub struct SuccessScore;
impl<T: Topology> Score<T> for SuccessScore { impl<T: Topology> Score<T> for SuccessScore {
@ -81,7 +82,7 @@ impl<T: Topology> Interpret<T> for DummyInterpret {
/// Score that always panics. This is only useful for development/testing purposes. It does nothing /// Score that always panics. This is only useful for development/testing purposes. It does nothing
/// except panic! with an error message when interpreted /// except panic! with an error message when interpreted
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize)]
pub struct PanicScore; pub struct PanicScore;
impl<T: Topology> Score<T> for PanicScore { impl<T: Topology> Score<T> for PanicScore {

View File

@ -1,5 +1,6 @@
use async_trait::async_trait; use async_trait::async_trait;
use derive_new::new; use derive_new::new;
use serde::Serialize;
use crate::{ use crate::{
data::{Id, Version}, data::{Id, Version},
@ -9,7 +10,7 @@ use crate::{
topology::{HttpServer, Topology, Url}, topology::{HttpServer, Topology, Url},
}; };
#[derive(Debug, new, Clone)] #[derive(Debug, new, Clone, Serialize)]
pub struct HttpScore { pub struct HttpScore {
files_to_serve: Url, files_to_serve: Url,
} }

View File

@ -1,11 +1,16 @@
use k8s_openapi::api::apps::v1::Deployment; use k8s_openapi::api::apps::v1::Deployment;
use serde::Serialize;
use serde_json::json; 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}; use super::resource::{K8sResourceInterpret, K8sResourceScore};
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize)]
pub struct K8sDeploymentScore { pub struct K8sDeploymentScore {
pub name: String, pub name: String,
pub image: String, pub image: String,

View File

@ -1,7 +1,7 @@
use async_trait::async_trait; use async_trait::async_trait;
use k8s_openapi::NamespaceResourceScope; use k8s_openapi::NamespaceResourceScope;
use kube::Resource; use kube::Resource;
use serde::de::DeserializeOwned; use serde::{Serialize, de::DeserializeOwned};
use crate::{ use crate::{
data::{Id, Version}, data::{Id, Version},
@ -11,7 +11,7 @@ use crate::{
topology::{OcK8sclient, Topology}, topology::{OcK8sclient, Topology},
}; };
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize)]
pub struct K8sResourceScore<K: Resource + std::fmt::Debug> { pub struct K8sResourceScore<K: Resource + std::fmt::Debug> {
pub resource: Vec<K>, pub resource: Vec<K>,
} }

View File

@ -1,6 +1,7 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use async_trait::async_trait; use async_trait::async_trait;
use serde::Serialize;
use crate::{ use crate::{
data::{Id, Version}, data::{Id, Version},
@ -11,7 +12,7 @@ use crate::{
topology::{OcK8sclient, Topology, Url}, topology::{OcK8sclient, Topology, Url},
}; };
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize)]
pub struct LAMPScore { pub struct LAMPScore {
pub name: String, pub name: String,
pub domain: Url, pub domain: Url,
@ -19,7 +20,7 @@ pub struct LAMPScore {
pub php_version: Version, pub php_version: Version,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize)]
pub struct LAMPConfig { pub struct LAMPConfig {
pub project_root: PathBuf, pub project_root: PathBuf,
pub ssl_enabled: bool, pub ssl_enabled: bool,

View File

@ -1,15 +1,16 @@
use async_trait::async_trait; use async_trait::async_trait;
use log::info; use log::info;
use serde::Serialize;
use crate::{ use crate::{
data::{Id, Version}, data::{Id, Version},
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
inventory::Inventory, inventory::Inventory,
score::{FrontendScore, Score}, score::Score,
topology::{LoadBalancer, LoadBalancerService, Topology}, topology::{LoadBalancer, LoadBalancerService, Topology},
}; };
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize)]
pub struct LoadBalancerScore { pub struct LoadBalancerScore {
pub public_services: Vec<LoadBalancerService>, pub public_services: Vec<LoadBalancerService>,
pub private_services: Vec<LoadBalancerService>, pub private_services: Vec<LoadBalancerService>,
@ -19,8 +20,6 @@ pub struct LoadBalancerScore {
// uuid? // uuid?
} }
impl <T: Topology + LoadBalancer> FrontendScore<T> for LoadBalancerScore {}
impl<T: Topology + LoadBalancer> Score<T> for LoadBalancerScore { impl<T: Topology + LoadBalancer> Score<T> for LoadBalancerScore {
fn create_interpret(&self) -> Box<dyn Interpret<T>> { fn create_interpret(&self) -> Box<dyn Interpret<T>> {
Box::new(LoadBalancerInterpret::new(self.clone())) Box::new(LoadBalancerInterpret::new(self.clone()))

View File

@ -3,8 +3,8 @@ pub mod dns;
pub mod dummy; pub mod dummy;
pub mod http; pub mod http;
pub mod k8s; pub mod k8s;
pub mod lamp;
pub mod load_balancer; pub mod load_balancer;
pub mod okd; pub mod okd;
pub mod opnsense; pub mod opnsense;
pub mod tftp; pub mod tftp;
pub mod lamp;

View File

@ -1,3 +1,5 @@
use serde::Serialize;
use crate::{ use crate::{
interpret::Interpret, interpret::Interpret,
inventory::Inventory, inventory::Inventory,
@ -6,7 +8,7 @@ use crate::{
topology::{DhcpServer, HAClusterTopology, HostBinding, Topology}, topology::{DhcpServer, HAClusterTopology, HostBinding, Topology},
}; };
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize)]
pub struct OKDBootstrapDhcpScore { pub struct OKDBootstrapDhcpScore {
dhcp_score: DhcpScore, dhcp_score: DhcpScore,
} }

View File

@ -1,15 +1,18 @@
use std::net::SocketAddr; use std::net::SocketAddr;
use serde::Serialize;
use crate::{ use crate::{
interpret::Interpret, interpret::Interpret,
modules::load_balancer::LoadBalancerScore, modules::load_balancer::LoadBalancerScore,
score::Score, score::Score,
topology::{ 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 { pub struct OKDBootstrapLoadBalancerScore {
load_balancer_score: LoadBalancerScore, load_balancer_score: LoadBalancerScore,
} }

View File

@ -1,3 +1,5 @@
use serde::Serialize;
use crate::{ use crate::{
interpret::Interpret, interpret::Interpret,
inventory::Inventory, inventory::Inventory,
@ -6,7 +8,7 @@ use crate::{
topology::{DhcpServer, HAClusterTopology, HostBinding, Topology}, topology::{DhcpServer, HAClusterTopology, HostBinding, Topology},
}; };
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize)]
pub struct OKDDhcpScore { pub struct OKDDhcpScore {
dhcp_score: DhcpScore, dhcp_score: DhcpScore,
} }

View File

@ -1,3 +1,5 @@
use serde::Serialize;
use crate::{ use crate::{
interpret::Interpret, interpret::Interpret,
modules::dns::DnsScore, modules::dns::DnsScore,
@ -5,7 +7,7 @@ use crate::{
topology::{DnsRecord, DnsRecordType, DnsServer, HAClusterTopology, Topology}, topology::{DnsRecord, DnsRecordType, DnsServer, HAClusterTopology, Topology},
}; };
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize)]
pub struct OKDDnsScore { pub struct OKDDnsScore {
dns_score: DnsScore, dns_score: DnsScore,
} }

View File

@ -1,11 +1,14 @@
use std::net::SocketAddr; use std::net::SocketAddr;
use serde::Serialize;
use crate::{ use crate::{
interpret::Interpret, interpret::Interpret,
modules::load_balancer::LoadBalancerScore, modules::load_balancer::LoadBalancerScore,
score::{FrontendScore, Score}, score::Score,
topology::{ 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 <T: Topology + LoadBalancer> FrontendScore<T> for OKDLoadBalancerScore {} #[derive(Debug, Clone, Serialize)]
#[derive(Debug, Clone)]
pub struct OKDLoadBalancerScore { pub struct OKDLoadBalancerScore {
load_balancer_score: LoadBalancerScore, load_balancer_score: LoadBalancerScore,
} }

View File

@ -1,6 +1,4 @@
mod shell; mod shell;
mod upgrade; mod upgrade;
pub use shell::*; pub use shell::*;
pub use upgrade::*; pub use upgrade::*;

View File

@ -1,6 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use serde::Serialize;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use crate::{ use crate::{
@ -13,10 +14,27 @@ use crate::{
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct OPNsenseShellCommandScore { 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<RwLock<opnsense_config::Config>>, pub opnsense: Arc<RwLock<opnsense_config::Config>>,
pub command: String, pub command: String,
} }
impl Serialize for OPNsenseShellCommandScore {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
todo!("See comment about moving opnsense_config::Config outside the score")
}
}
impl<T: Topology> Score<T> for OPNsenseShellCommandScore { impl<T: Topology> Score<T> for OPNsenseShellCommandScore {
fn create_interpret(&self) -> Box<dyn Interpret<T>> { fn create_interpret(&self) -> Box<dyn Interpret<T>> {
Box::new(OPNsenseShellInterpret { Box::new(OPNsenseShellInterpret {

View File

@ -1,5 +1,6 @@
use std::sync::Arc; use std::sync::Arc;
use serde::Serialize;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use crate::{ use crate::{
@ -15,6 +16,15 @@ pub struct OPNSenseLaunchUpgrade {
pub opnsense: Arc<RwLock<opnsense_config::Config>>, pub opnsense: Arc<RwLock<opnsense_config::Config>>,
} }
impl Serialize for OPNSenseLaunchUpgrade {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
todo!("See comment in OPNSenseShellCommandScore and apply the same idea here")
}
}
impl<T: Topology> Score<T> for OPNSenseLaunchUpgrade { impl<T: Topology> Score<T> for OPNSenseLaunchUpgrade {
fn create_interpret(&self) -> Box<dyn Interpret<T>> { fn create_interpret(&self) -> Box<dyn Interpret<T>> {
let score = OPNsenseShellCommandScore { let score = OPNsenseShellCommandScore {

View File

@ -1,5 +1,6 @@
use async_trait::async_trait; use async_trait::async_trait;
use derive_new::new; use derive_new::new;
use serde::Serialize;
use crate::{ use crate::{
data::{Id, Version}, data::{Id, Version},
@ -9,7 +10,7 @@ use crate::{
topology::{Router, TftpServer, Topology, Url}, topology::{Router, TftpServer, Topology, Url},
}; };
#[derive(Debug, new, Clone)] #[derive(Debug, new, Clone, Serialize)]
pub struct TftpScore { pub struct TftpScore {
files_to_serve: Url, files_to_serve: Url,
} }

View File

@ -1,10 +1,13 @@
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use crossterm::event::{Event, KeyCode, KeyEventKind}; 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 log::{info, warn};
use ratatui::{ 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; use tokio::sync::mpsc;
@ -23,19 +26,20 @@ struct Execution<T: Topology> {
score: Box<dyn Score<T>>, score: Box<dyn Score<T>>,
} }
impl <T: Topology + LoadBalancer> FrontendScore<T> for OKDLoadBalancerScore {}
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct ScoreListWidget<T: Topology> { pub(crate) struct ScoreListWidget<T: Topology> {
list_state: Arc<RwLock<ListState>>, list_state: Arc<RwLock<ListState>>,
scores: Vec<Box<dyn FrontendScore<T>>>, scores: Vec<Box<dyn Score<T>>>,
execution: Option<Execution<T>>, execution: Option<Execution<T>>,
execution_history: Vec<Execution<T>>, execution_history: Vec<Execution<T>>,
sender: mpsc::Sender<HarmonyTuiEvent<T>>, sender: mpsc::Sender<HarmonyTuiEvent<T>>,
} }
impl<T: Topology + std::fmt::Debug> ScoreListWidget<T> { impl<T: Topology + std::fmt::Debug> ScoreListWidget<T> {
pub(crate) fn new(scores: Vec<Box<dyn Score<T>>>, sender: mpsc::Sender<HarmonyTuiEvent<T>>) -> Self { pub(crate) fn new(
scores: Vec<Box<dyn Score<T>>>,
sender: mpsc::Sender<HarmonyTuiEvent<T>>,
) -> Self {
let mut list_state = ListState::default(); let mut list_state = ListState::default();
list_state.select_first(); list_state.select_first();
let list_state = Arc::new(RwLock::new(list_state)); let list_state = Arc::new(RwLock::new(list_state));
@ -141,4 +145,3 @@ impl<T: Topology> Widget for &ScoreListWidget<T> {
fn score_to_list_item<'a, T: Topology>(score: &'a Box<dyn Score<T>>) -> ListItem<'a> { fn score_to_list_item<'a, T: Topology>(score: &'a Box<dyn Score<T>>) -> ListItem<'a> {
ListItem::new(score.name()) ListItem::new(score.name())
} }

View File

@ -4,3 +4,6 @@ edition = "2024"
version.workspace = true version.workspace = true
readme.workspace = true readme.workspace = true
license.workspace = true license.workspace = true
[dependencies]
serde = { version = "1.0.209", features = ["derive"] }

View File

@ -1,5 +1,7 @@
pub mod net { 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]); pub struct MacAddress(pub [u8; 6]);
impl MacAddress { impl MacAddress {

View File

@ -11,6 +11,7 @@ use crate::{
use log::{debug, info, trace, warn}; use log::{debug, info, trace, warn};
use opnsense_config_xml::OPNsense; use opnsense_config_xml::OPNsense;
use russh::client; use russh::client;
use serde::Serialize;
use super::{ConfigManager, OPNsenseShell}; use super::{ConfigManager, OPNsenseShell};
@ -21,6 +22,15 @@ pub struct Config {
shell: Arc<dyn OPNsenseShell>, shell: Arc<dyn OPNsenseShell>,
} }
impl Serialize for Config {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
todo!()
}
}
impl Config { impl Config {
pub async fn new( pub async fn new(
repository: Arc<dyn ConfigManager>, repository: Arc<dyn ConfigManager>,