From 6267c2757f306f9d123ffe4e283eae814339e66c Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Wed, 15 Apr 2026 15:48:22 -0400 Subject: [PATCH 1/2] feat: Disable ipv4 address conflict detection score. This is useful when setting up bonds as the wrong mac may get a dhcp offer and then the system will perceive it as a conflict when it sets up the bond correctly --- Cargo.lock | 16 --- harmony/src/modules/okd/crd/machine_config.rs | 133 ++++++++++++++++++ harmony/src/modules/okd/crd/mod.rs | 1 + harmony/src/modules/okd/disable_dad_score.rs | 35 +++++ harmony/src/modules/okd/mod.rs | 1 + 5 files changed, 170 insertions(+), 16 deletions(-) create mode 100644 harmony/src/modules/okd/crd/machine_config.rs create mode 100644 harmony/src/modules/okd/disable_dad_score.rs diff --git a/Cargo.lock b/Cargo.lock index 4cf88dd..b53845b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1262,22 +1262,6 @@ dependencies = [ "url", ] -[[package]] -name = "brocade-switch-oricom-configuration" -version = "0.1.0" -dependencies = [ - "async-trait", - "brocade", - "env_logger", - "harmony", - "harmony_cli", - "harmony_macros", - "harmony_types", - "log", - "serde", - "tokio", -] - [[package]] name = "brotli" version = "8.0.2" diff --git a/harmony/src/modules/okd/crd/machine_config.rs b/harmony/src/modules/okd/crd/machine_config.rs new file mode 100644 index 0000000..f0f252a --- /dev/null +++ b/harmony/src/modules/okd/crd/machine_config.rs @@ -0,0 +1,133 @@ +use std::collections::BTreeMap; + +use base64::prelude::*; +use kube::{CustomResource, api::ObjectMeta}; +use serde::{Deserialize, Serialize}; + +#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, Default)] +#[kube( + group = "machineconfiguration.openshift.io", + version = "v1", + kind = "MachineConfig", + plural = "machineconfigs", + namespaced = false, + schema = "disabled" +)] +#[serde(rename_all = "camelCase")] +pub struct MachineConfigSpec { + #[serde(skip_serializing_if = "Option::is_none")] + pub config: Option, +} + +impl Default for MachineConfig { + fn default() -> Self { + Self { + metadata: ObjectMeta::default(), + spec: MachineConfigSpec::default(), + } + } +} + +#[derive(Deserialize, Serialize, Clone, Debug, Default)] +#[serde(rename_all = "camelCase")] +pub struct IgnitionConfig { + #[serde(skip_serializing_if = "Option::is_none")] + pub ignition: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub storage: Option, +} + +#[derive(Deserialize, Serialize, Clone, Debug, Default)] +pub struct Ignition { + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, +} + +#[derive(Deserialize, Serialize, Clone, Debug, Default)] +pub struct Storage { + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub files: Vec, +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct IgnitionFile { + pub path: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub mode: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub overwrite: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub contents: Option, +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct IgnitionFileContents { + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub compression: Option, +} + +impl MachineConfig { + pub fn disable_ipv4_dad(pool: MachineConfigPoolRole) -> Self { + let conf_content = "# Disable IPv4 Address Conflict Detection (ACD/DAD)\n\ +# Workaround for false positive conflict detection on\n\ +# 802.3ad LACP bonds where the second member's permanent\n\ +# MAC address triggers a spurious duplicate detection.\n\ +[connection]\n\ +ipv4.dad-timeout=0\n"; + + let encoded = BASE64_STANDARD.encode(conf_content); + let source = format!("data:text/plain;charset=utf-8;base64,{encoded}"); + + Self { + metadata: ObjectMeta { + name: Some(format!("99-{}-disable-dad", pool.label_value())), + labels: Some(pool.labels()), + ..Default::default() + }, + spec: MachineConfigSpec { + config: Some(IgnitionConfig { + ignition: Some(Ignition { + version: Some("3.2.0".to_string()), + }), + storage: Some(Storage { + files: vec![IgnitionFile { + path: "/etc/NetworkManager/conf.d/99-disable-ipv4-dad.conf".to_string(), + mode: Some(0o644), + overwrite: Some(true), + contents: Some(IgnitionFileContents { + source: Some(source), + compression: None, + }), + }], + }), + }), + }, + } + } +} + +#[derive(Debug, Clone, Copy, Serialize)] +pub enum MachineConfigPoolRole { + Master, + Worker, +} + +impl MachineConfigPoolRole { + pub fn label_value(&self) -> &'static str { + match self { + Self::Master => "master", + Self::Worker => "worker", + } + } + + pub fn labels(&self) -> BTreeMap { + let mut labels = BTreeMap::new(); + labels.insert( + "machineconfiguration.openshift.io/role".to_string(), + self.label_value().to_string(), + ); + labels + } +} diff --git a/harmony/src/modules/okd/crd/mod.rs b/harmony/src/modules/okd/crd/mod.rs index dae9c51..f2af923 100644 --- a/harmony/src/modules/okd/crd/mod.rs +++ b/harmony/src/modules/okd/crd/mod.rs @@ -1,4 +1,5 @@ pub mod ingresses_config; pub mod kubelet_config; +pub mod machine_config; pub mod nmstate; pub mod route; diff --git a/harmony/src/modules/okd/disable_dad_score.rs b/harmony/src/modules/okd/disable_dad_score.rs new file mode 100644 index 0000000..9583f30 --- /dev/null +++ b/harmony/src/modules/okd/disable_dad_score.rs @@ -0,0 +1,35 @@ +use serde::Serialize; + +use crate::{ + interpret::Interpret, + modules::{ + k8s::resource::K8sResourceScore, + okd::crd::machine_config::{MachineConfig, MachineConfigPoolRole}, + }, + score::Score, + topology::{K8sclient, Topology}, +}; + +#[derive(Debug, Clone, Serialize)] +pub struct DisableDadScore { + pub pool: MachineConfigPoolRole, +} + +impl Default for DisableDadScore { + fn default() -> Self { + Self { + pool: MachineConfigPoolRole::Worker, + } + } +} + +impl Score for DisableDadScore { + fn name(&self) -> String { + "DisableDadScore".to_string() + } + + fn create_interpret(&self) -> Box> { + let mc = MachineConfig::disable_ipv4_dad(self.pool); + K8sResourceScore::single(mc, None).create_interpret() + } +} diff --git a/harmony/src/modules/okd/mod.rs b/harmony/src/modules/okd/mod.rs index e1719e9..bd5acf9 100644 --- a/harmony/src/modules/okd/mod.rs +++ b/harmony/src/modules/okd/mod.rs @@ -25,5 +25,6 @@ pub use bootstrap_05_sanity_check::*; pub use bootstrap_06_installation_report::*; pub use bootstrap_persist_network_bond::*; pub mod crd; +pub mod disable_dad_score; pub mod host_network; pub mod system_reserved_score; -- 2.39.5 From 1414fcb98510609afcec824e35b2117cea37a4da Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Fri, 17 Apr 2026 16:56:06 -0400 Subject: [PATCH 2/2] feat: Refactor dad score into reusable node file score using machine config --- harmony/src/modules/okd/crd/machine_config.rs | 25 +++++----- harmony/src/modules/okd/disable_dad_score.rs | 18 +++++-- harmony/src/modules/okd/mod.rs | 1 + harmony/src/modules/okd/node_file_score.rs | 49 +++++++++++++++++++ opencode.json | 3 ++ 5 files changed, 80 insertions(+), 16 deletions(-) create mode 100644 harmony/src/modules/okd/node_file_score.rs diff --git a/harmony/src/modules/okd/crd/machine_config.rs b/harmony/src/modules/okd/crd/machine_config.rs index f0f252a..c426a9c 100644 --- a/harmony/src/modules/okd/crd/machine_config.rs +++ b/harmony/src/modules/okd/crd/machine_config.rs @@ -1,7 +1,7 @@ use std::collections::BTreeMap; use base64::prelude::*; -use kube::{CustomResource, api::ObjectMeta}; +use kube::{api::ObjectMeta, CustomResource}; use serde::{Deserialize, Serialize}; #[derive(CustomResource, Deserialize, Serialize, Clone, Debug, Default)] @@ -69,20 +69,19 @@ pub struct IgnitionFileContents { } impl MachineConfig { - pub fn disable_ipv4_dad(pool: MachineConfigPoolRole) -> Self { - let conf_content = "# Disable IPv4 Address Conflict Detection (ACD/DAD)\n\ -# Workaround for false positive conflict detection on\n\ -# 802.3ad LACP bonds where the second member's permanent\n\ -# MAC address triggers a spurious duplicate detection.\n\ -[connection]\n\ -ipv4.dad-timeout=0\n"; - - let encoded = BASE64_STANDARD.encode(conf_content); + pub fn with_file( + pool: MachineConfigPoolRole, + resource_name: &str, + path: &str, + content: &str, + mode: Option, + ) -> Self { + let encoded = BASE64_STANDARD.encode(content); let source = format!("data:text/plain;charset=utf-8;base64,{encoded}"); Self { metadata: ObjectMeta { - name: Some(format!("99-{}-disable-dad", pool.label_value())), + name: Some(format!("{}-{}", pool.label_value(), resource_name)), labels: Some(pool.labels()), ..Default::default() }, @@ -93,8 +92,8 @@ ipv4.dad-timeout=0\n"; }), storage: Some(Storage { files: vec![IgnitionFile { - path: "/etc/NetworkManager/conf.d/99-disable-ipv4-dad.conf".to_string(), - mode: Some(0o644), + path: path.to_string(), + mode, overwrite: Some(true), contents: Some(IgnitionFileContents { source: Some(source), diff --git a/harmony/src/modules/okd/disable_dad_score.rs b/harmony/src/modules/okd/disable_dad_score.rs index 9583f30..dfbf734 100644 --- a/harmony/src/modules/okd/disable_dad_score.rs +++ b/harmony/src/modules/okd/disable_dad_score.rs @@ -4,7 +4,7 @@ use crate::{ interpret::Interpret, modules::{ k8s::resource::K8sResourceScore, - okd::crd::machine_config::{MachineConfig, MachineConfigPoolRole}, + okd::{crd::machine_config::MachineConfigPoolRole, node_file_score::NodeFileScore}, }, score::Score, topology::{K8sclient, Topology}, @@ -29,7 +29,19 @@ impl Score for DisableDadScore { } fn create_interpret(&self) -> Box> { - let mc = MachineConfig::disable_ipv4_dad(self.pool); - K8sResourceScore::single(mc, None).create_interpret() + let score = NodeFileScore { + pool: self.pool, + resource_name: "disable-dad".to_string(), + path: "/etc/NetworkManager/conf.d/99-disable-ipv4-dad.conf".to_string(), + content: "# Disable IPv4 Address Conflict Detection (ACD/DAD)\n\ +# Workaround for false positive conflict detection on\n\ +# 802.3ad LACP bonds where the second member's permanent\n\ +# MAC address triggers a spurious duplicate detection.\n\ +[connection]\n\ +ipv4.dad-timeout=0\n" + .to_string(), + mode: Some(0o644), + }; + score.create_interpret() } } diff --git a/harmony/src/modules/okd/mod.rs b/harmony/src/modules/okd/mod.rs index bd5acf9..5fafe15 100644 --- a/harmony/src/modules/okd/mod.rs +++ b/harmony/src/modules/okd/mod.rs @@ -27,4 +27,5 @@ pub use bootstrap_persist_network_bond::*; pub mod crd; pub mod disable_dad_score; pub mod host_network; +pub mod node_file_score; pub mod system_reserved_score; diff --git a/harmony/src/modules/okd/node_file_score.rs b/harmony/src/modules/okd/node_file_score.rs new file mode 100644 index 0000000..c1710e3 --- /dev/null +++ b/harmony/src/modules/okd/node_file_score.rs @@ -0,0 +1,49 @@ +use serde::Serialize; + +use crate::{ + interpret::Interpret, + modules::{ + k8s::resource::K8sResourceScore, + okd::crd::machine_config::{MachineConfig, MachineConfigPoolRole}, + }, + score::Score, + topology::{K8sclient, Topology}, +}; + +#[derive(Debug, Clone, Serialize)] +pub struct NodeFileScore { + pub pool: MachineConfigPoolRole, + pub resource_name: String, + pub path: String, + pub content: String, + pub mode: Option, +} + +impl Default for NodeFileScore { + fn default() -> Self { + Self { + pool: MachineConfigPoolRole::Worker, + resource_name: "generic-file".to_string(), + path: "/etc/placeholder".to_string(), + content: "".to_string(), + mode: None, + } + } +} + +impl Score for NodeFileScore { + fn name(&self) -> String { + format!("NodeFileScore({})", self.path) + } + + fn create_interpret(&self) -> Box> { + let mc = MachineConfig::with_file( + self.pool, + &self.resource_name, + &self.path, + &self.content, + self.mode, + ); + K8sResourceScore::single(mc, None).create_interpret() + } +} diff --git a/opencode.json b/opencode.json index 536a5bf..81b3ae2 100644 --- a/opencode.json +++ b/opencode.json @@ -10,6 +10,9 @@ "models": { "qwen3-coder-next:q4_K_M": { "name": "qwen3-coder-next:q4_K_M" + }, + "gemma4:31b": { + "name": "Gemma 4 31b" } } }, -- 2.39.5