Compare commits
8 Commits
78e595e696
...
feat/rebui
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ee3f8a4ad | |||
| d3634a6313 | |||
| a0a8d5277c | |||
| 43b04edbae | |||
| 755a4b7749 | |||
| 66d346a10c | |||
| 06a004a65d | |||
| 9d4e6acac0 |
50
Cargo.lock
generated
50
Cargo.lock
generated
@@ -690,6 +690,23 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "brocade-switch"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"brocade",
|
||||||
|
"env_logger",
|
||||||
|
"harmony",
|
||||||
|
"harmony_cli",
|
||||||
|
"harmony_macros",
|
||||||
|
"harmony_types",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"tokio",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "brotli"
|
name = "brotli"
|
||||||
version = "8.0.2"
|
version = "8.0.2"
|
||||||
@@ -1804,25 +1821,6 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "example-okd-cluster-alerts"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"brocade",
|
|
||||||
"cidr",
|
|
||||||
"env_logger",
|
|
||||||
"harmony",
|
|
||||||
"harmony_cli",
|
|
||||||
"harmony_macros",
|
|
||||||
"harmony_secret",
|
|
||||||
"harmony_secret_derive",
|
|
||||||
"harmony_types",
|
|
||||||
"log",
|
|
||||||
"serde",
|
|
||||||
"tokio",
|
|
||||||
"url",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "example-okd-install"
|
name = "example-okd-install"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -2498,6 +2496,19 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "harmony_inventory_builder"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"cidr",
|
||||||
|
"harmony",
|
||||||
|
"harmony_cli",
|
||||||
|
"harmony_macros",
|
||||||
|
"harmony_types",
|
||||||
|
"tokio",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "harmony_macros"
|
name = "harmony_macros"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -2563,6 +2574,7 @@ dependencies = [
|
|||||||
name = "harmony_types"
|
name = "harmony_types"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"log",
|
||||||
"rand 0.9.2",
|
"rand 0.9.2",
|
||||||
"serde",
|
"serde",
|
||||||
"url",
|
"url",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::net::{IpAddr, Ipv4Addr};
|
use std::net::{IpAddr, Ipv4Addr};
|
||||||
|
|
||||||
use brocade::BrocadeOptions;
|
use brocade::{BrocadeOptions, ssh};
|
||||||
use harmony_secret::{Secret, SecretManager};
|
use harmony_secret::{Secret, SecretManager};
|
||||||
use harmony_types::switch::PortLocation;
|
use harmony_types::switch::PortLocation;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -16,23 +16,28 @@ async fn main() {
|
|||||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
||||||
|
|
||||||
// let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 250)); // old brocade @ ianlet
|
// let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 250)); // old brocade @ ianlet
|
||||||
let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 55, 101)); // brocade @ sto1
|
let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); // brocade @ sto1
|
||||||
// let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 4, 11)); // brocade @ st
|
// let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 4, 11)); // brocade @ st
|
||||||
let switch_addresses = vec![ip];
|
let switch_addresses = vec![ip];
|
||||||
|
|
||||||
let config = SecretManager::get_or_prompt::<BrocadeSwitchAuth>()
|
// let config = SecretManager::get_or_prompt::<BrocadeSwitchAuth>()
|
||||||
.await
|
// .await
|
||||||
.unwrap();
|
// .unwrap();
|
||||||
|
|
||||||
let brocade = brocade::init(
|
let brocade = brocade::init(
|
||||||
&switch_addresses,
|
&switch_addresses,
|
||||||
22,
|
// &config.username,
|
||||||
&config.username,
|
// &config.password,
|
||||||
&config.password,
|
"admin",
|
||||||
Some(BrocadeOptions {
|
"password",
|
||||||
|
BrocadeOptions {
|
||||||
dry_run: true,
|
dry_run: true,
|
||||||
|
ssh: ssh::SshOptions {
|
||||||
|
port: 2222,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.expect("Brocade client failed to connect");
|
.expect("Brocade client failed to connect");
|
||||||
@@ -54,6 +59,7 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
println!("--------------");
|
println!("--------------");
|
||||||
|
todo!();
|
||||||
let channel_name = "1";
|
let channel_name = "1";
|
||||||
brocade.clear_port_channel(channel_name).await.unwrap();
|
brocade.clear_port_channel(channel_name).await.unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ impl BrocadeClient for FastIronClient {
|
|||||||
|
|
||||||
async fn configure_interfaces(
|
async fn configure_interfaces(
|
||||||
&self,
|
&self,
|
||||||
_interfaces: Vec<(String, PortOperatingMode)>,
|
_interfaces: &Vec<(String, PortOperatingMode)>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,12 @@ use async_trait::async_trait;
|
|||||||
use harmony_types::net::MacAddress;
|
use harmony_types::net::MacAddress;
|
||||||
use harmony_types::switch::{PortDeclaration, PortLocation};
|
use harmony_types::switch::{PortDeclaration, PortLocation};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
mod fast_iron;
|
mod fast_iron;
|
||||||
mod network_operating_system;
|
mod network_operating_system;
|
||||||
mod shell;
|
mod shell;
|
||||||
mod ssh;
|
pub mod ssh;
|
||||||
|
|
||||||
#[derive(Default, Clone, Debug)]
|
#[derive(Default, Clone, Debug)]
|
||||||
pub struct BrocadeOptions {
|
pub struct BrocadeOptions {
|
||||||
@@ -118,7 +119,7 @@ impl fmt::Display for InterfaceType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Defines the primary configuration mode of a switch interface, representing mutually exclusive roles.
|
/// Defines the primary configuration mode of a switch interface, representing mutually exclusive roles.
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
|
||||||
pub enum PortOperatingMode {
|
pub enum PortOperatingMode {
|
||||||
/// The interface is explicitly configured for Brocade fabric roles (ISL or Trunk enabled).
|
/// The interface is explicitly configured for Brocade fabric roles (ISL or Trunk enabled).
|
||||||
Fabric,
|
Fabric,
|
||||||
@@ -141,12 +142,11 @@ pub enum InterfaceStatus {
|
|||||||
|
|
||||||
pub async fn init(
|
pub async fn init(
|
||||||
ip_addresses: &[IpAddr],
|
ip_addresses: &[IpAddr],
|
||||||
port: u16,
|
|
||||||
username: &str,
|
username: &str,
|
||||||
password: &str,
|
password: &str,
|
||||||
options: Option<BrocadeOptions>,
|
options: BrocadeOptions,
|
||||||
) -> Result<Box<dyn BrocadeClient + Send + Sync>, Error> {
|
) -> Result<Box<dyn BrocadeClient + Send + Sync>, Error> {
|
||||||
let shell = BrocadeShell::init(ip_addresses, port, username, password, options).await?;
|
let shell = BrocadeShell::init(ip_addresses, username, password, options).await?;
|
||||||
|
|
||||||
let version_info = shell
|
let version_info = shell
|
||||||
.with_session(ExecutionMode::Regular, |session| {
|
.with_session(ExecutionMode::Regular, |session| {
|
||||||
@@ -208,7 +208,7 @@ pub trait BrocadeClient: std::fmt::Debug {
|
|||||||
/// Configures a set of interfaces to be operated with a specified mode (access ports, ISL, etc.).
|
/// Configures a set of interfaces to be operated with a specified mode (access ports, ISL, etc.).
|
||||||
async fn configure_interfaces(
|
async fn configure_interfaces(
|
||||||
&self,
|
&self,
|
||||||
interfaces: Vec<(String, PortOperatingMode)>,
|
interfaces: &Vec<(String, PortOperatingMode)>,
|
||||||
) -> Result<(), Error>;
|
) -> Result<(), Error>;
|
||||||
|
|
||||||
/// Scans the existing configuration to find the next available (unused)
|
/// Scans the existing configuration to find the next available (unused)
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ impl BrocadeClient for NetworkOperatingSystemClient {
|
|||||||
|
|
||||||
async fn configure_interfaces(
|
async fn configure_interfaces(
|
||||||
&self,
|
&self,
|
||||||
interfaces: Vec<(String, PortOperatingMode)>,
|
interfaces: &Vec<(String, PortOperatingMode)>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
info!("[Brocade] Configuring {} interface(s)...", interfaces.len());
|
info!("[Brocade] Configuring {} interface(s)...", interfaces.len());
|
||||||
|
|
||||||
@@ -204,9 +204,12 @@ impl BrocadeClient for NetworkOperatingSystemClient {
|
|||||||
PortOperatingMode::Trunk => {
|
PortOperatingMode::Trunk => {
|
||||||
commands.push("switchport".into());
|
commands.push("switchport".into());
|
||||||
commands.push("switchport mode trunk".into());
|
commands.push("switchport mode trunk".into());
|
||||||
commands.push("no spanning-tree shutdown".into());
|
commands.push("switchport trunk allowed vlan all".into());
|
||||||
|
commands.push("no switchport trunk tag native-vlan".into());
|
||||||
|
commands.push("spanning-tree shutdown".into());
|
||||||
commands.push("no fabric isl enable".into());
|
commands.push("no fabric isl enable".into());
|
||||||
commands.push("no fabric trunk enable".into());
|
commands.push("no fabric trunk enable".into());
|
||||||
|
commands.push("no shutdown".into());
|
||||||
}
|
}
|
||||||
PortOperatingMode::Access => {
|
PortOperatingMode::Access => {
|
||||||
commands.push("switchport".into());
|
commands.push("switchport".into());
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ use tokio::time::timeout;
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct BrocadeShell {
|
pub struct BrocadeShell {
|
||||||
ip: IpAddr,
|
ip: IpAddr,
|
||||||
port: u16,
|
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: String,
|
||||||
options: BrocadeOptions,
|
options: BrocadeOptions,
|
||||||
@@ -27,33 +26,31 @@ pub struct BrocadeShell {
|
|||||||
impl BrocadeShell {
|
impl BrocadeShell {
|
||||||
pub async fn init(
|
pub async fn init(
|
||||||
ip_addresses: &[IpAddr],
|
ip_addresses: &[IpAddr],
|
||||||
port: u16,
|
|
||||||
username: &str,
|
username: &str,
|
||||||
password: &str,
|
password: &str,
|
||||||
options: Option<BrocadeOptions>,
|
options: BrocadeOptions,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
let ip = ip_addresses
|
let ip = ip_addresses
|
||||||
.first()
|
.first()
|
||||||
.ok_or_else(|| Error::ConfigurationError("No IP addresses provided".to_string()))?;
|
.ok_or_else(|| Error::ConfigurationError("No IP addresses provided".to_string()))?;
|
||||||
|
|
||||||
let base_options = options.unwrap_or_default();
|
let brocade_ssh_client_options =
|
||||||
let options = ssh::try_init_client(username, password, ip, base_options).await?;
|
ssh::try_init_client(username, password, ip, options).await?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
ip: *ip,
|
ip: *ip,
|
||||||
port,
|
|
||||||
username: username.to_string(),
|
username: username.to_string(),
|
||||||
password: password.to_string(),
|
password: password.to_string(),
|
||||||
before_all_commands: vec![],
|
before_all_commands: vec![],
|
||||||
after_all_commands: vec![],
|
after_all_commands: vec![],
|
||||||
options,
|
options: brocade_ssh_client_options,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn open_session(&self, mode: ExecutionMode) -> Result<BrocadeSession, Error> {
|
pub async fn open_session(&self, mode: ExecutionMode) -> Result<BrocadeSession, Error> {
|
||||||
BrocadeSession::open(
|
BrocadeSession::open(
|
||||||
self.ip,
|
self.ip,
|
||||||
self.port,
|
self.options.ssh.port,
|
||||||
&self.username,
|
&self.username,
|
||||||
&self.password,
|
&self.password,
|
||||||
self.options.clone(),
|
self.options.clone(),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use std::borrow::Cow;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use log::debug;
|
||||||
use russh::client::Handler;
|
use russh::client::Handler;
|
||||||
use russh::kex::DH_G1_SHA1;
|
use russh::kex::DH_G1_SHA1;
|
||||||
use russh::kex::ECDH_SHA2_NISTP256;
|
use russh::kex::ECDH_SHA2_NISTP256;
|
||||||
@@ -10,29 +11,43 @@ use russh_keys::key::SSH_RSA;
|
|||||||
use super::BrocadeOptions;
|
use super::BrocadeOptions;
|
||||||
use super::Error;
|
use super::Error;
|
||||||
|
|
||||||
#[derive(Default, Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct SshOptions {
|
pub struct SshOptions {
|
||||||
pub preferred_algorithms: russh::Preferred,
|
pub preferred_algorithms: russh::Preferred,
|
||||||
|
pub port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SshOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
preferred_algorithms: Default::default(),
|
||||||
|
port: 22,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SshOptions {
|
impl SshOptions {
|
||||||
fn ecdhsa_sha2_nistp256() -> Self {
|
fn ecdhsa_sha2_nistp256(port: u16) -> Self {
|
||||||
Self {
|
Self {
|
||||||
preferred_algorithms: russh::Preferred {
|
preferred_algorithms: russh::Preferred {
|
||||||
kex: Cow::Borrowed(&[ECDH_SHA2_NISTP256]),
|
kex: Cow::Borrowed(&[ECDH_SHA2_NISTP256]),
|
||||||
key: Cow::Borrowed(&[SSH_RSA]),
|
key: Cow::Borrowed(&[SSH_RSA]),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
port,
|
||||||
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn legacy() -> Self {
|
fn legacy(port: u16) -> Self {
|
||||||
Self {
|
Self {
|
||||||
preferred_algorithms: russh::Preferred {
|
preferred_algorithms: russh::Preferred {
|
||||||
kex: Cow::Borrowed(&[DH_G1_SHA1]),
|
kex: Cow::Borrowed(&[DH_G1_SHA1]),
|
||||||
key: Cow::Borrowed(&[SSH_RSA]),
|
key: Cow::Borrowed(&[SSH_RSA]),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
port,
|
||||||
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,18 +72,21 @@ pub async fn try_init_client(
|
|||||||
ip: &std::net::IpAddr,
|
ip: &std::net::IpAddr,
|
||||||
base_options: BrocadeOptions,
|
base_options: BrocadeOptions,
|
||||||
) -> Result<BrocadeOptions, Error> {
|
) -> Result<BrocadeOptions, Error> {
|
||||||
|
let mut default = SshOptions::default();
|
||||||
|
default.port = base_options.ssh.port;
|
||||||
let ssh_options = vec![
|
let ssh_options = vec![
|
||||||
SshOptions::default(),
|
default,
|
||||||
SshOptions::ecdhsa_sha2_nistp256(),
|
SshOptions::ecdhsa_sha2_nistp256(base_options.ssh.port),
|
||||||
SshOptions::legacy(),
|
SshOptions::legacy(base_options.ssh.port),
|
||||||
];
|
];
|
||||||
|
|
||||||
for ssh in ssh_options {
|
for ssh in ssh_options {
|
||||||
let opts = BrocadeOptions {
|
let opts = BrocadeOptions {
|
||||||
ssh,
|
ssh: ssh.clone(),
|
||||||
..base_options.clone()
|
..base_options.clone()
|
||||||
};
|
};
|
||||||
let client = create_client(*ip, 22, username, password, &opts).await;
|
debug!("Creating client {ip}:{} {username}", ssh.port);
|
||||||
|
let client = create_client(*ip, ssh.port, username, password, &opts).await;
|
||||||
|
|
||||||
match client {
|
match client {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
|
|||||||
BIN
data/pxe/okd/http_files/harmony_inventory_agent
(Stored with Git LFS)
BIN
data/pxe/okd/http_files/harmony_inventory_agent
(Stored with Git LFS)
Binary file not shown.
BIN
empty_database.sqlite
Normal file
BIN
empty_database.sqlite
Normal file
Binary file not shown.
@@ -1,22 +1,19 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "example-okd-cluster-alerts"
|
name = "brocade-switch"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
readme.workspace = true
|
readme.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
publish = false
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
harmony = { path = "../../harmony" }
|
harmony = { path = "../../harmony" }
|
||||||
harmony_cli = { path = "../../harmony_cli" }
|
harmony_cli = { path = "../../harmony_cli" }
|
||||||
harmony_types = { path = "../../harmony_types" }
|
|
||||||
harmony_secret = { path = "../../harmony_secret" }
|
|
||||||
harmony_secret_derive = { path = "../../harmony_secret_derive" }
|
|
||||||
cidr = { workspace = true }
|
|
||||||
tokio = { workspace = true }
|
|
||||||
harmony_macros = { path = "../../harmony_macros" }
|
harmony_macros = { path = "../../harmony_macros" }
|
||||||
log = { workspace = true }
|
harmony_types = { path = "../../harmony_types" }
|
||||||
env_logger = { workspace = true }
|
tokio.workspace = true
|
||||||
url = { workspace = true }
|
url.workspace = true
|
||||||
|
async-trait.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
env_logger.workspace = true
|
||||||
brocade = { path = "../../brocade" }
|
brocade = { path = "../../brocade" }
|
||||||
157
examples/brocade_switch/src/main.rs
Normal file
157
examples/brocade_switch/src/main.rs
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use brocade::{BrocadeOptions, PortOperatingMode};
|
||||||
|
use harmony::{
|
||||||
|
data::Version,
|
||||||
|
infra::brocade::BrocadeSwitchClient,
|
||||||
|
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
||||||
|
inventory::Inventory,
|
||||||
|
score::Score,
|
||||||
|
topology::{
|
||||||
|
HostNetworkConfig, PortConfig, PreparationError, PreparationOutcome, Switch, SwitchClient,
|
||||||
|
SwitchError, Topology,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use harmony_macros::ip;
|
||||||
|
use harmony_types::{id::Id, net::MacAddress, switch::PortLocation};
|
||||||
|
use log::{debug, info};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let switch_score = BrocadeSwitchScore {
|
||||||
|
port_channels_to_clear: vec![
|
||||||
|
Id::from_str("17").unwrap(),
|
||||||
|
Id::from_str("19").unwrap(),
|
||||||
|
Id::from_str("18").unwrap(),
|
||||||
|
],
|
||||||
|
ports_to_configure: vec![
|
||||||
|
(PortLocation(2, 0, 17), PortOperatingMode::Trunk),
|
||||||
|
(PortLocation(2, 0, 19), PortOperatingMode::Trunk),
|
||||||
|
(PortLocation(1, 0, 18), PortOperatingMode::Trunk),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
harmony_cli::run(
|
||||||
|
Inventory::autoload(),
|
||||||
|
SwitchTopology::new().await,
|
||||||
|
vec![Box::new(switch_score)],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize)]
|
||||||
|
struct BrocadeSwitchScore {
|
||||||
|
port_channels_to_clear: Vec<Id>,
|
||||||
|
ports_to_configure: Vec<PortConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Topology + Switch> Score<T> for BrocadeSwitchScore {
|
||||||
|
fn name(&self) -> String {
|
||||||
|
"BrocadeSwitchScore".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||||
|
Box::new(BrocadeSwitchInterpret {
|
||||||
|
score: self.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct BrocadeSwitchInterpret {
|
||||||
|
score: BrocadeSwitchScore,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T: Topology + Switch> Interpret<T> for BrocadeSwitchInterpret {
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
_inventory: &Inventory,
|
||||||
|
topology: &T,
|
||||||
|
) -> Result<Outcome, InterpretError> {
|
||||||
|
info!("Applying switch configuration {:?}", self.score);
|
||||||
|
debug!(
|
||||||
|
"Clearing port channel {:?}",
|
||||||
|
self.score.port_channels_to_clear
|
||||||
|
);
|
||||||
|
topology
|
||||||
|
.clear_port_channel(&self.score.port_channels_to_clear)
|
||||||
|
.await
|
||||||
|
.map_err(|e| InterpretError::new(e.to_string()))?;
|
||||||
|
debug!("Configuring interfaces {:?}", self.score.ports_to_configure);
|
||||||
|
topology
|
||||||
|
.configure_interface(&self.score.ports_to_configure)
|
||||||
|
.await
|
||||||
|
.map_err(|e| InterpretError::new(e.to_string()))?;
|
||||||
|
Ok(Outcome::success("switch configured".to_string()))
|
||||||
|
}
|
||||||
|
fn get_name(&self) -> InterpretName {
|
||||||
|
InterpretName::Custom("BrocadeSwitchInterpret")
|
||||||
|
}
|
||||||
|
fn get_version(&self) -> Version {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
fn get_status(&self) -> InterpretStatus {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
fn get_children(&self) -> Vec<Id> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SwitchTopology {
|
||||||
|
client: Box<dyn SwitchClient>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Topology for SwitchTopology {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"SwitchTopology"
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn ensure_ready(&self) -> Result<PreparationOutcome, PreparationError> {
|
||||||
|
Ok(PreparationOutcome::Noop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SwitchTopology {
|
||||||
|
async fn new() -> Self {
|
||||||
|
let mut options = BrocadeOptions::default();
|
||||||
|
options.ssh.port = 2222;
|
||||||
|
let client =
|
||||||
|
BrocadeSwitchClient::init(&vec![ip!("127.0.0.1")], &"admin", &"password", options)
|
||||||
|
.await
|
||||||
|
.expect("Failed to connect to switch");
|
||||||
|
|
||||||
|
let client = Box::new(client);
|
||||||
|
Self { client }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Switch for SwitchTopology {
|
||||||
|
async fn setup_switch(&self) -> Result<(), SwitchError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_port_for_mac_address(
|
||||||
|
&self,
|
||||||
|
_mac_address: &MacAddress,
|
||||||
|
) -> Result<Option<PortLocation>, SwitchError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn configure_port_channel(&self, _config: &HostNetworkConfig) -> Result<(), SwitchError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
async fn clear_port_channel(&self, ids: &Vec<Id>) -> Result<(), SwitchError> {
|
||||||
|
self.client.clear_port_channel(ids).await
|
||||||
|
}
|
||||||
|
async fn configure_interface(&self, ports: &Vec<PortConfig>) -> Result<(), SwitchError> {
|
||||||
|
self.client.configure_interface(ports).await
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ use harmony::{
|
|||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
modules::{
|
modules::{
|
||||||
dummy::{ErrorScore, PanicScore, SuccessScore},
|
dummy::{ErrorScore, PanicScore, SuccessScore},
|
||||||
inventory::LaunchDiscoverInventoryAgentScore,
|
inventory::{HarmonyDiscoveryStrategy, LaunchDiscoverInventoryAgentScore},
|
||||||
},
|
},
|
||||||
topology::LocalhostTopology,
|
topology::LocalhostTopology,
|
||||||
};
|
};
|
||||||
@@ -18,6 +18,7 @@ async fn main() {
|
|||||||
Box::new(PanicScore {}),
|
Box::new(PanicScore {}),
|
||||||
Box::new(LaunchDiscoverInventoryAgentScore {
|
Box::new(LaunchDiscoverInventoryAgentScore {
|
||||||
discovery_timeout: Some(10),
|
discovery_timeout: Some(10),
|
||||||
|
discovery_strategy: HarmonyDiscoveryStrategy::MDNS,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
None,
|
None,
|
||||||
|
|||||||
15
examples/harmony_inventory_builder/Cargo.toml
Normal file
15
examples/harmony_inventory_builder/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "harmony_inventory_builder"
|
||||||
|
edition = "2024"
|
||||||
|
version.workspace = true
|
||||||
|
readme.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
harmony = { path = "../../harmony" }
|
||||||
|
harmony_cli = { path = "../../harmony_cli" }
|
||||||
|
harmony_macros = { path = "../../harmony_macros" }
|
||||||
|
harmony_types = { path = "../../harmony_types" }
|
||||||
|
tokio.workspace = true
|
||||||
|
url.workspace = true
|
||||||
|
cidr.workspace = true
|
||||||
11
examples/harmony_inventory_builder/build_docker.sh
Executable file
11
examples/harmony_inventory_builder/build_docker.sh
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
cargo build -p harmony_inventory_builder --release --target x86_64-unknown-linux-musl
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(dirname ${0})"
|
||||||
|
|
||||||
|
cd "${SCRIPT_DIR}/docker/"
|
||||||
|
|
||||||
|
cp ../../../target/x86_64-unknown-linux-musl/release/harmony_inventory_builder .
|
||||||
|
|
||||||
|
docker build . -t hub.nationtech.io/harmony/harmony_inventory_builder
|
||||||
|
|
||||||
|
docker push hub.nationtech.io/harmony/harmony_inventory_builder
|
||||||
10
examples/harmony_inventory_builder/docker/Dockerfile
Normal file
10
examples/harmony_inventory_builder/docker/Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
FROM debian:12-slim
|
||||||
|
|
||||||
|
RUN mkdir /app
|
||||||
|
WORKDIR /app/
|
||||||
|
|
||||||
|
COPY harmony_inventory_builder /app/
|
||||||
|
|
||||||
|
ENV RUST_LOG=info
|
||||||
|
|
||||||
|
CMD ["sleep", "infinity"]
|
||||||
36
examples/harmony_inventory_builder/src/main.rs
Normal file
36
examples/harmony_inventory_builder/src/main.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
use harmony::{
|
||||||
|
inventory::{HostRole, Inventory},
|
||||||
|
modules::inventory::{DiscoverHostForRoleScore, HarmonyDiscoveryStrategy},
|
||||||
|
topology::LocalhostTopology,
|
||||||
|
};
|
||||||
|
use harmony_macros::cidrv4;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let discover_worker = DiscoverHostForRoleScore {
|
||||||
|
role: HostRole::Worker,
|
||||||
|
number_desired_hosts: 3,
|
||||||
|
discovery_strategy: HarmonyDiscoveryStrategy::SUBNET {
|
||||||
|
cidr: cidrv4!("192.168.0.1/25"),
|
||||||
|
port: 25000,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let discover_control_plane = DiscoverHostForRoleScore {
|
||||||
|
role: HostRole::ControlPlane,
|
||||||
|
number_desired_hosts: 3,
|
||||||
|
discovery_strategy: HarmonyDiscoveryStrategy::SUBNET {
|
||||||
|
cidr: cidrv4!("192.168.0.1/25"),
|
||||||
|
port: 25000,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
harmony_cli::run(
|
||||||
|
Inventory::autoload(),
|
||||||
|
LocalhostTopology::new(),
|
||||||
|
vec![Box::new(discover_worker), Box::new(discover_control_plane)],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
@@ -24,14 +24,13 @@ use harmony::{
|
|||||||
},
|
},
|
||||||
topology::K8sAnywhereTopology,
|
topology::K8sAnywhereTopology,
|
||||||
};
|
};
|
||||||
use harmony_types::{k8s_name::K8sName, net::Url};
|
use harmony_types::net::Url;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let discord_receiver = DiscordWebhook {
|
let discord_receiver = DiscordWebhook {
|
||||||
name: K8sName("test-discord".to_string()),
|
name: "test-discord".to_string(),
|
||||||
url: Url::Url(url::Url::parse("https://discord.doesnt.exist.com").unwrap()),
|
url: Url::Url(url::Url::parse("https://discord.doesnt.exist.com").unwrap()),
|
||||||
selectors: vec![],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let high_pvc_fill_rate_over_two_days_alert = high_pvc_fill_rate_over_two_days();
|
let high_pvc_fill_rate_over_two_days_alert = high_pvc_fill_rate_over_two_days();
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ use harmony::{
|
|||||||
tenant::{ResourceLimits, TenantConfig, TenantNetworkPolicy},
|
tenant::{ResourceLimits, TenantConfig, TenantNetworkPolicy},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use harmony_types::id::Id;
|
||||||
use harmony_types::net::Url;
|
use harmony_types::net::Url;
|
||||||
use harmony_types::{id::Id, k8s_name::K8sName};
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
@@ -43,9 +43,8 @@ async fn main() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let discord_receiver = DiscordWebhook {
|
let discord_receiver = DiscordWebhook {
|
||||||
name: K8sName("test-discord".to_string()),
|
name: "test-discord".to_string(),
|
||||||
url: Url::Url(url::Url::parse("https://discord.doesnt.exist.com").unwrap()),
|
url: Url::Url(url::Url::parse("https://discord.doesnt.exist.com").unwrap()),
|
||||||
selectors: vec![],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let high_pvc_fill_rate_over_two_days_alert = high_pvc_fill_rate_over_two_days();
|
let high_pvc_fill_rate_over_two_days_alert = high_pvc_fill_rate_over_two_days();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::{
|
use std::{
|
||||||
net::{IpAddr, Ipv4Addr},
|
net::{IpAddr, Ipv4Addr},
|
||||||
sync::Arc,
|
sync::{Arc, OnceLock},
|
||||||
};
|
};
|
||||||
|
|
||||||
use brocade::BrocadeOptions;
|
use brocade::BrocadeOptions;
|
||||||
@@ -39,10 +39,10 @@ async fn main() {
|
|||||||
.expect("Failed to get credentials");
|
.expect("Failed to get credentials");
|
||||||
|
|
||||||
let switches: Vec<IpAddr> = vec![ip!("192.168.33.101")];
|
let switches: Vec<IpAddr> = vec![ip!("192.168.33.101")];
|
||||||
let brocade_options = Some(BrocadeOptions {
|
let brocade_options = BrocadeOptions {
|
||||||
dry_run: *harmony::config::DRY_RUN,
|
dry_run: *harmony::config::DRY_RUN,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
};
|
||||||
let switch_client = BrocadeSwitchClient::init(
|
let switch_client = BrocadeSwitchClient::init(
|
||||||
&switches,
|
&switches,
|
||||||
&switch_auth.username,
|
&switch_auth.username,
|
||||||
@@ -107,6 +107,7 @@ async fn main() {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
switch_client: switch_client.clone(),
|
switch_client: switch_client.clone(),
|
||||||
|
network_manager: OnceLock::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let inventory = Inventory {
|
let inventory = Inventory {
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use harmony::{
|
|
||||||
inventory::Inventory,
|
|
||||||
modules::monitoring::{
|
|
||||||
alert_channel::discord_alert_channel::DiscordWebhook,
|
|
||||||
okd::cluster_monitoring::OpenshiftClusterAlertScore,
|
|
||||||
},
|
|
||||||
topology::K8sAnywhereTopology,
|
|
||||||
};
|
|
||||||
use harmony_macros::hurl;
|
|
||||||
use harmony_types::k8s_name::K8sName;
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
let mut sel = HashMap::new();
|
|
||||||
sel.insert(
|
|
||||||
"openshift_io_alert_source".to_string(),
|
|
||||||
"platform".to_string(),
|
|
||||||
);
|
|
||||||
let mut sel2 = HashMap::new();
|
|
||||||
sel2.insert("openshift_io_alert_source".to_string(), "".to_string());
|
|
||||||
let selectors = vec![sel, sel2];
|
|
||||||
harmony_cli::run(
|
|
||||||
Inventory::autoload(),
|
|
||||||
K8sAnywhereTopology::from_env(),
|
|
||||||
vec![Box::new(OpenshiftClusterAlertScore {
|
|
||||||
receivers: vec![Box::new(DiscordWebhook {
|
|
||||||
name: K8sName("wills-discord-webhook-example".to_string()),
|
|
||||||
url: hurl!("https://something.io"),
|
|
||||||
selectors: selectors,
|
|
||||||
})],
|
|
||||||
})],
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,10 @@ use harmony::{
|
|||||||
use harmony_macros::{ip, ipv4};
|
use harmony_macros::{ip, ipv4};
|
||||||
use harmony_secret::{Secret, SecretManager};
|
use harmony_secret::{Secret, SecretManager};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{net::IpAddr, sync::Arc};
|
use std::{
|
||||||
|
net::IpAddr,
|
||||||
|
sync::{Arc, OnceLock},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Secret, Serialize, Deserialize, Debug, PartialEq)]
|
#[derive(Secret, Serialize, Deserialize, Debug, PartialEq)]
|
||||||
struct OPNSenseFirewallConfig {
|
struct OPNSenseFirewallConfig {
|
||||||
@@ -28,10 +31,10 @@ pub async fn get_topology() -> HAClusterTopology {
|
|||||||
.expect("Failed to get credentials");
|
.expect("Failed to get credentials");
|
||||||
|
|
||||||
let switches: Vec<IpAddr> = vec![ip!("192.168.1.101")]; // TODO: Adjust me
|
let switches: Vec<IpAddr> = vec![ip!("192.168.1.101")]; // TODO: Adjust me
|
||||||
let brocade_options = Some(BrocadeOptions {
|
let brocade_options = BrocadeOptions {
|
||||||
dry_run: *harmony::config::DRY_RUN,
|
dry_run: *harmony::config::DRY_RUN,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
};
|
||||||
let switch_client = BrocadeSwitchClient::init(
|
let switch_client = BrocadeSwitchClient::init(
|
||||||
&switches,
|
&switches,
|
||||||
&switch_auth.username,
|
&switch_auth.username,
|
||||||
@@ -81,6 +84,7 @@ pub async fn get_topology() -> HAClusterTopology {
|
|||||||
},
|
},
|
||||||
workers: vec![],
|
workers: vec![],
|
||||||
switch_client: switch_client.clone(),
|
switch_client: switch_client.clone(),
|
||||||
|
network_manager: OnceLock::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ use harmony::{
|
|||||||
use harmony_macros::{ip, ipv4};
|
use harmony_macros::{ip, ipv4};
|
||||||
use harmony_secret::{Secret, SecretManager};
|
use harmony_secret::{Secret, SecretManager};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{net::IpAddr, sync::Arc};
|
use std::{
|
||||||
|
net::IpAddr,
|
||||||
|
sync::{Arc, OnceLock},
|
||||||
|
};
|
||||||
|
|
||||||
pub async fn get_topology() -> HAClusterTopology {
|
pub async fn get_topology() -> HAClusterTopology {
|
||||||
let firewall = harmony::topology::LogicalHost {
|
let firewall = harmony::topology::LogicalHost {
|
||||||
@@ -23,10 +26,10 @@ pub async fn get_topology() -> HAClusterTopology {
|
|||||||
.expect("Failed to get credentials");
|
.expect("Failed to get credentials");
|
||||||
|
|
||||||
let switches: Vec<IpAddr> = vec![ip!("192.168.1.101")]; // TODO: Adjust me
|
let switches: Vec<IpAddr> = vec![ip!("192.168.1.101")]; // TODO: Adjust me
|
||||||
let brocade_options = Some(BrocadeOptions {
|
let brocade_options = BrocadeOptions {
|
||||||
dry_run: *harmony::config::DRY_RUN,
|
dry_run: *harmony::config::DRY_RUN,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
};
|
||||||
let switch_client = BrocadeSwitchClient::init(
|
let switch_client = BrocadeSwitchClient::init(
|
||||||
&switches,
|
&switches,
|
||||||
&switch_auth.username,
|
&switch_auth.username,
|
||||||
@@ -76,6 +79,7 @@ pub async fn get_topology() -> HAClusterTopology {
|
|||||||
},
|
},
|
||||||
workers: vec![],
|
workers: vec![],
|
||||||
switch_client: switch_client.clone(),
|
switch_client: switch_client.clone(),
|
||||||
|
network_manager: OnceLock::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::{
|
use std::{
|
||||||
net::{IpAddr, Ipv4Addr},
|
net::{IpAddr, Ipv4Addr},
|
||||||
sync::Arc,
|
sync::{Arc, OnceLock},
|
||||||
};
|
};
|
||||||
|
|
||||||
use brocade::BrocadeOptions;
|
use brocade::BrocadeOptions;
|
||||||
@@ -35,10 +35,10 @@ async fn main() {
|
|||||||
.expect("Failed to get credentials");
|
.expect("Failed to get credentials");
|
||||||
|
|
||||||
let switches: Vec<IpAddr> = vec![ip!("192.168.5.101")]; // TODO: Adjust me
|
let switches: Vec<IpAddr> = vec![ip!("192.168.5.101")]; // TODO: Adjust me
|
||||||
let brocade_options = Some(BrocadeOptions {
|
let brocade_options = BrocadeOptions {
|
||||||
dry_run: *harmony::config::DRY_RUN,
|
dry_run: *harmony::config::DRY_RUN,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
};
|
||||||
let switch_client = BrocadeSwitchClient::init(
|
let switch_client = BrocadeSwitchClient::init(
|
||||||
&switches,
|
&switches,
|
||||||
&switch_auth.username,
|
&switch_auth.username,
|
||||||
@@ -79,6 +79,7 @@ async fn main() {
|
|||||||
},
|
},
|
||||||
workers: vec![],
|
workers: vec![],
|
||||||
switch_client: switch_client.clone(),
|
switch_client: switch_client.clone(),
|
||||||
|
network_manager: OnceLock::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let inventory = Inventory {
|
let inventory = Inventory {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::{collections::HashMap, path::PathBuf, sync::Arc};
|
use std::{path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
use harmony::{
|
use harmony::{
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
@@ -10,7 +10,7 @@ use harmony::{
|
|||||||
},
|
},
|
||||||
topology::K8sAnywhereTopology,
|
topology::K8sAnywhereTopology,
|
||||||
};
|
};
|
||||||
use harmony_types::{k8s_name::K8sName, net::Url};
|
use harmony_types::net::Url;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
@@ -22,9 +22,8 @@ async fn main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let discord_receiver = DiscordWebhook {
|
let discord_receiver = DiscordWebhook {
|
||||||
name: K8sName("test-discord".to_string()),
|
name: "test-discord".to_string(),
|
||||||
url: Url::Url(url::Url::parse("https://discord.doesnt.exist.com").unwrap()),
|
url: Url::Url(url::Url::parse("https://discord.doesnt.exist.com").unwrap()),
|
||||||
selectors: vec![],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let app = ApplicationScore {
|
let app = ApplicationScore {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::{collections::HashMap, path::PathBuf, sync::Arc};
|
use std::{path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
use harmony::{
|
use harmony::{
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
@@ -14,7 +14,6 @@ use harmony::{
|
|||||||
topology::K8sAnywhereTopology,
|
topology::K8sAnywhereTopology,
|
||||||
};
|
};
|
||||||
use harmony_macros::hurl;
|
use harmony_macros::hurl;
|
||||||
use harmony_types::k8s_name::K8sName;
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
@@ -26,9 +25,8 @@ async fn main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let discord_receiver = DiscordWebhook {
|
let discord_receiver = DiscordWebhook {
|
||||||
name: K8sName("test-discord".to_string()),
|
name: "test-discord".to_string(),
|
||||||
url: hurl!("https://discord.doesnt.exist.com"),
|
url: hurl!("https://discord.doesnt.exist.com"),
|
||||||
selectors: vec![],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let webhook_receiver = WebhookReceiver {
|
let webhook_receiver = WebhookReceiver {
|
||||||
|
|||||||
Binary file not shown.
@@ -1,7 +0,0 @@
|
|||||||
|
|
||||||
apiVersion: v2
|
|
||||||
name: harmony-example-rust-webapp-chart
|
|
||||||
description: A Helm chart for the harmony-example-rust-webapp web application.
|
|
||||||
type: application
|
|
||||||
version: 0.1.0
|
|
||||||
appVersion: "latest"
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
|
|
||||||
{{/*
|
|
||||||
Expand the name of the chart.
|
|
||||||
*/}}
|
|
||||||
{{- define "chart.name" -}}
|
|
||||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Create a default fully qualified app name.
|
|
||||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
|
||||||
*/}}
|
|
||||||
{{- define "chart.fullname" -}}
|
|
||||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
|
||||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
|
||||||
{{- end }}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: {{ include "chart.fullname" . }}
|
|
||||||
spec:
|
|
||||||
replicas: {{ .Values.replicaCount }}
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: {{ include "chart.name" . }}
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: {{ include "chart.name" . }}
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: {{ .Chart.Name }}
|
|
||||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
|
||||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
containerPort: 3000
|
|
||||||
protocol: TCP
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
|
|
||||||
{{- if .Values.ingress.enabled -}}
|
|
||||||
apiVersion: networking.k8s.io/v1
|
|
||||||
kind: Ingress
|
|
||||||
metadata:
|
|
||||||
name: {{ include "chart.fullname" . }}
|
|
||||||
annotations:
|
|
||||||
{{- toYaml .Values.ingress.annotations | nindent 4 }}
|
|
||||||
spec:
|
|
||||||
{{- if .Values.ingress.tls }}
|
|
||||||
tls:
|
|
||||||
{{- range .Values.ingress.tls }}
|
|
||||||
- hosts:
|
|
||||||
{{- range .hosts }}
|
|
||||||
- {{ . | quote }}
|
|
||||||
{{- end }}
|
|
||||||
secretName: {{ .secretName }}
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
rules:
|
|
||||||
{{- range .Values.ingress.hosts }}
|
|
||||||
- host: {{ .host | quote }}
|
|
||||||
http:
|
|
||||||
paths:
|
|
||||||
{{- range .paths }}
|
|
||||||
- path: {{ .path }}
|
|
||||||
pathType: {{ .pathType }}
|
|
||||||
backend:
|
|
||||||
service:
|
|
||||||
name: {{ include "chart.fullname" $ }}
|
|
||||||
port:
|
|
||||||
number: 3000
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: {{ include "chart.fullname" . }}
|
|
||||||
spec:
|
|
||||||
type: {{ .Values.service.type }}
|
|
||||||
ports:
|
|
||||||
- port: {{ .Values.service.port }}
|
|
||||||
targetPort: 3000
|
|
||||||
protocol: TCP
|
|
||||||
name: http
|
|
||||||
selector:
|
|
||||||
app: {{ include "chart.name" . }}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
|
|
||||||
# Default values for harmony-example-rust-webapp-chart.
|
|
||||||
# This is a YAML-formatted file.
|
|
||||||
# Declare variables to be passed into your templates.
|
|
||||||
|
|
||||||
replicaCount: 1
|
|
||||||
|
|
||||||
image:
|
|
||||||
repository: hub.nationtech.io/harmony/harmony-example-rust-webapp
|
|
||||||
pullPolicy: IfNotPresent
|
|
||||||
# Overridden by the chart's appVersion
|
|
||||||
tag: "latest"
|
|
||||||
|
|
||||||
service:
|
|
||||||
type: ClusterIP
|
|
||||||
port: 3000
|
|
||||||
|
|
||||||
ingress:
|
|
||||||
enabled: true
|
|
||||||
# Annotations for cert-manager to handle SSL.
|
|
||||||
annotations:
|
|
||||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
|
||||||
# Add other annotations like nginx ingress class if needed
|
|
||||||
# kubernetes.io/ingress.class: nginx
|
|
||||||
hosts:
|
|
||||||
- host: chart-example.local
|
|
||||||
paths:
|
|
||||||
- path: /
|
|
||||||
pathType: ImplementationSpecific
|
|
||||||
tls:
|
|
||||||
- secretName: harmony-example-rust-webapp-tls
|
|
||||||
hosts:
|
|
||||||
- chart-example.local
|
|
||||||
|
|
||||||
@@ -10,7 +10,6 @@ use harmony::{
|
|||||||
topology::K8sAnywhereTopology,
|
topology::K8sAnywhereTopology,
|
||||||
};
|
};
|
||||||
use harmony_macros::hurl;
|
use harmony_macros::hurl;
|
||||||
use harmony_types::k8s_name::K8sName;
|
|
||||||
use std::{path::PathBuf, sync::Arc};
|
use std::{path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -32,9 +31,8 @@ async fn main() {
|
|||||||
Box::new(Monitoring {
|
Box::new(Monitoring {
|
||||||
application: application.clone(),
|
application: application.clone(),
|
||||||
alert_receiver: vec![Box::new(DiscordWebhook {
|
alert_receiver: vec![Box::new(DiscordWebhook {
|
||||||
name: K8sName("test-discord".to_string()),
|
name: "test-discord".to_string(),
|
||||||
url: hurl!("https://discord.doesnt.exist.com"),
|
url: hurl!("https://discord.doesnt.exist.com"),
|
||||||
selectors: vec![],
|
|
||||||
})],
|
})],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -152,10 +152,10 @@ impl PhysicalHost {
|
|||||||
pub fn parts_list(&self) -> String {
|
pub fn parts_list(&self) -> String {
|
||||||
let PhysicalHost {
|
let PhysicalHost {
|
||||||
id,
|
id,
|
||||||
category,
|
category: _,
|
||||||
network,
|
network,
|
||||||
storage,
|
storage,
|
||||||
labels,
|
labels: _,
|
||||||
memory_modules,
|
memory_modules,
|
||||||
cpus,
|
cpus,
|
||||||
} = self;
|
} = self;
|
||||||
@@ -226,8 +226,8 @@ impl PhysicalHost {
|
|||||||
speed_mhz,
|
speed_mhz,
|
||||||
manufacturer,
|
manufacturer,
|
||||||
part_number,
|
part_number,
|
||||||
serial_number,
|
serial_number: _,
|
||||||
rank,
|
rank: _,
|
||||||
} = mem;
|
} = mem;
|
||||||
parts_list.push_str(&format!(
|
parts_list.push_str(&format!(
|
||||||
"\n{}Gb, {}Mhz, Manufacturer ({}), Part Number ({})",
|
"\n{}Gb, {}Mhz, Manufacturer ({}), Part Number ({})",
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ use std::error::Error;
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use derive_new::new;
|
use derive_new::new;
|
||||||
|
|
||||||
|
use crate::inventory::HostRole;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
data::Version, executors::ExecutorError, inventory::Inventory, topology::PreparationError,
|
data::Version, executors::ExecutorError, inventory::Inventory, topology::PreparationError,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,29 +1,26 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use brocade::PortOperatingMode;
|
||||||
use harmony_macros::ip;
|
use harmony_macros::ip;
|
||||||
use harmony_types::{
|
use harmony_types::{
|
||||||
|
id::Id,
|
||||||
net::{MacAddress, Url},
|
net::{MacAddress, Url},
|
||||||
switch::PortLocation,
|
switch::PortLocation,
|
||||||
};
|
};
|
||||||
use kube::api::ObjectMeta;
|
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use log::info;
|
use log::info;
|
||||||
|
|
||||||
use crate::modules::okd::crd::nmstate::{self, NodeNetworkConfigurationPolicy};
|
use crate::{infra::network_manager::OpenShiftNmStateNetworkManager, topology::PortConfig};
|
||||||
use crate::topology::PxeOptions;
|
use crate::topology::PxeOptions;
|
||||||
use crate::{data::FileContent, modules::okd::crd::nmstate::NMState};
|
use crate::{data::FileContent, executors::ExecutorError};
|
||||||
use crate::{
|
|
||||||
executors::ExecutorError, modules::okd::crd::nmstate::NodeNetworkConfigurationPolicySpec,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
DHCPStaticEntry, DhcpServer, DnsRecord, DnsRecordType, DnsServer, Firewall, HostNetworkConfig,
|
DHCPStaticEntry, DhcpServer, DnsRecord, DnsRecordType, DnsServer, Firewall, HostNetworkConfig,
|
||||||
HttpServer, IpAddress, K8sclient, LoadBalancer, LoadBalancerService, LogicalHost,
|
HttpServer, IpAddress, K8sclient, LoadBalancer, LoadBalancerService, LogicalHost, NetworkError,
|
||||||
PreparationError, PreparationOutcome, Router, Switch, SwitchClient, SwitchError, TftpServer,
|
NetworkManager, PreparationError, PreparationOutcome, Router, Switch, SwitchClient,
|
||||||
Topology, k8s::K8sClient,
|
SwitchError, TftpServer, Topology, k8s::K8sClient,
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::sync::{Arc, OnceLock};
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct HAClusterTopology {
|
pub struct HAClusterTopology {
|
||||||
@@ -40,6 +37,7 @@ pub struct HAClusterTopology {
|
|||||||
pub control_plane: Vec<LogicalHost>,
|
pub control_plane: Vec<LogicalHost>,
|
||||||
pub workers: Vec<LogicalHost>,
|
pub workers: Vec<LogicalHost>,
|
||||||
pub kubeconfig: Option<String>,
|
pub kubeconfig: Option<String>,
|
||||||
|
pub network_manager: OnceLock<Arc<dyn NetworkManager>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -63,7 +61,7 @@ impl K8sclient for HAClusterTopology {
|
|||||||
K8sClient::try_default().await.map_err(|e| e.to_string())?,
|
K8sClient::try_default().await.map_err(|e| e.to_string())?,
|
||||||
)),
|
)),
|
||||||
Some(kubeconfig) => {
|
Some(kubeconfig) => {
|
||||||
let Some(client) = K8sClient::from_kubeconfig(&kubeconfig).await else {
|
let Some(client) = K8sClient::from_kubeconfig(kubeconfig).await else {
|
||||||
return Err("Failed to create k8s client".to_string());
|
return Err("Failed to create k8s client".to_string());
|
||||||
};
|
};
|
||||||
Ok(Arc::new(client))
|
Ok(Arc::new(client))
|
||||||
@@ -93,191 +91,12 @@ impl HAClusterTopology {
|
|||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn ensure_nmstate_operator_installed(&self) -> Result<(), String> {
|
pub async fn network_manager(&self) -> &dyn NetworkManager {
|
||||||
let k8s_client = self.k8s_client().await?;
|
let k8s_client = self.k8s_client().await.unwrap();
|
||||||
|
|
||||||
debug!("Installing NMState controller...");
|
self.network_manager
|
||||||
k8s_client.apply_url(url::Url::parse("https://github.com/nmstate/kubernetes-nmstate/releases/download/v0.84.0/nmstate.io_nmstates.yaml
|
.get_or_init(|| Arc::new(OpenShiftNmStateNetworkManager::new(k8s_client.clone())))
|
||||||
").unwrap(), Some("nmstate"))
|
.as_ref()
|
||||||
.await
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
debug!("Creating NMState namespace...");
|
|
||||||
k8s_client.apply_url(url::Url::parse("https://github.com/nmstate/kubernetes-nmstate/releases/download/v0.84.0/namespace.yaml
|
|
||||||
").unwrap(), Some("nmstate"))
|
|
||||||
.await
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
debug!("Creating NMState service account...");
|
|
||||||
k8s_client.apply_url(url::Url::parse("https://github.com/nmstate/kubernetes-nmstate/releases/download/v0.84.0/service_account.yaml
|
|
||||||
").unwrap(), Some("nmstate"))
|
|
||||||
.await
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
debug!("Creating NMState role...");
|
|
||||||
k8s_client.apply_url(url::Url::parse("https://github.com/nmstate/kubernetes-nmstate/releases/download/v0.84.0/role.yaml
|
|
||||||
").unwrap(), Some("nmstate"))
|
|
||||||
.await
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
debug!("Creating NMState role binding...");
|
|
||||||
k8s_client.apply_url(url::Url::parse("https://github.com/nmstate/kubernetes-nmstate/releases/download/v0.84.0/role_binding.yaml
|
|
||||||
").unwrap(), Some("nmstate"))
|
|
||||||
.await
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
debug!("Creating NMState operator...");
|
|
||||||
k8s_client.apply_url(url::Url::parse("https://github.com/nmstate/kubernetes-nmstate/releases/download/v0.84.0/operator.yaml
|
|
||||||
").unwrap(), Some("nmstate"))
|
|
||||||
.await
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
k8s_client
|
|
||||||
.wait_until_deployment_ready("nmstate-operator", Some("nmstate"), None)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let nmstate = NMState {
|
|
||||||
metadata: ObjectMeta {
|
|
||||||
name: Some("nmstate".to_string()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
debug!("Creating NMState: {nmstate:#?}");
|
|
||||||
k8s_client
|
|
||||||
.apply(&nmstate, None)
|
|
||||||
.await
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_next_bond_id(&self) -> u8 {
|
|
||||||
42 // FIXME: Find a better way to declare the bond id
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn configure_bond(&self, config: &HostNetworkConfig) -> Result<(), SwitchError> {
|
|
||||||
self.ensure_nmstate_operator_installed()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
SwitchError::new(format!(
|
|
||||||
"Can't configure bond, NMState operator not available: {e}"
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let bond_config = self.create_bond_configuration(config);
|
|
||||||
debug!(
|
|
||||||
"Applying NMState bond config for host {}: {bond_config:#?}",
|
|
||||||
config.host_id
|
|
||||||
);
|
|
||||||
self.k8s_client()
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.apply(&bond_config, None)
|
|
||||||
.await
|
|
||||||
.map_err(|e| SwitchError::new(format!("Failed to configure bond: {e}")))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_bond_configuration(
|
|
||||||
&self,
|
|
||||||
config: &HostNetworkConfig,
|
|
||||||
) -> NodeNetworkConfigurationPolicy {
|
|
||||||
let host_name = &config.host_id;
|
|
||||||
let bond_id = self.get_next_bond_id();
|
|
||||||
let bond_name = format!("bond{bond_id}");
|
|
||||||
|
|
||||||
info!("Configuring bond '{bond_name}' for host '{host_name}'...");
|
|
||||||
|
|
||||||
let mut bond_mtu: Option<u32> = None;
|
|
||||||
let mut copy_mac_from: Option<String> = None;
|
|
||||||
let mut bond_ports = Vec::new();
|
|
||||||
let mut interfaces: Vec<nmstate::InterfaceSpec> = Vec::new();
|
|
||||||
|
|
||||||
for switch_port in &config.switch_ports {
|
|
||||||
let interface_name = switch_port.interface.name.clone();
|
|
||||||
|
|
||||||
interfaces.push(nmstate::InterfaceSpec {
|
|
||||||
name: interface_name.clone(),
|
|
||||||
description: Some(format!("Member of bond {bond_name}")),
|
|
||||||
r#type: "ethernet".to_string(),
|
|
||||||
state: "up".to_string(),
|
|
||||||
mtu: Some(switch_port.interface.mtu),
|
|
||||||
mac_address: Some(switch_port.interface.mac_address.to_string()),
|
|
||||||
ipv4: Some(nmstate::IpStackSpec {
|
|
||||||
enabled: Some(false),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
ipv6: Some(nmstate::IpStackSpec {
|
|
||||||
enabled: Some(false),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
link_aggregation: None,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
|
|
||||||
bond_ports.push(interface_name.clone());
|
|
||||||
|
|
||||||
// Use the first port's details for the bond mtu and mac address
|
|
||||||
if bond_mtu.is_none() {
|
|
||||||
bond_mtu = Some(switch_port.interface.mtu);
|
|
||||||
}
|
|
||||||
if copy_mac_from.is_none() {
|
|
||||||
copy_mac_from = Some(interface_name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interfaces.push(nmstate::InterfaceSpec {
|
|
||||||
name: bond_name.clone(),
|
|
||||||
description: Some(format!("Network bond for host {host_name}")),
|
|
||||||
r#type: "bond".to_string(),
|
|
||||||
state: "up".to_string(),
|
|
||||||
copy_mac_from,
|
|
||||||
ipv4: Some(nmstate::IpStackSpec {
|
|
||||||
dhcp: Some(true),
|
|
||||||
enabled: Some(true),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
ipv6: Some(nmstate::IpStackSpec {
|
|
||||||
dhcp: Some(true),
|
|
||||||
autoconf: Some(true),
|
|
||||||
enabled: Some(true),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
link_aggregation: Some(nmstate::BondSpec {
|
|
||||||
mode: "802.3ad".to_string(),
|
|
||||||
ports: bond_ports,
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
|
|
||||||
NodeNetworkConfigurationPolicy {
|
|
||||||
metadata: ObjectMeta {
|
|
||||||
name: Some(format!("{host_name}-bond-config")),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
spec: NodeNetworkConfigurationPolicySpec {
|
|
||||||
node_selector: Some(BTreeMap::from([(
|
|
||||||
"kubernetes.io/hostname".to_string(),
|
|
||||||
host_name.to_string(),
|
|
||||||
)])),
|
|
||||||
desired_state: nmstate::DesiredStateSpec { interfaces },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn configure_port_channel(&self, config: &HostNetworkConfig) -> Result<(), SwitchError> {
|
|
||||||
debug!("Configuring port channel: {config:#?}");
|
|
||||||
let switch_ports = config.switch_ports.iter().map(|s| s.port.clone()).collect();
|
|
||||||
|
|
||||||
self.switch_client
|
|
||||||
.configure_port_channel(&format!("Harmony_{}", config.host_id), switch_ports)
|
|
||||||
.await
|
|
||||||
.map_err(|e| SwitchError::new(format!("Failed to configure switch: {e}")))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn autoload() -> Self {
|
pub fn autoload() -> Self {
|
||||||
@@ -301,6 +120,7 @@ impl HAClusterTopology {
|
|||||||
bootstrap_host: dummy_host,
|
bootstrap_host: dummy_host,
|
||||||
control_plane: vec![],
|
control_plane: vec![],
|
||||||
workers: vec![],
|
workers: vec![],
|
||||||
|
network_manager: OnceLock::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -458,21 +278,50 @@ impl HttpServer for HAClusterTopology {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Switch for HAClusterTopology {
|
impl Switch for HAClusterTopology {
|
||||||
async fn setup_switch(&self) -> Result<(), SwitchError> {
|
async fn setup_switch(&self) -> Result<(), SwitchError> {
|
||||||
self.switch_client.setup().await?;
|
self.switch_client.setup().await.map(|_| ())
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_port_for_mac_address(
|
async fn get_port_for_mac_address(
|
||||||
&self,
|
&self,
|
||||||
mac_address: &MacAddress,
|
mac_address: &MacAddress,
|
||||||
) -> Result<Option<PortLocation>, SwitchError> {
|
) -> Result<Option<PortLocation>, SwitchError> {
|
||||||
let port = self.switch_client.find_port(mac_address).await?;
|
self.switch_client.find_port(mac_address).await
|
||||||
Ok(port)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn configure_host_network(&self, config: &HostNetworkConfig) -> Result<(), SwitchError> {
|
async fn configure_port_channel(&self, config: &HostNetworkConfig) -> Result<(), SwitchError> {
|
||||||
self.configure_bond(config).await?;
|
debug!("Configuring port channel: {config:#?}");
|
||||||
self.configure_port_channel(config).await
|
let switch_ports = config.switch_ports.iter().map(|s| s.port.clone()).collect();
|
||||||
|
|
||||||
|
self.switch_client
|
||||||
|
.configure_port_channel(&format!("Harmony_{}", config.host_id), switch_ports)
|
||||||
|
.await
|
||||||
|
.map_err(|e| SwitchError::new(format!("Failed to configure port-channel: {e}")))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn clear_port_channel(&self, ids: &Vec<Id>) -> Result<(), SwitchError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
async fn configure_interface(
|
||||||
|
&self,
|
||||||
|
ports: &Vec<PortConfig>,
|
||||||
|
) -> Result<(), SwitchError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl NetworkManager for HAClusterTopology {
|
||||||
|
async fn ensure_network_manager_installed(&self) -> Result<(), NetworkError> {
|
||||||
|
self.network_manager()
|
||||||
|
.await
|
||||||
|
.ensure_network_manager_installed()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn configure_bond(&self, config: &HostNetworkConfig) -> Result<(), NetworkError> {
|
||||||
|
self.network_manager().await.configure_bond(config).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -683,4 +532,6 @@ impl SwitchClient for DummyInfra {
|
|||||||
) -> Result<u8, SwitchError> {
|
) -> Result<u8, SwitchError> {
|
||||||
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
|
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
|
||||||
}
|
}
|
||||||
|
async fn clear_port_channel(&self, ids: &Vec<Id>) -> Result<(), SwitchError> {todo!()}
|
||||||
|
async fn configure_interface(&self, ports: &Vec<PortConfig>) -> Result<(), SwitchError> {todo!()}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,15 +5,17 @@ use k8s_openapi::{
|
|||||||
ClusterResourceScope, NamespaceResourceScope,
|
ClusterResourceScope, NamespaceResourceScope,
|
||||||
api::{
|
api::{
|
||||||
apps::v1::Deployment,
|
apps::v1::Deployment,
|
||||||
core::v1::{Pod, ServiceAccount},
|
core::v1::{Node, Pod, ServiceAccount},
|
||||||
},
|
},
|
||||||
apimachinery::pkg::version::Info,
|
apimachinery::pkg::version::Info,
|
||||||
};
|
};
|
||||||
use kube::{
|
use kube::{
|
||||||
Client, Config, Discovery, Error, Resource,
|
Client, Config, Discovery, Error, Resource,
|
||||||
api::{Api, AttachParams, DeleteParams, ListParams, Patch, PatchParams, ResourceExt},
|
api::{
|
||||||
|
Api, AttachParams, DeleteParams, ListParams, ObjectList, Patch, PatchParams, ResourceExt,
|
||||||
|
},
|
||||||
config::{KubeConfigOptions, Kubeconfig},
|
config::{KubeConfigOptions, Kubeconfig},
|
||||||
core::{DynamicResourceScope, ErrorResponse},
|
core::ErrorResponse,
|
||||||
discovery::{ApiCapabilities, Scope},
|
discovery::{ApiCapabilities, Scope},
|
||||||
error::DiscoveryError,
|
error::DiscoveryError,
|
||||||
runtime::reflector::Lookup,
|
runtime::reflector::Lookup,
|
||||||
@@ -23,7 +25,7 @@ use kube::{
|
|||||||
api::{ApiResource, GroupVersionKind},
|
api::{ApiResource, GroupVersionKind},
|
||||||
runtime::wait::await_condition,
|
runtime::wait::await_condition,
|
||||||
};
|
};
|
||||||
use log::{debug, error, info, trace, warn};
|
use log::{debug, error, trace, warn};
|
||||||
use serde::{Serialize, de::DeserializeOwned};
|
use serde::{Serialize, de::DeserializeOwned};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use similar::TextDiff;
|
use similar::TextDiff;
|
||||||
@@ -94,23 +96,6 @@ impl K8sClient {
|
|||||||
resource.get(name).await
|
resource.get(name).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_secret_json_value(
|
|
||||||
&self,
|
|
||||||
name: &str,
|
|
||||||
namespace: Option<&str>,
|
|
||||||
) -> Result<DynamicObject, Error> {
|
|
||||||
self.get_resource_json_value(
|
|
||||||
name,
|
|
||||||
namespace,
|
|
||||||
&GroupVersionKind {
|
|
||||||
group: "".to_string(),
|
|
||||||
version: "v1".to_string(),
|
|
||||||
kind: "Secret".to_string(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_deployment(
|
pub async fn get_deployment(
|
||||||
&self,
|
&self,
|
||||||
name: &str,
|
name: &str,
|
||||||
@@ -354,169 +339,6 @@ impl K8sClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_api_for_dynamic_object(
|
|
||||||
&self,
|
|
||||||
object: &DynamicObject,
|
|
||||||
ns: Option<&str>,
|
|
||||||
) -> Result<Api<DynamicObject>, Error> {
|
|
||||||
let api_resource = object
|
|
||||||
.types
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|t| {
|
|
||||||
let parts: Vec<&str> = t.api_version.split('/').collect();
|
|
||||||
match parts.as_slice() {
|
|
||||||
[version] => Some(ApiResource::from_gvk(&GroupVersionKind::gvk(
|
|
||||||
"", version, &t.kind,
|
|
||||||
))),
|
|
||||||
[group, version] => Some(ApiResource::from_gvk(&GroupVersionKind::gvk(
|
|
||||||
group, version, &t.kind,
|
|
||||||
))),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.ok_or_else(|| {
|
|
||||||
Error::BuildRequest(kube::core::request::Error::Validation(
|
|
||||||
"Invalid apiVersion in DynamicObject {object:#?}".to_string(),
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
match ns {
|
|
||||||
Some(ns) => Ok(Api::namespaced_with(self.client.clone(), ns, &api_resource)),
|
|
||||||
None => Ok(Api::default_namespaced_with(
|
|
||||||
self.client.clone(),
|
|
||||||
&api_resource,
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn apply_dynamic_many(
|
|
||||||
&self,
|
|
||||||
resource: &[DynamicObject],
|
|
||||||
namespace: Option<&str>,
|
|
||||||
force_conflicts: bool,
|
|
||||||
) -> Result<Vec<DynamicObject>, Error> {
|
|
||||||
let mut result = Vec::new();
|
|
||||||
for r in resource.iter() {
|
|
||||||
result.push(self.apply_dynamic(r, namespace, force_conflicts).await?);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Apply DynamicObject resource to the cluster
|
|
||||||
pub async fn apply_dynamic(
|
|
||||||
&self,
|
|
||||||
resource: &DynamicObject,
|
|
||||||
namespace: Option<&str>,
|
|
||||||
force_conflicts: bool,
|
|
||||||
) -> Result<DynamicObject, Error> {
|
|
||||||
// Build API for this dynamic object
|
|
||||||
let api = self.get_api_for_dynamic_object(resource, namespace)?;
|
|
||||||
let name = resource
|
|
||||||
.metadata
|
|
||||||
.name
|
|
||||||
.as_ref()
|
|
||||||
.ok_or_else(|| {
|
|
||||||
Error::BuildRequest(kube::core::request::Error::Validation(
|
|
||||||
"DynamicObject must have metadata.name".to_string(),
|
|
||||||
))
|
|
||||||
})?
|
|
||||||
.as_str();
|
|
||||||
|
|
||||||
debug!(
|
|
||||||
"Applying dynamic resource kind={:?} apiVersion={:?} name='{}' ns={:?}",
|
|
||||||
resource.types.as_ref().map(|t| &t.kind),
|
|
||||||
resource.types.as_ref().map(|t| &t.api_version),
|
|
||||||
name,
|
|
||||||
namespace
|
|
||||||
);
|
|
||||||
trace!(
|
|
||||||
"Dynamic resource payload:\n{:#}",
|
|
||||||
serde_json::to_value(resource).unwrap_or(serde_json::Value::Null)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Using same field manager as in apply()
|
|
||||||
let mut patch_params = PatchParams::apply("harmony");
|
|
||||||
patch_params.force = force_conflicts;
|
|
||||||
|
|
||||||
if *crate::config::DRY_RUN {
|
|
||||||
// Dry-run path: fetch current, show diff, and return appropriate object
|
|
||||||
match api.get(name).await {
|
|
||||||
Ok(current) => {
|
|
||||||
trace!("Received current dynamic value {current:#?}");
|
|
||||||
|
|
||||||
println!("\nPerforming dry-run for resource: '{}'", name);
|
|
||||||
|
|
||||||
// Serialize current and new, and strip status from current if present
|
|
||||||
let mut current_yaml =
|
|
||||||
serde_yaml::to_value(¤t).unwrap_or_else(|_| serde_yaml::Value::Null);
|
|
||||||
if let Some(map) = current_yaml.as_mapping_mut() {
|
|
||||||
if map.contains_key(&serde_yaml::Value::String("status".to_string())) {
|
|
||||||
let removed =
|
|
||||||
map.remove(&serde_yaml::Value::String("status".to_string()));
|
|
||||||
trace!("Removed status from current dynamic object: {:?}", removed);
|
|
||||||
} else {
|
|
||||||
trace!(
|
|
||||||
"Did not find status entry for current dynamic object {}/{}",
|
|
||||||
current.metadata.namespace.as_deref().unwrap_or(""),
|
|
||||||
current.metadata.name.as_deref().unwrap_or("")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let current_yaml = serde_yaml::to_string(¤t_yaml)
|
|
||||||
.unwrap_or_else(|_| "Failed to serialize current resource".to_string());
|
|
||||||
let new_yaml = serde_yaml::to_string(resource)
|
|
||||||
.unwrap_or_else(|_| "Failed to serialize new resource".to_string());
|
|
||||||
|
|
||||||
if current_yaml == new_yaml {
|
|
||||||
println!("No changes detected.");
|
|
||||||
return Ok(current);
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("Changes detected:");
|
|
||||||
let diff = TextDiff::from_lines(¤t_yaml, &new_yaml);
|
|
||||||
for change in diff.iter_all_changes() {
|
|
||||||
let sign = match change.tag() {
|
|
||||||
similar::ChangeTag::Delete => "-",
|
|
||||||
similar::ChangeTag::Insert => "+",
|
|
||||||
similar::ChangeTag::Equal => " ",
|
|
||||||
};
|
|
||||||
print!("{}{}", sign, change);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the incoming resource as the would-be applied state
|
|
||||||
Ok(resource.clone())
|
|
||||||
}
|
|
||||||
Err(Error::Api(ErrorResponse { code: 404, .. })) => {
|
|
||||||
println!("\nPerforming dry-run for new resource: '{}'", name);
|
|
||||||
println!(
|
|
||||||
"Resource does not exist. It would be created with the following content:"
|
|
||||||
);
|
|
||||||
let new_yaml = serde_yaml::to_string(resource)
|
|
||||||
.unwrap_or_else(|_| "Failed to serialize new resource".to_string());
|
|
||||||
for line in new_yaml.lines() {
|
|
||||||
println!("+{}", line);
|
|
||||||
}
|
|
||||||
Ok(resource.clone())
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Failed to get dynamic resource '{}': {}", name, e);
|
|
||||||
Err(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Real apply via server-side apply
|
|
||||||
debug!("Patching (server-side apply) dynamic resource '{}'", name);
|
|
||||||
api.patch(name, &patch_params, &Patch::Apply(resource))
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
error!("Failed to apply dynamic resource '{}': {}", name, e);
|
|
||||||
e
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Apply a resource in namespace
|
/// Apply a resource in namespace
|
||||||
///
|
///
|
||||||
/// See `kubectl apply` for more information on the expected behavior of this function
|
/// See `kubectl apply` for more information on the expected behavior of this function
|
||||||
@@ -744,7 +566,58 @@ impl K8sClient {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn from_kubeconfig(path: &str) -> Option<K8sClient> {
|
/// Gets a single named resource of a specific type `K`.
|
||||||
|
///
|
||||||
|
/// This function uses the `ApplyStrategy` trait to correctly determine
|
||||||
|
/// whether to look in a specific namespace or in the entire cluster.
|
||||||
|
///
|
||||||
|
/// Returns `Ok(None)` if the resource is not found (404).
|
||||||
|
pub async fn get_resource<K>(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
namespace: Option<&str>,
|
||||||
|
) -> Result<Option<K>, Error>
|
||||||
|
where
|
||||||
|
K: Resource + Clone + std::fmt::Debug + DeserializeOwned,
|
||||||
|
<K as Resource>::Scope: ApplyStrategy<K>,
|
||||||
|
<K as kube::Resource>::DynamicType: Default,
|
||||||
|
{
|
||||||
|
let api: Api<K> =
|
||||||
|
<<K as Resource>::Scope as ApplyStrategy<K>>::get_api(&self.client, namespace);
|
||||||
|
|
||||||
|
api.get_opt(name).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lists all resources of a specific type `K`.
|
||||||
|
///
|
||||||
|
/// This function uses the `ApplyStrategy` trait to correctly determine
|
||||||
|
/// whether to list from a specific namespace or from the entire cluster.
|
||||||
|
pub async fn list_resources<K>(
|
||||||
|
&self,
|
||||||
|
namespace: Option<&str>,
|
||||||
|
list_params: Option<ListParams>,
|
||||||
|
) -> Result<ObjectList<K>, Error>
|
||||||
|
where
|
||||||
|
K: Resource + Clone + std::fmt::Debug + DeserializeOwned,
|
||||||
|
<K as Resource>::Scope: ApplyStrategy<K>,
|
||||||
|
<K as kube::Resource>::DynamicType: Default,
|
||||||
|
{
|
||||||
|
let api: Api<K> =
|
||||||
|
<<K as Resource>::Scope as ApplyStrategy<K>>::get_api(&self.client, namespace);
|
||||||
|
|
||||||
|
let list_params = list_params.unwrap_or_default();
|
||||||
|
api.list(&list_params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches a list of all Nodes in the cluster.
|
||||||
|
pub async fn get_nodes(
|
||||||
|
&self,
|
||||||
|
list_params: Option<ListParams>,
|
||||||
|
) -> Result<ObjectList<Node>, Error> {
|
||||||
|
self.list_resources(None, list_params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn from_kubeconfig(path: &str) -> Option<K8sClient> {
|
||||||
let k = match Kubeconfig::read_from(path) {
|
let k = match Kubeconfig::read_from(path) {
|
||||||
Ok(k) => k,
|
Ok(k) => k,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use brocade::PortOperatingMode;
|
||||||
use derive_new::new;
|
use derive_new::new;
|
||||||
use harmony_types::{
|
use harmony_types::{
|
||||||
id::Id,
|
id::Id,
|
||||||
@@ -15,7 +16,7 @@ use harmony_types::{
|
|||||||
};
|
};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::{executors::ExecutorError, hardware::PhysicalHost};
|
use crate::executors::ExecutorError;
|
||||||
|
|
||||||
use super::{LogicalHost, k8s::K8sClient};
|
use super::{LogicalHost, k8s::K8sClient};
|
||||||
|
|
||||||
@@ -183,6 +184,39 @@ impl FromStr for DnsRecordType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait NetworkManager: Debug + Send + Sync {
|
||||||
|
async fn ensure_network_manager_installed(&self) -> Result<(), NetworkError>;
|
||||||
|
async fn configure_bond(&self, config: &HostNetworkConfig) -> Result<(), NetworkError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, new)]
|
||||||
|
pub struct NetworkError {
|
||||||
|
msg: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for NetworkError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str(&self.msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for NetworkError {}
|
||||||
|
|
||||||
|
impl From<kube::Error> for NetworkError {
|
||||||
|
fn from(value: kube::Error) -> Self {
|
||||||
|
NetworkError::new(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for NetworkError {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
NetworkError::new(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type PortConfig = (PortLocation, PortOperatingMode);
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait Switch: Send + Sync {
|
pub trait Switch: Send + Sync {
|
||||||
async fn setup_switch(&self) -> Result<(), SwitchError>;
|
async fn setup_switch(&self) -> Result<(), SwitchError>;
|
||||||
@@ -192,7 +226,9 @@ pub trait Switch: Send + Sync {
|
|||||||
mac_address: &MacAddress,
|
mac_address: &MacAddress,
|
||||||
) -> Result<Option<PortLocation>, SwitchError>;
|
) -> Result<Option<PortLocation>, SwitchError>;
|
||||||
|
|
||||||
async fn configure_host_network(&self, config: &HostNetworkConfig) -> Result<(), SwitchError>;
|
async fn configure_port_channel(&self, config: &HostNetworkConfig) -> Result<(), SwitchError>;
|
||||||
|
async fn clear_port_channel(&self, ids: &Vec<Id>) -> Result<(), SwitchError>;
|
||||||
|
async fn configure_interface(&self, ports: &Vec<PortConfig>) -> Result<(), SwitchError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
@@ -252,6 +288,9 @@ pub trait SwitchClient: Debug + Send + Sync {
|
|||||||
channel_name: &str,
|
channel_name: &str,
|
||||||
switch_ports: Vec<PortLocation>,
|
switch_ports: Vec<PortLocation>,
|
||||||
) -> Result<u8, SwitchError>;
|
) -> Result<u8, SwitchError>;
|
||||||
|
|
||||||
|
async fn clear_port_channel(&self, ids: &Vec<Id>) -> Result<(), SwitchError>;
|
||||||
|
async fn configure_interface(&self, ports: &Vec<PortConfig>) -> Result<(), SwitchError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use std::{any::Any, collections::HashMap};
|
use std::any::Any;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use kube::api::DynamicObject;
|
|
||||||
use log::debug;
|
use log::debug;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -77,15 +76,6 @@ pub trait AlertReceiver<S: AlertSender>: std::fmt::Debug + Send + Sync {
|
|||||||
fn name(&self) -> String;
|
fn name(&self) -> String;
|
||||||
fn clone_box(&self) -> Box<dyn AlertReceiver<S>>;
|
fn clone_box(&self) -> Box<dyn AlertReceiver<S>>;
|
||||||
fn as_any(&self) -> &dyn Any;
|
fn as_any(&self) -> &dyn Any;
|
||||||
fn as_alertmanager_receiver(&self) -> Result<AlertManagerReceiver, String>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct AlertManagerReceiver {
|
|
||||||
pub receiver_config: serde_json::Value,
|
|
||||||
// FIXME we should not leak k8s here. DynamicObject is k8s specific
|
|
||||||
pub additional_ressources: Vec<DynamicObject>,
|
|
||||||
pub route_config: serde_json::Value,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use brocade::{BrocadeClient, BrocadeOptions, InterSwitchLink, InterfaceStatus, PortOperatingMode};
|
use brocade::{BrocadeClient, BrocadeOptions, InterSwitchLink, InterfaceStatus, PortOperatingMode};
|
||||||
use harmony_types::{
|
use harmony_types::{
|
||||||
|
id::Id,
|
||||||
net::{IpAddress, MacAddress},
|
net::{IpAddress, MacAddress},
|
||||||
switch::{PortDeclaration, PortLocation},
|
switch::{PortDeclaration, PortLocation},
|
||||||
};
|
};
|
||||||
use option_ext::OptionExt;
|
use option_ext::OptionExt;
|
||||||
|
|
||||||
use crate::topology::{SwitchClient, SwitchError};
|
use crate::topology::{PortConfig, SwitchClient, SwitchError};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct BrocadeSwitchClient {
|
pub struct BrocadeSwitchClient {
|
||||||
@@ -18,9 +19,9 @@ impl BrocadeSwitchClient {
|
|||||||
ip_addresses: &[IpAddress],
|
ip_addresses: &[IpAddress],
|
||||||
username: &str,
|
username: &str,
|
||||||
password: &str,
|
password: &str,
|
||||||
options: Option<BrocadeOptions>,
|
options: BrocadeOptions,
|
||||||
) -> Result<Self, brocade::Error> {
|
) -> Result<Self, brocade::Error> {
|
||||||
let brocade = brocade::init(ip_addresses, 22, username, password, options).await?;
|
let brocade = brocade::init(ip_addresses, username, password, options).await?;
|
||||||
Ok(Self { brocade })
|
Ok(Self { brocade })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,7 +60,7 @@ impl SwitchClient for BrocadeSwitchClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.brocade
|
self.brocade
|
||||||
.configure_interfaces(interfaces)
|
.configure_interfaces(&interfaces)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| SwitchError::new(e.to_string()))?;
|
.map_err(|e| SwitchError::new(e.to_string()))?;
|
||||||
|
|
||||||
@@ -111,6 +112,24 @@ impl SwitchClient for BrocadeSwitchClient {
|
|||||||
|
|
||||||
Ok(channel_id)
|
Ok(channel_id)
|
||||||
}
|
}
|
||||||
|
async fn clear_port_channel(&self, ids: &Vec<Id>) -> Result<(), SwitchError> {
|
||||||
|
for i in ids {
|
||||||
|
self.brocade
|
||||||
|
.clear_port_channel(&i.to_string())
|
||||||
|
.await
|
||||||
|
.map_err(|e| SwitchError::new(e.to_string()))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn configure_interface(&self, ports: &Vec<PortConfig>) -> Result<(), SwitchError> {
|
||||||
|
// FIXME hardcoded TenGigabitEthernet = bad
|
||||||
|
let ports = ports.iter().map(|p| (format!("TenGigabitEthernet {}", p.0), p.1.clone())).collect();
|
||||||
|
self.brocade
|
||||||
|
.configure_interfaces(&ports)
|
||||||
|
.await
|
||||||
|
.map_err(|e| SwitchError::new(e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -147,8 +166,8 @@ mod tests {
|
|||||||
|
|
||||||
let configured_interfaces = brocade.configured_interfaces.lock().unwrap();
|
let configured_interfaces = brocade.configured_interfaces.lock().unwrap();
|
||||||
assert_that!(*configured_interfaces).contains_exactly(vec![
|
assert_that!(*configured_interfaces).contains_exactly(vec![
|
||||||
(first_interface.name.clone(), PortOperatingMode::Access),
|
(first_interface.port_location, PortOperatingMode::Access),
|
||||||
(second_interface.name.clone(), PortOperatingMode::Access),
|
(second_interface.port_location, PortOperatingMode::Access),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,10 +274,10 @@ mod tests {
|
|||||||
|
|
||||||
async fn configure_interfaces(
|
async fn configure_interfaces(
|
||||||
&self,
|
&self,
|
||||||
interfaces: Vec<(String, PortOperatingMode)>,
|
interfaces: &Vec<(String, PortOperatingMode)>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let mut configured_interfaces = self.configured_interfaces.lock().unwrap();
|
let mut configured_interfaces = self.configured_interfaces.lock().unwrap();
|
||||||
*configured_interfaces = interfaces;
|
*configured_interfaces = interfaces.clone();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
fn deployment_to_dynamic_roundtrip() {
|
fn deployment_to_dynamic_roundtrip() {
|
||||||
// Create a sample Deployment with nested structures
|
// Create a sample Deployment with nested structures
|
||||||
let mut deployment = Deployment {
|
let deployment = Deployment {
|
||||||
metadata: ObjectMeta {
|
metadata: ObjectMeta {
|
||||||
name: Some("my-deployment".to_string()),
|
name: Some("my-deployment".to_string()),
|
||||||
labels: Some({
|
labels: Some({
|
||||||
|
|||||||
@@ -4,5 +4,6 @@ pub mod hp_ilo;
|
|||||||
pub mod intel_amt;
|
pub mod intel_amt;
|
||||||
pub mod inventory;
|
pub mod inventory;
|
||||||
pub mod kube;
|
pub mod kube;
|
||||||
|
pub mod network_manager;
|
||||||
pub mod opnsense;
|
pub mod opnsense;
|
||||||
mod sqlx;
|
mod sqlx;
|
||||||
|
|||||||
259
harmony/src/infra/network_manager.rs
Normal file
259
harmony/src/infra/network_manager.rs
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
use std::{
|
||||||
|
collections::{BTreeMap, HashSet},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use harmony_types::id::Id;
|
||||||
|
use k8s_openapi::api::core::v1::Node;
|
||||||
|
use kube::{
|
||||||
|
ResourceExt,
|
||||||
|
api::{ObjectList, ObjectMeta},
|
||||||
|
};
|
||||||
|
use log::{debug, info};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
modules::okd::crd::nmstate,
|
||||||
|
topology::{HostNetworkConfig, NetworkError, NetworkManager, k8s::K8sClient},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct OpenShiftNmStateNetworkManager {
|
||||||
|
k8s_client: Arc<K8sClient>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for OpenShiftNmStateNetworkManager {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("OpenShiftNmStateNetworkManager").finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl NetworkManager for OpenShiftNmStateNetworkManager {
|
||||||
|
async fn ensure_network_manager_installed(&self) -> Result<(), NetworkError> {
|
||||||
|
debug!("Installing NMState controller...");
|
||||||
|
self.k8s_client.apply_url(url::Url::parse("https://github.com/nmstate/kubernetes-nmstate/releases/download/v0.84.0/nmstate.io_nmstates.yaml
|
||||||
|
").unwrap(), Some("nmstate"))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
debug!("Creating NMState namespace...");
|
||||||
|
self.k8s_client.apply_url(url::Url::parse("https://github.com/nmstate/kubernetes-nmstate/releases/download/v0.84.0/namespace.yaml
|
||||||
|
").unwrap(), Some("nmstate"))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
debug!("Creating NMState service account...");
|
||||||
|
self.k8s_client.apply_url(url::Url::parse("https://github.com/nmstate/kubernetes-nmstate/releases/download/v0.84.0/service_account.yaml
|
||||||
|
").unwrap(), Some("nmstate"))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
debug!("Creating NMState role...");
|
||||||
|
self.k8s_client.apply_url(url::Url::parse("https://github.com/nmstate/kubernetes-nmstate/releases/download/v0.84.0/role.yaml
|
||||||
|
").unwrap(), Some("nmstate"))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
debug!("Creating NMState role binding...");
|
||||||
|
self.k8s_client.apply_url(url::Url::parse("https://github.com/nmstate/kubernetes-nmstate/releases/download/v0.84.0/role_binding.yaml
|
||||||
|
").unwrap(), Some("nmstate"))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
debug!("Creating NMState operator...");
|
||||||
|
self.k8s_client.apply_url(url::Url::parse("https://github.com/nmstate/kubernetes-nmstate/releases/download/v0.84.0/operator.yaml
|
||||||
|
").unwrap(), Some("nmstate"))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
self.k8s_client
|
||||||
|
.wait_until_deployment_ready("nmstate-operator", Some("nmstate"), None)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let nmstate = nmstate::NMState {
|
||||||
|
metadata: ObjectMeta {
|
||||||
|
name: Some("nmstate".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
debug!(
|
||||||
|
"Creating NMState:\n{}",
|
||||||
|
serde_yaml::to_string(&nmstate).unwrap()
|
||||||
|
);
|
||||||
|
self.k8s_client.apply(&nmstate, None).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn configure_bond(&self, config: &HostNetworkConfig) -> Result<(), NetworkError> {
|
||||||
|
let hostname = self.get_hostname(&config.host_id).await.map_err(|e| {
|
||||||
|
NetworkError::new(format!(
|
||||||
|
"Can't configure bond, can't get hostname for host '{}': {e}",
|
||||||
|
config.host_id
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let bond_id = self.get_next_bond_id(&hostname).await.map_err(|e| {
|
||||||
|
NetworkError::new(format!(
|
||||||
|
"Can't configure bond, can't get an available bond id for host '{}': {e}",
|
||||||
|
config.host_id
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let bond_config = self.create_bond_configuration(&hostname, &bond_id, config);
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"Applying NMState bond config for host {}:\n{}",
|
||||||
|
config.host_id,
|
||||||
|
serde_yaml::to_string(&bond_config).unwrap(),
|
||||||
|
);
|
||||||
|
self.k8s_client
|
||||||
|
.apply(&bond_config, None)
|
||||||
|
.await
|
||||||
|
.map_err(|e| NetworkError::new(format!("Failed to configure bond: {e}")))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OpenShiftNmStateNetworkManager {
|
||||||
|
pub fn new(k8s_client: Arc<K8sClient>) -> Self {
|
||||||
|
Self { k8s_client }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_bond_configuration(
|
||||||
|
&self,
|
||||||
|
host: &str,
|
||||||
|
bond_name: &str,
|
||||||
|
config: &HostNetworkConfig,
|
||||||
|
) -> nmstate::NodeNetworkConfigurationPolicy {
|
||||||
|
info!("Configuring bond '{bond_name}' for host '{host}'...");
|
||||||
|
|
||||||
|
let mut bond_mtu: Option<u32> = None;
|
||||||
|
let mut copy_mac_from: Option<String> = None;
|
||||||
|
let mut bond_ports = Vec::new();
|
||||||
|
let mut interfaces: Vec<nmstate::Interface> = Vec::new();
|
||||||
|
|
||||||
|
for switch_port in &config.switch_ports {
|
||||||
|
let interface_name = switch_port.interface.name.clone();
|
||||||
|
|
||||||
|
interfaces.push(nmstate::Interface {
|
||||||
|
name: interface_name.clone(),
|
||||||
|
description: Some(format!("Member of bond {bond_name}")),
|
||||||
|
r#type: nmstate::InterfaceType::Ethernet,
|
||||||
|
state: "up".to_string(),
|
||||||
|
mtu: Some(switch_port.interface.mtu),
|
||||||
|
mac_address: Some(switch_port.interface.mac_address.to_string()),
|
||||||
|
ipv4: Some(nmstate::IpStackSpec {
|
||||||
|
enabled: Some(false),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
ipv6: Some(nmstate::IpStackSpec {
|
||||||
|
enabled: Some(false),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
link_aggregation: None,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
bond_ports.push(interface_name.clone());
|
||||||
|
|
||||||
|
// Use the first port's details for the bond mtu and mac address
|
||||||
|
if bond_mtu.is_none() {
|
||||||
|
bond_mtu = Some(switch_port.interface.mtu);
|
||||||
|
}
|
||||||
|
if copy_mac_from.is_none() {
|
||||||
|
copy_mac_from = Some(interface_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interfaces.push(nmstate::Interface {
|
||||||
|
name: bond_name.to_string(),
|
||||||
|
description: Some(format!("Network bond for host {host}")),
|
||||||
|
r#type: nmstate::InterfaceType::Bond,
|
||||||
|
state: "up".to_string(),
|
||||||
|
copy_mac_from,
|
||||||
|
ipv4: Some(nmstate::IpStackSpec {
|
||||||
|
dhcp: Some(true),
|
||||||
|
enabled: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
ipv6: Some(nmstate::IpStackSpec {
|
||||||
|
dhcp: Some(true),
|
||||||
|
autoconf: Some(true),
|
||||||
|
enabled: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
link_aggregation: Some(nmstate::BondSpec {
|
||||||
|
mode: "802.3ad".to_string(),
|
||||||
|
ports: bond_ports,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
nmstate::NodeNetworkConfigurationPolicy {
|
||||||
|
metadata: ObjectMeta {
|
||||||
|
name: Some(format!("{host}-bond-config")),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
spec: nmstate::NodeNetworkConfigurationPolicySpec {
|
||||||
|
node_selector: Some(BTreeMap::from([(
|
||||||
|
"kubernetes.io/hostname".to_string(),
|
||||||
|
host.to_string(),
|
||||||
|
)])),
|
||||||
|
desired_state: nmstate::NetworkState {
|
||||||
|
interfaces,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_hostname(&self, host_id: &Id) -> Result<String, String> {
|
||||||
|
let nodes: ObjectList<Node> = self
|
||||||
|
.k8s_client
|
||||||
|
.list_resources(None, None)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to list nodes: {e}"))?;
|
||||||
|
|
||||||
|
let Some(node) = nodes.iter().find(|n| {
|
||||||
|
n.status
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|s| s.node_info.as_ref())
|
||||||
|
.map(|i| i.system_uuid == host_id.to_string())
|
||||||
|
.unwrap_or(false)
|
||||||
|
}) else {
|
||||||
|
return Err(format!("No node found for host '{host_id}'"));
|
||||||
|
};
|
||||||
|
|
||||||
|
node.labels()
|
||||||
|
.get("kubernetes.io/hostname")
|
||||||
|
.ok_or(format!(
|
||||||
|
"Node '{host_id}' has no kubernetes.io/hostname label"
|
||||||
|
))
|
||||||
|
.cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_next_bond_id(&self, hostname: &str) -> Result<String, String> {
|
||||||
|
let network_state: Option<nmstate::NodeNetworkState> = self
|
||||||
|
.k8s_client
|
||||||
|
.get_resource(hostname, None)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to list nodes: {e}"))?;
|
||||||
|
|
||||||
|
let interfaces = vec![];
|
||||||
|
let existing_bonds: Vec<&nmstate::Interface> = network_state
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|network_state| network_state.status.current_state.as_ref())
|
||||||
|
.map_or(&interfaces, |current_state| ¤t_state.interfaces)
|
||||||
|
.iter()
|
||||||
|
.filter(|i| i.r#type == nmstate::InterfaceType::Bond)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let used_ids: HashSet<u32> = existing_bonds
|
||||||
|
.iter()
|
||||||
|
.filter_map(|i| {
|
||||||
|
i.name
|
||||||
|
.strip_prefix("bond")
|
||||||
|
.and_then(|id| id.parse::<u32>().ok())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let next_id = (0..).find(|id| !used_ids.contains(id)).unwrap();
|
||||||
|
Ok(format!("bond{next_id}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,6 @@ mod tftp;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub use management::*;
|
pub use management::*;
|
||||||
use opnsense_config_xml::Host;
|
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
use crate::{executors::ExecutorError, topology::LogicalHost};
|
use crate::{executors::ExecutorError, topology::LogicalHost};
|
||||||
|
|||||||
@@ -19,8 +19,11 @@ pub struct DhcpScore {
|
|||||||
pub host_binding: Vec<HostBinding>,
|
pub host_binding: Vec<HostBinding>,
|
||||||
pub next_server: Option<IpAddress>,
|
pub next_server: Option<IpAddress>,
|
||||||
pub boot_filename: Option<String>,
|
pub boot_filename: Option<String>,
|
||||||
|
/// Boot filename to be provided to PXE clients identifying as BIOS
|
||||||
pub filename: Option<String>,
|
pub filename: Option<String>,
|
||||||
|
/// Boot filename to be provided to PXE clients identifying as uefi but NOT iPXE
|
||||||
pub filename64: Option<String>,
|
pub filename64: Option<String>,
|
||||||
|
/// Boot filename to be provided to PXE clients identifying as iPXE
|
||||||
pub filenameipxe: Option<String>,
|
pub filenameipxe: Option<String>,
|
||||||
pub dhcp_range: (IpAddress, IpAddress),
|
pub dhcp_range: (IpAddress, IpAddress),
|
||||||
pub domain: Option<String>,
|
pub domain: Option<String>,
|
||||||
|
|||||||
@@ -5,11 +5,10 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
data::Version,
|
data::Version,
|
||||||
hardware::PhysicalHost,
|
|
||||||
infra::inventory::InventoryRepositoryFactory,
|
infra::inventory::InventoryRepositoryFactory,
|
||||||
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
||||||
inventory::{HostRole, Inventory},
|
inventory::{HostRole, Inventory},
|
||||||
modules::inventory::LaunchDiscoverInventoryAgentScore,
|
modules::inventory::{HarmonyDiscoveryStrategy, LaunchDiscoverInventoryAgentScore},
|
||||||
score::Score,
|
score::Score,
|
||||||
topology::Topology,
|
topology::Topology,
|
||||||
};
|
};
|
||||||
@@ -17,11 +16,13 @@ use crate::{
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct DiscoverHostForRoleScore {
|
pub struct DiscoverHostForRoleScore {
|
||||||
pub role: HostRole,
|
pub role: HostRole,
|
||||||
|
pub number_desired_hosts: i16,
|
||||||
|
pub discovery_strategy : HarmonyDiscoveryStrategy,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Topology> Score<T> for DiscoverHostForRoleScore {
|
impl<T: Topology> Score<T> for DiscoverHostForRoleScore {
|
||||||
fn name(&self) -> String {
|
fn name(&self) -> String {
|
||||||
"DiscoverInventoryAgentScore".to_string()
|
format!("DiscoverHostForRoleScore({:?})", self.role)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||||
@@ -48,13 +49,15 @@ impl<T: Topology> Interpret<T> for DiscoverHostForRoleInterpret {
|
|||||||
);
|
);
|
||||||
LaunchDiscoverInventoryAgentScore {
|
LaunchDiscoverInventoryAgentScore {
|
||||||
discovery_timeout: None,
|
discovery_timeout: None,
|
||||||
|
discovery_strategy: self.score.discovery_strategy.clone(),
|
||||||
}
|
}
|
||||||
.interpret(inventory, topology)
|
.interpret(inventory, topology)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let host: PhysicalHost;
|
let mut chosen_hosts = vec![];
|
||||||
let host_repo = InventoryRepositoryFactory::build().await?;
|
let host_repo = InventoryRepositoryFactory::build().await?;
|
||||||
|
|
||||||
|
let mut assigned_hosts = 0;
|
||||||
loop {
|
loop {
|
||||||
let all_hosts = host_repo.get_all_hosts().await?;
|
let all_hosts = host_repo.get_all_hosts().await?;
|
||||||
|
|
||||||
@@ -74,12 +77,25 @@ impl<T: Topology> Interpret<T> for DiscoverHostForRoleInterpret {
|
|||||||
|
|
||||||
match ans {
|
match ans {
|
||||||
Ok(choice) => {
|
Ok(choice) => {
|
||||||
info!("Selected {} as the bootstrap node.", choice.summary());
|
info!(
|
||||||
|
"Assigned role {:?} for node {}",
|
||||||
|
self.score.role,
|
||||||
|
choice.summary()
|
||||||
|
);
|
||||||
host_repo
|
host_repo
|
||||||
.save_role_mapping(&self.score.role, &choice)
|
.save_role_mapping(&self.score.role, &choice)
|
||||||
.await?;
|
.await?;
|
||||||
host = choice;
|
chosen_hosts.push(choice);
|
||||||
break;
|
assigned_hosts += 1;
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Found {assigned_hosts} hosts for role {:?}",
|
||||||
|
self.score.role
|
||||||
|
);
|
||||||
|
|
||||||
|
if assigned_hosts == self.score.number_desired_hosts {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(inquire::InquireError::OperationCanceled) => {
|
Err(inquire::InquireError::OperationCanceled) => {
|
||||||
info!("Refresh requested. Fetching list of discovered hosts again...");
|
info!("Refresh requested. Fetching list of discovered hosts again...");
|
||||||
@@ -90,17 +106,19 @@ impl<T: Topology> Interpret<T> for DiscoverHostForRoleInterpret {
|
|||||||
"Failed to select node for role {:?} : {}",
|
"Failed to select node for role {:?} : {}",
|
||||||
self.score.role, e
|
self.score.role, e
|
||||||
);
|
);
|
||||||
return Err(InterpretError::new(format!(
|
return Err(InterpretError::new(format!("Could not select host : {e}")));
|
||||||
"Could not select host : {}",
|
|
||||||
e.to_string()
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Outcome::success(format!(
|
Ok(Outcome::success(format!(
|
||||||
"Successfully discovered host {} for role {:?}",
|
"Successfully discovered {} hosts {} for role {:?}",
|
||||||
host.summary(),
|
self.score.number_desired_hosts,
|
||||||
|
chosen_hosts
|
||||||
|
.iter()
|
||||||
|
.map(|h| h.summary())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(", "),
|
||||||
self.score.role
|
self.score.role
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
mod discovery;
|
mod discovery;
|
||||||
pub mod inspect;
|
pub mod inspect;
|
||||||
|
use std::net::Ipv4Addr;
|
||||||
|
|
||||||
|
use cidr::{Ipv4Cidr, Ipv4Inet};
|
||||||
pub use discovery::*;
|
pub use discovery::*;
|
||||||
|
use tokio::time::{Duration, timeout};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use harmony_inventory_agent::local_presence::DiscoveryEvent;
|
use harmony_inventory_agent::local_presence::DiscoveryEvent;
|
||||||
@@ -24,6 +28,7 @@ use harmony_types::id::Id;
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct LaunchDiscoverInventoryAgentScore {
|
pub struct LaunchDiscoverInventoryAgentScore {
|
||||||
pub discovery_timeout: Option<u64>,
|
pub discovery_timeout: Option<u64>,
|
||||||
|
pub discovery_strategy: HarmonyDiscoveryStrategy,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Topology> Score<T> for LaunchDiscoverInventoryAgentScore {
|
impl<T: Topology> Score<T> for LaunchDiscoverInventoryAgentScore {
|
||||||
@@ -43,6 +48,12 @@ struct DiscoverInventoryAgentInterpret {
|
|||||||
score: LaunchDiscoverInventoryAgentScore,
|
score: LaunchDiscoverInventoryAgentScore,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum HarmonyDiscoveryStrategy {
|
||||||
|
MDNS,
|
||||||
|
SUBNET { cidr: cidr::Ipv4Cidr, port: u16 },
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<T: Topology> Interpret<T> for DiscoverInventoryAgentInterpret {
|
impl<T: Topology> Interpret<T> for DiscoverInventoryAgentInterpret {
|
||||||
async fn execute(
|
async fn execute(
|
||||||
@@ -57,6 +68,37 @@ impl<T: Topology> Interpret<T> for DiscoverInventoryAgentInterpret {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
match self.score.discovery_strategy {
|
||||||
|
HarmonyDiscoveryStrategy::MDNS => self.launch_mdns_discovery().await,
|
||||||
|
HarmonyDiscoveryStrategy::SUBNET { cidr, port } => {
|
||||||
|
self.launch_cidr_discovery(&cidr, port).await
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Outcome::success(
|
||||||
|
"Discovery process completed successfully".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_name(&self) -> InterpretName {
|
||||||
|
InterpretName::DiscoverInventoryAgent
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_version(&self) -> Version {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_status(&self) -> InterpretStatus {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_children(&self) -> Vec<Id> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DiscoverInventoryAgentInterpret {
|
||||||
|
async fn launch_mdns_discovery(&self) {
|
||||||
harmony_inventory_agent::local_presence::discover_agents(
|
harmony_inventory_agent::local_presence::discover_agents(
|
||||||
self.score.discovery_timeout,
|
self.score.discovery_timeout,
|
||||||
|event: DiscoveryEvent| -> Result<(), String> {
|
|event: DiscoveryEvent| -> Result<(), String> {
|
||||||
@@ -112,6 +154,8 @@ impl<T: Topology> Interpret<T> for DiscoverInventoryAgentInterpret {
|
|||||||
cpus,
|
cpus,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// FIXME only save the host when it is new or something changed in it.
|
||||||
|
// we currently are saving the host every time it is discovered.
|
||||||
let repo = InventoryRepositoryFactory::build()
|
let repo = InventoryRepositoryFactory::build()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Could not build repository : {e}"))
|
.map_err(|e| format!("Could not build repository : {e}"))
|
||||||
@@ -132,25 +176,111 @@ impl<T: Topology> Interpret<T> for DiscoverInventoryAgentInterpret {
|
|||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await;
|
.await
|
||||||
Ok(Outcome::success(
|
|
||||||
"Discovery process completed successfully".to_string(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_name(&self) -> InterpretName {
|
// async fn launch_cidr_discovery(&self, cidr : &Ipv4Cidr, port: u16) {
|
||||||
InterpretName::DiscoverInventoryAgent
|
// todo!("launnch cidr discovery for {cidr} : {port}
|
||||||
}
|
// - Iterate over all possible addresses in cidr
|
||||||
|
// - make calls in batches of 20 attempting to reach harmony inventory agent on <addr, port> using same as above harmony_inventory_agent::client::get_host_inventory(&address, port)
|
||||||
|
// - Log warn when response is 404, it means the port was used by something else unexpected
|
||||||
|
// - Log error when response is 5xx
|
||||||
|
// - Log debug when no response (timeout 15 seconds)
|
||||||
|
// - Log info when found and response is 2xx
|
||||||
|
// ");
|
||||||
|
// }
|
||||||
|
async fn launch_cidr_discovery(&self, cidr: &Ipv4Cidr, port: u16) {
|
||||||
|
let addrs: Vec<Ipv4Inet> = cidr.iter().collect();
|
||||||
|
let total = addrs.len();
|
||||||
|
info!(
|
||||||
|
"Starting CIDR discovery for {} hosts on {}/{} (port {})",
|
||||||
|
total,
|
||||||
|
cidr.network_length(),
|
||||||
|
cidr,
|
||||||
|
port
|
||||||
|
);
|
||||||
|
|
||||||
fn get_version(&self) -> Version {
|
let batch_size: usize = 20;
|
||||||
todo!()
|
let timeout_secs = 5;
|
||||||
}
|
let request_timeout = Duration::from_secs(timeout_secs);
|
||||||
|
|
||||||
fn get_status(&self) -> InterpretStatus {
|
let mut current_batch = 0;
|
||||||
todo!()
|
let num_batches = addrs.len() / batch_size;
|
||||||
}
|
|
||||||
|
|
||||||
fn get_children(&self) -> Vec<Id> {
|
for batch in addrs.chunks(batch_size) {
|
||||||
todo!()
|
current_batch += 1;
|
||||||
|
info!("Starting query batch {current_batch} of {num_batches}, timeout {timeout_secs}");
|
||||||
|
let mut tasks = Vec::with_capacity(batch.len());
|
||||||
|
|
||||||
|
for addr in batch {
|
||||||
|
let addr = addr.address().to_string();
|
||||||
|
let port = port;
|
||||||
|
|
||||||
|
let task = tokio::spawn(async move {
|
||||||
|
match timeout(
|
||||||
|
request_timeout,
|
||||||
|
harmony_inventory_agent::client::get_host_inventory(&addr, port),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(Ok(host)) => {
|
||||||
|
info!("Found and response is 2xx for {addr}:{port}");
|
||||||
|
|
||||||
|
// Reuse the same conversion to PhysicalHost as MDNS flow
|
||||||
|
let harmony_inventory_agent::hwinfo::PhysicalHost {
|
||||||
|
storage_drives,
|
||||||
|
storage_controller,
|
||||||
|
memory_modules,
|
||||||
|
cpus,
|
||||||
|
chipset,
|
||||||
|
network_interfaces,
|
||||||
|
management_interface,
|
||||||
|
host_uuid,
|
||||||
|
} = host;
|
||||||
|
|
||||||
|
let host = PhysicalHost {
|
||||||
|
id: Id::from(host_uuid),
|
||||||
|
category: HostCategory::Server,
|
||||||
|
network: network_interfaces,
|
||||||
|
storage: storage_drives,
|
||||||
|
labels: vec![Label {
|
||||||
|
name: "discovered-by".to_string(),
|
||||||
|
value: "harmony-inventory-agent".to_string(),
|
||||||
|
}],
|
||||||
|
memory_modules,
|
||||||
|
cpus,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save host to inventory
|
||||||
|
let repo = InventoryRepositoryFactory::build()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Could not build repository : {e}"))
|
||||||
|
.unwrap();
|
||||||
|
if let Err(e) = repo.save(&host).await {
|
||||||
|
log::debug!("Failed to save host {}: {e}", host.id);
|
||||||
|
} else {
|
||||||
|
info!("Saved host id {}, summary : {}", host.id, host.summary());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
log::info!("Error querying inventory agent on {addr}:{port} : {e}");
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// Timeout for this host
|
||||||
|
log::debug!("No response (timeout) for {addr}:{port}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tasks.push(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for this batch to complete
|
||||||
|
for t in tasks {
|
||||||
|
let _ = t.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("CIDR discovery completed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,18 @@
|
|||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use harmony_types::k8s_name::K8sName;
|
|
||||||
use k8s_openapi::api::core::v1::Secret;
|
use k8s_openapi::api::core::v1::Secret;
|
||||||
use kube::Resource;
|
use kube::api::ObjectMeta;
|
||||||
use kube::api::{DynamicObject, ObjectMeta};
|
use log::debug;
|
||||||
use log::{debug, trace};
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use serde_yaml::{Mapping, Value};
|
use serde_yaml::{Mapping, Value};
|
||||||
|
|
||||||
use crate::infra::kube::kube_resource_to_dynamic;
|
|
||||||
use crate::modules::monitoring::kube_prometheus::crd::crd_alertmanager_config::{
|
use crate::modules::monitoring::kube_prometheus::crd::crd_alertmanager_config::{
|
||||||
AlertmanagerConfig, AlertmanagerConfigSpec, CRDPrometheus,
|
AlertmanagerConfig, AlertmanagerConfigSpec, CRDPrometheus,
|
||||||
};
|
};
|
||||||
use crate::modules::monitoring::kube_prometheus::crd::rhob_alertmanager_config::RHOBObservability;
|
use crate::modules::monitoring::kube_prometheus::crd::rhob_alertmanager_config::RHOBObservability;
|
||||||
use crate::modules::monitoring::okd::OpenshiftClusterAlertSender;
|
|
||||||
use crate::topology::oberservability::monitoring::AlertManagerReceiver;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
interpret::{InterpretError, Outcome},
|
interpret::{InterpretError, Outcome},
|
||||||
modules::monitoring::{
|
modules::monitoring::{
|
||||||
@@ -33,13 +28,14 @@ use harmony_types::net::Url;
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct DiscordWebhook {
|
pub struct DiscordWebhook {
|
||||||
pub name: K8sName,
|
pub name: String,
|
||||||
pub url: Url,
|
pub url: Url,
|
||||||
pub selectors: Vec<HashMap<String, String>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DiscordWebhook {
|
#[async_trait]
|
||||||
fn get_receiver_config(&self) -> Result<AlertManagerReceiver, String> {
|
impl AlertReceiver<RHOBObservability> for DiscordWebhook {
|
||||||
|
async fn install(&self, sender: &RHOBObservability) -> Result<Outcome, InterpretError> {
|
||||||
|
let ns = sender.namespace.clone();
|
||||||
let secret_name = format!("{}-secret", self.name.clone());
|
let secret_name = format!("{}-secret", self.name.clone());
|
||||||
let webhook_key = format!("{}", self.url.clone());
|
let webhook_key = format!("{}", self.url.clone());
|
||||||
|
|
||||||
@@ -56,91 +52,33 @@ impl DiscordWebhook {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut matchers: Vec<String> = Vec::new();
|
let _ = sender.client.apply(&secret, Some(&ns)).await;
|
||||||
for selector in &self.selectors {
|
|
||||||
trace!("selector: {:#?}", selector);
|
|
||||||
for (k, v) in selector {
|
|
||||||
matchers.push(format!("{} = {}", k, v));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(AlertManagerReceiver {
|
|
||||||
additional_ressources: vec![kube_resource_to_dynamic(&secret)?],
|
|
||||||
|
|
||||||
receiver_config: json!({
|
|
||||||
"name": self.name,
|
|
||||||
"discord_configs": [
|
|
||||||
{
|
|
||||||
"webhook_url": self.url.clone(),
|
|
||||||
"title": "{{ template \"discord.default.title\" . }}",
|
|
||||||
"message": "{{ template \"discord.default.message\" . }}"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}),
|
|
||||||
route_config: json!({
|
|
||||||
"receiver": self.name,
|
|
||||||
"matchers": matchers,
|
|
||||||
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AlertReceiver<OpenshiftClusterAlertSender> for DiscordWebhook {
|
|
||||||
async fn install(
|
|
||||||
&self,
|
|
||||||
sender: &OpenshiftClusterAlertSender,
|
|
||||||
) -> Result<Outcome, InterpretError> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> String {
|
|
||||||
self.name.clone().to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clone_box(&self) -> Box<dyn AlertReceiver<OpenshiftClusterAlertSender>> {
|
|
||||||
Box::new(self.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_any(&self) -> &dyn Any {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_alertmanager_receiver(&self) -> Result<AlertManagerReceiver, String> {
|
|
||||||
self.get_receiver_config()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AlertReceiver<RHOBObservability> for DiscordWebhook {
|
|
||||||
fn as_alertmanager_receiver(&self) -> Result<AlertManagerReceiver, String> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn install(&self, sender: &RHOBObservability) -> Result<Outcome, InterpretError> {
|
|
||||||
let ns = sender.namespace.clone();
|
|
||||||
|
|
||||||
let config = self.get_receiver_config()?;
|
|
||||||
for resource in config.additional_ressources.iter() {
|
|
||||||
todo!("can I apply a dynamicresource");
|
|
||||||
// sender.client.apply(resource, Some(&ns)).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
let spec = crate::modules::monitoring::kube_prometheus::crd::rhob_alertmanager_config::AlertmanagerConfigSpec {
|
let spec = crate::modules::monitoring::kube_prometheus::crd::rhob_alertmanager_config::AlertmanagerConfigSpec {
|
||||||
data: json!({
|
data: json!({
|
||||||
"route": {
|
"route": {
|
||||||
"receiver": self.name,
|
"receiver": self.name,
|
||||||
},
|
},
|
||||||
"receivers": [
|
"receivers": [
|
||||||
config.receiver_config
|
{
|
||||||
|
"name": self.name,
|
||||||
|
"discordConfigs": [
|
||||||
|
{
|
||||||
|
"apiURL": {
|
||||||
|
"name": secret_name,
|
||||||
|
"key": "webhook-url",
|
||||||
|
},
|
||||||
|
"title": "{{ template \"discord.default.title\" . }}",
|
||||||
|
"message": "{{ template \"discord.default.message\" . }}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
let alertmanager_configs = crate::modules::monitoring::kube_prometheus::crd::rhob_alertmanager_config::AlertmanagerConfig {
|
let alertmanager_configs = crate::modules::monitoring::kube_prometheus::crd::rhob_alertmanager_config::AlertmanagerConfig {
|
||||||
metadata: ObjectMeta {
|
metadata: ObjectMeta {
|
||||||
name: Some(self.name.clone().to_string()),
|
name: Some(self.name.clone()),
|
||||||
labels: Some(std::collections::BTreeMap::from([(
|
labels: Some(std::collections::BTreeMap::from([(
|
||||||
"alertmanagerConfig".to_string(),
|
"alertmanagerConfig".to_string(),
|
||||||
"enabled".to_string(),
|
"enabled".to_string(),
|
||||||
@@ -184,9 +122,6 @@ impl AlertReceiver<RHOBObservability> for DiscordWebhook {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AlertReceiver<CRDPrometheus> for DiscordWebhook {
|
impl AlertReceiver<CRDPrometheus> for DiscordWebhook {
|
||||||
fn as_alertmanager_receiver(&self) -> Result<AlertManagerReceiver, String> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
async fn install(&self, sender: &CRDPrometheus) -> Result<Outcome, InterpretError> {
|
async fn install(&self, sender: &CRDPrometheus) -> Result<Outcome, InterpretError> {
|
||||||
let ns = sender.namespace.clone();
|
let ns = sender.namespace.clone();
|
||||||
let secret_name = format!("{}-secret", self.name.clone());
|
let secret_name = format!("{}-secret", self.name.clone());
|
||||||
@@ -232,7 +167,7 @@ impl AlertReceiver<CRDPrometheus> for DiscordWebhook {
|
|||||||
|
|
||||||
let alertmanager_configs = AlertmanagerConfig {
|
let alertmanager_configs = AlertmanagerConfig {
|
||||||
metadata: ObjectMeta {
|
metadata: ObjectMeta {
|
||||||
name: Some(self.name.clone().to_string()),
|
name: Some(self.name.clone()),
|
||||||
labels: Some(std::collections::BTreeMap::from([(
|
labels: Some(std::collections::BTreeMap::from([(
|
||||||
"alertmanagerConfig".to_string(),
|
"alertmanagerConfig".to_string(),
|
||||||
"enabled".to_string(),
|
"enabled".to_string(),
|
||||||
@@ -265,9 +200,6 @@ impl AlertReceiver<CRDPrometheus> for DiscordWebhook {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AlertReceiver<Prometheus> for DiscordWebhook {
|
impl AlertReceiver<Prometheus> for DiscordWebhook {
|
||||||
fn as_alertmanager_receiver(&self) -> Result<AlertManagerReceiver, String> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
async fn install(&self, sender: &Prometheus) -> Result<Outcome, InterpretError> {
|
async fn install(&self, sender: &Prometheus) -> Result<Outcome, InterpretError> {
|
||||||
sender.install_receiver(self).await
|
sender.install_receiver(self).await
|
||||||
}
|
}
|
||||||
@@ -285,7 +217,7 @@ impl AlertReceiver<Prometheus> for DiscordWebhook {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl PrometheusReceiver for DiscordWebhook {
|
impl PrometheusReceiver for DiscordWebhook {
|
||||||
fn name(&self) -> String {
|
fn name(&self) -> String {
|
||||||
self.name.clone().to_string()
|
self.name.clone()
|
||||||
}
|
}
|
||||||
async fn configure_receiver(&self) -> AlertManagerChannelConfig {
|
async fn configure_receiver(&self) -> AlertManagerChannelConfig {
|
||||||
self.get_config().await
|
self.get_config().await
|
||||||
@@ -294,9 +226,6 @@ impl PrometheusReceiver for DiscordWebhook {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AlertReceiver<KubePrometheus> for DiscordWebhook {
|
impl AlertReceiver<KubePrometheus> for DiscordWebhook {
|
||||||
fn as_alertmanager_receiver(&self) -> Result<AlertManagerReceiver, String> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
async fn install(&self, sender: &KubePrometheus) -> Result<Outcome, InterpretError> {
|
async fn install(&self, sender: &KubePrometheus) -> Result<Outcome, InterpretError> {
|
||||||
sender.install_receiver(self).await
|
sender.install_receiver(self).await
|
||||||
}
|
}
|
||||||
@@ -314,7 +243,7 @@ impl AlertReceiver<KubePrometheus> for DiscordWebhook {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl KubePrometheusReceiver for DiscordWebhook {
|
impl KubePrometheusReceiver for DiscordWebhook {
|
||||||
fn name(&self) -> String {
|
fn name(&self) -> String {
|
||||||
self.name.clone().to_string()
|
self.name.clone()
|
||||||
}
|
}
|
||||||
async fn configure_receiver(&self) -> AlertManagerChannelConfig {
|
async fn configure_receiver(&self) -> AlertManagerChannelConfig {
|
||||||
self.get_config().await
|
self.get_config().await
|
||||||
@@ -341,7 +270,7 @@ impl DiscordWebhook {
|
|||||||
let mut route = Mapping::new();
|
let mut route = Mapping::new();
|
||||||
route.insert(
|
route.insert(
|
||||||
Value::String("receiver".to_string()),
|
Value::String("receiver".to_string()),
|
||||||
Value::String(self.name.clone().to_string()),
|
Value::String(self.name.clone()),
|
||||||
);
|
);
|
||||||
route.insert(
|
route.insert(
|
||||||
Value::String("matchers".to_string()),
|
Value::String("matchers".to_string()),
|
||||||
@@ -355,7 +284,7 @@ impl DiscordWebhook {
|
|||||||
let mut receiver = Mapping::new();
|
let mut receiver = Mapping::new();
|
||||||
receiver.insert(
|
receiver.insert(
|
||||||
Value::String("name".to_string()),
|
Value::String("name".to_string()),
|
||||||
Value::String(self.name.clone().to_string()),
|
Value::String(self.name.clone()),
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut discord_config = Mapping::new();
|
let mut discord_config = Mapping::new();
|
||||||
@@ -380,9 +309,8 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn discord_serialize_should_match() {
|
async fn discord_serialize_should_match() {
|
||||||
let discord_receiver = DiscordWebhook {
|
let discord_receiver = DiscordWebhook {
|
||||||
name: K8sName("test-discord".to_string()),
|
name: "test-discord".to_string(),
|
||||||
url: Url::Url(url::Url::parse("https://discord.i.dont.exist.com").unwrap()),
|
url: Url::Url(url::Url::parse("https://discord.i.dont.exist.com").unwrap()),
|
||||||
selectors: vec![],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let discord_receiver_receiver =
|
let discord_receiver_receiver =
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
prometheus::prometheus::{Prometheus, PrometheusReceiver},
|
prometheus::prometheus::{Prometheus, PrometheusReceiver},
|
||||||
},
|
},
|
||||||
topology::oberservability::monitoring::{AlertManagerReceiver, AlertReceiver},
|
topology::oberservability::monitoring::AlertReceiver,
|
||||||
};
|
};
|
||||||
use harmony_types::net::Url;
|
use harmony_types::net::Url;
|
||||||
|
|
||||||
@@ -31,9 +31,6 @@ pub struct WebhookReceiver {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AlertReceiver<RHOBObservability> for WebhookReceiver {
|
impl AlertReceiver<RHOBObservability> for WebhookReceiver {
|
||||||
fn as_alertmanager_receiver(&self) -> Result<AlertManagerReceiver, String> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
async fn install(&self, sender: &RHOBObservability) -> Result<Outcome, InterpretError> {
|
async fn install(&self, sender: &RHOBObservability) -> Result<Outcome, InterpretError> {
|
||||||
let spec = crate::modules::monitoring::kube_prometheus::crd::rhob_alertmanager_config::AlertmanagerConfigSpec {
|
let spec = crate::modules::monitoring::kube_prometheus::crd::rhob_alertmanager_config::AlertmanagerConfigSpec {
|
||||||
data: json!({
|
data: json!({
|
||||||
@@ -100,9 +97,6 @@ impl AlertReceiver<RHOBObservability> for WebhookReceiver {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AlertReceiver<CRDPrometheus> for WebhookReceiver {
|
impl AlertReceiver<CRDPrometheus> for WebhookReceiver {
|
||||||
fn as_alertmanager_receiver(&self) -> Result<AlertManagerReceiver, String> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
async fn install(&self, sender: &CRDPrometheus) -> Result<Outcome, InterpretError> {
|
async fn install(&self, sender: &CRDPrometheus) -> Result<Outcome, InterpretError> {
|
||||||
let spec = crate::modules::monitoring::kube_prometheus::crd::crd_alertmanager_config::AlertmanagerConfigSpec {
|
let spec = crate::modules::monitoring::kube_prometheus::crd::crd_alertmanager_config::AlertmanagerConfigSpec {
|
||||||
data: json!({
|
data: json!({
|
||||||
@@ -164,9 +158,6 @@ impl AlertReceiver<CRDPrometheus> for WebhookReceiver {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AlertReceiver<Prometheus> for WebhookReceiver {
|
impl AlertReceiver<Prometheus> for WebhookReceiver {
|
||||||
fn as_alertmanager_receiver(&self) -> Result<AlertManagerReceiver, String> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
async fn install(&self, sender: &Prometheus) -> Result<Outcome, InterpretError> {
|
async fn install(&self, sender: &Prometheus) -> Result<Outcome, InterpretError> {
|
||||||
sender.install_receiver(self).await
|
sender.install_receiver(self).await
|
||||||
}
|
}
|
||||||
@@ -193,9 +184,6 @@ impl PrometheusReceiver for WebhookReceiver {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AlertReceiver<KubePrometheus> for WebhookReceiver {
|
impl AlertReceiver<KubePrometheus> for WebhookReceiver {
|
||||||
fn as_alertmanager_receiver(&self) -> Result<AlertManagerReceiver, String> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
async fn install(&self, sender: &KubePrometheus) -> Result<Outcome, InterpretError> {
|
async fn install(&self, sender: &KubePrometheus) -> Result<Outcome, InterpretError> {
|
||||||
sender.install_receiver(self).await
|
sender.install_receiver(self).await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,270 +0,0 @@
|
|||||||
use base64::prelude::*;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use harmony_types::id::Id;
|
|
||||||
use kube::api::DynamicObject;
|
|
||||||
use log::{debug, info, trace};
|
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
data::Version,
|
|
||||||
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
|
||||||
inventory::Inventory,
|
|
||||||
modules::monitoring::okd::OpenshiftClusterAlertSender,
|
|
||||||
score::Score,
|
|
||||||
topology::{K8sclient, Topology, oberservability::monitoring::AlertReceiver},
|
|
||||||
};
|
|
||||||
|
|
||||||
impl Clone for Box<dyn AlertReceiver<OpenshiftClusterAlertSender>> {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
self.clone_box()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Serialize for Box<dyn AlertReceiver<OpenshiftClusterAlertSender>> {
|
|
||||||
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: serde::Serializer,
|
|
||||||
{
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
pub struct OpenshiftClusterAlertScore {
|
|
||||||
pub receivers: Vec<Box<dyn AlertReceiver<OpenshiftClusterAlertSender>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Topology + K8sclient> Score<T> for OpenshiftClusterAlertScore {
|
|
||||||
fn name(&self) -> String {
|
|
||||||
"ClusterAlertScore".to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
|
||||||
Box::new(OpenshiftClusterAlertInterpret {
|
|
||||||
receivers: self.receivers.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct OpenshiftClusterAlertInterpret {
|
|
||||||
receivers: Vec<Box<dyn AlertReceiver<OpenshiftClusterAlertSender>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<T: Topology + K8sclient> Interpret<T> for OpenshiftClusterAlertInterpret {
|
|
||||||
async fn execute(
|
|
||||||
&self,
|
|
||||||
_inventory: &Inventory,
|
|
||||||
topology: &T,
|
|
||||||
) -> Result<Outcome, InterpretError> {
|
|
||||||
let client = topology.k8s_client().await?;
|
|
||||||
let openshift_monitoring_namespace = "openshift-monitoring";
|
|
||||||
|
|
||||||
let mut alertmanager_main_secret: DynamicObject = client
|
|
||||||
.get_secret_json_value("alertmanager-main", Some(openshift_monitoring_namespace))
|
|
||||||
.await?;
|
|
||||||
trace!("Got secret {alertmanager_main_secret:#?}");
|
|
||||||
|
|
||||||
let data: &mut serde_json::Value = &mut alertmanager_main_secret.data;
|
|
||||||
trace!("Alertmanager-main secret data {data:#?}");
|
|
||||||
let data_obj = data
|
|
||||||
.get_mut("data")
|
|
||||||
.ok_or(InterpretError::new(
|
|
||||||
"Missing 'data' field in alertmanager-main secret.".to_string(),
|
|
||||||
))?
|
|
||||||
.as_object_mut()
|
|
||||||
.ok_or(InterpretError::new(
|
|
||||||
"'data' field in alertmanager-main secret is expected to be an object ."
|
|
||||||
.to_string(),
|
|
||||||
))?;
|
|
||||||
|
|
||||||
let config_b64 = data_obj
|
|
||||||
.get("alertmanager.yaml")
|
|
||||||
.ok_or(InterpretError::new(
|
|
||||||
"Missing 'alertmanager.yaml' in alertmanager-main secret data".to_string(),
|
|
||||||
))?
|
|
||||||
.as_str()
|
|
||||||
.unwrap_or("");
|
|
||||||
trace!("Config base64 {config_b64}");
|
|
||||||
|
|
||||||
let config_bytes = BASE64_STANDARD.decode(config_b64).unwrap_or_default();
|
|
||||||
|
|
||||||
let mut am_config: serde_yaml::Value =
|
|
||||||
serde_yaml::from_str(&String::from_utf8(config_bytes).unwrap_or_default())
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
debug!("Current alertmanager config {am_config:#?}");
|
|
||||||
|
|
||||||
let existing_receivers_sequence = if let Some(receivers) = am_config.get_mut("receivers") {
|
|
||||||
match receivers.as_sequence_mut() {
|
|
||||||
Some(seq) => seq,
|
|
||||||
None => {
|
|
||||||
return Err(InterpretError::new(format!(
|
|
||||||
"Expected alertmanager config receivers to be a sequence, got {:?}",
|
|
||||||
receivers
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
&mut serde_yaml::Sequence::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut additional_resources = vec![];
|
|
||||||
|
|
||||||
for custom_receiver in &self.receivers {
|
|
||||||
let name = custom_receiver.name();
|
|
||||||
let alertmanager_receiver = custom_receiver.as_alertmanager_receiver()?;
|
|
||||||
|
|
||||||
let receiver_json_value = alertmanager_receiver.receiver_config;
|
|
||||||
|
|
||||||
let receiver_yaml_string =
|
|
||||||
serde_json::to_string(&receiver_json_value).map_err(|e| {
|
|
||||||
InterpretError::new(format!("Failed to serialize receiver config: {}", e))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let receiver_yaml_value: serde_yaml::Value =
|
|
||||||
serde_yaml::from_str(&receiver_yaml_string).map_err(|e| {
|
|
||||||
InterpretError::new(format!("Failed to parse receiver config as YAML: {}", e))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if let Some(idx) = existing_receivers_sequence.iter().position(|r| {
|
|
||||||
r.get("name")
|
|
||||||
.and_then(|n| n.as_str())
|
|
||||||
.map_or(false, |n| n == name)
|
|
||||||
}) {
|
|
||||||
info!("Replacing existing AlertManager receiver: {}", name);
|
|
||||||
existing_receivers_sequence[idx] = receiver_yaml_value;
|
|
||||||
} else {
|
|
||||||
debug!("Adding new AlertManager receiver: {}", name);
|
|
||||||
existing_receivers_sequence.push(receiver_yaml_value);
|
|
||||||
}
|
|
||||||
|
|
||||||
additional_resources.push(alertmanager_receiver.additional_ressources);
|
|
||||||
}
|
|
||||||
|
|
||||||
let existing_route_mapping = if let Some(route) = am_config.get_mut("route") {
|
|
||||||
match route.as_mapping_mut() {
|
|
||||||
Some(map) => map,
|
|
||||||
None => {
|
|
||||||
return Err(InterpretError::new(format!(
|
|
||||||
"Expected alertmanager config route to be a mapping, got {:?}",
|
|
||||||
route
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
&mut serde_yaml::Mapping::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let existing_route_sequence = if let Some(routes) = existing_route_mapping.get_mut("routes")
|
|
||||||
{
|
|
||||||
match routes.as_sequence_mut() {
|
|
||||||
Some(seq) => seq,
|
|
||||||
None => {
|
|
||||||
return Err(InterpretError::new(format!(
|
|
||||||
"Expected alertmanager config routes to be a sequence, got {:?}",
|
|
||||||
routes
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
&mut serde_yaml::Sequence::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
for custom_receiver in &self.receivers {
|
|
||||||
let name = custom_receiver.name();
|
|
||||||
let alertmanager_receiver = custom_receiver.as_alertmanager_receiver()?;
|
|
||||||
|
|
||||||
let route_json_value = alertmanager_receiver.route_config;
|
|
||||||
let route_yaml_string = serde_json::to_string(&route_json_value).map_err(|e| {
|
|
||||||
InterpretError::new(format!("Failed to serialize route config: {}", e))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let route_yaml_value: serde_yaml::Value = serde_yaml::from_str(&route_yaml_string)
|
|
||||||
.map_err(|e| {
|
|
||||||
InterpretError::new(format!("Failed to parse route config as YAML: {}", e))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if let Some(idy) = existing_route_sequence.iter().position(|r| {
|
|
||||||
r.get("receiver")
|
|
||||||
.and_then(|n| n.as_str())
|
|
||||||
.map_or(false, |n| n == name)
|
|
||||||
}) {
|
|
||||||
info!("Replacing existing AlertManager receiver: {}", name);
|
|
||||||
existing_route_sequence[idy] = route_yaml_value;
|
|
||||||
} else {
|
|
||||||
debug!("Adding new AlertManager receiver: {}", name);
|
|
||||||
existing_route_sequence.push(route_yaml_value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
debug!("Current alertmanager config {am_config:#?}");
|
|
||||||
// TODO
|
|
||||||
// - save new version of alertmanager config
|
|
||||||
// - write additional ressources to the cluster
|
|
||||||
let am_config = serde_yaml::to_string(&am_config).map_err(|e| {
|
|
||||||
InterpretError::new(format!(
|
|
||||||
"Failed to serialize new alertmanager config to string : {e}"
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let mut am_config_b64 = String::new();
|
|
||||||
BASE64_STANDARD.encode_string(am_config, &mut am_config_b64);
|
|
||||||
|
|
||||||
// TODO put update configmap value and save new value
|
|
||||||
data_obj.insert(
|
|
||||||
"alertmanager.yaml".to_string(),
|
|
||||||
serde_json::Value::String(am_config_b64),
|
|
||||||
);
|
|
||||||
|
|
||||||
// https://kubernetes.io/docs/reference/using-api/server-side-apply/#field-management
|
|
||||||
alertmanager_main_secret.metadata.managed_fields = None;
|
|
||||||
|
|
||||||
trace!("Applying new alertmanager_main_secret {alertmanager_main_secret:#?}");
|
|
||||||
client
|
|
||||||
.apply_dynamic(
|
|
||||||
&alertmanager_main_secret,
|
|
||||||
Some(openshift_monitoring_namespace),
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let additional_resources = additional_resources.concat();
|
|
||||||
trace!("Applying additional ressources for alert receivers {additional_resources:#?}");
|
|
||||||
client
|
|
||||||
.apply_dynamic_many(
|
|
||||||
&additional_resources,
|
|
||||||
Some(openshift_monitoring_namespace),
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(Outcome::success(format!(
|
|
||||||
"Successfully configured {} cluster alert receivers: {}",
|
|
||||||
self.receivers.len(),
|
|
||||||
self.receivers
|
|
||||||
.iter()
|
|
||||||
.map(|r| r.name())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", ")
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_name(&self) -> InterpretName {
|
|
||||||
InterpretName::Custom("OpenshiftClusterAlertInterpret")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_version(&self) -> Version {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_status(&self) -> InterpretStatus {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_children(&self) -> Vec<Id> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
use std::{collections::BTreeMap, sync::Arc};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
interpret::{InterpretError, Outcome},
|
|
||||||
topology::k8s::K8sClient,
|
|
||||||
};
|
|
||||||
use k8s_openapi::api::core::v1::ConfigMap;
|
|
||||||
use kube::api::ObjectMeta;
|
|
||||||
|
|
||||||
pub(crate) struct Config;
|
|
||||||
|
|
||||||
impl Config {
|
|
||||||
pub async fn create_cluster_monitoring_config_cm(
|
|
||||||
client: &Arc<K8sClient>,
|
|
||||||
) -> Result<Outcome, InterpretError> {
|
|
||||||
let mut data = BTreeMap::new();
|
|
||||||
data.insert(
|
|
||||||
"config.yaml".to_string(),
|
|
||||||
r#"
|
|
||||||
enableUserWorkload: true
|
|
||||||
alertmanagerMain:
|
|
||||||
enableUserAlertmanagerConfig: true
|
|
||||||
"#
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let cm = ConfigMap {
|
|
||||||
metadata: ObjectMeta {
|
|
||||||
name: Some("cluster-monitoring-config".to_string()),
|
|
||||||
namespace: Some("openshift-monitoring".to_string()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
data: Some(data),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
client.apply(&cm, Some("openshift-monitoring")).await?;
|
|
||||||
|
|
||||||
Ok(Outcome::success(
|
|
||||||
"updated cluster-monitoring-config-map".to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn create_user_workload_monitoring_config_cm(
|
|
||||||
client: &Arc<K8sClient>,
|
|
||||||
) -> Result<Outcome, InterpretError> {
|
|
||||||
let mut data = BTreeMap::new();
|
|
||||||
data.insert(
|
|
||||||
"config.yaml".to_string(),
|
|
||||||
r#"
|
|
||||||
alertmanager:
|
|
||||||
enabled: true
|
|
||||||
enableAlertmanagerConfig: true
|
|
||||||
"#
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
let cm = ConfigMap {
|
|
||||||
metadata: ObjectMeta {
|
|
||||||
name: Some("user-workload-monitoring-config".to_string()),
|
|
||||||
namespace: Some("openshift-user-workload-monitoring".to_string()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
data: Some(data),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
client
|
|
||||||
.apply(&cm, Some("openshift-user-workload-monitoring"))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(Outcome::success(
|
|
||||||
"updated openshift-user-monitoring-config-map".to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn verify_user_workload(client: &Arc<K8sClient>) -> Result<Outcome, InterpretError> {
|
|
||||||
let namespace = "openshift-user-workload-monitoring";
|
|
||||||
let alertmanager_name = "alertmanager-user-workload-0";
|
|
||||||
let prometheus_name = "prometheus-user-workload-0";
|
|
||||||
client
|
|
||||||
.wait_for_pod_ready(alertmanager_name, Some(namespace))
|
|
||||||
.await?;
|
|
||||||
client
|
|
||||||
.wait_for_pod_ready(prometheus_name, Some(namespace))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(Outcome::success(format!(
|
|
||||||
"pods: {}, {} ready in ns: {}",
|
|
||||||
alertmanager_name, prometheus_name, namespace
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
|
use std::{collections::BTreeMap, sync::Arc};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
data::Version,
|
data::Version,
|
||||||
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
modules::monitoring::okd::config::Config,
|
|
||||||
score::Score,
|
score::Score,
|
||||||
topology::{K8sclient, Topology},
|
topology::{K8sclient, Topology, k8s::K8sClient},
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use harmony_types::id::Id;
|
use harmony_types::id::Id;
|
||||||
|
use k8s_openapi::api::core::v1::ConfigMap;
|
||||||
|
use kube::api::ObjectMeta;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
@@ -34,9 +37,10 @@ impl<T: Topology + K8sclient> Interpret<T> for OpenshiftUserWorkloadMonitoringIn
|
|||||||
topology: &T,
|
topology: &T,
|
||||||
) -> Result<Outcome, InterpretError> {
|
) -> Result<Outcome, InterpretError> {
|
||||||
let client = topology.k8s_client().await.unwrap();
|
let client = topology.k8s_client().await.unwrap();
|
||||||
Config::create_cluster_monitoring_config_cm(&client).await?;
|
self.update_cluster_monitoring_config_cm(&client).await?;
|
||||||
Config::create_user_workload_monitoring_config_cm(&client).await?;
|
self.update_user_workload_monitoring_config_cm(&client)
|
||||||
Config::verify_user_workload(&client).await?;
|
.await?;
|
||||||
|
self.verify_user_workload(&client).await?;
|
||||||
Ok(Outcome::success(
|
Ok(Outcome::success(
|
||||||
"successfully enabled user-workload-monitoring".to_string(),
|
"successfully enabled user-workload-monitoring".to_string(),
|
||||||
))
|
))
|
||||||
@@ -58,3 +62,88 @@ impl<T: Topology + K8sclient> Interpret<T> for OpenshiftUserWorkloadMonitoringIn
|
|||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl OpenshiftUserWorkloadMonitoringInterpret {
|
||||||
|
pub async fn update_cluster_monitoring_config_cm(
|
||||||
|
&self,
|
||||||
|
client: &Arc<K8sClient>,
|
||||||
|
) -> Result<Outcome, InterpretError> {
|
||||||
|
let mut data = BTreeMap::new();
|
||||||
|
data.insert(
|
||||||
|
"config.yaml".to_string(),
|
||||||
|
r#"
|
||||||
|
enableUserWorkload: true
|
||||||
|
alertmanagerMain:
|
||||||
|
enableUserAlertmanagerConfig: true
|
||||||
|
"#
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let cm = ConfigMap {
|
||||||
|
metadata: ObjectMeta {
|
||||||
|
name: Some("cluster-monitoring-config".to_string()),
|
||||||
|
namespace: Some("openshift-monitoring".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
data: Some(data),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
client.apply(&cm, Some("openshift-monitoring")).await?;
|
||||||
|
|
||||||
|
Ok(Outcome::success(
|
||||||
|
"updated cluster-monitoring-config-map".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_user_workload_monitoring_config_cm(
|
||||||
|
&self,
|
||||||
|
client: &Arc<K8sClient>,
|
||||||
|
) -> Result<Outcome, InterpretError> {
|
||||||
|
let mut data = BTreeMap::new();
|
||||||
|
data.insert(
|
||||||
|
"config.yaml".to_string(),
|
||||||
|
r#"
|
||||||
|
alertmanager:
|
||||||
|
enabled: true
|
||||||
|
enableAlertmanagerConfig: true
|
||||||
|
"#
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
let cm = ConfigMap {
|
||||||
|
metadata: ObjectMeta {
|
||||||
|
name: Some("user-workload-monitoring-config".to_string()),
|
||||||
|
namespace: Some("openshift-user-workload-monitoring".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
data: Some(data),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
client
|
||||||
|
.apply(&cm, Some("openshift-user-workload-monitoring"))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Outcome::success(
|
||||||
|
"updated openshift-user-monitoring-config-map".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn verify_user_workload(
|
||||||
|
&self,
|
||||||
|
client: &Arc<K8sClient>,
|
||||||
|
) -> Result<Outcome, InterpretError> {
|
||||||
|
let namespace = "openshift-user-workload-monitoring";
|
||||||
|
let alertmanager_name = "alertmanager-user-workload-0";
|
||||||
|
let prometheus_name = "prometheus-user-workload-0";
|
||||||
|
client
|
||||||
|
.wait_for_pod_ready(alertmanager_name, Some(namespace))
|
||||||
|
.await?;
|
||||||
|
client
|
||||||
|
.wait_for_pod_ready(prometheus_name, Some(namespace))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Outcome::success(format!(
|
||||||
|
"pods: {}, {} ready in ns: {}",
|
||||||
|
alertmanager_name, prometheus_name, namespace
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1 @@
|
|||||||
use crate::topology::oberservability::monitoring::AlertSender;
|
|
||||||
|
|
||||||
pub mod cluster_monitoring;
|
|
||||||
pub(crate) mod config;
|
|
||||||
pub mod enable_user_workload;
|
pub mod enable_user_workload;
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct OpenshiftClusterAlertSender;
|
|
||||||
|
|
||||||
impl AlertSender for OpenshiftClusterAlertSender {
|
|
||||||
fn name(&self) -> String {
|
|
||||||
"OpenshiftClusterAlertSender".to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use crate::{
|
|||||||
infra::inventory::InventoryRepositoryFactory,
|
infra::inventory::InventoryRepositoryFactory,
|
||||||
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
||||||
inventory::{HostRole, Inventory},
|
inventory::{HostRole, Inventory},
|
||||||
modules::inventory::DiscoverHostForRoleScore,
|
modules::inventory::{DiscoverHostForRoleScore, HarmonyDiscoveryStrategy},
|
||||||
score::Score,
|
score::Score,
|
||||||
topology::HAClusterTopology,
|
topology::HAClusterTopology,
|
||||||
};
|
};
|
||||||
@@ -104,6 +104,8 @@ When you can dig them, confirm to continue.
|
|||||||
bootstrap_host = hosts.into_iter().next().to_owned();
|
bootstrap_host = hosts.into_iter().next().to_owned();
|
||||||
DiscoverHostForRoleScore {
|
DiscoverHostForRoleScore {
|
||||||
role: HostRole::Bootstrap,
|
role: HostRole::Bootstrap,
|
||||||
|
number_desired_hosts: 1,
|
||||||
|
discovery_strategy: HarmonyDiscoveryStrategy::MDNS,
|
||||||
}
|
}
|
||||||
.interpret(inventory, topology)
|
.interpret(inventory, topology)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use crate::{
|
|||||||
inventory::{HostRole, Inventory},
|
inventory::{HostRole, Inventory},
|
||||||
modules::{
|
modules::{
|
||||||
dhcp::DhcpHostBindingScore, http::IPxeMacBootFileScore,
|
dhcp::DhcpHostBindingScore, http::IPxeMacBootFileScore,
|
||||||
inventory::DiscoverHostForRoleScore, okd::templates::BootstrapIpxeTpl,
|
inventory::{DiscoverHostForRoleScore, HarmonyDiscoveryStrategy}, okd::templates::BootstrapIpxeTpl,
|
||||||
},
|
},
|
||||||
score::Score,
|
score::Score,
|
||||||
topology::{HAClusterTopology, HostBinding},
|
topology::{HAClusterTopology, HostBinding},
|
||||||
@@ -58,38 +58,39 @@ impl OKDSetup03ControlPlaneInterpret {
|
|||||||
inventory: &Inventory,
|
inventory: &Inventory,
|
||||||
topology: &HAClusterTopology,
|
topology: &HAClusterTopology,
|
||||||
) -> Result<Vec<PhysicalHost>, InterpretError> {
|
) -> Result<Vec<PhysicalHost>, InterpretError> {
|
||||||
const REQUIRED_HOSTS: usize = 3;
|
const REQUIRED_HOSTS: i16 = 3;
|
||||||
let repo = InventoryRepositoryFactory::build().await?;
|
let repo = InventoryRepositoryFactory::build().await?;
|
||||||
let mut control_plane_hosts = repo.get_host_for_role(&HostRole::ControlPlane).await?;
|
let control_plane_hosts = repo.get_host_for_role(&HostRole::ControlPlane).await?;
|
||||||
|
|
||||||
while control_plane_hosts.len() < REQUIRED_HOSTS {
|
info!(
|
||||||
info!(
|
"Discovery of {} control plane hosts in progress, current number {}",
|
||||||
"Discovery of {} control plane hosts in progress, current number {}",
|
REQUIRED_HOSTS,
|
||||||
REQUIRED_HOSTS,
|
control_plane_hosts.len()
|
||||||
control_plane_hosts.len()
|
);
|
||||||
);
|
// This score triggers the discovery agent for a specific role.
|
||||||
// This score triggers the discovery agent for a specific role.
|
DiscoverHostForRoleScore {
|
||||||
DiscoverHostForRoleScore {
|
role: HostRole::ControlPlane,
|
||||||
role: HostRole::ControlPlane,
|
number_desired_hosts: REQUIRED_HOSTS,
|
||||||
}
|
discovery_strategy: HarmonyDiscoveryStrategy::MDNS,
|
||||||
.interpret(inventory, topology)
|
|
||||||
.await?;
|
|
||||||
control_plane_hosts = repo.get_host_for_role(&HostRole::ControlPlane).await?;
|
|
||||||
}
|
}
|
||||||
|
.interpret(inventory, topology)
|
||||||
|
.await?;
|
||||||
|
|
||||||
if control_plane_hosts.len() < REQUIRED_HOSTS {
|
let control_plane_hosts = repo.get_host_for_role(&HostRole::ControlPlane).await?;
|
||||||
Err(InterpretError::new(format!(
|
|
||||||
|
if control_plane_hosts.len() < REQUIRED_HOSTS as usize {
|
||||||
|
return Err(InterpretError::new(format!(
|
||||||
"OKD Requires at least {} control plane hosts, but only found {}. Cannot proceed.",
|
"OKD Requires at least {} control plane hosts, but only found {}. Cannot proceed.",
|
||||||
REQUIRED_HOSTS,
|
REQUIRED_HOSTS,
|
||||||
control_plane_hosts.len()
|
control_plane_hosts.len()
|
||||||
)))
|
)));
|
||||||
} else {
|
|
||||||
// Take exactly the number of required hosts to ensure consistency.
|
|
||||||
Ok(control_plane_hosts
|
|
||||||
.into_iter()
|
|
||||||
.take(REQUIRED_HOSTS)
|
|
||||||
.collect())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Take exactly the number of required hosts to ensure consistency.
|
||||||
|
Ok(control_plane_hosts
|
||||||
|
.into_iter()
|
||||||
|
.take(REQUIRED_HOSTS as usize)
|
||||||
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configures DHCP host bindings for all control plane nodes.
|
/// Configures DHCP host bindings for all control plane nodes.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use kube::CustomResource;
|
use k8s_openapi::{ClusterResourceScope, Resource};
|
||||||
|
use kube::{CustomResource, api::ObjectMeta};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
@@ -47,28 +48,223 @@ pub struct ProbeDns {
|
|||||||
group = "nmstate.io",
|
group = "nmstate.io",
|
||||||
version = "v1",
|
version = "v1",
|
||||||
kind = "NodeNetworkConfigurationPolicy",
|
kind = "NodeNetworkConfigurationPolicy",
|
||||||
namespaced
|
namespaced = false
|
||||||
)]
|
)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct NodeNetworkConfigurationPolicySpec {
|
pub struct NodeNetworkConfigurationPolicySpec {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub node_selector: Option<BTreeMap<String, String>>,
|
pub node_selector: Option<BTreeMap<String, String>>,
|
||||||
pub desired_state: DesiredStateSpec,
|
pub desired_state: NetworkState,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Currently, kube-rs derive doesn't support resources without a `spec` field, so we have
|
||||||
|
// to implement it ourselves.
|
||||||
|
//
|
||||||
|
// Ref:
|
||||||
|
// - https://github.com/kube-rs/kube/issues/1763
|
||||||
|
// - https://github.com/kube-rs/kube/discussions/1762
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct NodeNetworkState {
|
||||||
|
metadata: ObjectMeta,
|
||||||
|
pub status: NodeNetworkStateStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Resource for NodeNetworkState {
|
||||||
|
const API_VERSION: &'static str = "nmstate.io/v1beta1";
|
||||||
|
const GROUP: &'static str = "nmstate.io";
|
||||||
|
const VERSION: &'static str = "v1beta1";
|
||||||
|
const KIND: &'static str = "NodeNetworkState";
|
||||||
|
const URL_PATH_SEGMENT: &'static str = "nodenetworkstates";
|
||||||
|
type Scope = ClusterResourceScope;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl k8s_openapi::Metadata for NodeNetworkState {
|
||||||
|
type Ty = ObjectMeta;
|
||||||
|
|
||||||
|
fn metadata(&self) -> &Self::Ty {
|
||||||
|
&self.metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
fn metadata_mut(&mut self) -> &mut Self::Ty {
|
||||||
|
&mut self.metadata
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct NodeNetworkStateStatus {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub current_state: Option<NetworkState>,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub handler_nmstate_version: Option<String>,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub host_network_manager_version: Option<String>,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub last_successful_update_time: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The NetworkState is the top-level struct, representing the entire
|
||||||
|
/// desired or current network state.
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct DesiredStateSpec {
|
#[serde(deny_unknown_fields)]
|
||||||
pub interfaces: Vec<InterfaceSpec>,
|
pub struct NetworkState {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub hostname: Option<HostNameState>,
|
||||||
|
#[serde(rename = "dns-resolver", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub dns: Option<DnsState>,
|
||||||
|
#[serde(rename = "route-rules", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub rules: Option<RouteRuleState>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub routes: Option<RouteState>,
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub interfaces: Vec<Interface>,
|
||||||
|
#[serde(rename = "ovs-db", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub ovsdb: Option<OvsDbGlobalConfig>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub ovn: Option<OvnConfiguration>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct InterfaceSpec {
|
pub struct HostNameState {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub running: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub config: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct DnsState {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub running: Option<DnsResolverConfig>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub config: Option<DnsResolverConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct DnsResolverConfig {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub search: Option<Vec<String>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub server: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct RouteRuleState {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub config: Option<Vec<RouteRule>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub running: Option<Vec<RouteRule>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct RouteState {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub config: Option<Vec<Route>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub running: Option<Vec<Route>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct RouteRule {
|
||||||
|
#[serde(rename = "ip-from", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub ip_from: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub priority: Option<u32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub route_table: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct Route {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub destination: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub metric: Option<u32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub next_hop_address: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub next_hop_interface: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub table_id: Option<u32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub mtu: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct OvsDbGlobalConfig {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub external_ids: Option<BTreeMap<String, String>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub other_config: Option<BTreeMap<String, String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct OvnConfiguration {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub bridge_mappings: Option<Vec<OvnBridgeMapping>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct OvnBridgeMapping {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub localnet: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub bridge: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub enum StpSpec {
|
||||||
|
Bool(bool),
|
||||||
|
Options(StpOptions),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct LldpState {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub enabled: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct OvsDb {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub external_ids: Option<BTreeMap<String, String>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub other_config: Option<BTreeMap<String, String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct PatchState {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub peer: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct Interface {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub r#type: String,
|
pub r#type: InterfaceType,
|
||||||
pub state: String,
|
pub state: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub mac_address: Option<String>,
|
pub mac_address: Option<String>,
|
||||||
@@ -99,9 +295,81 @@ pub struct InterfaceSpec {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub linux_bridge: Option<LinuxBridgeSpec>,
|
pub linux_bridge: Option<LinuxBridgeSpec>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[serde(alias = "bridge")]
|
||||||
pub ovs_bridge: Option<OvsBridgeSpec>,
|
pub ovs_bridge: Option<OvsBridgeSpec>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub ethtool: Option<EthtoolSpec>,
|
pub ethtool: Option<Value>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub accept_all_mac_addresses: Option<bool>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub identifier: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub lldp: Option<LldpState>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub permanent_mac_address: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub max_mtu: Option<u32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub min_mtu: Option<u32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub mptcp: Option<Value>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub profile_name: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub wait_ip: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub ovs_db: Option<OvsDb>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub driver: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub patch: Option<PatchState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, JsonSchema)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub enum InterfaceType {
|
||||||
|
#[serde(rename = "unknown")]
|
||||||
|
Unknown,
|
||||||
|
#[serde(rename = "dummy")]
|
||||||
|
Dummy,
|
||||||
|
#[serde(rename = "loopback")]
|
||||||
|
Loopback,
|
||||||
|
#[serde(rename = "linux-bridge")]
|
||||||
|
LinuxBridge,
|
||||||
|
#[serde(rename = "ovs-bridge")]
|
||||||
|
OvsBridge,
|
||||||
|
#[serde(rename = "ovs-interface")]
|
||||||
|
OvsInterface,
|
||||||
|
#[serde(rename = "bond")]
|
||||||
|
Bond,
|
||||||
|
#[serde(rename = "ipvlan")]
|
||||||
|
IpVlan,
|
||||||
|
#[serde(rename = "vlan")]
|
||||||
|
Vlan,
|
||||||
|
#[serde(rename = "vxlan")]
|
||||||
|
Vxlan,
|
||||||
|
#[serde(rename = "mac-vlan")]
|
||||||
|
Macvlan,
|
||||||
|
#[serde(rename = "mac-vtap")]
|
||||||
|
Macvtap,
|
||||||
|
#[serde(rename = "ethernet")]
|
||||||
|
Ethernet,
|
||||||
|
#[serde(rename = "infiniband")]
|
||||||
|
Infiniband,
|
||||||
|
#[serde(rename = "vrf")]
|
||||||
|
Vrf,
|
||||||
|
#[serde(rename = "veth")]
|
||||||
|
Veth,
|
||||||
|
#[serde(rename = "ipsec")]
|
||||||
|
Ipsec,
|
||||||
|
#[serde(rename = "hsr")]
|
||||||
|
Hrs,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for InterfaceType {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Loopback
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||||
@@ -287,11 +555,15 @@ pub struct OvsBridgeSpec {
|
|||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct OvsBridgeOptions {
|
pub struct OvsBridgeOptions {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub stp: Option<bool>,
|
pub stp: Option<StpSpec>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub rstp: Option<bool>,
|
pub rstp: Option<bool>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub mcast_snooping_enable: Option<bool>,
|
pub mcast_snooping_enable: Option<bool>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub datapath: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub fail_mode: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||||
@@ -305,18 +577,3 @@ pub struct OvsPortSpec {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub r#type: Option<String>,
|
pub r#type: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
|
||||||
#[serde(rename_all = "kebab-case")]
|
|
||||||
pub struct EthtoolSpec {
|
|
||||||
// TODO: Properly describe this spec (https://nmstate.io/devel/yaml_api.html#ethtool)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
|
||||||
#[serde(rename_all = "kebab-case")]
|
|
||||||
pub struct EthtoolFecSpec {
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub auto: Option<bool>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub mode: Option<String>,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use harmony_types::id::Id;
|
use harmony_types::id::Id;
|
||||||
use log::{debug, info};
|
use log::{info, warn};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -9,7 +9,7 @@ use crate::{
|
|||||||
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
score::Score,
|
score::Score,
|
||||||
topology::{HostNetworkConfig, NetworkInterface, Switch, SwitchPort, Topology},
|
topology::{HostNetworkConfig, NetworkInterface, NetworkManager, Switch, SwitchPort, Topology},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
@@ -17,7 +17,7 @@ pub struct HostNetworkConfigurationScore {
|
|||||||
pub hosts: Vec<PhysicalHost>,
|
pub hosts: Vec<PhysicalHost>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Topology + Switch> Score<T> for HostNetworkConfigurationScore {
|
impl<T: Topology + NetworkManager + Switch> Score<T> for HostNetworkConfigurationScore {
|
||||||
fn name(&self) -> String {
|
fn name(&self) -> String {
|
||||||
"HostNetworkConfigurationScore".into()
|
"HostNetworkConfigurationScore".into()
|
||||||
}
|
}
|
||||||
@@ -35,7 +35,7 @@ pub struct HostNetworkConfigurationInterpret {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl HostNetworkConfigurationInterpret {
|
impl HostNetworkConfigurationInterpret {
|
||||||
async fn configure_network_for_host<T: Topology + Switch>(
|
async fn configure_network_for_host<T: Topology + NetworkManager + Switch>(
|
||||||
&self,
|
&self,
|
||||||
topology: &T,
|
topology: &T,
|
||||||
host: &PhysicalHost,
|
host: &PhysicalHost,
|
||||||
@@ -49,6 +49,13 @@ impl HostNetworkConfigurationInterpret {
|
|||||||
switch_ports: vec![],
|
switch_ports: vec![],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if host.network.len() == 1 {
|
||||||
|
info!("[Host {current_host}/{total_hosts}] Only one interface to configure, skipping");
|
||||||
|
return Ok(HostNetworkConfig {
|
||||||
|
host_id: host.id.clone(),
|
||||||
|
switch_ports: vec![],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let switch_ports = self
|
let switch_ports = self
|
||||||
.collect_switch_ports_for_host(topology, host, current_host, total_hosts)
|
.collect_switch_ports_for_host(topology, host, current_host, total_hosts)
|
||||||
@@ -59,7 +66,7 @@ impl HostNetworkConfigurationInterpret {
|
|||||||
switch_ports,
|
switch_ports,
|
||||||
};
|
};
|
||||||
|
|
||||||
if !config.switch_ports.is_empty() {
|
if config.switch_ports.len() > 1 {
|
||||||
info!(
|
info!(
|
||||||
"[Host {current_host}/{total_hosts}] Found {} ports for {} interfaces",
|
"[Host {current_host}/{total_hosts}] Found {} ports for {} interfaces",
|
||||||
config.switch_ports.len(),
|
config.switch_ports.len(),
|
||||||
@@ -67,15 +74,25 @@ impl HostNetworkConfigurationInterpret {
|
|||||||
);
|
);
|
||||||
|
|
||||||
info!("[Host {current_host}/{total_hosts}] Configuring host network...");
|
info!("[Host {current_host}/{total_hosts}] Configuring host network...");
|
||||||
|
topology.configure_bond(&config).await.map_err(|e| {
|
||||||
|
InterpretError::new(format!("Failed to configure host network: {e}"))
|
||||||
|
})?;
|
||||||
topology
|
topology
|
||||||
.configure_host_network(&config)
|
.configure_port_channel(&config)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| InterpretError::new(format!("Failed to configure host: {e}")))?;
|
.map_err(|e| {
|
||||||
} else {
|
InterpretError::new(format!("Failed to configure host network: {e}"))
|
||||||
|
})?;
|
||||||
|
} else if config.switch_ports.is_empty() {
|
||||||
info!(
|
info!(
|
||||||
"[Host {current_host}/{total_hosts}] No ports found for {} interfaces, skipping",
|
"[Host {current_host}/{total_hosts}] No ports found for {} interfaces, skipping",
|
||||||
host.network.len()
|
host.network.len()
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
"[Host {current_host}/{total_hosts}] Found a single port for {} interfaces, skipping",
|
||||||
|
host.network.len()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(config)
|
Ok(config)
|
||||||
@@ -113,7 +130,7 @@ impl HostNetworkConfigurationInterpret {
|
|||||||
port,
|
port,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(None) => debug!("No port found for '{mac_address}', skipping"),
|
Ok(None) => {}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return Err(InterpretError::new(format!(
|
return Err(InterpretError::new(format!(
|
||||||
"Failed to get port for host '{}': {}",
|
"Failed to get port for host '{}': {}",
|
||||||
@@ -133,15 +150,6 @@ impl HostNetworkConfigurationInterpret {
|
|||||||
];
|
];
|
||||||
|
|
||||||
for config in configs {
|
for config in configs {
|
||||||
let host = self
|
|
||||||
.score
|
|
||||||
.hosts
|
|
||||||
.iter()
|
|
||||||
.find(|h| h.id == config.host_id)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
println!("[Host] {host}");
|
|
||||||
|
|
||||||
if config.switch_ports.is_empty() {
|
if config.switch_ports.is_empty() {
|
||||||
report.push(format!(
|
report.push(format!(
|
||||||
"⏭️ Host {}: SKIPPED (No matching switch ports found)",
|
"⏭️ Host {}: SKIPPED (No matching switch ports found)",
|
||||||
@@ -169,7 +177,7 @@ impl HostNetworkConfigurationInterpret {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<T: Topology + Switch> Interpret<T> for HostNetworkConfigurationInterpret {
|
impl<T: Topology + NetworkManager + Switch> Interpret<T> for HostNetworkConfigurationInterpret {
|
||||||
fn get_name(&self) -> InterpretName {
|
fn get_name(&self) -> InterpretName {
|
||||||
InterpretName::Custom("HostNetworkConfigurationInterpret")
|
InterpretName::Custom("HostNetworkConfigurationInterpret")
|
||||||
}
|
}
|
||||||
@@ -198,6 +206,12 @@ impl<T: Topology + Switch> Interpret<T> for HostNetworkConfigurationInterpret {
|
|||||||
let host_count = self.score.hosts.len();
|
let host_count = self.score.hosts.len();
|
||||||
info!("Started network configuration for {host_count} host(s)...",);
|
info!("Started network configuration for {host_count} host(s)...",);
|
||||||
|
|
||||||
|
info!("Setting up NetworkManager...",);
|
||||||
|
topology
|
||||||
|
.ensure_network_manager_installed()
|
||||||
|
.await
|
||||||
|
.map_err(|e| InterpretError::new(format!("NetworkManager setup failed: {e}")))?;
|
||||||
|
|
||||||
info!("Setting up switch with sane defaults...");
|
info!("Setting up switch with sane defaults...");
|
||||||
topology
|
topology
|
||||||
.setup_switch()
|
.setup_switch()
|
||||||
@@ -216,6 +230,7 @@ impl<T: Topology + Switch> Interpret<T> for HostNetworkConfigurationInterpret {
|
|||||||
host_configurations.push(host_configuration);
|
host_configurations.push(host_configuration);
|
||||||
current_host += 1;
|
current_host += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if current_host > 1 {
|
if current_host > 1 {
|
||||||
let details = self.format_host_configuration(host_configurations);
|
let details = self.format_host_configuration(host_configurations);
|
||||||
|
|
||||||
@@ -236,13 +251,14 @@ impl<T: Topology + Switch> Interpret<T> for HostNetworkConfigurationInterpret {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use assertor::*;
|
use assertor::*;
|
||||||
|
use brocade::PortOperatingMode;
|
||||||
use harmony_types::{net::MacAddress, switch::PortLocation};
|
use harmony_types::{net::MacAddress, switch::PortLocation};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
hardware::HostCategory,
|
hardware::HostCategory,
|
||||||
topology::{
|
topology::{
|
||||||
HostNetworkConfig, PreparationError, PreparationOutcome, SwitchError, SwitchPort,
|
HostNetworkConfig, NetworkError, PortConfig, PreparationError, PreparationOutcome, SwitchError, SwitchPort
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
@@ -267,6 +283,18 @@ mod tests {
|
|||||||
speed_mbps: None,
|
speed_mbps: None,
|
||||||
mtu: 1,
|
mtu: 1,
|
||||||
};
|
};
|
||||||
|
pub static ref YET_ANOTHER_EXISTING_INTERFACE: NetworkInterface = NetworkInterface {
|
||||||
|
mac_address: MacAddress::try_from("AA:BB:CC:DD:EE:F3".to_string()).unwrap(),
|
||||||
|
name: "interface-3".into(),
|
||||||
|
speed_mbps: None,
|
||||||
|
mtu: 1,
|
||||||
|
};
|
||||||
|
pub static ref LAST_EXISTING_INTERFACE: NetworkInterface = NetworkInterface {
|
||||||
|
mac_address: MacAddress::try_from("AA:BB:CC:DD:EE:F4".to_string()).unwrap(),
|
||||||
|
name: "interface-4".into(),
|
||||||
|
speed_mbps: None,
|
||||||
|
mtu: 1,
|
||||||
|
};
|
||||||
pub static ref UNKNOWN_INTERFACE: NetworkInterface = NetworkInterface {
|
pub static ref UNKNOWN_INTERFACE: NetworkInterface = NetworkInterface {
|
||||||
mac_address: MacAddress::try_from("11:22:33:44:55:61".to_string()).unwrap(),
|
mac_address: MacAddress::try_from("11:22:33:44:55:61".to_string()).unwrap(),
|
||||||
name: "unknown-interface".into(),
|
name: "unknown-interface".into(),
|
||||||
@@ -275,6 +303,8 @@ mod tests {
|
|||||||
};
|
};
|
||||||
pub static ref PORT: PortLocation = PortLocation(1, 0, 42);
|
pub static ref PORT: PortLocation = PortLocation(1, 0, 42);
|
||||||
pub static ref ANOTHER_PORT: PortLocation = PortLocation(2, 0, 42);
|
pub static ref ANOTHER_PORT: PortLocation = PortLocation(2, 0, 42);
|
||||||
|
pub static ref YET_ANOTHER_PORT: PortLocation = PortLocation(1, 0, 45);
|
||||||
|
pub static ref LAST_PORT: PortLocation = PortLocation(2, 0, 45);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -290,28 +320,33 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn host_with_one_mac_address_should_create_bond_with_one_interface() {
|
async fn should_setup_network_manager() {
|
||||||
let host = given_host(&HOST_ID, vec![EXISTING_INTERFACE.clone()]);
|
let host = given_host(&HOST_ID, vec![EXISTING_INTERFACE.clone()]);
|
||||||
let score = given_score(vec![host]);
|
let score = given_score(vec![host]);
|
||||||
let topology = TopologyWithSwitch::new();
|
let topology = TopologyWithSwitch::new();
|
||||||
|
|
||||||
let _ = score.interpret(&Inventory::empty(), &topology).await;
|
let _ = score.interpret(&Inventory::empty(), &topology).await;
|
||||||
|
|
||||||
let configured_host_networks = topology.configured_host_networks.lock().unwrap();
|
let network_manager_setup = topology.network_manager_setup.lock().unwrap();
|
||||||
assert_that!(*configured_host_networks).contains_exactly(vec![(
|
assert_that!(*network_manager_setup).is_true();
|
||||||
HOST_ID.clone(),
|
|
||||||
HostNetworkConfig {
|
|
||||||
host_id: HOST_ID.clone(),
|
|
||||||
switch_ports: vec![SwitchPort {
|
|
||||||
interface: EXISTING_INTERFACE.clone(),
|
|
||||||
port: PORT.clone(),
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
)]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn host_with_multiple_mac_addresses_should_create_one_bond_with_all_interfaces() {
|
async fn host_with_one_mac_address_should_skip_host_configuration() {
|
||||||
|
let host = given_host(&HOST_ID, vec![EXISTING_INTERFACE.clone()]);
|
||||||
|
let score = given_score(vec![host]);
|
||||||
|
let topology = TopologyWithSwitch::new();
|
||||||
|
|
||||||
|
let _ = score.interpret(&Inventory::empty(), &topology).await;
|
||||||
|
|
||||||
|
let config = topology.configured_bonds.lock().unwrap();
|
||||||
|
assert_that!(*config).is_empty();
|
||||||
|
let config = topology.configured_port_channels.lock().unwrap();
|
||||||
|
assert_that!(*config).is_empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn host_with_multiple_mac_addresses_should_configure_one_bond_with_all_interfaces() {
|
||||||
let score = given_score(vec![given_host(
|
let score = given_score(vec![given_host(
|
||||||
&HOST_ID,
|
&HOST_ID,
|
||||||
vec![
|
vec![
|
||||||
@@ -323,8 +358,8 @@ mod tests {
|
|||||||
|
|
||||||
let _ = score.interpret(&Inventory::empty(), &topology).await;
|
let _ = score.interpret(&Inventory::empty(), &topology).await;
|
||||||
|
|
||||||
let configured_host_networks = topology.configured_host_networks.lock().unwrap();
|
let config = topology.configured_bonds.lock().unwrap();
|
||||||
assert_that!(*configured_host_networks).contains_exactly(vec![(
|
assert_that!(*config).contains_exactly(vec![(
|
||||||
HOST_ID.clone(),
|
HOST_ID.clone(),
|
||||||
HostNetworkConfig {
|
HostNetworkConfig {
|
||||||
host_id: HOST_ID.clone(),
|
host_id: HOST_ID.clone(),
|
||||||
@@ -343,49 +378,183 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn multiple_hosts_should_create_one_bond_per_host() {
|
async fn host_with_multiple_mac_addresses_should_configure_one_port_channel_with_all_interfaces()
|
||||||
|
{
|
||||||
|
let score = given_score(vec![given_host(
|
||||||
|
&HOST_ID,
|
||||||
|
vec![
|
||||||
|
EXISTING_INTERFACE.clone(),
|
||||||
|
ANOTHER_EXISTING_INTERFACE.clone(),
|
||||||
|
],
|
||||||
|
)]);
|
||||||
|
let topology = TopologyWithSwitch::new();
|
||||||
|
|
||||||
|
let _ = score.interpret(&Inventory::empty(), &topology).await;
|
||||||
|
|
||||||
|
let config = topology.configured_port_channels.lock().unwrap();
|
||||||
|
assert_that!(*config).contains_exactly(vec![(
|
||||||
|
HOST_ID.clone(),
|
||||||
|
HostNetworkConfig {
|
||||||
|
host_id: HOST_ID.clone(),
|
||||||
|
switch_ports: vec![
|
||||||
|
SwitchPort {
|
||||||
|
interface: EXISTING_INTERFACE.clone(),
|
||||||
|
port: PORT.clone(),
|
||||||
|
},
|
||||||
|
SwitchPort {
|
||||||
|
interface: ANOTHER_EXISTING_INTERFACE.clone(),
|
||||||
|
port: ANOTHER_PORT.clone(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn multiple_hosts_should_configure_one_bond_per_host() {
|
||||||
let score = given_score(vec![
|
let score = given_score(vec![
|
||||||
given_host(&HOST_ID, vec![EXISTING_INTERFACE.clone()]),
|
given_host(
|
||||||
given_host(&ANOTHER_HOST_ID, vec![ANOTHER_EXISTING_INTERFACE.clone()]),
|
&HOST_ID,
|
||||||
|
vec![
|
||||||
|
EXISTING_INTERFACE.clone(),
|
||||||
|
ANOTHER_EXISTING_INTERFACE.clone(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
given_host(
|
||||||
|
&ANOTHER_HOST_ID,
|
||||||
|
vec![
|
||||||
|
YET_ANOTHER_EXISTING_INTERFACE.clone(),
|
||||||
|
LAST_EXISTING_INTERFACE.clone(),
|
||||||
|
],
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
let topology = TopologyWithSwitch::new();
|
let topology = TopologyWithSwitch::new();
|
||||||
|
|
||||||
let _ = score.interpret(&Inventory::empty(), &topology).await;
|
let _ = score.interpret(&Inventory::empty(), &topology).await;
|
||||||
|
|
||||||
let configured_host_networks = topology.configured_host_networks.lock().unwrap();
|
let config = topology.configured_bonds.lock().unwrap();
|
||||||
assert_that!(*configured_host_networks).contains_exactly(vec![
|
assert_that!(*config).contains_exactly(vec![
|
||||||
(
|
(
|
||||||
HOST_ID.clone(),
|
HOST_ID.clone(),
|
||||||
HostNetworkConfig {
|
HostNetworkConfig {
|
||||||
host_id: HOST_ID.clone(),
|
host_id: HOST_ID.clone(),
|
||||||
switch_ports: vec![SwitchPort {
|
switch_ports: vec![
|
||||||
interface: EXISTING_INTERFACE.clone(),
|
SwitchPort {
|
||||||
port: PORT.clone(),
|
interface: EXISTING_INTERFACE.clone(),
|
||||||
}],
|
port: PORT.clone(),
|
||||||
|
},
|
||||||
|
SwitchPort {
|
||||||
|
interface: ANOTHER_EXISTING_INTERFACE.clone(),
|
||||||
|
port: ANOTHER_PORT.clone(),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
ANOTHER_HOST_ID.clone(),
|
ANOTHER_HOST_ID.clone(),
|
||||||
HostNetworkConfig {
|
HostNetworkConfig {
|
||||||
host_id: ANOTHER_HOST_ID.clone(),
|
host_id: ANOTHER_HOST_ID.clone(),
|
||||||
switch_ports: vec![SwitchPort {
|
switch_ports: vec![
|
||||||
interface: ANOTHER_EXISTING_INTERFACE.clone(),
|
SwitchPort {
|
||||||
port: ANOTHER_PORT.clone(),
|
interface: YET_ANOTHER_EXISTING_INTERFACE.clone(),
|
||||||
}],
|
port: YET_ANOTHER_PORT.clone(),
|
||||||
|
},
|
||||||
|
SwitchPort {
|
||||||
|
interface: LAST_EXISTING_INTERFACE.clone(),
|
||||||
|
port: LAST_PORT.clone(),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn port_not_found_for_mac_address_should_not_configure_interface() {
|
async fn multiple_hosts_should_configure_one_port_channel_per_host() {
|
||||||
|
let score = given_score(vec![
|
||||||
|
given_host(
|
||||||
|
&HOST_ID,
|
||||||
|
vec![
|
||||||
|
EXISTING_INTERFACE.clone(),
|
||||||
|
ANOTHER_EXISTING_INTERFACE.clone(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
given_host(
|
||||||
|
&ANOTHER_HOST_ID,
|
||||||
|
vec![
|
||||||
|
YET_ANOTHER_EXISTING_INTERFACE.clone(),
|
||||||
|
LAST_EXISTING_INTERFACE.clone(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
let topology = TopologyWithSwitch::new();
|
||||||
|
|
||||||
|
let _ = score.interpret(&Inventory::empty(), &topology).await;
|
||||||
|
|
||||||
|
let config = topology.configured_port_channels.lock().unwrap();
|
||||||
|
assert_that!(*config).contains_exactly(vec![
|
||||||
|
(
|
||||||
|
HOST_ID.clone(),
|
||||||
|
HostNetworkConfig {
|
||||||
|
host_id: HOST_ID.clone(),
|
||||||
|
switch_ports: vec![
|
||||||
|
SwitchPort {
|
||||||
|
interface: EXISTING_INTERFACE.clone(),
|
||||||
|
port: PORT.clone(),
|
||||||
|
},
|
||||||
|
SwitchPort {
|
||||||
|
interface: ANOTHER_EXISTING_INTERFACE.clone(),
|
||||||
|
port: ANOTHER_PORT.clone(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
ANOTHER_HOST_ID.clone(),
|
||||||
|
HostNetworkConfig {
|
||||||
|
host_id: ANOTHER_HOST_ID.clone(),
|
||||||
|
switch_ports: vec![
|
||||||
|
SwitchPort {
|
||||||
|
interface: YET_ANOTHER_EXISTING_INTERFACE.clone(),
|
||||||
|
port: YET_ANOTHER_PORT.clone(),
|
||||||
|
},
|
||||||
|
SwitchPort {
|
||||||
|
interface: LAST_EXISTING_INTERFACE.clone(),
|
||||||
|
port: LAST_PORT.clone(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn port_not_found_for_mac_address_should_not_configure_host() {
|
||||||
let score = given_score(vec![given_host(&HOST_ID, vec![UNKNOWN_INTERFACE.clone()])]);
|
let score = given_score(vec![given_host(&HOST_ID, vec![UNKNOWN_INTERFACE.clone()])]);
|
||||||
let topology = TopologyWithSwitch::new_port_not_found();
|
let topology = TopologyWithSwitch::new_port_not_found();
|
||||||
|
|
||||||
let _ = score.interpret(&Inventory::empty(), &topology).await;
|
let _ = score.interpret(&Inventory::empty(), &topology).await;
|
||||||
|
|
||||||
let configured_host_networks = topology.configured_host_networks.lock().unwrap();
|
let config = topology.configured_port_channels.lock().unwrap();
|
||||||
assert_that!(*configured_host_networks).is_empty();
|
assert_that!(*config).is_empty();
|
||||||
|
let config = topology.configured_bonds.lock().unwrap();
|
||||||
|
assert_that!(*config).is_empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn only_one_port_found_for_multiple_mac_addresses_should_not_configure_host() {
|
||||||
|
let score = given_score(vec![given_host(
|
||||||
|
&HOST_ID,
|
||||||
|
vec![EXISTING_INTERFACE.clone(), UNKNOWN_INTERFACE.clone()],
|
||||||
|
)]);
|
||||||
|
let topology = TopologyWithSwitch::new_single_port_found();
|
||||||
|
|
||||||
|
let _ = score.interpret(&Inventory::empty(), &topology).await;
|
||||||
|
|
||||||
|
let config = topology.configured_port_channels.lock().unwrap();
|
||||||
|
assert_that!(*config).is_empty();
|
||||||
|
let config = topology.configured_bonds.lock().unwrap();
|
||||||
|
assert_that!(*config).is_empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn given_score(hosts: Vec<PhysicalHost>) -> HostNetworkConfigurationScore {
|
fn given_score(hosts: Vec<PhysicalHost>) -> HostNetworkConfigurationScore {
|
||||||
@@ -422,26 +591,48 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
struct TopologyWithSwitch {
|
struct TopologyWithSwitch {
|
||||||
available_ports: Arc<Mutex<Vec<PortLocation>>>,
|
available_ports: Arc<Mutex<Vec<PortLocation>>>,
|
||||||
configured_host_networks: Arc<Mutex<Vec<(Id, HostNetworkConfig)>>>,
|
configured_port_channels: Arc<Mutex<Vec<(Id, HostNetworkConfig)>>>,
|
||||||
switch_setup: Arc<Mutex<bool>>,
|
switch_setup: Arc<Mutex<bool>>,
|
||||||
|
network_manager_setup: Arc<Mutex<bool>>,
|
||||||
|
configured_bonds: Arc<Mutex<Vec<(Id, HostNetworkConfig)>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TopologyWithSwitch {
|
impl TopologyWithSwitch {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
available_ports: Arc::new(Mutex::new(vec![PORT.clone(), ANOTHER_PORT.clone()])),
|
available_ports: Arc::new(Mutex::new(vec![
|
||||||
configured_host_networks: Arc::new(Mutex::new(vec![])),
|
PORT.clone(),
|
||||||
|
ANOTHER_PORT.clone(),
|
||||||
|
YET_ANOTHER_PORT.clone(),
|
||||||
|
LAST_PORT.clone(),
|
||||||
|
])),
|
||||||
|
configured_port_channels: Arc::new(Mutex::new(vec![])),
|
||||||
switch_setup: Arc::new(Mutex::new(false)),
|
switch_setup: Arc::new(Mutex::new(false)),
|
||||||
|
network_manager_setup: Arc::new(Mutex::new(false)),
|
||||||
|
configured_bonds: Arc::new(Mutex::new(vec![])),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_port_not_found() -> Self {
|
fn new_port_not_found() -> Self {
|
||||||
Self {
|
Self {
|
||||||
available_ports: Arc::new(Mutex::new(vec![])),
|
available_ports: Arc::new(Mutex::new(vec![])),
|
||||||
configured_host_networks: Arc::new(Mutex::new(vec![])),
|
configured_port_channels: Arc::new(Mutex::new(vec![])),
|
||||||
switch_setup: Arc::new(Mutex::new(false)),
|
switch_setup: Arc::new(Mutex::new(false)),
|
||||||
|
network_manager_setup: Arc::new(Mutex::new(false)),
|
||||||
|
configured_bonds: Arc::new(Mutex::new(vec![])),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_single_port_found() -> Self {
|
||||||
|
Self {
|
||||||
|
available_ports: Arc::new(Mutex::new(vec![PORT.clone()])),
|
||||||
|
configured_port_channels: Arc::new(Mutex::new(vec![])),
|
||||||
|
switch_setup: Arc::new(Mutex::new(false)),
|
||||||
|
network_manager_setup: Arc::new(Mutex::new(false)),
|
||||||
|
configured_bonds: Arc::new(Mutex::new(vec![])),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -457,6 +648,22 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl NetworkManager for TopologyWithSwitch {
|
||||||
|
async fn ensure_network_manager_installed(&self) -> Result<(), NetworkError> {
|
||||||
|
let mut network_manager_installed = self.network_manager_setup.lock().unwrap();
|
||||||
|
*network_manager_installed = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn configure_bond(&self, config: &HostNetworkConfig) -> Result<(), NetworkError> {
|
||||||
|
let mut configured_bonds = self.configured_bonds.lock().unwrap();
|
||||||
|
configured_bonds.push((config.host_id.clone(), config.clone()));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Switch for TopologyWithSwitch {
|
impl Switch for TopologyWithSwitch {
|
||||||
async fn setup_switch(&self) -> Result<(), SwitchError> {
|
async fn setup_switch(&self) -> Result<(), SwitchError> {
|
||||||
@@ -476,14 +683,23 @@ mod tests {
|
|||||||
Ok(Some(ports.remove(0)))
|
Ok(Some(ports.remove(0)))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn configure_host_network(
|
async fn configure_port_channel(
|
||||||
&self,
|
&self,
|
||||||
config: &HostNetworkConfig,
|
config: &HostNetworkConfig,
|
||||||
) -> Result<(), SwitchError> {
|
) -> Result<(), SwitchError> {
|
||||||
let mut configured_host_networks = self.configured_host_networks.lock().unwrap();
|
let mut configured_port_channels = self.configured_port_channels.lock().unwrap();
|
||||||
configured_host_networks.push((config.host_id.clone(), config.clone()));
|
configured_port_channels.push((config.host_id.clone(), config.clone()));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
async fn clear_port_channel(&self, ids: &Vec<Id>) -> Result<(), SwitchError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
async fn configure_interface(
|
||||||
|
&self,
|
||||||
|
port_config: &Vec<PortConfig>,
|
||||||
|
) -> Result<(), SwitchError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
harmony_inventory_agent/build_docker.sh
Executable file
11
harmony_inventory_agent/build_docker.sh
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
cargo build -p harmony_inventory_agent --release --target x86_64-unknown-linux-musl
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(dirname ${0})"
|
||||||
|
|
||||||
|
cd "${SCRIPT_DIR}/docker/"
|
||||||
|
|
||||||
|
cp ../../target/x86_64-unknown-linux-musl/release/harmony_inventory_agent .
|
||||||
|
|
||||||
|
docker build . -t hub.nationtech.io/harmony/harmony_inventory_agent
|
||||||
|
|
||||||
|
docker push hub.nationtech.io/harmony/harmony_inventory_agent
|
||||||
1
harmony_inventory_agent/docker/.gitignore
vendored
Normal file
1
harmony_inventory_agent/docker/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
harmony_inventory_agent
|
||||||
17
harmony_inventory_agent/docker/Dockerfile
Normal file
17
harmony_inventory_agent/docker/Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
FROM debian:12-slim
|
||||||
|
|
||||||
|
# install packages required to make these commands available : lspci, lsmod, dmidecode, smartctl, ip
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends pciutils kmod dmidecode smartmontools iproute2 && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|
||||||
|
RUN mkdir /app
|
||||||
|
WORKDIR /app/
|
||||||
|
|
||||||
|
COPY harmony_inventory_agent /app/
|
||||||
|
|
||||||
|
ENV RUST_LOG=info
|
||||||
|
|
||||||
|
CMD [ "/app/harmony_inventory_agent" ]
|
||||||
|
|
||||||
117
harmony_inventory_agent/harmony-inventory-agent-daemonset.yaml
Normal file
117
harmony_inventory_agent/harmony-inventory-agent-daemonset.yaml
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: harmony-inventory-agent
|
||||||
|
labels:
|
||||||
|
pod-security.kubernetes.io/enforce: privileged
|
||||||
|
pod-security.kubernetes.io/audit: privileged
|
||||||
|
pod-security.kubernetes.io/warn: privileged
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: harmony-inventory-agent
|
||||||
|
namespace: harmony-inventory-agent
|
||||||
|
---
|
||||||
|
# Grant the built-in "privileged" SCC to the SA
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: Role
|
||||||
|
metadata:
|
||||||
|
name: use-privileged-scc
|
||||||
|
namespace: harmony-inventory-agent
|
||||||
|
rules:
|
||||||
|
- apiGroups: ["security.openshift.io"]
|
||||||
|
resources: ["securitycontextconstraints"]
|
||||||
|
resourceNames: ["privileged"]
|
||||||
|
verbs: ["use"]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: use-privileged-scc
|
||||||
|
namespace: harmony-inventory-agent
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: harmony-inventory-agent
|
||||||
|
namespace: harmony-inventory-agent
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: Role
|
||||||
|
name: use-privileged-scc
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: DaemonSet
|
||||||
|
metadata:
|
||||||
|
name: harmony-inventory-agent
|
||||||
|
namespace: harmony-inventory-agent
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: harmony-inventory-agent
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: harmony-inventory-agent
|
||||||
|
spec:
|
||||||
|
serviceAccountName: harmony-inventory-agent
|
||||||
|
hostNetwork: true
|
||||||
|
dnsPolicy: ClusterFirstWithHostNet
|
||||||
|
tolerations:
|
||||||
|
- key: "node-role.kubernetes.io/master"
|
||||||
|
operator: "Exists"
|
||||||
|
effect: "NoSchedule"
|
||||||
|
containers:
|
||||||
|
- name: inventory-agent
|
||||||
|
image: hub.nationtech.io/harmony/harmony_inventory_agent
|
||||||
|
imagePullPolicy: Always
|
||||||
|
env:
|
||||||
|
- name: RUST_LOG
|
||||||
|
value: "harmony_inventory_agent=trace,info"
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: 200m
|
||||||
|
memory: 256Mi
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 128Mi
|
||||||
|
securityContext:
|
||||||
|
privileged: true
|
||||||
|
# optional: leave the rest unset since privileged SCC allows it
|
||||||
|
#
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: harmony-inventory-builder
|
||||||
|
namespace: harmony-inventory-agent
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
strategy: {}
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: harmony-inventory-builder
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: harmony-inventory-builder
|
||||||
|
spec:
|
||||||
|
serviceAccountName: harmony-inventory-agent
|
||||||
|
hostNetwork: true
|
||||||
|
dnsPolicy: ClusterFirstWithHostNet
|
||||||
|
containers:
|
||||||
|
- name: inventory-agent
|
||||||
|
image: hub.nationtech.io/harmony/harmony_inventory_builder
|
||||||
|
imagePullPolicy: Always
|
||||||
|
env:
|
||||||
|
- name: RUST_LOG
|
||||||
|
value: "harmony_inventory_builder=trace,info"
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: 200m
|
||||||
|
memory: 256Mi
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 128Mi
|
||||||
|
securityContext:
|
||||||
|
privileged: true
|
||||||
|
# optional: leave the rest unset since privileged SCC allows it
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
use harmony_types::net::MacAddress;
|
use harmony_types::net::MacAddress;
|
||||||
use log::{debug, warn};
|
use log::{debug, trace, warn};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@@ -121,20 +121,48 @@ pub struct ManagementInterface {
|
|||||||
|
|
||||||
impl PhysicalHost {
|
impl PhysicalHost {
|
||||||
pub fn gather() -> Result<Self, String> {
|
pub fn gather() -> Result<Self, String> {
|
||||||
|
trace!("Start gathering physical host information");
|
||||||
let mut sys = System::new_all();
|
let mut sys = System::new_all();
|
||||||
|
trace!("System new_all called");
|
||||||
sys.refresh_all();
|
sys.refresh_all();
|
||||||
|
trace!("System refresh_all called");
|
||||||
|
|
||||||
Self::all_tools_available()?;
|
Self::all_tools_available()?;
|
||||||
|
|
||||||
|
trace!("All tools_available success");
|
||||||
|
|
||||||
|
let storage_drives = Self::gather_storage_drives()?;
|
||||||
|
trace!("got storage drives");
|
||||||
|
|
||||||
|
let storage_controller = Self::gather_storage_controller()?;
|
||||||
|
trace!("got storage controller");
|
||||||
|
|
||||||
|
let memory_modules = Self::gather_memory_modules()?;
|
||||||
|
trace!("got memory_modules");
|
||||||
|
|
||||||
|
let cpus = Self::gather_cpus(&sys)?;
|
||||||
|
trace!("got cpus");
|
||||||
|
|
||||||
|
let chipset = Self::gather_chipset()?;
|
||||||
|
trace!("got chipsets");
|
||||||
|
|
||||||
|
let network_interfaces = Self::gather_network_interfaces()?;
|
||||||
|
trace!("got network_interfaces");
|
||||||
|
|
||||||
|
let management_interface = Self::gather_management_interface()?;
|
||||||
|
trace!("got management_interface");
|
||||||
|
|
||||||
|
let host_uuid = Self::get_host_uuid()?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
storage_drives: Self::gather_storage_drives()?,
|
storage_drives,
|
||||||
storage_controller: Self::gather_storage_controller()?,
|
storage_controller,
|
||||||
memory_modules: Self::gather_memory_modules()?,
|
memory_modules,
|
||||||
cpus: Self::gather_cpus(&sys)?,
|
cpus,
|
||||||
chipset: Self::gather_chipset()?,
|
chipset,
|
||||||
network_interfaces: Self::gather_network_interfaces()?,
|
network_interfaces,
|
||||||
management_interface: Self::gather_management_interface()?,
|
management_interface,
|
||||||
host_uuid: Self::get_host_uuid()?,
|
host_uuid,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,6 +236,8 @@ impl PhysicalHost {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug!("All tools found!");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,7 +261,10 @@ impl PhysicalHost {
|
|||||||
fn gather_storage_drives() -> Result<Vec<StorageDrive>, String> {
|
fn gather_storage_drives() -> Result<Vec<StorageDrive>, String> {
|
||||||
let mut drives = Vec::new();
|
let mut drives = Vec::new();
|
||||||
|
|
||||||
|
trace!("Starting storage drive discovery using lsblk");
|
||||||
|
|
||||||
// Use lsblk with JSON output for robust parsing
|
// Use lsblk with JSON output for robust parsing
|
||||||
|
trace!("Executing 'lsblk -d -o NAME,MODEL,SERIAL,SIZE,ROTA,WWN -n -e 7 --json'");
|
||||||
let output = Command::new("lsblk")
|
let output = Command::new("lsblk")
|
||||||
.args([
|
.args([
|
||||||
"-d",
|
"-d",
|
||||||
@@ -245,13 +278,18 @@ impl PhysicalHost {
|
|||||||
.output()
|
.output()
|
||||||
.map_err(|e| format!("Failed to execute lsblk: {}", e))?;
|
.map_err(|e| format!("Failed to execute lsblk: {}", e))?;
|
||||||
|
|
||||||
|
trace!(
|
||||||
|
"lsblk command executed successfully (status: {:?})",
|
||||||
|
output.status
|
||||||
|
);
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
return Err(format!(
|
let stderr_str = String::from_utf8_lossy(&output.stderr);
|
||||||
"lsblk command failed: {}",
|
debug!("lsblk command failed: {stderr_str}");
|
||||||
String::from_utf8_lossy(&output.stderr)
|
return Err(format!("lsblk command failed: {stderr_str}"));
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trace!("Parsing lsblk JSON output");
|
||||||
let json: Value = serde_json::from_slice(&output.stdout)
|
let json: Value = serde_json::from_slice(&output.stdout)
|
||||||
.map_err(|e| format!("Failed to parse lsblk JSON output: {}", e))?;
|
.map_err(|e| format!("Failed to parse lsblk JSON output: {}", e))?;
|
||||||
|
|
||||||
@@ -260,6 +298,8 @@ impl PhysicalHost {
|
|||||||
.and_then(|v| v.as_array())
|
.and_then(|v| v.as_array())
|
||||||
.ok_or("Invalid lsblk JSON: missing 'blockdevices' array")?;
|
.ok_or("Invalid lsblk JSON: missing 'blockdevices' array")?;
|
||||||
|
|
||||||
|
trace!("Found {} blockdevices in lsblk output", blockdevices.len());
|
||||||
|
|
||||||
for device in blockdevices {
|
for device in blockdevices {
|
||||||
let name = device
|
let name = device
|
||||||
.get("name")
|
.get("name")
|
||||||
@@ -268,52 +308,72 @@ impl PhysicalHost {
|
|||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
if name.is_empty() {
|
if name.is_empty() {
|
||||||
|
trace!("Skipping unnamed device entry: {:?}", device);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trace!("Inspecting block device: {name}");
|
||||||
|
|
||||||
|
// Extract metadata fields
|
||||||
let model = device
|
let model = device
|
||||||
.get("model")
|
.get("model")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.map(|s| s.trim().to_string())
|
.map(|s| s.trim().to_string())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
trace!("Model for {name}: '{}'", model);
|
||||||
|
|
||||||
let serial = device
|
let serial = device
|
||||||
.get("serial")
|
.get("serial")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.map(|s| s.trim().to_string())
|
.map(|s| s.trim().to_string())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
trace!("Serial for {name}: '{}'", serial);
|
||||||
|
|
||||||
let size_str = device
|
let size_str = device
|
||||||
.get("size")
|
.get("size")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.ok_or("Missing 'size' in lsblk device")?;
|
.ok_or("Missing 'size' in lsblk device")?;
|
||||||
|
trace!("Reported size for {name}: {}", size_str);
|
||||||
let size_bytes = Self::parse_size(size_str)?;
|
let size_bytes = Self::parse_size(size_str)?;
|
||||||
|
trace!("Parsed size for {name}: {} bytes", size_bytes);
|
||||||
|
|
||||||
let rotational = device
|
let rotational = device
|
||||||
.get("rota")
|
.get("rota")
|
||||||
.and_then(|v| v.as_bool())
|
.and_then(|v| v.as_bool())
|
||||||
.ok_or("Missing 'rota' in lsblk device")?;
|
.ok_or("Missing 'rota' in lsblk device")?;
|
||||||
|
trace!("Rotational flag for {name}: {}", rotational);
|
||||||
|
|
||||||
let wwn = device
|
let wwn = device
|
||||||
.get("wwn")
|
.get("wwn")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.map(|s| s.trim().to_string())
|
.map(|s| s.trim().to_string())
|
||||||
.filter(|s| !s.is_empty() && s != "null");
|
.filter(|s| !s.is_empty() && s != "null");
|
||||||
|
trace!("WWN for {name}: {:?}", wwn);
|
||||||
|
|
||||||
let device_path = Path::new("/sys/block").join(&name);
|
let device_path = Path::new("/sys/block").join(&name);
|
||||||
|
trace!("Sysfs path for {name}: {:?}", device_path);
|
||||||
|
|
||||||
|
trace!("Reading logical block size for {name}");
|
||||||
let logical_block_size = Self::read_sysfs_u32(
|
let logical_block_size = Self::read_sysfs_u32(
|
||||||
&device_path.join("queue/logical_block_size"),
|
&device_path.join("queue/logical_block_size"),
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("Failed to read logical block size for {}: {}", name, e))?;
|
.map_err(|e| format!("Failed to read logical block size for {}: {}", name, e))?;
|
||||||
|
trace!("Logical block size for {name}: {}", logical_block_size);
|
||||||
|
|
||||||
|
trace!("Reading physical block size for {name}");
|
||||||
let physical_block_size = Self::read_sysfs_u32(
|
let physical_block_size = Self::read_sysfs_u32(
|
||||||
&device_path.join("queue/physical_block_size"),
|
&device_path.join("queue/physical_block_size"),
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("Failed to read physical block size for {}: {}", name, e))?;
|
.map_err(|e| format!("Failed to read physical block size for {}: {}", name, e))?;
|
||||||
|
trace!("Physical block size for {name}: {}", physical_block_size);
|
||||||
|
|
||||||
|
trace!("Determining interface type for {name}");
|
||||||
let interface_type = Self::get_interface_type(&name, &device_path)?;
|
let interface_type = Self::get_interface_type(&name, &device_path)?;
|
||||||
|
trace!("Interface type for {name}: {}", interface_type);
|
||||||
|
|
||||||
|
trace!("Getting SMART status for {name}");
|
||||||
let smart_status = Self::get_smart_status(&name)?;
|
let smart_status = Self::get_smart_status(&name)?;
|
||||||
|
trace!("SMART status for {name}: {:?}", smart_status);
|
||||||
|
|
||||||
let mut drive = StorageDrive {
|
let mut drive = StorageDrive {
|
||||||
name: name.clone(),
|
name: name.clone(),
|
||||||
@@ -330,19 +390,31 @@ impl PhysicalHost {
|
|||||||
|
|
||||||
// Enhance with additional sysfs info if available
|
// Enhance with additional sysfs info if available
|
||||||
if device_path.exists() {
|
if device_path.exists() {
|
||||||
|
trace!("Enhancing drive {name} with extra sysfs metadata");
|
||||||
if drive.model.is_empty() {
|
if drive.model.is_empty() {
|
||||||
|
trace!("Reading model from sysfs for {name}");
|
||||||
drive.model = Self::read_sysfs_string(&device_path.join("device/model"))
|
drive.model = Self::read_sysfs_string(&device_path.join("device/model"))
|
||||||
.unwrap_or(format!("Failed to read model for {}", name));
|
.unwrap_or_else(|_| format!("Failed to read model for {}", name));
|
||||||
}
|
}
|
||||||
if drive.serial.is_empty() {
|
if drive.serial.is_empty() {
|
||||||
|
trace!("Reading serial from sysfs for {name}");
|
||||||
drive.serial = Self::read_sysfs_string(&device_path.join("device/serial"))
|
drive.serial = Self::read_sysfs_string(&device_path.join("device/serial"))
|
||||||
.unwrap_or(format!("Failed to read serial for {}", name));
|
.unwrap_or_else(|_| format!("Failed to read serial for {}", name));
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
trace!(
|
||||||
|
"Sysfs path {:?} not found for drive {name}, skipping extra metadata",
|
||||||
|
device_path
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug!("Discovered storage drive: {drive:?}");
|
||||||
drives.push(drive);
|
drives.push(drive);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug!("Discovered total {} storage drives", drives.len());
|
||||||
|
trace!("All discovered dives: {drives:?}");
|
||||||
|
|
||||||
Ok(drives)
|
Ok(drives)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,6 +490,8 @@ impl PhysicalHost {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug!("Found storage controller {controller:?}");
|
||||||
|
|
||||||
Ok(controller)
|
Ok(controller)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,6 +560,7 @@ impl PhysicalHost {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug!("Found memory modules {modules:?}");
|
||||||
Ok(modules)
|
Ok(modules)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -501,22 +576,30 @@ impl PhysicalHost {
|
|||||||
frequency_mhz: global_cpu.frequency(),
|
frequency_mhz: global_cpu.frequency(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
debug!("Found cpus {cpus:?}");
|
||||||
|
|
||||||
Ok(cpus)
|
Ok(cpus)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gather_chipset() -> Result<Chipset, String> {
|
fn gather_chipset() -> Result<Chipset, String> {
|
||||||
Ok(Chipset {
|
let chipset = Chipset {
|
||||||
name: Self::read_dmi("baseboard-product-name")?,
|
name: Self::read_dmi("baseboard-product-name")?,
|
||||||
vendor: Self::read_dmi("baseboard-manufacturer")?,
|
vendor: Self::read_dmi("baseboard-manufacturer")?,
|
||||||
})
|
};
|
||||||
|
|
||||||
|
debug!("Found chipset {chipset:?}");
|
||||||
|
|
||||||
|
Ok(chipset)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gather_network_interfaces() -> Result<Vec<NetworkInterface>, String> {
|
fn gather_network_interfaces() -> Result<Vec<NetworkInterface>, String> {
|
||||||
let mut interfaces = Vec::new();
|
let mut interfaces = Vec::new();
|
||||||
let sys_net_path = Path::new("/sys/class/net");
|
let sys_net_path = Path::new("/sys/class/net");
|
||||||
|
trace!("Reading /sys/class/net");
|
||||||
|
|
||||||
let entries = fs::read_dir(sys_net_path)
|
let entries = fs::read_dir(sys_net_path)
|
||||||
.map_err(|e| format!("Failed to read /sys/class/net: {}", e))?;
|
.map_err(|e| format!("Failed to read /sys/class/net: {}", e))?;
|
||||||
|
trace!("Got entries {entries:?}");
|
||||||
|
|
||||||
for entry in entries {
|
for entry in entries {
|
||||||
let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?;
|
let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?;
|
||||||
@@ -525,6 +608,7 @@ impl PhysicalHost {
|
|||||||
.into_string()
|
.into_string()
|
||||||
.map_err(|_| "Invalid UTF-8 in interface name")?;
|
.map_err(|_| "Invalid UTF-8 in interface name")?;
|
||||||
let iface_path = entry.path();
|
let iface_path = entry.path();
|
||||||
|
trace!("Inspecting interface {iface_name} path {iface_path:?}");
|
||||||
|
|
||||||
// Skip virtual interfaces
|
// Skip virtual interfaces
|
||||||
if iface_name.starts_with("lo")
|
if iface_name.starts_with("lo")
|
||||||
@@ -535,70 +619,101 @@ impl PhysicalHost {
|
|||||||
|| iface_name.starts_with("tun")
|
|| iface_name.starts_with("tun")
|
||||||
|| iface_name.starts_with("wg")
|
|| iface_name.starts_with("wg")
|
||||||
{
|
{
|
||||||
|
trace!(
|
||||||
|
"Skipping interface {iface_name} because it appears to be virtual/unsupported"
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if it's a physical interface by looking for device directory
|
// Check if it's a physical interface by looking for device directory
|
||||||
if !iface_path.join("device").exists() {
|
if !iface_path.join("device").exists() {
|
||||||
|
trace!(
|
||||||
|
"Skipping interface {iface_name} since {iface_path:?}/device does not exist"
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trace!("Reading MAC address for {iface_name}");
|
||||||
let mac_address = Self::read_sysfs_string(&iface_path.join("address"))
|
let mac_address = Self::read_sysfs_string(&iface_path.join("address"))
|
||||||
.map_err(|e| format!("Failed to read MAC address for {}: {}", iface_name, e))?;
|
.map_err(|e| format!("Failed to read MAC address for {}: {}", iface_name, e))?;
|
||||||
let mac_address = MacAddress::try_from(mac_address).map_err(|e| e.to_string())?;
|
let mac_address = MacAddress::try_from(mac_address).map_err(|e| e.to_string())?;
|
||||||
|
trace!("MAC address for {iface_name}: {mac_address}");
|
||||||
|
|
||||||
let speed_mbps = if iface_path.join("speed").exists() {
|
let speed_path = iface_path.join("speed");
|
||||||
match Self::read_sysfs_u32(&iface_path.join("speed")) {
|
let speed_mbps = if speed_path.exists() {
|
||||||
Ok(speed) => Some(speed),
|
trace!("Reading speed for {iface_name} from {:?}", speed_path);
|
||||||
|
match Self::read_sysfs_u32(&speed_path) {
|
||||||
|
Ok(speed) => {
|
||||||
|
trace!("Speed for {iface_name}: {speed} Mbps");
|
||||||
|
Some(speed)
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
debug!(
|
debug!(
|
||||||
"Failed to read speed for {}: {} . This is expected to fail on wifi interfaces.",
|
"Failed to read speed for {}: {} (this may be expected on Wi‑Fi interfaces)",
|
||||||
iface_name, e
|
iface_name, e
|
||||||
);
|
);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
trace!("Speed file not found for {iface_name}, skipping");
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
trace!("Reading operstate for {iface_name}");
|
||||||
let operstate = Self::read_sysfs_string(&iface_path.join("operstate"))
|
let operstate = Self::read_sysfs_string(&iface_path.join("operstate"))
|
||||||
.map_err(|e| format!("Failed to read operstate for {}: {}", iface_name, e))?;
|
.map_err(|e| format!("Failed to read operstate for {}: {}", iface_name, e))?;
|
||||||
|
trace!("Operstate for {iface_name}: {operstate}");
|
||||||
|
|
||||||
|
trace!("Reading MTU for {iface_name}");
|
||||||
let mtu = Self::read_sysfs_u32(&iface_path.join("mtu"))
|
let mtu = Self::read_sysfs_u32(&iface_path.join("mtu"))
|
||||||
.map_err(|e| format!("Failed to read MTU for {}: {}", iface_name, e))?;
|
.map_err(|e| format!("Failed to read MTU for {}: {}", iface_name, e))?;
|
||||||
|
trace!("MTU for {iface_name}: {mtu}");
|
||||||
|
|
||||||
|
trace!("Reading driver for {iface_name}");
|
||||||
let driver =
|
let driver =
|
||||||
Self::read_sysfs_symlink_basename(&iface_path.join("device/driver/module"))
|
Self::read_sysfs_symlink_basename(&iface_path.join("device/driver/module"))
|
||||||
.map_err(|e| format!("Failed to read driver for {}: {}", iface_name, e))?;
|
.map_err(|e| format!("Failed to read driver for {}: {}", iface_name, e))?;
|
||||||
|
trace!("Driver for {iface_name}: {driver}");
|
||||||
|
|
||||||
|
trace!("Reading firmware version for {iface_name}");
|
||||||
let firmware_version = Self::read_sysfs_opt_string(
|
let firmware_version = Self::read_sysfs_opt_string(
|
||||||
&iface_path.join("device/firmware_version"),
|
&iface_path.join("device/firmware_version"),
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("Failed to read firmware version for {}: {}", iface_name, e))?;
|
.map_err(|e| format!("Failed to read firmware version for {}: {}", iface_name, e))?;
|
||||||
|
trace!("Firmware version for {iface_name}: {firmware_version:?}");
|
||||||
|
|
||||||
// Get IP addresses using ip command with JSON output
|
trace!("Fetching IP addresses for {iface_name}");
|
||||||
let (ipv4_addresses, ipv6_addresses) = Self::get_interface_ips_json(&iface_name)
|
let (ipv4_addresses, ipv6_addresses) = Self::get_interface_ips_json(&iface_name)
|
||||||
.map_err(|e| format!("Failed to get IP addresses for {}: {}", iface_name, e))?;
|
.map_err(|e| format!("Failed to get IP addresses for {}: {}", iface_name, e))?;
|
||||||
|
trace!("Interface {iface_name} has IPv4: {ipv4_addresses:?}, IPv6: {ipv6_addresses:?}");
|
||||||
|
|
||||||
interfaces.push(NetworkInterface {
|
let is_up = operstate == "up";
|
||||||
name: iface_name,
|
trace!("Constructing NetworkInterface for {iface_name} (is_up={is_up})");
|
||||||
|
|
||||||
|
let iface = NetworkInterface {
|
||||||
|
name: iface_name.clone(),
|
||||||
mac_address,
|
mac_address,
|
||||||
speed_mbps,
|
speed_mbps,
|
||||||
is_up: operstate == "up",
|
is_up,
|
||||||
mtu,
|
mtu,
|
||||||
ipv4_addresses,
|
ipv4_addresses,
|
||||||
ipv6_addresses,
|
ipv6_addresses,
|
||||||
driver,
|
driver,
|
||||||
firmware_version,
|
firmware_version,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
debug!("Discovered interface: {iface:?}");
|
||||||
|
interfaces.push(iface);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug!("Discovered total {} network interfaces", interfaces.len());
|
||||||
|
trace!("Interfaces collected: {interfaces:?}");
|
||||||
Ok(interfaces)
|
Ok(interfaces)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gather_management_interface() -> Result<Option<ManagementInterface>, String> {
|
fn gather_management_interface() -> Result<Option<ManagementInterface>, String> {
|
||||||
if Path::new("/dev/ipmi0").exists() {
|
let mgmt = if Path::new("/dev/ipmi0").exists() {
|
||||||
Ok(Some(ManagementInterface {
|
Ok(Some(ManagementInterface {
|
||||||
kind: "IPMI".to_string(),
|
kind: "IPMI".to_string(),
|
||||||
address: None,
|
address: None,
|
||||||
@@ -612,11 +727,16 @@ impl PhysicalHost {
|
|||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
};
|
||||||
|
|
||||||
|
debug!("Found management interface {mgmt:?}");
|
||||||
|
mgmt
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_host_uuid() -> Result<String, String> {
|
fn get_host_uuid() -> Result<String, String> {
|
||||||
Self::read_dmi("system-uuid")
|
let uuid = Self::read_dmi("system-uuid");
|
||||||
|
debug!("Found uuid {uuid:?}");
|
||||||
|
uuid
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper methods
|
// Helper methods
|
||||||
@@ -709,7 +829,8 @@ impl PhysicalHost {
|
|||||||
Ok("Ramdisk".to_string())
|
Ok("Ramdisk".to_string())
|
||||||
} else {
|
} else {
|
||||||
// Try to determine from device path
|
// Try to determine from device path
|
||||||
let subsystem = Self::read_sysfs_string(&device_path.join("device/subsystem"))?;
|
let subsystem = Self::read_sysfs_string(&device_path.join("device/subsystem"))
|
||||||
|
.unwrap_or(String::new());
|
||||||
Ok(subsystem
|
Ok(subsystem
|
||||||
.split('/')
|
.split('/')
|
||||||
.next_back()
|
.next_back()
|
||||||
@@ -779,6 +900,8 @@ impl PhysicalHost {
|
|||||||
size.map(|s| s as u64)
|
size.map(|s| s as u64)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME when scanning an interface that is part of a bond/bridge we won't get an address on the
|
||||||
|
// interface, we should be looking at the bond/bridge device. For example, br-ex on k8s nodes.
|
||||||
fn get_interface_ips_json(iface_name: &str) -> Result<(Vec<String>, Vec<String>), String> {
|
fn get_interface_ips_json(iface_name: &str) -> Result<(Vec<String>, Vec<String>), String> {
|
||||||
let mut ipv4 = Vec::new();
|
let mut ipv4 = Vec::new();
|
||||||
let mut ipv6 = Vec::new();
|
let mut ipv6 = Vec::new();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use log::{debug, error, info, warn};
|
use log::{debug, error, info, trace, warn};
|
||||||
use mdns_sd::{ServiceDaemon, ServiceInfo};
|
use mdns_sd::{ServiceDaemon, ServiceInfo};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
@@ -12,6 +12,7 @@ use crate::{
|
|||||||
/// This function is synchronous and non-blocking. It spawns a background Tokio task
|
/// This function is synchronous and non-blocking. It spawns a background Tokio task
|
||||||
/// to handle the mDNS advertisement for the lifetime of the application.
|
/// to handle the mDNS advertisement for the lifetime of the application.
|
||||||
pub fn advertise(service_port: u16) -> Result<(), PresenceError> {
|
pub fn advertise(service_port: u16) -> Result<(), PresenceError> {
|
||||||
|
trace!("starting advertisement process for port {service_port}");
|
||||||
let host_id = match PhysicalHost::gather() {
|
let host_id = match PhysicalHost::gather() {
|
||||||
Ok(host) => Some(host.host_uuid),
|
Ok(host) => Some(host.host_uuid),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -20,11 +21,15 @@ pub fn advertise(service_port: u16) -> Result<(), PresenceError> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
trace!("Found host id {host_id:?}");
|
||||||
|
|
||||||
let instance_name = format!(
|
let instance_name = format!(
|
||||||
"inventory-agent-{}",
|
"inventory-agent-{}",
|
||||||
host_id.clone().unwrap_or("unknown".to_string())
|
host_id.clone().unwrap_or("unknown".to_string())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
trace!("Found host id {host_id:?}, name : {instance_name}");
|
||||||
|
|
||||||
let spawned_msg = format!("Spawned local presence advertisement task for '{instance_name}'.");
|
let spawned_msg = format!("Spawned local presence advertisement task for '{instance_name}'.");
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ async fn inventory() -> impl Responder {
|
|||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
|
||||||
let port = env::var("HARMONY_INVENTORY_AGENT_PORT").unwrap_or_else(|_| "8080".to_string());
|
let port = env::var("HARMONY_INVENTORY_AGENT_PORT").unwrap_or_else(|_| "25000".to_string());
|
||||||
let port = port
|
let port = port
|
||||||
.parse::<u16>()
|
.parse::<u16>()
|
||||||
.expect(&format!("Invalid port number, cannot parse to u16 {port}"));
|
.expect(&format!("Invalid port number, cannot parse to u16 {port}"));
|
||||||
|
|||||||
@@ -9,3 +9,4 @@ license.workspace = true
|
|||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
|
|
||||||
pub struct K8sName(pub String);
|
|
||||||
|
|
||||||
impl K8sName {
|
|
||||||
#[cfg(test)]
|
|
||||||
pub fn dummy() -> Self {
|
|
||||||
K8sName("example".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_valid(name: &str) -> bool {
|
|
||||||
if name.is_empty() || name.len() > 63 {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let b = name.as_bytes();
|
|
||||||
|
|
||||||
if !b[0].is_ascii_alphanumeric() || !b[b.len() - 1].is_ascii_alphanumeric() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
b.iter()
|
|
||||||
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || *c == b'-')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for K8sName {
|
|
||||||
type Err = K8sNameError;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
if !Self::is_valid(s) {
|
|
||||||
return Err(K8sNameError::InvalidFormat(format!(
|
|
||||||
"Invalid Kubernetes resource name '{s}': \
|
|
||||||
must match DNS-1123 (lowercase alphanumeric, hyphens, <=63 chars)"
|
|
||||||
)));
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(K8sName(s.to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum K8sNameError {
|
|
||||||
InvalidFormat(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&K8sName> for String {
|
|
||||||
fn from(value: &K8sName) -> Self {
|
|
||||||
value.0.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for K8sName {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.write_str(&self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_valid_name() {
|
|
||||||
assert!(K8sName::from_str("k8s-name-test").is_ok());
|
|
||||||
assert!(K8sName::from_str("n").is_ok());
|
|
||||||
assert!(K8sName::from_str("node1").is_ok());
|
|
||||||
assert!(K8sName::from_str("my-app-v2").is_ok());
|
|
||||||
assert!(K8sName::from_str("service123").is_ok());
|
|
||||||
assert!(K8sName::from_str("abcdefghijklmnopqrstuvwxyz-1234567890").is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_name() {
|
|
||||||
assert!(K8sName::from_str("").is_err());
|
|
||||||
assert!(K8sName::from_str(".config").is_err());
|
|
||||||
assert!(K8sName::from_str("_hidden").is_err());
|
|
||||||
assert!(K8sName::from_str("UPPER-CASE").is_err());
|
|
||||||
assert!(K8sName::from_str("123-$$$").is_err());
|
|
||||||
assert!(K8sName::from_str("app!name").is_err());
|
|
||||||
assert!(K8sName::from_str("my..app").is_err());
|
|
||||||
assert!(K8sName::from_str("backend-").is_err());
|
|
||||||
assert!(K8sName::from_str("-frontend").is_err());
|
|
||||||
assert!(K8sName::from_str("InvalidName").is_err());
|
|
||||||
assert!(
|
|
||||||
K8sName::from_str("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
|
||||||
.is_err()
|
|
||||||
);
|
|
||||||
assert!(K8sName::from_str("k8s name").is_err());
|
|
||||||
assert!(K8sName::from_str("k8s_name").is_err());
|
|
||||||
assert!(K8sName::from_str("k8s@name").is_err());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
pub mod id;
|
pub mod id;
|
||||||
pub mod k8s_name;
|
|
||||||
pub mod net;
|
pub mod net;
|
||||||
pub mod switch;
|
pub mod switch;
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use std::{fmt, str::FromStr};
|
use std::{fmt, str::FromStr};
|
||||||
|
use log::trace;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
/// Simple error type for port parsing failures.
|
/// Simple error type for port parsing failures.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -21,7 +23,7 @@ impl fmt::Display for PortParseError {
|
|||||||
/// Represents the atomic, physical location of a switch port: `<Stack>/<Module>/<Port>`.
|
/// Represents the atomic, physical location of a switch port: `<Stack>/<Module>/<Port>`.
|
||||||
///
|
///
|
||||||
/// Example: `1/1/1`
|
/// Example: `1/1/1`
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Serialize)]
|
||||||
pub struct PortLocation(pub u8, pub u8, pub u8);
|
pub struct PortLocation(pub u8, pub u8, pub u8);
|
||||||
|
|
||||||
impl fmt::Display for PortLocation {
|
impl fmt::Display for PortLocation {
|
||||||
@@ -70,6 +72,11 @@ impl FromStr for PortLocation {
|
|||||||
pub enum PortDeclaration {
|
pub enum PortDeclaration {
|
||||||
/// A single switch port defined by its location. Example: `PortDeclaration::Single(1/1/1)`
|
/// A single switch port defined by its location. Example: `PortDeclaration::Single(1/1/1)`
|
||||||
Single(PortLocation),
|
Single(PortLocation),
|
||||||
|
/// A Named port, often used for virtual ports such as PortChannels. Example
|
||||||
|
/// ```rust
|
||||||
|
/// PortDeclaration::Named("1".to_string())
|
||||||
|
/// ```
|
||||||
|
Named(String),
|
||||||
/// A strictly sequential range defined by two endpoints using the hyphen separator (`-`).
|
/// A strictly sequential range defined by two endpoints using the hyphen separator (`-`).
|
||||||
/// All ports between the endpoints (inclusive) are implicitly included.
|
/// All ports between the endpoints (inclusive) are implicitly included.
|
||||||
/// Example: `PortDeclaration::Range(1/1/1, 1/1/4)`
|
/// Example: `PortDeclaration::Range(1/1/1, 1/1/4)`
|
||||||
@@ -130,8 +137,14 @@ impl PortDeclaration {
|
|||||||
return Ok(PortDeclaration::Set(start_port, end_port));
|
return Ok(PortDeclaration::Set(start_port, end_port));
|
||||||
}
|
}
|
||||||
|
|
||||||
let location = PortLocation::from_str(port_str)?;
|
match PortLocation::from_str(port_str) {
|
||||||
Ok(PortDeclaration::Single(location))
|
Ok(loc) => Ok(PortDeclaration::Single(loc)),
|
||||||
|
Err(e) => {
|
||||||
|
trace!("Failed to parse PortLocation {port_str} : {e}");
|
||||||
|
trace!("Falling back on named port");
|
||||||
|
Ok(PortDeclaration::Named(port_str.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,6 +154,7 @@ impl fmt::Display for PortDeclaration {
|
|||||||
PortDeclaration::Single(port) => write!(f, "{port}"),
|
PortDeclaration::Single(port) => write!(f, "{port}"),
|
||||||
PortDeclaration::Range(start, end) => write!(f, "{start}-{end}"),
|
PortDeclaration::Range(start, end) => write!(f, "{start}-{end}"),
|
||||||
PortDeclaration::Set(start, end) => write!(f, "{start}*{end}"),
|
PortDeclaration::Set(start, end) => write!(f, "{start}*{end}"),
|
||||||
|
PortDeclaration::Named(name) => write!(f, "{name}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ pub struct System {
|
|||||||
pub disablechecksumoffloading: u8,
|
pub disablechecksumoffloading: u8,
|
||||||
pub disablesegmentationoffloading: u8,
|
pub disablesegmentationoffloading: u8,
|
||||||
pub disablelargereceiveoffloading: u8,
|
pub disablelargereceiveoffloading: u8,
|
||||||
pub ipv6allow: u8,
|
pub ipv6allow: Option<u8>,
|
||||||
pub powerd_ac_mode: String,
|
pub powerd_ac_mode: String,
|
||||||
pub powerd_battery_mode: String,
|
pub powerd_battery_mode: String,
|
||||||
pub powerd_normal_mode: String,
|
pub powerd_normal_mode: String,
|
||||||
@@ -226,6 +226,7 @@ pub struct System {
|
|||||||
pub dns6gw: Option<String>,
|
pub dns6gw: Option<String>,
|
||||||
pub dns7gw: Option<String>,
|
pub dns7gw: Option<String>,
|
||||||
pub dns8gw: Option<String>,
|
pub dns8gw: Option<String>,
|
||||||
|
pub prefer_ipv4: Option<String>,
|
||||||
pub dnsallowoverride: u8,
|
pub dnsallowoverride: u8,
|
||||||
pub dnsallowoverride_exclude: Option<MaybeString>,
|
pub dnsallowoverride_exclude: Option<MaybeString>,
|
||||||
}
|
}
|
||||||
@@ -329,6 +330,7 @@ pub struct Range {
|
|||||||
pub struct StaticMap {
|
pub struct StaticMap {
|
||||||
pub mac: String,
|
pub mac: String,
|
||||||
pub ipaddr: String,
|
pub ipaddr: String,
|
||||||
|
pub cid: Option<MaybeString>,
|
||||||
pub hostname: String,
|
pub hostname: String,
|
||||||
pub descr: Option<MaybeString>,
|
pub descr: Option<MaybeString>,
|
||||||
pub winsserver: MaybeString,
|
pub winsserver: MaybeString,
|
||||||
@@ -764,9 +766,19 @@ pub struct Jobs {
|
|||||||
pub struct Job {
|
pub struct Job {
|
||||||
#[yaserde(attribute = true)]
|
#[yaserde(attribute = true)]
|
||||||
pub uuid: MaybeString,
|
pub uuid: MaybeString,
|
||||||
#[yaserde(rename = "name")]
|
pub name: Option<MaybeString>,
|
||||||
pub name: MaybeString,
|
|
||||||
// Add other fields as needed
|
// Add other fields as needed
|
||||||
|
pub origin: Option<MaybeString>,
|
||||||
|
pub enabled: Option<MaybeString>,
|
||||||
|
pub minutes: Option<MaybeString>,
|
||||||
|
pub hours: Option<MaybeString>,
|
||||||
|
pub days: Option<MaybeString>,
|
||||||
|
pub months: Option<MaybeString>,
|
||||||
|
pub weekdays: Option<MaybeString>,
|
||||||
|
pub who: Option<MaybeString>,
|
||||||
|
pub command: Option<MaybeString>,
|
||||||
|
pub parameters: Option<MaybeString>,
|
||||||
|
pub description: Option<MaybeString>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
|
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
|
||||||
@@ -895,28 +907,28 @@ pub struct Proxy {
|
|||||||
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
|
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
|
||||||
pub struct ProxyGeneral {
|
pub struct ProxyGeneral {
|
||||||
pub enabled: i8,
|
pub enabled: i8,
|
||||||
pub error_pages: String,
|
pub error_pages: Option<MaybeString>,
|
||||||
#[yaserde(rename = "icpPort")]
|
#[yaserde(rename = "icpPort")]
|
||||||
pub icp_port: MaybeString,
|
pub icp_port: MaybeString,
|
||||||
pub logging: Logging,
|
pub logging: Logging,
|
||||||
#[yaserde(rename = "alternateDNSservers")]
|
#[yaserde(rename = "alternateDNSservers")]
|
||||||
pub alternate_dns_servers: MaybeString,
|
pub alternate_dns_servers: MaybeString,
|
||||||
#[yaserde(rename = "dnsV4First")]
|
#[yaserde(rename = "dnsV4First")]
|
||||||
pub dns_v4_first: i8,
|
pub dns_v4_first: Option<MaybeString>,
|
||||||
#[yaserde(rename = "forwardedForHandling")]
|
#[yaserde(rename = "forwardedForHandling")]
|
||||||
pub forwarded_for_handling: String,
|
pub forwarded_for_handling: Option<MaybeString>,
|
||||||
#[yaserde(rename = "uriWhitespaceHandling")]
|
#[yaserde(rename = "uriWhitespaceHandling")]
|
||||||
pub uri_whitespace_handling: String,
|
pub uri_whitespace_handling: Option<MaybeString>,
|
||||||
#[yaserde(rename = "enablePinger")]
|
#[yaserde(rename = "enablePinger")]
|
||||||
pub enable_pinger: i8,
|
pub enable_pinger: i8,
|
||||||
#[yaserde(rename = "useViaHeader")]
|
#[yaserde(rename = "useViaHeader")]
|
||||||
pub use_via_header: i8,
|
pub use_via_header: Option<MaybeString>,
|
||||||
#[yaserde(rename = "suppressVersion")]
|
#[yaserde(rename = "suppressVersion")]
|
||||||
pub suppress_version: i32,
|
pub suppress_version: Option<MaybeString>,
|
||||||
#[yaserde(rename = "connecttimeout")]
|
#[yaserde(rename = "connecttimeout")]
|
||||||
pub connect_timeout: MaybeString,
|
pub connect_timeout: Option<MaybeString>,
|
||||||
#[yaserde(rename = "VisibleEmail")]
|
#[yaserde(rename = "VisibleEmail")]
|
||||||
pub visible_email: String,
|
pub visible_email: Option<MaybeString>,
|
||||||
#[yaserde(rename = "VisibleHostname")]
|
#[yaserde(rename = "VisibleHostname")]
|
||||||
pub visible_hostname: MaybeString,
|
pub visible_hostname: MaybeString,
|
||||||
pub cache: Cache,
|
pub cache: Cache,
|
||||||
@@ -953,7 +965,7 @@ pub struct LocalCache {
|
|||||||
pub cache_mem: i32,
|
pub cache_mem: i32,
|
||||||
pub maximum_object_size: MaybeString,
|
pub maximum_object_size: MaybeString,
|
||||||
pub maximum_object_size_in_memory: MaybeString,
|
pub maximum_object_size_in_memory: MaybeString,
|
||||||
pub memory_cache_mode: String,
|
pub memory_cache_mode: MaybeString,
|
||||||
pub size: i32,
|
pub size: i32,
|
||||||
pub l1: i32,
|
pub l1: i32,
|
||||||
pub l2: i32,
|
pub l2: i32,
|
||||||
@@ -965,13 +977,13 @@ pub struct LocalCache {
|
|||||||
pub struct Traffic {
|
pub struct Traffic {
|
||||||
pub enabled: i32,
|
pub enabled: i32,
|
||||||
#[yaserde(rename = "maxDownloadSize")]
|
#[yaserde(rename = "maxDownloadSize")]
|
||||||
pub max_download_size: i32,
|
pub max_download_size: MaybeString,
|
||||||
#[yaserde(rename = "maxUploadSize")]
|
#[yaserde(rename = "maxUploadSize")]
|
||||||
pub max_upload_size: i32,
|
pub max_upload_size: MaybeString,
|
||||||
#[yaserde(rename = "OverallBandwidthTrotteling")]
|
#[yaserde(rename = "OverallBandwidthTrotteling")]
|
||||||
pub overall_bandwidth_trotteling: i32,
|
pub overall_bandwidth_trotteling: MaybeString,
|
||||||
#[yaserde(rename = "perHostTrotteling")]
|
#[yaserde(rename = "perHostTrotteling")]
|
||||||
pub per_host_trotteling: i32,
|
pub per_host_trotteling: MaybeString,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
|
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
|
||||||
@@ -988,7 +1000,7 @@ pub struct ParentProxy {
|
|||||||
|
|
||||||
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
|
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
|
||||||
pub struct Forward {
|
pub struct Forward {
|
||||||
pub interfaces: String,
|
pub interfaces: MaybeString,
|
||||||
pub port: i32,
|
pub port: i32,
|
||||||
pub sslbumpport: i32,
|
pub sslbumpport: i32,
|
||||||
pub sslbump: i32,
|
pub sslbump: i32,
|
||||||
@@ -1033,9 +1045,9 @@ pub struct Acl {
|
|||||||
pub google_apps: MaybeString,
|
pub google_apps: MaybeString,
|
||||||
pub youtube: MaybeString,
|
pub youtube: MaybeString,
|
||||||
#[yaserde(rename = "safePorts")]
|
#[yaserde(rename = "safePorts")]
|
||||||
pub safe_ports: String,
|
pub safe_ports: MaybeString,
|
||||||
#[yaserde(rename = "sslPorts")]
|
#[yaserde(rename = "sslPorts")]
|
||||||
pub ssl_ports: String,
|
pub ssl_ports: MaybeString,
|
||||||
#[yaserde(rename = "remoteACLs")]
|
#[yaserde(rename = "remoteACLs")]
|
||||||
pub remote_acls: RemoteAcls,
|
pub remote_acls: RemoteAcls,
|
||||||
}
|
}
|
||||||
@@ -1051,9 +1063,9 @@ pub struct RemoteAcls {
|
|||||||
pub struct Icap {
|
pub struct Icap {
|
||||||
pub enable: i32,
|
pub enable: i32,
|
||||||
#[yaserde(rename = "RequestURL")]
|
#[yaserde(rename = "RequestURL")]
|
||||||
pub request_url: String,
|
pub request_url: MaybeString,
|
||||||
#[yaserde(rename = "ResponseURL")]
|
#[yaserde(rename = "ResponseURL")]
|
||||||
pub response_url: String,
|
pub response_url: MaybeString,
|
||||||
#[yaserde(rename = "SendClientIP")]
|
#[yaserde(rename = "SendClientIP")]
|
||||||
pub send_client_ip: i32,
|
pub send_client_ip: i32,
|
||||||
#[yaserde(rename = "SendUsername")]
|
#[yaserde(rename = "SendUsername")]
|
||||||
@@ -1061,7 +1073,7 @@ pub struct Icap {
|
|||||||
#[yaserde(rename = "EncodeUsername")]
|
#[yaserde(rename = "EncodeUsername")]
|
||||||
pub encode_username: i32,
|
pub encode_username: i32,
|
||||||
#[yaserde(rename = "UsernameHeader")]
|
#[yaserde(rename = "UsernameHeader")]
|
||||||
pub username_header: String,
|
pub username_header: MaybeString,
|
||||||
#[yaserde(rename = "EnablePreview")]
|
#[yaserde(rename = "EnablePreview")]
|
||||||
pub enable_preview: i32,
|
pub enable_preview: i32,
|
||||||
#[yaserde(rename = "PreviewSize")]
|
#[yaserde(rename = "PreviewSize")]
|
||||||
@@ -1076,9 +1088,9 @@ pub struct Authentication {
|
|||||||
pub method: MaybeString,
|
pub method: MaybeString,
|
||||||
#[yaserde(rename = "authEnforceGroup")]
|
#[yaserde(rename = "authEnforceGroup")]
|
||||||
pub auth_enforce_group: MaybeString,
|
pub auth_enforce_group: MaybeString,
|
||||||
pub realm: String,
|
pub realm: MaybeString,
|
||||||
pub credentialsttl: i32, // This field is already in snake_case
|
pub credentialsttl: MaybeString, // This field is already in snake_case
|
||||||
pub children: i32,
|
pub children: MaybeString,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
|
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
|
||||||
@@ -1293,6 +1305,7 @@ pub struct WireguardServerItem {
|
|||||||
pub peers: String,
|
pub peers: String,
|
||||||
pub endpoint: MaybeString,
|
pub endpoint: MaybeString,
|
||||||
pub peer_dns: MaybeString,
|
pub peer_dns: MaybeString,
|
||||||
|
pub debug: Option<MaybeString>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
|
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
|
||||||
@@ -1477,6 +1490,7 @@ pub struct Ppp {
|
|||||||
pub ports: Option<MaybeString>,
|
pub ports: Option<MaybeString>,
|
||||||
pub username: Option<MaybeString>,
|
pub username: Option<MaybeString>,
|
||||||
pub password: Option<MaybeString>,
|
pub password: Option<MaybeString>,
|
||||||
|
pub provider: Option<MaybeString>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
|
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
|
||||||
|
|||||||
@@ -86,10 +86,7 @@ impl<'a> DhcpConfigLegacyISC<'a> {
|
|||||||
mac,
|
mac,
|
||||||
ipaddr: ipaddr.to_string(),
|
ipaddr: ipaddr.to_string(),
|
||||||
hostname,
|
hostname,
|
||||||
descr: Default::default(),
|
..Default::default()
|
||||||
winsserver: Default::default(),
|
|
||||||
dnsserver: Default::default(),
|
|
||||||
ntpserver: Default::default(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
existing_mappings.push(static_map);
|
existing_mappings.push(static_map);
|
||||||
@@ -126,9 +123,7 @@ impl<'a> DhcpConfigLegacyISC<'a> {
|
|||||||
ipaddr: entry["ipaddr"].as_str().unwrap_or_default().to_string(),
|
ipaddr: entry["ipaddr"].as_str().unwrap_or_default().to_string(),
|
||||||
hostname: entry["hostname"].as_str().unwrap_or_default().to_string(),
|
hostname: entry["hostname"].as_str().unwrap_or_default().to_string(),
|
||||||
descr: entry["descr"].as_str().map(MaybeString::from),
|
descr: entry["descr"].as_str().map(MaybeString::from),
|
||||||
winsserver: MaybeString::default(),
|
..Default::default()
|
||||||
dnsserver: MaybeString::default(),
|
|
||||||
ntpserver: MaybeString::default(),
|
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user