Compare commits
23 Commits
switch-cli
...
fix/opnsen
| Author | SHA1 | Date | |
|---|---|---|---|
| 43a17811cc | |||
| 29c82db70d | |||
| 83c1cc82b6 | |||
| 66d346a10c | |||
| 06a004a65d | |||
| 9d4e6acac0 | |||
| 4ff57062ae | |||
| 50ce54ea66 | |||
|
|
827a49e56b | ||
| 95cfc03518 | |||
| c80ede706b | |||
| b2825ec1ef | |||
| 609d7acb5d | |||
| de761cf538 | |||
| c069207f12 | |||
| ce91ee0168 | |||
| c0d54a4466 | |||
| fc384599a1 | |||
| 7dff70edcf | |||
| 06a0c44c3c | |||
| 85bec66e58 | |||
| 1f3796f503 | |||
| 58b6268989 |
@@ -31,6 +31,7 @@ pub struct BrocadeOptions {
|
|||||||
pub struct TimeoutConfig {
|
pub struct TimeoutConfig {
|
||||||
pub shell_ready: Duration,
|
pub shell_ready: Duration,
|
||||||
pub command_execution: Duration,
|
pub command_execution: Duration,
|
||||||
|
pub command_output: Duration,
|
||||||
pub cleanup: Duration,
|
pub cleanup: Duration,
|
||||||
pub message_wait: Duration,
|
pub message_wait: Duration,
|
||||||
}
|
}
|
||||||
@@ -40,6 +41,7 @@ impl Default for TimeoutConfig {
|
|||||||
Self {
|
Self {
|
||||||
shell_ready: Duration::from_secs(10),
|
shell_ready: Duration::from_secs(10),
|
||||||
command_execution: Duration::from_secs(60), // Commands like `deploy` (for a LAG) can take a while
|
command_execution: Duration::from_secs(60), // Commands like `deploy` (for a LAG) can take a while
|
||||||
|
command_output: Duration::from_secs(5), // Delay to start logging "waiting for command output"
|
||||||
cleanup: Duration::from_secs(10),
|
cleanup: Duration::from_secs(10),
|
||||||
message_wait: Duration::from_millis(500),
|
message_wait: Duration::from_millis(500),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use std::str::FromStr;
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use harmony_types::switch::{PortDeclaration, PortLocation};
|
use harmony_types::switch::{PortDeclaration, PortLocation};
|
||||||
use log::{debug, info};
|
use log::{debug, info};
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
BrocadeClient, BrocadeInfo, Error, ExecutionMode, InterSwitchLink, InterfaceInfo,
|
BrocadeClient, BrocadeInfo, Error, ExecutionMode, InterSwitchLink, InterfaceInfo,
|
||||||
@@ -103,13 +104,37 @@ impl NetworkOperatingSystemClient {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Some(Ok(InterfaceInfo {
|
Some(Ok(InterfaceInfo {
|
||||||
name: format!("{} {}", interface_type, port_location),
|
name: format!("{interface_type} {port_location}"),
|
||||||
port_location,
|
port_location,
|
||||||
interface_type,
|
interface_type,
|
||||||
operating_mode,
|
operating_mode,
|
||||||
status,
|
status,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn map_configure_interfaces_error(&self, err: Error) -> Error {
|
||||||
|
debug!("[Brocade] {err}");
|
||||||
|
|
||||||
|
if let Error::CommandError(message) = &err {
|
||||||
|
if message.contains("switchport")
|
||||||
|
&& message.contains("Cannot configure aggregator member")
|
||||||
|
{
|
||||||
|
let re = Regex::new(r"\(conf-if-([a-zA-Z]+)-([\d/]+)\)#").unwrap();
|
||||||
|
|
||||||
|
if let Some(caps) = re.captures(message) {
|
||||||
|
let interface_type = &caps[1];
|
||||||
|
let port_location = &caps[2];
|
||||||
|
let interface = format!("{interface_type} {port_location}");
|
||||||
|
|
||||||
|
return Error::CommandError(format!(
|
||||||
|
"Cannot configure interface '{interface}', it is a member of a port-channel (LAG)"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -197,11 +222,10 @@ impl BrocadeClient for NetworkOperatingSystemClient {
|
|||||||
commands.push("exit".into());
|
commands.push("exit".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
commands.push("write memory".into());
|
|
||||||
|
|
||||||
self.shell
|
self.shell
|
||||||
.run_commands(commands, ExecutionMode::Regular)
|
.run_commands(commands, ExecutionMode::Regular)
|
||||||
.await?;
|
.await
|
||||||
|
.map_err(|err| self.map_configure_interfaces_error(err))?;
|
||||||
|
|
||||||
info!("[Brocade] Interfaces configured.");
|
info!("[Brocade] Interfaces configured.");
|
||||||
|
|
||||||
@@ -213,7 +237,7 @@ impl BrocadeClient for NetworkOperatingSystemClient {
|
|||||||
|
|
||||||
let output = self
|
let output = self
|
||||||
.shell
|
.shell
|
||||||
.run_command("show port-channel", ExecutionMode::Regular)
|
.run_command("show port-channel summary", ExecutionMode::Regular)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let used_ids: Vec<u8> = output
|
let used_ids: Vec<u8> = output
|
||||||
@@ -248,7 +272,12 @@ impl BrocadeClient for NetworkOperatingSystemClient {
|
|||||||
ports: &[PortLocation],
|
ports: &[PortLocation],
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
info!(
|
info!(
|
||||||
"[Brocade] Configuring port-channel '{channel_name} {channel_id}' with ports: {ports:?}"
|
"[Brocade] Configuring port-channel '{channel_id} {channel_name}' with ports: {}",
|
||||||
|
ports
|
||||||
|
.iter()
|
||||||
|
.map(|p| format!("{p}"))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(", ")
|
||||||
);
|
);
|
||||||
|
|
||||||
let interfaces = self.get_interfaces().await?;
|
let interfaces = self.get_interfaces().await?;
|
||||||
@@ -276,8 +305,6 @@ impl BrocadeClient for NetworkOperatingSystemClient {
|
|||||||
commands.push("exit".into());
|
commands.push("exit".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
commands.push("write memory".into());
|
|
||||||
|
|
||||||
self.shell
|
self.shell
|
||||||
.run_commands(commands, ExecutionMode::Regular)
|
.run_commands(commands, ExecutionMode::Regular)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -294,7 +321,6 @@ impl BrocadeClient for NetworkOperatingSystemClient {
|
|||||||
"configure terminal".into(),
|
"configure terminal".into(),
|
||||||
format!("no interface port-channel {}", channel_name),
|
format!("no interface port-channel {}", channel_name),
|
||||||
"exit".into(),
|
"exit".into(),
|
||||||
"write memory".into(),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
self.shell
|
self.shell
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ impl BrocadeSession {
|
|||||||
let mut output = Vec::new();
|
let mut output = Vec::new();
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let read_timeout = Duration::from_millis(500);
|
let read_timeout = Duration::from_millis(500);
|
||||||
let log_interval = Duration::from_secs(3);
|
let log_interval = Duration::from_secs(5);
|
||||||
let mut last_log = Instant::now();
|
let mut last_log = Instant::now();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
@@ -221,7 +221,9 @@ impl BrocadeSession {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if start.elapsed() > Duration::from_secs(5) && last_log.elapsed() > log_interval {
|
if start.elapsed() > self.options.timeouts.command_output
|
||||||
|
&& last_log.elapsed() > log_interval
|
||||||
|
{
|
||||||
info!("[Brocade] Waiting for command output...");
|
info!("[Brocade] Waiting for command output...");
|
||||||
last_log = Instant::now();
|
last_log = Instant::now();
|
||||||
}
|
}
|
||||||
@@ -276,7 +278,7 @@ impl BrocadeSession {
|
|||||||
let output_lower = output.to_lowercase();
|
let output_lower = output.to_lowercase();
|
||||||
if ERROR_PATTERNS.iter().any(|&p| output_lower.contains(p)) {
|
if ERROR_PATTERNS.iter().any(|&p| output_lower.contains(p)) {
|
||||||
return Err(Error::CommandError(format!(
|
return Err(Error::CommandError(format!(
|
||||||
"Command '{command}' failed: {}",
|
"Command error: {}",
|
||||||
output.trim()
|
output.trim()
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -61,6 +61,7 @@ async fn main() {
|
|||||||
let gateway_ipv4 = Ipv4Addr::new(192, 168, 33, 1);
|
let gateway_ipv4 = Ipv4Addr::new(192, 168, 33, 1);
|
||||||
let gateway_ip = IpAddr::V4(gateway_ipv4);
|
let gateway_ip = IpAddr::V4(gateway_ipv4);
|
||||||
let topology = harmony::topology::HAClusterTopology {
|
let topology = harmony::topology::HAClusterTopology {
|
||||||
|
kubeconfig: None,
|
||||||
domain_name: "ncd0.harmony.mcd".to_string(), // TODO this must be set manually correctly
|
domain_name: "ncd0.harmony.mcd".to_string(), // TODO this must be set manually correctly
|
||||||
// when setting up the opnsense firewall
|
// when setting up the opnsense firewall
|
||||||
router: Arc::new(UnmanagedRouter::new(
|
router: Arc::new(UnmanagedRouter::new(
|
||||||
@@ -106,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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -59,6 +62,7 @@ pub async fn get_topology() -> HAClusterTopology {
|
|||||||
let gateway_ipv4 = ipv4!("192.168.1.1");
|
let gateway_ipv4 = ipv4!("192.168.1.1");
|
||||||
let gateway_ip = IpAddr::V4(gateway_ipv4);
|
let gateway_ip = IpAddr::V4(gateway_ipv4);
|
||||||
harmony::topology::HAClusterTopology {
|
harmony::topology::HAClusterTopology {
|
||||||
|
kubeconfig: None,
|
||||||
domain_name: "demo.harmony.mcd".to_string(),
|
domain_name: "demo.harmony.mcd".to_string(),
|
||||||
router: Arc::new(UnmanagedRouter::new(
|
router: Arc::new(UnmanagedRouter::new(
|
||||||
gateway_ip,
|
gateway_ip,
|
||||||
@@ -80,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 {
|
||||||
@@ -54,6 +57,7 @@ pub async fn get_topology() -> HAClusterTopology {
|
|||||||
let gateway_ipv4 = ipv4!("192.168.1.1");
|
let gateway_ipv4 = ipv4!("192.168.1.1");
|
||||||
let gateway_ip = IpAddr::V4(gateway_ipv4);
|
let gateway_ip = IpAddr::V4(gateway_ipv4);
|
||||||
harmony::topology::HAClusterTopology {
|
harmony::topology::HAClusterTopology {
|
||||||
|
kubeconfig: None,
|
||||||
domain_name: "demo.harmony.mcd".to_string(),
|
domain_name: "demo.harmony.mcd".to_string(),
|
||||||
router: Arc::new(UnmanagedRouter::new(
|
router: Arc::new(UnmanagedRouter::new(
|
||||||
gateway_ip,
|
gateway_ip,
|
||||||
@@ -75,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;
|
||||||
@@ -57,6 +57,7 @@ async fn main() {
|
|||||||
let gateway_ipv4 = Ipv4Addr::new(10, 100, 8, 1);
|
let gateway_ipv4 = Ipv4Addr::new(10, 100, 8, 1);
|
||||||
let gateway_ip = IpAddr::V4(gateway_ipv4);
|
let gateway_ip = IpAddr::V4(gateway_ipv4);
|
||||||
let topology = harmony::topology::HAClusterTopology {
|
let topology = harmony::topology::HAClusterTopology {
|
||||||
|
kubeconfig: None,
|
||||||
domain_name: "demo.harmony.mcd".to_string(),
|
domain_name: "demo.harmony.mcd".to_string(),
|
||||||
router: Arc::new(UnmanagedRouter::new(
|
router: Arc::new(UnmanagedRouter::new(
|
||||||
gateway_ip,
|
gateway_ip,
|
||||||
@@ -78,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 {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use harmony::{
|
|||||||
modules::{
|
modules::{
|
||||||
application::{
|
application::{
|
||||||
ApplicationScore, RustWebFramework, RustWebapp,
|
ApplicationScore, RustWebFramework, RustWebapp,
|
||||||
features::{PackagingDeployment, rhob_monitoring::Monitoring},
|
features::{Monitoring, PackagingDeployment},
|
||||||
},
|
},
|
||||||
monitoring::alert_channel::discord_alert_channel::DiscordWebhook,
|
monitoring::alert_channel::discord_alert_channel::DiscordWebhook,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,32 +1,25 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
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 k8s_openapi::api::core::v1::Namespace;
|
|
||||||
use kube::api::ObjectMeta;
|
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use log::info;
|
use log::info;
|
||||||
|
|
||||||
use crate::data::FileContent;
|
use crate::infra::network_manager::OpenShiftNmStateNetworkManager;
|
||||||
use crate::executors::ExecutorError;
|
|
||||||
use crate::hardware::PhysicalHost;
|
|
||||||
use crate::modules::okd::crd::{
|
|
||||||
InstallPlanApproval, OperatorGroup, OperatorGroupSpec, Subscription, SubscriptionSpec,
|
|
||||||
nmstate::{self, NMState, NodeNetworkConfigurationPolicy, NodeNetworkConfigurationPolicySpec},
|
|
||||||
};
|
|
||||||
use crate::topology::PxeOptions;
|
use crate::topology::PxeOptions;
|
||||||
|
use crate::{data::FileContent, executors::ExecutorError};
|
||||||
|
|
||||||
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 {
|
||||||
@@ -42,6 +35,8 @@ pub struct HAClusterTopology {
|
|||||||
pub bootstrap_host: LogicalHost,
|
pub bootstrap_host: LogicalHost,
|
||||||
pub control_plane: Vec<LogicalHost>,
|
pub control_plane: Vec<LogicalHost>,
|
||||||
pub workers: Vec<LogicalHost>,
|
pub workers: Vec<LogicalHost>,
|
||||||
|
pub kubeconfig: Option<String>,
|
||||||
|
pub network_manager: OnceLock<Arc<dyn NetworkManager>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -60,9 +55,17 @@ impl Topology for HAClusterTopology {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl K8sclient for HAClusterTopology {
|
impl K8sclient for HAClusterTopology {
|
||||||
async fn k8s_client(&self) -> Result<Arc<K8sClient>, String> {
|
async fn k8s_client(&self) -> Result<Arc<K8sClient>, String> {
|
||||||
Ok(Arc::new(
|
match &self.kubeconfig {
|
||||||
K8sClient::try_default().await.map_err(|e| e.to_string())?,
|
None => Ok(Arc::new(
|
||||||
))
|
K8sClient::try_default().await.map_err(|e| e.to_string())?,
|
||||||
|
)),
|
||||||
|
Some(kubeconfig) => {
|
||||||
|
let Some(client) = K8sClient::from_kubeconfig(kubeconfig).await else {
|
||||||
|
return Err("Failed to create k8s client".to_string());
|
||||||
|
};
|
||||||
|
Ok(Arc::new(client))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,208 +90,12 @@ impl HAClusterTopology {
|
|||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn ensure_nmstate_operator_installed(&self) -> Result<(), String> {
|
pub async fn network_manager(&self) -> &dyn NetworkManager {
|
||||||
// FIXME: Find a way to check nmstate is already available (get pod -n openshift-nmstate)
|
let k8s_client = self.k8s_client().await.unwrap();
|
||||||
debug!("Installing NMState operator...");
|
|
||||||
let k8s_client = self.k8s_client().await?;
|
|
||||||
|
|
||||||
let nmstate_namespace = Namespace {
|
self.network_manager
|
||||||
metadata: ObjectMeta {
|
.get_or_init(|| Arc::new(OpenShiftNmStateNetworkManager::new(k8s_client.clone())))
|
||||||
name: Some("openshift-nmstate".to_string()),
|
.as_ref()
|
||||||
finalizers: Some(vec!["kubernetes".to_string()]),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
debug!("Creating NMState namespace: {nmstate_namespace:#?}");
|
|
||||||
k8s_client
|
|
||||||
.apply(&nmstate_namespace, None)
|
|
||||||
.await
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
let nmstate_operator_group = OperatorGroup {
|
|
||||||
metadata: ObjectMeta {
|
|
||||||
name: Some("openshift-nmstate".to_string()),
|
|
||||||
namespace: Some("openshift-nmstate".to_string()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
spec: OperatorGroupSpec {
|
|
||||||
target_namespaces: vec!["openshift-nmstate".to_string()],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
debug!("Creating NMState operator group: {nmstate_operator_group:#?}");
|
|
||||||
k8s_client
|
|
||||||
.apply(&nmstate_operator_group, None)
|
|
||||||
.await
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
let nmstate_subscription = Subscription {
|
|
||||||
metadata: ObjectMeta {
|
|
||||||
name: Some("kubernetes-nmstate-operator".to_string()),
|
|
||||||
namespace: Some("openshift-nmstate".to_string()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
spec: SubscriptionSpec {
|
|
||||||
channel: Some("stable".to_string()),
|
|
||||||
install_plan_approval: Some(InstallPlanApproval::Automatic),
|
|
||||||
name: "kubernetes-nmstate-operator".to_string(),
|
|
||||||
source: "redhat-operators".to_string(),
|
|
||||||
source_namespace: "openshift-marketplace".to_string(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
debug!("Subscribing to NMState Operator: {nmstate_subscription:#?}");
|
|
||||||
k8s_client
|
|
||||||
.apply(&nmstate_subscription, None)
|
|
||||||
.await
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
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,
|
|
||||||
host: &PhysicalHost,
|
|
||||||
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(host, config);
|
|
||||||
debug!("Configuring bond for host {host:?}: {bond_config:#?}");
|
|
||||||
self.k8s_client()
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.apply(&bond_config, None)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_bond_configuration(
|
|
||||||
&self,
|
|
||||||
host: &PhysicalHost,
|
|
||||||
config: &HostNetworkConfig,
|
|
||||||
) -> NodeNetworkConfigurationPolicy {
|
|
||||||
let host_name = host.id.clone();
|
|
||||||
|
|
||||||
let bond_id = self.get_next_bond_id();
|
|
||||||
let bond_name = format!("bond{bond_id}");
|
|
||||||
let mut bond_mtu: Option<u32> = None;
|
|
||||||
let mut bond_mac_address: 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);
|
|
||||||
|
|
||||||
// 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 bond_mac_address.is_none() {
|
|
||||||
bond_mac_address = Some(switch_port.interface.mac_address.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(),
|
|
||||||
mtu: bond_mtu,
|
|
||||||
mac_address: bond_mac_address,
|
|
||||||
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,
|
|
||||||
host: &PhysicalHost,
|
|
||||||
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_{}", 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 {
|
||||||
@@ -299,6 +106,7 @@ impl HAClusterTopology {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
kubeconfig: None,
|
||||||
domain_name: "DummyTopology".to_string(),
|
domain_name: "DummyTopology".to_string(),
|
||||||
router: dummy_infra.clone(),
|
router: dummy_infra.clone(),
|
||||||
load_balancer: dummy_infra.clone(),
|
load_balancer: dummy_infra.clone(),
|
||||||
@@ -311,6 +119,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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -468,25 +277,40 @@ 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(
|
async fn configure_port_channel(&self, config: &HostNetworkConfig) -> Result<(), SwitchError> {
|
||||||
&self,
|
debug!("Configuring port channel: {config:#?}");
|
||||||
host: &PhysicalHost,
|
let switch_ports = config.switch_ports.iter().map(|s| s.port.clone()).collect();
|
||||||
config: HostNetworkConfig,
|
|
||||||
) -> Result<(), SwitchError> {
|
self.switch_client
|
||||||
self.configure_bond(host, &config).await?;
|
.configure_port_channel(&format!("Harmony_{}", config.host_id), switch_ports)
|
||||||
self.configure_port_channel(host, &config).await
|
.await
|
||||||
|
.map_err(|e| SwitchError::new(format!("Failed to configure port-channel: {e}")))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,20 @@ use std::time::Duration;
|
|||||||
use derive_new::new;
|
use derive_new::new;
|
||||||
use k8s_openapi::{
|
use k8s_openapi::{
|
||||||
ClusterResourceScope, NamespaceResourceScope,
|
ClusterResourceScope, NamespaceResourceScope,
|
||||||
api::{apps::v1::Deployment, core::v1::Pod},
|
api::{
|
||||||
|
apps::v1::Deployment,
|
||||||
|
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::ErrorResponse,
|
core::ErrorResponse,
|
||||||
|
discovery::{ApiCapabilities, Scope},
|
||||||
error::DiscoveryError,
|
error::DiscoveryError,
|
||||||
runtime::reflector::Lookup,
|
runtime::reflector::Lookup,
|
||||||
};
|
};
|
||||||
@@ -19,11 +25,12 @@ use kube::{
|
|||||||
api::{ApiResource, GroupVersionKind},
|
api::{ApiResource, GroupVersionKind},
|
||||||
runtime::wait::await_condition,
|
runtime::wait::await_condition,
|
||||||
};
|
};
|
||||||
use log::{debug, error, info, trace};
|
use log::{debug, error, trace, warn};
|
||||||
use serde::{Serialize, de::DeserializeOwned};
|
use serde::{Serialize, de::DeserializeOwned};
|
||||||
use serde_json::{Value, json};
|
use serde_json::json;
|
||||||
use similar::TextDiff;
|
use similar::TextDiff;
|
||||||
use tokio::{io::AsyncReadExt, time::sleep};
|
use tokio::{io::AsyncReadExt, time::sleep};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
#[derive(new, Clone)]
|
#[derive(new, Clone)]
|
||||||
pub struct K8sClient {
|
pub struct K8sClient {
|
||||||
@@ -57,6 +64,11 @@ impl K8sClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn service_account_api(&self, namespace: &str) -> Api<ServiceAccount> {
|
||||||
|
let api: Api<ServiceAccount> = Api::namespaced(self.client.clone(), namespace);
|
||||||
|
api
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_apiserver_version(&self) -> Result<Info, Error> {
|
pub async fn get_apiserver_version(&self) -> Result<Info, Error> {
|
||||||
let client: Client = self.client.clone();
|
let client: Client = self.client.clone();
|
||||||
let version_info: Info = client.apiserver_version().await?;
|
let version_info: Info = client.apiserver_version().await?;
|
||||||
@@ -80,7 +92,8 @@ impl K8sClient {
|
|||||||
} else {
|
} else {
|
||||||
Api::default_namespaced_with(self.client.clone(), &gvk)
|
Api::default_namespaced_with(self.client.clone(), &gvk)
|
||||||
};
|
};
|
||||||
Ok(resource.get(name).await?)
|
|
||||||
|
resource.get(name).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_deployment(
|
pub async fn get_deployment(
|
||||||
@@ -95,8 +108,9 @@ impl K8sClient {
|
|||||||
debug!("getting default namespace deployment");
|
debug!("getting default namespace deployment");
|
||||||
Api::default_namespaced(self.client.clone())
|
Api::default_namespaced(self.client.clone())
|
||||||
};
|
};
|
||||||
|
|
||||||
debug!("getting deployment {} in ns {}", name, namespace.unwrap());
|
debug!("getting deployment {} in ns {}", name, namespace.unwrap());
|
||||||
Ok(deps.get_opt(name).await?)
|
deps.get_opt(name).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_pod(&self, name: &str, namespace: Option<&str>) -> Result<Option<Pod>, Error> {
|
pub async fn get_pod(&self, name: &str, namespace: Option<&str>) -> Result<Option<Pod>, Error> {
|
||||||
@@ -105,7 +119,8 @@ impl K8sClient {
|
|||||||
} else {
|
} else {
|
||||||
Api::default_namespaced(self.client.clone())
|
Api::default_namespaced(self.client.clone())
|
||||||
};
|
};
|
||||||
Ok(pods.get_opt(name).await?)
|
|
||||||
|
pods.get_opt(name).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn scale_deployment(
|
pub async fn scale_deployment(
|
||||||
@@ -148,9 +163,9 @@ impl K8sClient {
|
|||||||
|
|
||||||
pub async fn wait_until_deployment_ready(
|
pub async fn wait_until_deployment_ready(
|
||||||
&self,
|
&self,
|
||||||
name: String,
|
name: &str,
|
||||||
namespace: Option<&str>,
|
namespace: Option<&str>,
|
||||||
timeout: Option<u64>,
|
timeout: Option<Duration>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let api: Api<Deployment>;
|
let api: Api<Deployment>;
|
||||||
|
|
||||||
@@ -160,9 +175,9 @@ impl K8sClient {
|
|||||||
api = Api::default_namespaced(self.client.clone());
|
api = Api::default_namespaced(self.client.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
let establish = await_condition(api, name.as_str(), conditions::is_deployment_completed());
|
let establish = await_condition(api, name, conditions::is_deployment_completed());
|
||||||
let t = timeout.unwrap_or(300);
|
let timeout = timeout.unwrap_or(Duration::from_secs(120));
|
||||||
let res = tokio::time::timeout(std::time::Duration::from_secs(t), establish).await;
|
let res = tokio::time::timeout(timeout, establish).await;
|
||||||
|
|
||||||
if res.is_ok() {
|
if res.is_ok() {
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -252,7 +267,7 @@ impl K8sClient {
|
|||||||
|
|
||||||
if let Some(s) = status.status {
|
if let Some(s) = status.status {
|
||||||
let mut stdout_buf = String::new();
|
let mut stdout_buf = String::new();
|
||||||
if let Some(mut stdout) = process.stdout().take() {
|
if let Some(mut stdout) = process.stdout() {
|
||||||
stdout
|
stdout
|
||||||
.read_to_string(&mut stdout_buf)
|
.read_to_string(&mut stdout_buf)
|
||||||
.await
|
.await
|
||||||
@@ -358,14 +373,14 @@ impl K8sClient {
|
|||||||
Ok(current) => {
|
Ok(current) => {
|
||||||
trace!("Received current value {current:#?}");
|
trace!("Received current value {current:#?}");
|
||||||
// The resource exists, so we calculate and display a diff.
|
// The resource exists, so we calculate and display a diff.
|
||||||
println!("\nPerforming dry-run for resource: '{}'", name);
|
println!("\nPerforming dry-run for resource: '{name}'");
|
||||||
let mut current_yaml = serde_yaml::to_value(¤t).unwrap_or_else(|_| {
|
let mut current_yaml = serde_yaml::to_value(¤t).unwrap_or_else(|_| {
|
||||||
panic!("Could not serialize current value : {current:#?}")
|
panic!("Could not serialize current value : {current:#?}")
|
||||||
});
|
});
|
||||||
if current_yaml.is_mapping() && current_yaml.get("status").is_some() {
|
if current_yaml.is_mapping() && current_yaml.get("status").is_some() {
|
||||||
let map = current_yaml.as_mapping_mut().unwrap();
|
let map = current_yaml.as_mapping_mut().unwrap();
|
||||||
let removed = map.remove_entry("status");
|
let removed = map.remove_entry("status");
|
||||||
trace!("Removed status {:?}", removed);
|
trace!("Removed status {removed:?}");
|
||||||
} else {
|
} else {
|
||||||
trace!(
|
trace!(
|
||||||
"Did not find status entry for current object {}/{}",
|
"Did not find status entry for current object {}/{}",
|
||||||
@@ -394,14 +409,14 @@ impl K8sClient {
|
|||||||
similar::ChangeTag::Insert => "+",
|
similar::ChangeTag::Insert => "+",
|
||||||
similar::ChangeTag::Equal => " ",
|
similar::ChangeTag::Equal => " ",
|
||||||
};
|
};
|
||||||
print!("{}{}", sign, change);
|
print!("{sign}{change}");
|
||||||
}
|
}
|
||||||
// In a dry run, we return the new resource state that would have been applied.
|
// In a dry run, we return the new resource state that would have been applied.
|
||||||
Ok(resource.clone())
|
Ok(resource.clone())
|
||||||
}
|
}
|
||||||
Err(Error::Api(ErrorResponse { code: 404, .. })) => {
|
Err(Error::Api(ErrorResponse { code: 404, .. })) => {
|
||||||
// The resource does not exist, so the "diff" is the entire new resource.
|
// The resource does not exist, so the "diff" is the entire new resource.
|
||||||
println!("\nPerforming dry-run for new resource: '{}'", name);
|
println!("\nPerforming dry-run for new resource: '{name}'");
|
||||||
println!(
|
println!(
|
||||||
"Resource does not exist. It would be created with the following content:"
|
"Resource does not exist. It would be created with the following content:"
|
||||||
);
|
);
|
||||||
@@ -410,14 +425,14 @@ impl K8sClient {
|
|||||||
|
|
||||||
// Print each line of the new resource with a '+' prefix.
|
// Print each line of the new resource with a '+' prefix.
|
||||||
for line in new_yaml.lines() {
|
for line in new_yaml.lines() {
|
||||||
println!("+{}", line);
|
println!("+{line}");
|
||||||
}
|
}
|
||||||
// In a dry run, we return the new resource state that would have been created.
|
// In a dry run, we return the new resource state that would have been created.
|
||||||
Ok(resource.clone())
|
Ok(resource.clone())
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// Another API error occurred.
|
// Another API error occurred.
|
||||||
error!("Failed to get resource '{}': {}", name, e);
|
error!("Failed to get resource '{name}': {e}");
|
||||||
Err(e)
|
Err(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -432,7 +447,7 @@ impl K8sClient {
|
|||||||
where
|
where
|
||||||
K: Resource + Clone + std::fmt::Debug + DeserializeOwned + serde::Serialize,
|
K: Resource + Clone + std::fmt::Debug + DeserializeOwned + serde::Serialize,
|
||||||
<K as Resource>::Scope: ApplyStrategy<K>,
|
<K as Resource>::Scope: ApplyStrategy<K>,
|
||||||
<K as kube::Resource>::DynamicType: Default,
|
<K as Resource>::DynamicType: Default,
|
||||||
{
|
{
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
for r in resource.iter() {
|
for r in resource.iter() {
|
||||||
@@ -497,10 +512,7 @@ impl K8sClient {
|
|||||||
|
|
||||||
// 6. Apply the object to the cluster using Server-Side Apply.
|
// 6. Apply the object to the cluster using Server-Side Apply.
|
||||||
// This will create the resource if it doesn't exist, or update it if it does.
|
// This will create the resource if it doesn't exist, or update it if it does.
|
||||||
println!(
|
println!("Applying '{name}' in namespace '{namespace}'...",);
|
||||||
"Applying Argo Application '{}' in namespace '{}'...",
|
|
||||||
name, namespace
|
|
||||||
);
|
|
||||||
let patch_params = PatchParams::apply("harmony"); // Use a unique field manager name
|
let patch_params = PatchParams::apply("harmony"); // Use a unique field manager name
|
||||||
let result = api.patch(name, &patch_params, &Patch::Apply(&obj)).await?;
|
let result = api.patch(name, &patch_params, &Patch::Apply(&obj)).await?;
|
||||||
|
|
||||||
@@ -509,7 +521,103 @@ impl K8sClient {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn from_kubeconfig(path: &str) -> Option<K8sClient> {
|
/// Apply a resource from a URL
|
||||||
|
///
|
||||||
|
/// It is the equivalent of `kubectl apply -f <url>`
|
||||||
|
pub async fn apply_url(&self, url: Url, ns: Option<&str>) -> Result<(), Error> {
|
||||||
|
let patch_params = PatchParams::apply("harmony");
|
||||||
|
let discovery = kube::Discovery::new(self.client.clone()).run().await?;
|
||||||
|
|
||||||
|
let yaml = reqwest::get(url)
|
||||||
|
.await
|
||||||
|
.expect("Could not get URL")
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.expect("Could not get content from URL");
|
||||||
|
|
||||||
|
for doc in multidoc_deserialize(&yaml).expect("failed to parse YAML from file") {
|
||||||
|
let obj: DynamicObject =
|
||||||
|
serde_yaml::from_value(doc).expect("cannot apply without valid YAML");
|
||||||
|
let namespace = obj.metadata.namespace.as_deref().or(ns);
|
||||||
|
let type_meta = obj
|
||||||
|
.types
|
||||||
|
.as_ref()
|
||||||
|
.expect("cannot apply object without valid TypeMeta");
|
||||||
|
let gvk = GroupVersionKind::try_from(type_meta)
|
||||||
|
.expect("cannot apply object without valid GroupVersionKind");
|
||||||
|
let name = obj.name_any();
|
||||||
|
|
||||||
|
if let Some((ar, caps)) = discovery.resolve_gvk(&gvk) {
|
||||||
|
let api = get_dynamic_api(ar, caps, self.client.clone(), namespace, false);
|
||||||
|
trace!(
|
||||||
|
"Applying {}: \n{}",
|
||||||
|
gvk.kind,
|
||||||
|
serde_yaml::to_string(&obj).expect("Failed to serialize YAML")
|
||||||
|
);
|
||||||
|
let data: serde_json::Value =
|
||||||
|
serde_json::to_value(&obj).expect("Failed to serialize JSON");
|
||||||
|
let _r = api.patch(&name, &patch_params, &Patch::Apply(data)).await?;
|
||||||
|
debug!("applied {} {}", gvk.kind, name);
|
||||||
|
} else {
|
||||||
|
warn!("Cannot apply document for unknown {gvk:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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) => {
|
||||||
@@ -528,6 +636,31 @@ impl K8sClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_dynamic_api(
|
||||||
|
resource: ApiResource,
|
||||||
|
capabilities: ApiCapabilities,
|
||||||
|
client: Client,
|
||||||
|
ns: Option<&str>,
|
||||||
|
all: bool,
|
||||||
|
) -> Api<DynamicObject> {
|
||||||
|
if capabilities.scope == Scope::Cluster || all {
|
||||||
|
Api::all_with(client, &resource)
|
||||||
|
} else if let Some(namespace) = ns {
|
||||||
|
Api::namespaced_with(client, namespace, &resource)
|
||||||
|
} else {
|
||||||
|
Api::default_namespaced_with(client, &resource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn multidoc_deserialize(data: &str) -> Result<Vec<serde_yaml::Value>, serde_yaml::Error> {
|
||||||
|
use serde::Deserialize;
|
||||||
|
let mut docs = vec![];
|
||||||
|
for de in serde_yaml::Deserializer::from_str(data) {
|
||||||
|
docs.push(serde_yaml::Value::deserialize(de)?);
|
||||||
|
}
|
||||||
|
Ok(docs)
|
||||||
|
}
|
||||||
|
|
||||||
pub trait ApplyStrategy<K: Resource> {
|
pub trait ApplyStrategy<K: Resource> {
|
||||||
fn get_api(client: &Client, ns: Option<&str>) -> Api<K>;
|
fn get_api(client: &Client, ns: Option<&str>) -> Api<K>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
use std::{process::Command, sync::Arc};
|
use std::{collections::BTreeMap, process::Command, sync::Arc, time::Duration};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use kube::api::GroupVersionKind;
|
use base64::{Engine, engine::general_purpose};
|
||||||
|
use k8s_openapi::api::{
|
||||||
|
core::v1::Secret,
|
||||||
|
rbac::v1::{ClusterRoleBinding, RoleRef, Subject},
|
||||||
|
};
|
||||||
|
use kube::api::{DynamicObject, GroupVersionKind, ObjectMeta};
|
||||||
use log::{debug, info, warn};
|
use log::{debug, info, warn};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tokio::sync::OnceCell;
|
use tokio::sync::OnceCell;
|
||||||
@@ -12,14 +17,26 @@ use crate::{
|
|||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
modules::{
|
modules::{
|
||||||
k3d::K3DInstallationScore,
|
k3d::K3DInstallationScore,
|
||||||
monitoring::kube_prometheus::crd::{
|
k8s::ingress::{K8sIngressScore, PathType},
|
||||||
crd_alertmanager_config::CRDPrometheus,
|
monitoring::{
|
||||||
prometheus_operator::prometheus_operator_helm_chart_score,
|
grafana::{grafana::Grafana, helm::helm_grafana::grafana_helm_chart_score},
|
||||||
rhob_alertmanager_config::RHOBObservability,
|
kube_prometheus::crd::{
|
||||||
|
crd_alertmanager_config::CRDPrometheus,
|
||||||
|
crd_grafana::{
|
||||||
|
Grafana as GrafanaCRD, GrafanaCom, GrafanaDashboard,
|
||||||
|
GrafanaDashboardDatasource, GrafanaDashboardSpec, GrafanaDatasource,
|
||||||
|
GrafanaDatasourceConfig, GrafanaDatasourceJsonData,
|
||||||
|
GrafanaDatasourceSecureJsonData, GrafanaDatasourceSpec, GrafanaSpec,
|
||||||
|
},
|
||||||
|
crd_prometheuses::LabelSelector,
|
||||||
|
prometheus_operator::prometheus_operator_helm_chart_score,
|
||||||
|
rhob_alertmanager_config::RHOBObservability,
|
||||||
|
service_monitor::ServiceMonitor,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
prometheus::{
|
prometheus::{
|
||||||
k8s_prometheus_alerting_score::K8sPrometheusCRDAlertingScore,
|
k8s_prometheus_alerting_score::K8sPrometheusCRDAlertingScore,
|
||||||
prometheus::PrometheusApplicationMonitoring, rhob_alerting_score::RHOBAlertingScore,
|
prometheus::PrometheusMonitoring, rhob_alerting_score::RHOBAlertingScore,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
score::Score,
|
score::Score,
|
||||||
@@ -86,41 +103,172 @@ impl K8sclient for K8sAnywhereTopology {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl PrometheusApplicationMonitoring<CRDPrometheus> for K8sAnywhereTopology {
|
impl Grafana for K8sAnywhereTopology {
|
||||||
|
async fn ensure_grafana_operator(
|
||||||
|
&self,
|
||||||
|
inventory: &Inventory,
|
||||||
|
) -> Result<PreparationOutcome, PreparationError> {
|
||||||
|
debug!("ensure grafana operator");
|
||||||
|
let client = self.k8s_client().await.unwrap();
|
||||||
|
let grafana_gvk = GroupVersionKind {
|
||||||
|
group: "grafana.integreatly.org".to_string(),
|
||||||
|
version: "v1beta1".to_string(),
|
||||||
|
kind: "Grafana".to_string(),
|
||||||
|
};
|
||||||
|
let name = "grafanas.grafana.integreatly.org";
|
||||||
|
let ns = "grafana";
|
||||||
|
|
||||||
|
let grafana_crd = client
|
||||||
|
.get_resource_json_value(name, Some(ns), &grafana_gvk)
|
||||||
|
.await;
|
||||||
|
match grafana_crd {
|
||||||
|
Ok(_) => {
|
||||||
|
return Ok(PreparationOutcome::Success {
|
||||||
|
details: "Found grafana CRDs in cluster".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(_) => {
|
||||||
|
return self
|
||||||
|
.install_grafana_operator(inventory, Some("grafana"))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async fn install_grafana(&self) -> Result<PreparationOutcome, PreparationError> {
|
||||||
|
let ns = "grafana";
|
||||||
|
|
||||||
|
let mut label = BTreeMap::new();
|
||||||
|
|
||||||
|
label.insert("dashboards".to_string(), "grafana".to_string());
|
||||||
|
|
||||||
|
let label_selector = LabelSelector {
|
||||||
|
match_labels: label.clone(),
|
||||||
|
match_expressions: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let client = self.k8s_client().await?;
|
||||||
|
|
||||||
|
let grafana = self.build_grafana(ns, &label);
|
||||||
|
|
||||||
|
client.apply(&grafana, Some(ns)).await?;
|
||||||
|
//TODO change this to a ensure ready or something better than just a timeout
|
||||||
|
client
|
||||||
|
.wait_until_deployment_ready(
|
||||||
|
"grafana-grafana-deployment",
|
||||||
|
Some("grafana"),
|
||||||
|
Some(Duration::from_secs(30)),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let sa_name = "grafana-grafana-sa";
|
||||||
|
let token_secret_name = "grafana-sa-token-secret";
|
||||||
|
|
||||||
|
let sa_token_secret = self.build_sa_token_secret(token_secret_name, sa_name, ns);
|
||||||
|
|
||||||
|
client.apply(&sa_token_secret, Some(ns)).await?;
|
||||||
|
let secret_gvk = GroupVersionKind {
|
||||||
|
group: "".to_string(),
|
||||||
|
version: "v1".to_string(),
|
||||||
|
kind: "Secret".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let secret = client
|
||||||
|
.get_resource_json_value(token_secret_name, Some(ns), &secret_gvk)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let token = format!(
|
||||||
|
"Bearer {}",
|
||||||
|
self.extract_and_normalize_token(&secret).unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
debug!("creating grafana clusterrole binding");
|
||||||
|
|
||||||
|
let clusterrolebinding =
|
||||||
|
self.build_cluster_rolebinding(sa_name, "cluster-monitoring-view", ns);
|
||||||
|
|
||||||
|
client.apply(&clusterrolebinding, Some(ns)).await?;
|
||||||
|
|
||||||
|
debug!("creating grafana datasource crd");
|
||||||
|
|
||||||
|
let thanos_url = format!(
|
||||||
|
"https://{}",
|
||||||
|
self.get_domain("thanos-querier-openshift-monitoring")
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
let thanos_openshift_datasource = self.build_grafana_datasource(
|
||||||
|
"thanos-openshift-monitoring",
|
||||||
|
ns,
|
||||||
|
&label_selector,
|
||||||
|
&thanos_url,
|
||||||
|
&token,
|
||||||
|
);
|
||||||
|
|
||||||
|
client.apply(&thanos_openshift_datasource, Some(ns)).await?;
|
||||||
|
|
||||||
|
debug!("creating grafana dashboard crd");
|
||||||
|
let dashboard = self.build_grafana_dashboard(ns, &label_selector);
|
||||||
|
|
||||||
|
client.apply(&dashboard, Some(ns)).await?;
|
||||||
|
debug!("creating grafana ingress");
|
||||||
|
let grafana_ingress = self.build_grafana_ingress(ns).await;
|
||||||
|
|
||||||
|
grafana_ingress
|
||||||
|
.interpret(&Inventory::empty(), self)
|
||||||
|
.await
|
||||||
|
.map_err(|e| PreparationError::new(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(PreparationOutcome::Success {
|
||||||
|
details: "Installed grafana composants".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl PrometheusMonitoring<CRDPrometheus> for K8sAnywhereTopology {
|
||||||
async fn install_prometheus(
|
async fn install_prometheus(
|
||||||
&self,
|
&self,
|
||||||
sender: &CRDPrometheus,
|
sender: &CRDPrometheus,
|
||||||
inventory: &Inventory,
|
_inventory: &Inventory,
|
||||||
receivers: Option<Vec<Box<dyn AlertReceiver<CRDPrometheus>>>>,
|
_receivers: Option<Vec<Box<dyn AlertReceiver<CRDPrometheus>>>>,
|
||||||
|
) -> Result<PreparationOutcome, PreparationError> {
|
||||||
|
let client = self.k8s_client().await?;
|
||||||
|
|
||||||
|
for monitor in sender.service_monitor.iter() {
|
||||||
|
client
|
||||||
|
.apply(monitor, Some(&sender.namespace))
|
||||||
|
.await
|
||||||
|
.map_err(|e| PreparationError::new(e.to_string()))?;
|
||||||
|
}
|
||||||
|
Ok(PreparationOutcome::Success {
|
||||||
|
details: "successfuly installed prometheus components".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn ensure_prometheus_operator(
|
||||||
|
&self,
|
||||||
|
sender: &CRDPrometheus,
|
||||||
|
_inventory: &Inventory,
|
||||||
) -> Result<PreparationOutcome, PreparationError> {
|
) -> Result<PreparationOutcome, PreparationError> {
|
||||||
let po_result = self.ensure_prometheus_operator(sender).await?;
|
let po_result = self.ensure_prometheus_operator(sender).await?;
|
||||||
|
|
||||||
if po_result == PreparationOutcome::Noop {
|
match po_result {
|
||||||
debug!("Skipping Prometheus CR installation due to missing operator.");
|
PreparationOutcome::Success { details: _ } => {
|
||||||
return Ok(po_result);
|
debug!("Detected prometheus crds operator present in cluster.");
|
||||||
}
|
return Ok(po_result);
|
||||||
|
}
|
||||||
let result = self
|
PreparationOutcome::Noop => {
|
||||||
.get_k8s_prometheus_application_score(sender.clone(), receivers)
|
debug!("Skipping Prometheus CR installation due to missing operator.");
|
||||||
.await
|
return Ok(po_result);
|
||||||
.interpret(inventory, self)
|
}
|
||||||
.await;
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(outcome) => match outcome.status {
|
|
||||||
InterpretStatus::SUCCESS => Ok(PreparationOutcome::Success {
|
|
||||||
details: outcome.message,
|
|
||||||
}),
|
|
||||||
InterpretStatus::NOOP => Ok(PreparationOutcome::Noop),
|
|
||||||
_ => Err(PreparationError::new(outcome.message)),
|
|
||||||
},
|
|
||||||
Err(err) => Err(PreparationError::new(err.to_string())),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl PrometheusApplicationMonitoring<RHOBObservability> for K8sAnywhereTopology {
|
impl PrometheusMonitoring<RHOBObservability> for K8sAnywhereTopology {
|
||||||
async fn install_prometheus(
|
async fn install_prometheus(
|
||||||
&self,
|
&self,
|
||||||
sender: &RHOBObservability,
|
sender: &RHOBObservability,
|
||||||
@@ -154,6 +302,14 @@ impl PrometheusApplicationMonitoring<RHOBObservability> for K8sAnywhereTopology
|
|||||||
Err(err) => Err(PreparationError::new(err.to_string())),
|
Err(err) => Err(PreparationError::new(err.to_string())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn ensure_prometheus_operator(
|
||||||
|
&self,
|
||||||
|
sender: &RHOBObservability,
|
||||||
|
inventory: &Inventory,
|
||||||
|
) -> Result<PreparationOutcome, PreparationError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Serialize for K8sAnywhereTopology {
|
impl Serialize for K8sAnywhereTopology {
|
||||||
@@ -215,6 +371,180 @@ impl K8sAnywhereTopology {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn extract_and_normalize_token(&self, secret: &DynamicObject) -> Option<String> {
|
||||||
|
let token_b64 = secret
|
||||||
|
.data
|
||||||
|
.get("token")
|
||||||
|
.or_else(|| secret.data.get("data").and_then(|d| d.get("token")))
|
||||||
|
.and_then(|v| v.as_str())?;
|
||||||
|
|
||||||
|
let bytes = general_purpose::STANDARD.decode(token_b64).ok()?;
|
||||||
|
|
||||||
|
let s = String::from_utf8(bytes).ok()?;
|
||||||
|
|
||||||
|
let cleaned = s
|
||||||
|
.trim_matches(|c: char| c.is_whitespace() || c == '\0')
|
||||||
|
.to_string();
|
||||||
|
Some(cleaned)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_cluster_rolebinding(
|
||||||
|
&self,
|
||||||
|
service_account_name: &str,
|
||||||
|
clusterrole_name: &str,
|
||||||
|
ns: &str,
|
||||||
|
) -> ClusterRoleBinding {
|
||||||
|
ClusterRoleBinding {
|
||||||
|
metadata: ObjectMeta {
|
||||||
|
name: Some(format!("{}-view-binding", service_account_name)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
role_ref: RoleRef {
|
||||||
|
api_group: "rbac.authorization.k8s.io".into(),
|
||||||
|
kind: "ClusterRole".into(),
|
||||||
|
name: clusterrole_name.into(),
|
||||||
|
},
|
||||||
|
subjects: Some(vec![Subject {
|
||||||
|
kind: "ServiceAccount".into(),
|
||||||
|
name: service_account_name.into(),
|
||||||
|
namespace: Some(ns.into()),
|
||||||
|
..Default::default()
|
||||||
|
}]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_sa_token_secret(
|
||||||
|
&self,
|
||||||
|
secret_name: &str,
|
||||||
|
service_account_name: &str,
|
||||||
|
ns: &str,
|
||||||
|
) -> Secret {
|
||||||
|
let mut annotations = BTreeMap::new();
|
||||||
|
annotations.insert(
|
||||||
|
"kubernetes.io/service-account.name".to_string(),
|
||||||
|
service_account_name.to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Secret {
|
||||||
|
metadata: ObjectMeta {
|
||||||
|
name: Some(secret_name.into()),
|
||||||
|
namespace: Some(ns.into()),
|
||||||
|
annotations: Some(annotations),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
type_: Some("kubernetes.io/service-account-token".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_grafana_datasource(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
ns: &str,
|
||||||
|
label_selector: &LabelSelector,
|
||||||
|
url: &str,
|
||||||
|
token: &str,
|
||||||
|
) -> GrafanaDatasource {
|
||||||
|
let mut json_data = BTreeMap::new();
|
||||||
|
json_data.insert("timeInterval".to_string(), "5s".to_string());
|
||||||
|
|
||||||
|
GrafanaDatasource {
|
||||||
|
metadata: ObjectMeta {
|
||||||
|
name: Some(name.to_string()),
|
||||||
|
namespace: Some(ns.to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
spec: GrafanaDatasourceSpec {
|
||||||
|
instance_selector: label_selector.clone(),
|
||||||
|
allow_cross_namespace_import: Some(true),
|
||||||
|
values_from: None,
|
||||||
|
datasource: GrafanaDatasourceConfig {
|
||||||
|
access: "proxy".to_string(),
|
||||||
|
name: name.to_string(),
|
||||||
|
r#type: "prometheus".to_string(),
|
||||||
|
url: url.to_string(),
|
||||||
|
database: None,
|
||||||
|
json_data: Some(GrafanaDatasourceJsonData {
|
||||||
|
time_interval: Some("60s".to_string()),
|
||||||
|
http_header_name1: Some("Authorization".to_string()),
|
||||||
|
tls_skip_verify: Some(true),
|
||||||
|
oauth_pass_thru: Some(true),
|
||||||
|
}),
|
||||||
|
secure_json_data: Some(GrafanaDatasourceSecureJsonData {
|
||||||
|
http_header_value1: Some(format!("Bearer {token}")),
|
||||||
|
}),
|
||||||
|
is_default: Some(false),
|
||||||
|
editable: Some(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_grafana_dashboard(
|
||||||
|
&self,
|
||||||
|
ns: &str,
|
||||||
|
label_selector: &LabelSelector,
|
||||||
|
) -> GrafanaDashboard {
|
||||||
|
let graf_dashboard = GrafanaDashboard {
|
||||||
|
metadata: ObjectMeta {
|
||||||
|
name: Some(format!("grafana-dashboard-{}", ns)),
|
||||||
|
namespace: Some(ns.to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
spec: GrafanaDashboardSpec {
|
||||||
|
resync_period: Some("30s".to_string()),
|
||||||
|
instance_selector: label_selector.clone(),
|
||||||
|
datasources: Some(vec![GrafanaDashboardDatasource {
|
||||||
|
input_name: "DS_PROMETHEUS".to_string(),
|
||||||
|
datasource_name: "thanos-openshift-monitoring".to_string(),
|
||||||
|
}]),
|
||||||
|
json: None,
|
||||||
|
grafana_com: Some(GrafanaCom {
|
||||||
|
id: 17406,
|
||||||
|
revision: None,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
graf_dashboard
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_grafana(&self, ns: &str, labels: &BTreeMap<String, String>) -> GrafanaCRD {
|
||||||
|
let grafana = GrafanaCRD {
|
||||||
|
metadata: ObjectMeta {
|
||||||
|
name: Some(format!("grafana-{}", ns)),
|
||||||
|
namespace: Some(ns.to_string()),
|
||||||
|
labels: Some(labels.clone()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
spec: GrafanaSpec {
|
||||||
|
config: None,
|
||||||
|
admin_user: None,
|
||||||
|
admin_password: None,
|
||||||
|
ingress: None,
|
||||||
|
persistence: None,
|
||||||
|
resources: None,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
grafana
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn build_grafana_ingress(&self, ns: &str) -> K8sIngressScore {
|
||||||
|
let domain = self.get_domain(&format!("grafana-{}", ns)).await.unwrap();
|
||||||
|
let name = format!("{}-grafana", ns);
|
||||||
|
let backend_service = format!("grafana-{}-service", ns);
|
||||||
|
|
||||||
|
K8sIngressScore {
|
||||||
|
name: fqdn::fqdn!(&name),
|
||||||
|
host: fqdn::fqdn!(&domain),
|
||||||
|
backend_service: fqdn::fqdn!(&backend_service),
|
||||||
|
port: 3000,
|
||||||
|
path: Some("/".to_string()),
|
||||||
|
path_type: Some(PathType::Prefix),
|
||||||
|
namespace: Some(fqdn::fqdn!(&ns)),
|
||||||
|
ingress_class_name: Some("openshift-default".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn get_cluster_observability_operator_prometheus_application_score(
|
async fn get_cluster_observability_operator_prometheus_application_score(
|
||||||
&self,
|
&self,
|
||||||
sender: RHOBObservability,
|
sender: RHOBObservability,
|
||||||
@@ -232,13 +562,14 @@ impl K8sAnywhereTopology {
|
|||||||
&self,
|
&self,
|
||||||
sender: CRDPrometheus,
|
sender: CRDPrometheus,
|
||||||
receivers: Option<Vec<Box<dyn AlertReceiver<CRDPrometheus>>>>,
|
receivers: Option<Vec<Box<dyn AlertReceiver<CRDPrometheus>>>>,
|
||||||
|
service_monitors: Option<Vec<ServiceMonitor>>,
|
||||||
) -> K8sPrometheusCRDAlertingScore {
|
) -> K8sPrometheusCRDAlertingScore {
|
||||||
K8sPrometheusCRDAlertingScore {
|
return K8sPrometheusCRDAlertingScore {
|
||||||
sender,
|
sender,
|
||||||
receivers: receivers.unwrap_or_default(),
|
receivers: receivers.unwrap_or_default(),
|
||||||
service_monitors: vec![],
|
service_monitors: service_monitors.unwrap_or_default(),
|
||||||
prometheus_rules: vec![],
|
prometheus_rules: vec![],
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn openshift_ingress_operator_available(&self) -> Result<(), PreparationError> {
|
async fn openshift_ingress_operator_available(&self) -> Result<(), PreparationError> {
|
||||||
@@ -506,6 +837,30 @@ impl K8sAnywhereTopology {
|
|||||||
details: "prometheus operator present in cluster".into(),
|
details: "prometheus operator present in cluster".into(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn install_grafana_operator(
|
||||||
|
&self,
|
||||||
|
inventory: &Inventory,
|
||||||
|
ns: Option<&str>,
|
||||||
|
) -> Result<PreparationOutcome, PreparationError> {
|
||||||
|
let namespace = ns.unwrap_or("grafana");
|
||||||
|
info!("installing grafana operator in ns {namespace}");
|
||||||
|
let tenant = self.get_k8s_tenant_manager()?.get_tenant_config().await;
|
||||||
|
let mut namespace_scope = false;
|
||||||
|
if tenant.is_some() {
|
||||||
|
namespace_scope = true;
|
||||||
|
}
|
||||||
|
let _grafana_operator_score = grafana_helm_chart_score(namespace, namespace_scope)
|
||||||
|
.interpret(inventory, self)
|
||||||
|
.await
|
||||||
|
.map_err(|e| PreparationError::new(e.to_string()));
|
||||||
|
Ok(PreparationOutcome::Success {
|
||||||
|
details: format!(
|
||||||
|
"Successfully installed grafana operator in ns {}",
|
||||||
|
ns.unwrap()
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|||||||
@@ -9,12 +9,13 @@ use std::{
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use derive_new::new;
|
use derive_new::new;
|
||||||
use harmony_types::{
|
use harmony_types::{
|
||||||
|
id::Id,
|
||||||
net::{IpAddress, MacAddress},
|
net::{IpAddress, MacAddress},
|
||||||
switch::PortLocation,
|
switch::PortLocation,
|
||||||
};
|
};
|
||||||
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};
|
||||||
|
|
||||||
@@ -182,6 +183,37 @@ 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[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>;
|
||||||
@@ -191,15 +223,12 @@ pub trait Switch: Send + Sync {
|
|||||||
mac_address: &MacAddress,
|
mac_address: &MacAddress,
|
||||||
) -> Result<Option<PortLocation>, SwitchError>;
|
) -> Result<Option<PortLocation>, SwitchError>;
|
||||||
|
|
||||||
async fn configure_host_network(
|
async fn configure_port_channel(&self, config: &HostNetworkConfig) -> Result<(), SwitchError>;
|
||||||
&self,
|
|
||||||
host: &PhysicalHost,
|
|
||||||
config: HostNetworkConfig,
|
|
||||||
) -> Result<(), SwitchError>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct HostNetworkConfig {
|
pub struct HostNetworkConfig {
|
||||||
|
pub host_id: Id,
|
||||||
pub switch_ports: Vec<SwitchPort>,
|
pub switch_ports: Vec<SwitchPort>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ impl<S: AlertSender + Installable<T>, T: Topology> Interpret<T> for AlertingInte
|
|||||||
inventory: &Inventory,
|
inventory: &Inventory,
|
||||||
topology: &T,
|
topology: &T,
|
||||||
) -> Result<Outcome, InterpretError> {
|
) -> Result<Outcome, InterpretError> {
|
||||||
|
debug!("hit sender configure for AlertingInterpret");
|
||||||
self.sender.configure(inventory, topology).await?;
|
self.sender.configure(inventory, topology).await?;
|
||||||
for receiver in self.receivers.iter() {
|
for receiver in self.receivers.iter() {
|
||||||
receiver.install(&self.sender).await?;
|
receiver.install(&self.sender).await?;
|
||||||
@@ -86,4 +87,5 @@ pub trait AlertRule<S: AlertSender>: std::fmt::Debug + Send + Sync {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait ScrapeTarget<S: AlertSender>: std::fmt::Debug + Send + Sync {
|
pub trait ScrapeTarget<S: AlertSender>: std::fmt::Debug + Send + Sync {
|
||||||
async fn install(&self, sender: &S) -> Result<Outcome, InterpretError>;
|
async fn install(&self, sender: &S) -> Result<Outcome, InterpretError>;
|
||||||
|
fn clone_box(&self) -> Box<dyn ScrapeTarget<S>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ pub struct InventoryRepositoryFactory;
|
|||||||
impl InventoryRepositoryFactory {
|
impl InventoryRepositoryFactory {
|
||||||
pub async fn build() -> Result<Box<dyn InventoryRepository>, RepoError> {
|
pub async fn build() -> Result<Box<dyn InventoryRepository>, RepoError> {
|
||||||
Ok(Box::new(
|
Ok(Box::new(
|
||||||
SqliteInventoryRepository::new(&(*DATABASE_URL)).await?,
|
SqliteInventoryRepository::new(&DATABASE_URL).await?,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
182
harmony/src/infra/kube.rs
Normal file
182
harmony/src/infra/kube.rs
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
use k8s_openapi::Resource as K8sResource;
|
||||||
|
use kube::api::{ApiResource, DynamicObject, GroupVersionKind};
|
||||||
|
use kube::core::TypeMeta;
|
||||||
|
use serde::Serialize;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
/// Convert a typed Kubernetes resource `K` into a `DynamicObject`.
|
||||||
|
///
|
||||||
|
/// Requirements:
|
||||||
|
/// - `K` must be a k8s_openapi resource (provides static GVK via `Resource`).
|
||||||
|
/// - `K` must have standard Kubernetes shape (metadata + payload fields).
|
||||||
|
///
|
||||||
|
/// Notes:
|
||||||
|
/// - We set `types` (apiVersion/kind) and copy `metadata`.
|
||||||
|
/// - We place the remaining top-level fields into `obj.data` as JSON.
|
||||||
|
/// - Scope is not encoded on the object itself; you still need the corresponding
|
||||||
|
/// `DynamicResource` (derived from K::group/version/kind) when constructing an Api.
|
||||||
|
///
|
||||||
|
/// Example usage:
|
||||||
|
/// let dyn_obj = kube_resource_to_dynamic(secret)?;
|
||||||
|
/// let api: Api<DynamicObject> = Api::namespaced_with(client, "ns", &dr);
|
||||||
|
/// api.patch(&dyn_obj.name_any(), &PatchParams::apply("mgr"), &Patch::Apply(dyn_obj)).await?;
|
||||||
|
pub fn kube_resource_to_dynamic<K>(res: &K) -> Result<DynamicObject, String>
|
||||||
|
where
|
||||||
|
K: K8sResource + Serialize + DeserializeOwned,
|
||||||
|
{
|
||||||
|
// Serialize the typed resource to JSON so we can split metadata and payload
|
||||||
|
let mut v = serde_json::to_value(res).map_err(|e| format!("Failed to serialize : {e}"))?;
|
||||||
|
let obj = v
|
||||||
|
.as_object_mut()
|
||||||
|
.ok_or_else(|| "expected object JSON".to_string())?;
|
||||||
|
|
||||||
|
// Extract and parse metadata into kube::core::ObjectMeta
|
||||||
|
let metadata_value = obj
|
||||||
|
.remove("metadata")
|
||||||
|
.ok_or_else(|| "missing metadata".to_string())?;
|
||||||
|
let metadata: kube::core::ObjectMeta = serde_json::from_value(metadata_value)
|
||||||
|
.map_err(|e| format!("Failed to deserialize : {e}"))?;
|
||||||
|
|
||||||
|
// Name is required for DynamicObject::new; prefer metadata.name
|
||||||
|
let name = metadata
|
||||||
|
.name
|
||||||
|
.clone()
|
||||||
|
.ok_or_else(|| "metadata.name is required".to_string())?;
|
||||||
|
|
||||||
|
// Remaining fields (spec/status/data/etc.) become the dynamic payload
|
||||||
|
let payload = Value::Object(obj.clone());
|
||||||
|
|
||||||
|
// Construct the DynamicObject
|
||||||
|
let mut dyn_obj = DynamicObject::new(
|
||||||
|
&name,
|
||||||
|
&ApiResource::from_gvk(&GroupVersionKind::gvk(K::GROUP, K::VERSION, K::KIND)),
|
||||||
|
);
|
||||||
|
dyn_obj.types = Some(TypeMeta {
|
||||||
|
api_version: api_version_for::<K>(),
|
||||||
|
kind: K::KIND.into(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Preserve namespace/labels/annotations/etc.
|
||||||
|
dyn_obj.metadata = metadata;
|
||||||
|
|
||||||
|
// Attach payload
|
||||||
|
dyn_obj.data = payload;
|
||||||
|
|
||||||
|
Ok(dyn_obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper: compute apiVersion string ("group/version" or "v1" for core).
|
||||||
|
fn api_version_for<K>() -> String
|
||||||
|
where
|
||||||
|
K: K8sResource,
|
||||||
|
{
|
||||||
|
let group = K::GROUP;
|
||||||
|
let version = K::VERSION;
|
||||||
|
if group.is_empty() {
|
||||||
|
version.to_string() // core/v1 => "v1"
|
||||||
|
} else {
|
||||||
|
format!("{}/{}", group, version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use k8s_openapi::api::{
|
||||||
|
apps::v1::{Deployment, DeploymentSpec},
|
||||||
|
core::v1::{PodTemplateSpec, Secret},
|
||||||
|
};
|
||||||
|
use kube::api::ObjectMeta;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn secret_to_dynamic_roundtrip() {
|
||||||
|
// Create a sample Secret resource
|
||||||
|
let mut secret = Secret {
|
||||||
|
metadata: ObjectMeta {
|
||||||
|
name: Some("my-secret".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
type_: Some("kubernetes.io/service-account-token".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert to DynamicResource
|
||||||
|
let dynamic: DynamicObject =
|
||||||
|
kube_resource_to_dynamic(&secret).expect("Failed to convert Secret to DynamicResource");
|
||||||
|
|
||||||
|
// Serialize both the original and dynamic resources to Value
|
||||||
|
let original_value = serde_json::to_value(&secret).expect("Failed to serialize Secret");
|
||||||
|
let dynamic_value =
|
||||||
|
serde_json::to_value(&dynamic).expect("Failed to serialize DynamicResource");
|
||||||
|
|
||||||
|
// Assert that they are identical
|
||||||
|
assert_eq!(original_value, dynamic_value);
|
||||||
|
|
||||||
|
secret.metadata.namespace = Some("false".to_string());
|
||||||
|
let modified_value = serde_json::to_value(&secret).expect("Failed to serialize Secret");
|
||||||
|
assert_ne!(modified_value, dynamic_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deployment_to_dynamic_roundtrip() {
|
||||||
|
// Create a sample Deployment with nested structures
|
||||||
|
let mut deployment = Deployment {
|
||||||
|
metadata: ObjectMeta {
|
||||||
|
name: Some("my-deployment".to_string()),
|
||||||
|
labels: Some({
|
||||||
|
let mut map = std::collections::BTreeMap::new();
|
||||||
|
map.insert("app".to_string(), "nginx".to_string());
|
||||||
|
map
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
spec: Some(DeploymentSpec {
|
||||||
|
replicas: Some(3),
|
||||||
|
selector: Default::default(),
|
||||||
|
template: PodTemplateSpec {
|
||||||
|
metadata: Some(ObjectMeta {
|
||||||
|
labels: Some({
|
||||||
|
let mut map = std::collections::BTreeMap::new();
|
||||||
|
map.insert("app".to_string(), "nginx".to_string());
|
||||||
|
map
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
spec: Some(Default::default()), // PodSpec with empty containers for simplicity
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let dynamic = kube_resource_to_dynamic(&deployment).expect("Failed to convert Deployment");
|
||||||
|
|
||||||
|
let original_value = serde_json::to_value(&deployment).unwrap();
|
||||||
|
let dynamic_value = serde_json::to_value(&dynamic).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(original_value, dynamic_value);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
dynamic.data.get("spec").unwrap().get("replicas").unwrap(),
|
||||||
|
3
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
dynamic
|
||||||
|
.data
|
||||||
|
.get("spec")
|
||||||
|
.unwrap()
|
||||||
|
.get("template")
|
||||||
|
.unwrap()
|
||||||
|
.get("metadata")
|
||||||
|
.unwrap()
|
||||||
|
.get("labels")
|
||||||
|
.unwrap()
|
||||||
|
.get("app")
|
||||||
|
.unwrap()
|
||||||
|
.as_str()
|
||||||
|
.unwrap(),
|
||||||
|
"nginx".to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,5 +3,7 @@ pub mod executors;
|
|||||||
pub mod hp_ilo;
|
pub mod hp_ilo;
|
||||||
pub mod intel_amt;
|
pub mod intel_amt;
|
||||||
pub mod inventory;
|
pub mod inventory;
|
||||||
|
pub mod kube;
|
||||||
|
pub mod network_manager;
|
||||||
pub mod opnsense;
|
pub mod opnsense;
|
||||||
mod sqlx;
|
mod sqlx;
|
||||||
|
|||||||
257
harmony/src/infra/network_manager.rs
Normal file
257
harmony/src/infra/network_manager.rs
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
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(),
|
||||||
|
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!("HARMONY - 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}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,11 @@ use crate::modules::application::{
|
|||||||
Application, ApplicationFeature, InstallationError, InstallationOutcome,
|
Application, ApplicationFeature, InstallationError, InstallationOutcome,
|
||||||
};
|
};
|
||||||
use crate::modules::monitoring::application_monitoring::application_monitoring_score::ApplicationMonitoringScore;
|
use crate::modules::monitoring::application_monitoring::application_monitoring_score::ApplicationMonitoringScore;
|
||||||
|
use crate::modules::monitoring::grafana::grafana::Grafana;
|
||||||
use crate::modules::monitoring::kube_prometheus::crd::crd_alertmanager_config::CRDPrometheus;
|
use crate::modules::monitoring::kube_prometheus::crd::crd_alertmanager_config::CRDPrometheus;
|
||||||
|
use crate::modules::monitoring::kube_prometheus::crd::service_monitor::{
|
||||||
|
ServiceMonitor, ServiceMonitorSpec,
|
||||||
|
};
|
||||||
use crate::topology::MultiTargetTopology;
|
use crate::topology::MultiTargetTopology;
|
||||||
use crate::topology::ingress::Ingress;
|
use crate::topology::ingress::Ingress;
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -14,7 +18,7 @@ use crate::{
|
|||||||
topology::{HelmCommand, K8sclient, Topology, tenant::TenantManager},
|
topology::{HelmCommand, K8sclient, Topology, tenant::TenantManager},
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
modules::prometheus::prometheus::PrometheusApplicationMonitoring,
|
modules::prometheus::prometheus::PrometheusMonitoring,
|
||||||
topology::oberservability::monitoring::AlertReceiver,
|
topology::oberservability::monitoring::AlertReceiver,
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
@@ -22,6 +26,7 @@ use base64::{Engine as _, engine::general_purpose};
|
|||||||
use harmony_secret::SecretManager;
|
use harmony_secret::SecretManager;
|
||||||
use harmony_secret_derive::Secret;
|
use harmony_secret_derive::Secret;
|
||||||
use harmony_types::net::Url;
|
use harmony_types::net::Url;
|
||||||
|
use kube::api::ObjectMeta;
|
||||||
use log::{debug, info};
|
use log::{debug, info};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -40,7 +45,8 @@ impl<
|
|||||||
+ TenantManager
|
+ TenantManager
|
||||||
+ K8sclient
|
+ K8sclient
|
||||||
+ MultiTargetTopology
|
+ MultiTargetTopology
|
||||||
+ PrometheusApplicationMonitoring<CRDPrometheus>
|
+ PrometheusMonitoring<CRDPrometheus>
|
||||||
|
+ Grafana
|
||||||
+ Ingress
|
+ Ingress
|
||||||
+ std::fmt::Debug,
|
+ std::fmt::Debug,
|
||||||
> ApplicationFeature<T> for Monitoring
|
> ApplicationFeature<T> for Monitoring
|
||||||
@@ -57,10 +63,20 @@ impl<
|
|||||||
.unwrap_or_else(|| self.application.name());
|
.unwrap_or_else(|| self.application.name());
|
||||||
let domain = topology.get_domain("ntfy").await.unwrap();
|
let domain = topology.get_domain("ntfy").await.unwrap();
|
||||||
|
|
||||||
|
let app_service_monitor = ServiceMonitor {
|
||||||
|
metadata: ObjectMeta {
|
||||||
|
name: Some(self.application.name()),
|
||||||
|
namespace: Some(namespace.clone()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
spec: ServiceMonitorSpec::default(),
|
||||||
|
};
|
||||||
|
|
||||||
let mut alerting_score = ApplicationMonitoringScore {
|
let mut alerting_score = ApplicationMonitoringScore {
|
||||||
sender: CRDPrometheus {
|
sender: CRDPrometheus {
|
||||||
namespace: namespace.clone(),
|
namespace: namespace.clone(),
|
||||||
client: topology.k8s_client().await.unwrap(),
|
client: topology.k8s_client().await.unwrap(),
|
||||||
|
service_monitor: vec![app_service_monitor],
|
||||||
},
|
},
|
||||||
application: self.application.clone(),
|
application: self.application.clone(),
|
||||||
receivers: self.alert_receiver.clone(),
|
receivers: self.alert_receiver.clone(),
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use crate::{
|
|||||||
topology::{HelmCommand, K8sclient, Topology, tenant::TenantManager},
|
topology::{HelmCommand, K8sclient, Topology, tenant::TenantManager},
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
modules::prometheus::prometheus::PrometheusApplicationMonitoring,
|
modules::prometheus::prometheus::PrometheusMonitoring,
|
||||||
topology::oberservability::monitoring::AlertReceiver,
|
topology::oberservability::monitoring::AlertReceiver,
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
@@ -42,7 +42,7 @@ impl<
|
|||||||
+ MultiTargetTopology
|
+ MultiTargetTopology
|
||||||
+ Ingress
|
+ Ingress
|
||||||
+ std::fmt::Debug
|
+ std::fmt::Debug
|
||||||
+ PrometheusApplicationMonitoring<RHOBObservability>,
|
+ PrometheusMonitoring<RHOBObservability>,
|
||||||
> ApplicationFeature<T> for Monitoring
|
> ApplicationFeature<T> for Monitoring
|
||||||
{
|
{
|
||||||
async fn ensure_installed(
|
async fn ensure_installed(
|
||||||
|
|||||||
@@ -74,7 +74,11 @@ impl<T: Topology> Interpret<T> for DiscoverHostForRoleInterpret {
|
|||||||
|
|
||||||
match ans {
|
match ans {
|
||||||
Ok(choice) => {
|
Ok(choice) => {
|
||||||
info!("Selected {} as the bootstrap node.", choice.summary());
|
info!(
|
||||||
|
"Selected {} as the {:?} node.",
|
||||||
|
choice.summary(),
|
||||||
|
self.score.role
|
||||||
|
);
|
||||||
host_repo
|
host_repo
|
||||||
.save_role_mapping(&self.score.role, &choice)
|
.save_role_mapping(&self.score.role, &choice)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -90,10 +94,7 @@ 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()
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,13 +38,15 @@ impl<
|
|||||||
+ 'static
|
+ 'static
|
||||||
+ Send
|
+ Send
|
||||||
+ Clone,
|
+ Clone,
|
||||||
T: Topology,
|
T: Topology + K8sclient,
|
||||||
> Score<T> for K8sResourceScore<K>
|
> Score<T> for K8sResourceScore<K>
|
||||||
where
|
where
|
||||||
<K as kube::Resource>::DynamicType: Default,
|
<K as kube::Resource>::DynamicType: Default,
|
||||||
{
|
{
|
||||||
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||||
todo!()
|
Box::new(K8sResourceInterpret {
|
||||||
|
score: self.clone(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn name(&self) -> String {
|
fn name(&self) -> String {
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use log::debug;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
data::Version,
|
interpret::Interpret,
|
||||||
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
|
||||||
inventory::Inventory,
|
|
||||||
modules::{
|
modules::{
|
||||||
application::Application,
|
application::Application,
|
||||||
monitoring::kube_prometheus::crd::crd_alertmanager_config::CRDPrometheus,
|
monitoring::{
|
||||||
prometheus::prometheus::PrometheusApplicationMonitoring,
|
grafana::grafana::Grafana, kube_prometheus::crd::crd_alertmanager_config::CRDPrometheus,
|
||||||
|
},
|
||||||
|
prometheus::prometheus::PrometheusMonitoring,
|
||||||
},
|
},
|
||||||
score::Score,
|
score::Score,
|
||||||
topology::{PreparationOutcome, Topology, oberservability::monitoring::AlertReceiver},
|
topology::{
|
||||||
|
K8sclient, Topology,
|
||||||
|
oberservability::monitoring::{AlertReceiver, AlertingInterpret, ScrapeTarget},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use harmony_types::id::Id;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct ApplicationMonitoringScore {
|
pub struct ApplicationMonitoringScore {
|
||||||
@@ -24,12 +26,16 @@ pub struct ApplicationMonitoringScore {
|
|||||||
pub receivers: Vec<Box<dyn AlertReceiver<CRDPrometheus>>>,
|
pub receivers: Vec<Box<dyn AlertReceiver<CRDPrometheus>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Topology + PrometheusApplicationMonitoring<CRDPrometheus>> Score<T>
|
impl<T: Topology + PrometheusMonitoring<CRDPrometheus> + K8sclient + Grafana> Score<T>
|
||||||
for ApplicationMonitoringScore
|
for ApplicationMonitoringScore
|
||||||
{
|
{
|
||||||
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||||
Box::new(ApplicationMonitoringInterpret {
|
debug!("creating alerting interpret");
|
||||||
score: self.clone(),
|
Box::new(AlertingInterpret {
|
||||||
|
sender: self.sender.clone(),
|
||||||
|
receivers: self.receivers.clone(),
|
||||||
|
rules: vec![],
|
||||||
|
scrape_targets: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,55 +46,3 @@ impl<T: Topology + PrometheusApplicationMonitoring<CRDPrometheus>> Score<T>
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ApplicationMonitoringInterpret {
|
|
||||||
score: ApplicationMonitoringScore,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<T: Topology + PrometheusApplicationMonitoring<CRDPrometheus>> Interpret<T>
|
|
||||||
for ApplicationMonitoringInterpret
|
|
||||||
{
|
|
||||||
async fn execute(
|
|
||||||
&self,
|
|
||||||
inventory: &Inventory,
|
|
||||||
topology: &T,
|
|
||||||
) -> Result<Outcome, InterpretError> {
|
|
||||||
let result = topology
|
|
||||||
.install_prometheus(
|
|
||||||
&self.score.sender,
|
|
||||||
inventory,
|
|
||||||
Some(self.score.receivers.clone()),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(outcome) => match outcome {
|
|
||||||
PreparationOutcome::Success { details: _ } => {
|
|
||||||
Ok(Outcome::success("Prometheus installed".into()))
|
|
||||||
}
|
|
||||||
PreparationOutcome::Noop => {
|
|
||||||
Ok(Outcome::noop("Prometheus installation skipped".into()))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(err) => Err(InterpretError::from(err)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_name(&self) -> InterpretName {
|
|
||||||
InterpretName::ApplicationMonitoring
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_version(&self) -> Version {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_status(&self) -> InterpretStatus {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_children(&self) -> Vec<Id> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use crate::{
|
|||||||
monitoring::kube_prometheus::crd::{
|
monitoring::kube_prometheus::crd::{
|
||||||
crd_alertmanager_config::CRDPrometheus, rhob_alertmanager_config::RHOBObservability,
|
crd_alertmanager_config::CRDPrometheus, rhob_alertmanager_config::RHOBObservability,
|
||||||
},
|
},
|
||||||
prometheus::prometheus::PrometheusApplicationMonitoring,
|
prometheus::prometheus::PrometheusMonitoring,
|
||||||
},
|
},
|
||||||
score::Score,
|
score::Score,
|
||||||
topology::{PreparationOutcome, Topology, oberservability::monitoring::AlertReceiver},
|
topology::{PreparationOutcome, Topology, oberservability::monitoring::AlertReceiver},
|
||||||
@@ -26,7 +26,7 @@ pub struct ApplicationRHOBMonitoringScore {
|
|||||||
pub receivers: Vec<Box<dyn AlertReceiver<RHOBObservability>>>,
|
pub receivers: Vec<Box<dyn AlertReceiver<RHOBObservability>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Topology + PrometheusApplicationMonitoring<RHOBObservability>> Score<T>
|
impl<T: Topology + PrometheusMonitoring<RHOBObservability>> Score<T>
|
||||||
for ApplicationRHOBMonitoringScore
|
for ApplicationRHOBMonitoringScore
|
||||||
{
|
{
|
||||||
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||||
@@ -49,7 +49,7 @@ pub struct ApplicationRHOBMonitoringInterpret {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<T: Topology + PrometheusApplicationMonitoring<RHOBObservability>> Interpret<T>
|
impl<T: Topology + PrometheusMonitoring<RHOBObservability>> Interpret<T>
|
||||||
for ApplicationRHOBMonitoringInterpret
|
for ApplicationRHOBMonitoringInterpret
|
||||||
{
|
{
|
||||||
async fn execute(
|
async fn execute(
|
||||||
|
|||||||
17
harmony/src/modules/monitoring/grafana/grafana.rs
Normal file
17
harmony/src/modules/monitoring/grafana/grafana.rs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use k8s_openapi::Resource;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
inventory::Inventory,
|
||||||
|
topology::{PreparationError, PreparationOutcome},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait Grafana {
|
||||||
|
async fn ensure_grafana_operator(
|
||||||
|
&self,
|
||||||
|
inventory: &Inventory,
|
||||||
|
) -> Result<PreparationOutcome, PreparationError>;
|
||||||
|
|
||||||
|
async fn install_grafana(&self) -> Result<PreparationOutcome, PreparationError>;
|
||||||
|
}
|
||||||
@@ -1,27 +1,28 @@
|
|||||||
|
use harmony_macros::hurl;
|
||||||
use non_blank_string_rs::NonBlankString;
|
use non_blank_string_rs::NonBlankString;
|
||||||
use std::str::FromStr;
|
use std::{collections::HashMap, str::FromStr};
|
||||||
|
|
||||||
use crate::modules::helm::chart::HelmChartScore;
|
use crate::modules::helm::chart::{HelmChartScore, HelmRepository};
|
||||||
|
|
||||||
pub fn grafana_helm_chart_score(ns: &str) -> HelmChartScore {
|
|
||||||
let values = r#"
|
|
||||||
rbac:
|
|
||||||
namespaced: true
|
|
||||||
sidecar:
|
|
||||||
dashboards:
|
|
||||||
enabled: true
|
|
||||||
"#
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
|
pub fn grafana_helm_chart_score(ns: &str, namespace_scope: bool) -> HelmChartScore {
|
||||||
|
let mut values_overrides = HashMap::new();
|
||||||
|
values_overrides.insert(
|
||||||
|
NonBlankString::from_str("namespaceScope").unwrap(),
|
||||||
|
namespace_scope.to_string(),
|
||||||
|
);
|
||||||
HelmChartScore {
|
HelmChartScore {
|
||||||
namespace: Some(NonBlankString::from_str(ns).unwrap()),
|
namespace: Some(NonBlankString::from_str(ns).unwrap()),
|
||||||
release_name: NonBlankString::from_str("grafana").unwrap(),
|
release_name: NonBlankString::from_str("grafana-operator").unwrap(),
|
||||||
chart_name: NonBlankString::from_str("oci://ghcr.io/grafana/helm-charts/grafana").unwrap(),
|
chart_name: NonBlankString::from_str("grafana/grafana-operator").unwrap(),
|
||||||
chart_version: None,
|
chart_version: None,
|
||||||
values_overrides: None,
|
values_overrides: Some(values_overrides),
|
||||||
values_yaml: Some(values.to_string()),
|
values_yaml: None,
|
||||||
create_namespace: true,
|
create_namespace: true,
|
||||||
install_only: true,
|
install_only: true,
|
||||||
repository: None,
|
repository: Some(HelmRepository::new(
|
||||||
|
"grafana".to_string(),
|
||||||
|
hurl!("https://grafana.github.io/helm-charts"),
|
||||||
|
true,
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
|
pub mod grafana;
|
||||||
pub mod helm;
|
pub mod helm;
|
||||||
|
|||||||
@@ -1,12 +1,25 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
use kube::CustomResource;
|
use kube::CustomResource;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::topology::{
|
use crate::{
|
||||||
k8s::K8sClient,
|
interpret::{InterpretError, Outcome},
|
||||||
oberservability::monitoring::{AlertReceiver, AlertSender},
|
inventory::Inventory,
|
||||||
|
modules::{
|
||||||
|
monitoring::{
|
||||||
|
grafana::grafana::Grafana, kube_prometheus::crd::service_monitor::ServiceMonitor,
|
||||||
|
},
|
||||||
|
prometheus::prometheus::PrometheusMonitoring,
|
||||||
|
},
|
||||||
|
topology::{
|
||||||
|
K8sclient, Topology,
|
||||||
|
installable::Installable,
|
||||||
|
k8s::K8sClient,
|
||||||
|
oberservability::monitoring::{AlertReceiver, AlertSender, ScrapeTarget},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)]
|
#[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)]
|
||||||
@@ -26,6 +39,7 @@ pub struct AlertmanagerConfigSpec {
|
|||||||
pub struct CRDPrometheus {
|
pub struct CRDPrometheus {
|
||||||
pub namespace: String,
|
pub namespace: String,
|
||||||
pub client: Arc<K8sClient>,
|
pub client: Arc<K8sClient>,
|
||||||
|
pub service_monitor: Vec<ServiceMonitor>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AlertSender for CRDPrometheus {
|
impl AlertSender for CRDPrometheus {
|
||||||
@@ -40,6 +54,12 @@ impl Clone for Box<dyn AlertReceiver<CRDPrometheus>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Clone for Box<dyn ScrapeTarget<CRDPrometheus>> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
self.clone_box()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Serialize for Box<dyn AlertReceiver<CRDPrometheus>> {
|
impl Serialize for Box<dyn AlertReceiver<CRDPrometheus>> {
|
||||||
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
|
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
|
||||||
where
|
where
|
||||||
@@ -48,3 +68,24 @@ impl Serialize for Box<dyn AlertReceiver<CRDPrometheus>> {
|
|||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T: Topology + K8sclient + PrometheusMonitoring<CRDPrometheus> + Grafana> Installable<T>
|
||||||
|
for CRDPrometheus
|
||||||
|
{
|
||||||
|
async fn configure(&self, inventory: &Inventory, topology: &T) -> Result<(), InterpretError> {
|
||||||
|
topology.ensure_grafana_operator(inventory).await?;
|
||||||
|
topology.ensure_prometheus_operator(self, inventory).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn ensure_installed(
|
||||||
|
&self,
|
||||||
|
inventory: &Inventory,
|
||||||
|
topology: &T,
|
||||||
|
) -> Result<(), InterpretError> {
|
||||||
|
topology.install_grafana().await?;
|
||||||
|
topology.install_prometheus(&self, inventory, None).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -103,9 +103,34 @@ pub struct GrafanaDashboardSpec {
|
|||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub resync_period: Option<String>,
|
pub resync_period: Option<String>,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub datasources: Option<Vec<GrafanaDashboardDatasource>>,
|
||||||
|
|
||||||
pub instance_selector: LabelSelector,
|
pub instance_selector: LabelSelector,
|
||||||
|
|
||||||
pub json: String,
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub json: Option<String>,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub grafana_com: Option<GrafanaCom>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct GrafanaDashboardDatasource {
|
||||||
|
pub input_name: String,
|
||||||
|
pub datasource_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct GrafanaCom {
|
||||||
|
pub id: u32,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub revision: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------------------------
|
||||||
@@ -126,20 +151,79 @@ pub struct GrafanaDatasourceSpec {
|
|||||||
pub allow_cross_namespace_import: Option<bool>,
|
pub allow_cross_namespace_import: Option<bool>,
|
||||||
|
|
||||||
pub datasource: GrafanaDatasourceConfig,
|
pub datasource: GrafanaDatasourceConfig,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub values_from: Option<Vec<GrafanaValueFrom>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct GrafanaValueFrom {
|
||||||
|
pub target_path: String,
|
||||||
|
pub value_from: GrafanaValueSource,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct GrafanaValueSource {
|
||||||
|
pub secret_key_ref: GrafanaSecretKeyRef,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct GrafanaSecretKeyRef {
|
||||||
|
pub name: String,
|
||||||
|
pub key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
|
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct GrafanaDatasourceConfig {
|
pub struct GrafanaDatasourceConfig {
|
||||||
pub access: String,
|
pub access: String,
|
||||||
pub database: Option<String>,
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub json_data: Option<BTreeMap<String, String>>,
|
pub database: Option<String>,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub r#type: String,
|
pub r#type: String,
|
||||||
pub url: String,
|
pub url: String,
|
||||||
|
/// Represents jsonData in the GrafanaDatasource spec
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub json_data: Option<GrafanaDatasourceJsonData>,
|
||||||
|
|
||||||
|
/// Represents secureJsonData (secrets)
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub secure_json_data: Option<GrafanaDatasourceSecureJsonData>,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub is_default: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub editable: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct GrafanaDatasourceJsonData {
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub time_interval: Option<String>,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub http_header_name1: Option<String>,
|
||||||
|
|
||||||
|
/// Disable TLS skip verification (false = verify)
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub tls_skip_verify: Option<bool>,
|
||||||
|
|
||||||
|
/// Auth type - set to "forward" for OpenShift OAuth identity
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub oauth_pass_thru: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct GrafanaDatasourceSecureJsonData {
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub http_header_value1: Option<String>,
|
||||||
|
}
|
||||||
// ------------------------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, Default)]
|
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, Default)]
|
||||||
|
|||||||
@@ -100,11 +100,7 @@ impl<T: Topology + HelmCommand + K8sclient + MultiTargetTopology> Interpret<T> f
|
|||||||
|
|
||||||
info!("deploying ntfy...");
|
info!("deploying ntfy...");
|
||||||
client
|
client
|
||||||
.wait_until_deployment_ready(
|
.wait_until_deployment_ready("ntfy", Some(self.score.namespace.as_str()), None)
|
||||||
"ntfy".to_string(),
|
|
||||||
Some(self.score.namespace.as_str()),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
info!("ntfy deployed");
|
info!("ntfy deployed");
|
||||||
|
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ impl Prometheus {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Some(ns) = namespace.as_deref() {
|
if let Some(ns) = namespace.as_deref() {
|
||||||
grafana_helm_chart_score(ns)
|
grafana_helm_chart_score(ns, false)
|
||||||
.interpret(inventory, topology)
|
.interpret(inventory, topology)
|
||||||
.await
|
.await
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -73,4 +73,8 @@ impl ScrapeTarget<CRDPrometheus> for Server {
|
|||||||
self.name.clone()
|
self.name.clone()
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn clone_box(&self) -> Box<dyn ScrapeTarget<CRDPrometheus>> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ use crate::{
|
|||||||
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
||||||
inventory::{HostRole, Inventory},
|
inventory::{HostRole, Inventory},
|
||||||
modules::{
|
modules::{
|
||||||
dhcp::DhcpHostBindingScore,
|
dhcp::DhcpHostBindingScore, http::IPxeMacBootFileScore,
|
||||||
http::IPxeMacBootFileScore,
|
inventory::DiscoverHostForRoleScore, okd::templates::BootstrapIpxeTpl,
|
||||||
inventory::DiscoverHostForRoleScore,
|
|
||||||
okd::{host_network::HostNetworkConfigurationScore, templates::BootstrapIpxeTpl},
|
|
||||||
},
|
},
|
||||||
score::Score,
|
score::Score,
|
||||||
topology::{HAClusterTopology, HostBinding},
|
topology::{HAClusterTopology, HostBinding},
|
||||||
@@ -205,28 +203,6 @@ impl OKDSetup03ControlPlaneInterpret {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Placeholder for automating network bonding configuration.
|
|
||||||
async fn persist_network_bond(
|
|
||||||
&self,
|
|
||||||
inventory: &Inventory,
|
|
||||||
topology: &HAClusterTopology,
|
|
||||||
hosts: &Vec<PhysicalHost>,
|
|
||||||
) -> Result<(), InterpretError> {
|
|
||||||
info!("[ControlPlane] Ensuring persistent bonding");
|
|
||||||
let score = HostNetworkConfigurationScore {
|
|
||||||
hosts: hosts.clone(),
|
|
||||||
};
|
|
||||||
score.interpret(inventory, topology).await?;
|
|
||||||
|
|
||||||
inquire::Confirm::new(
|
|
||||||
"Network configuration for control plane nodes is not automated yet. Configure it manually if needed.",
|
|
||||||
)
|
|
||||||
.prompt()
|
|
||||||
.map_err(|e| InterpretError::new(format!("User prompt failed: {e}")))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -265,10 +241,6 @@ impl Interpret<HAClusterTopology> for OKDSetup03ControlPlaneInterpret {
|
|||||||
// 4. Reboot the nodes to start the OS installation.
|
// 4. Reboot the nodes to start the OS installation.
|
||||||
self.reboot_targets(&nodes).await?;
|
self.reboot_targets(&nodes).await?;
|
||||||
|
|
||||||
// 5. Placeholder for post-boot network configuration (e.g., bonding).
|
|
||||||
self.persist_network_bond(inventory, topology, &nodes)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// TODO: Implement a step to wait for the control plane nodes to join the cluster
|
// TODO: Implement a step to wait for the control plane nodes to join the cluster
|
||||||
// and for the cluster operators to become available. This would be similar to
|
// and for the cluster operators to become available. This would be similar to
|
||||||
// the `wait-for bootstrap-complete` command.
|
// the `wait-for bootstrap-complete` command.
|
||||||
|
|||||||
130
harmony/src/modules/okd/bootstrap_persist_network_bond.rs
Normal file
130
harmony/src/modules/okd/bootstrap_persist_network_bond.rs
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
use crate::{
|
||||||
|
data::Version,
|
||||||
|
hardware::PhysicalHost,
|
||||||
|
infra::inventory::InventoryRepositoryFactory,
|
||||||
|
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
||||||
|
inventory::{HostRole, Inventory},
|
||||||
|
modules::okd::host_network::HostNetworkConfigurationScore,
|
||||||
|
score::Score,
|
||||||
|
topology::HAClusterTopology,
|
||||||
|
};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use derive_new::new;
|
||||||
|
use harmony_types::id::Id;
|
||||||
|
use log::info;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------------------------------
|
||||||
|
// Persist Network Bond
|
||||||
|
// - Persist bonding via NMState
|
||||||
|
// - Persist port channels on the Switch
|
||||||
|
// -------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, new)]
|
||||||
|
pub struct OKDSetupPersistNetworkBondScore {}
|
||||||
|
|
||||||
|
impl Score<HAClusterTopology> for OKDSetupPersistNetworkBondScore {
|
||||||
|
fn create_interpret(&self) -> Box<dyn Interpret<HAClusterTopology>> {
|
||||||
|
Box::new(OKDSetupPersistNetworkBondInterpet::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> String {
|
||||||
|
"OKDSetupPersistNetworkBondScore".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct OKDSetupPersistNetworkBondInterpet {
|
||||||
|
version: Version,
|
||||||
|
status: InterpretStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OKDSetupPersistNetworkBondInterpet {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let version = Version::from("1.0.0").unwrap();
|
||||||
|
Self {
|
||||||
|
version,
|
||||||
|
status: InterpretStatus::QUEUED,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensures that three physical hosts are discovered and available for the ControlPlane role.
|
||||||
|
/// It will trigger discovery if not enough hosts are found.
|
||||||
|
async fn get_nodes(
|
||||||
|
&self,
|
||||||
|
_inventory: &Inventory,
|
||||||
|
_topology: &HAClusterTopology,
|
||||||
|
) -> Result<Vec<PhysicalHost>, InterpretError> {
|
||||||
|
const REQUIRED_HOSTS: usize = 3;
|
||||||
|
let repo = InventoryRepositoryFactory::build().await?;
|
||||||
|
let control_plane_hosts = repo.get_host_for_role(&HostRole::ControlPlane).await?;
|
||||||
|
|
||||||
|
if control_plane_hosts.len() < REQUIRED_HOSTS {
|
||||||
|
Err(InterpretError::new(format!(
|
||||||
|
"OKD Requires at least {} control plane hosts, but only found {}. Cannot proceed.",
|
||||||
|
REQUIRED_HOSTS,
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn persist_network_bond(
|
||||||
|
&self,
|
||||||
|
inventory: &Inventory,
|
||||||
|
topology: &HAClusterTopology,
|
||||||
|
hosts: &Vec<PhysicalHost>,
|
||||||
|
) -> Result<(), InterpretError> {
|
||||||
|
info!("Ensuring persistent bonding");
|
||||||
|
|
||||||
|
let score = HostNetworkConfigurationScore {
|
||||||
|
hosts: hosts.clone(),
|
||||||
|
};
|
||||||
|
score.interpret(inventory, topology).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Interpret<HAClusterTopology> for OKDSetupPersistNetworkBondInterpet {
|
||||||
|
fn get_name(&self) -> InterpretName {
|
||||||
|
InterpretName::Custom("OKDSetupPersistNetworkBondInterpet")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_version(&self) -> Version {
|
||||||
|
self.version.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_status(&self) -> InterpretStatus {
|
||||||
|
self.status.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_children(&self) -> Vec<Id> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
inventory: &Inventory,
|
||||||
|
topology: &HAClusterTopology,
|
||||||
|
) -> Result<Outcome, InterpretError> {
|
||||||
|
let nodes = self.get_nodes(inventory, topology).await?;
|
||||||
|
|
||||||
|
let res = self.persist_network_bond(inventory, topology, &nodes).await;
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(_) => Ok(Outcome::success(
|
||||||
|
"Network bond successfully persisted".into(),
|
||||||
|
)),
|
||||||
|
Err(_) => Err(InterpretError::new(
|
||||||
|
"Failed to persist network bond".to_string(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,41 +1 @@
|
|||||||
use kube::CustomResource;
|
|
||||||
use schemars::JsonSchema;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
pub mod nmstate;
|
pub mod nmstate;
|
||||||
|
|
||||||
#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
|
||||||
#[kube(
|
|
||||||
group = "operators.coreos.com",
|
|
||||||
version = "v1",
|
|
||||||
kind = "OperatorGroup",
|
|
||||||
namespaced
|
|
||||||
)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct OperatorGroupSpec {
|
|
||||||
pub target_namespaces: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
|
||||||
#[kube(
|
|
||||||
group = "operators.coreos.com",
|
|
||||||
version = "v1alpha1",
|
|
||||||
kind = "Subscription",
|
|
||||||
namespaced
|
|
||||||
)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct SubscriptionSpec {
|
|
||||||
pub name: String,
|
|
||||||
pub source: String,
|
|
||||||
pub source_namespace: String,
|
|
||||||
pub channel: Option<String>,
|
|
||||||
pub install_plan_approval: Option<InstallPlanApproval>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
|
||||||
pub enum InstallPlanApproval {
|
|
||||||
#[serde(rename = "Automatic")]
|
|
||||||
Automatic,
|
|
||||||
#[serde(rename = "Manual")]
|
|
||||||
Manual,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
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;
|
||||||
|
|
||||||
#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
||||||
#[kube(group = "nmstate.io", version = "v1", kind = "NMState", namespaced)]
|
#[kube(
|
||||||
|
group = "nmstate.io",
|
||||||
|
version = "v1",
|
||||||
|
kind = "NMState",
|
||||||
|
plural = "nmstates",
|
||||||
|
namespaced = false
|
||||||
|
)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct NMStateSpec {
|
pub struct NMStateSpec {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub probe_configuration: Option<ProbeConfig>,
|
pub probe_configuration: Option<ProbeConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,55 +48,350 @@ 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")]
|
||||||
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 {
|
||||||
pub name: String,
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub description: Option<String>,
|
pub running: Option<String>,
|
||||||
pub r#type: String,
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub state: String,
|
pub config: Option<String>,
|
||||||
pub mac_address: 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>,
|
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,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub r#type: InterfaceType,
|
||||||
|
pub state: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub mac_address: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub copy_mac_from: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub mtu: Option<u32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub controller: Option<String>,
|
pub controller: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub ipv4: Option<IpStackSpec>,
|
pub ipv4: Option<IpStackSpec>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub ipv6: Option<IpStackSpec>,
|
pub ipv6: Option<IpStackSpec>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub ethernet: Option<EthernetSpec>,
|
pub ethernet: Option<EthernetSpec>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub link_aggregation: Option<BondSpec>,
|
pub link_aggregation: Option<BondSpec>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub vlan: Option<VlanSpec>,
|
pub vlan: Option<VlanSpec>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub vxlan: Option<VxlanSpec>,
|
pub vxlan: Option<VxlanSpec>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub mac_vtap: Option<MacVtapSpec>,
|
pub mac_vtap: Option<MacVtapSpec>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub mac_vlan: Option<MacVlanSpec>,
|
pub mac_vlan: Option<MacVlanSpec>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub infiniband: Option<InfinibandSpec>,
|
pub infiniband: Option<InfinibandSpec>,
|
||||||
|
#[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(alias = "bridge")]
|
||||||
pub ovs_bridge: Option<OvsBridgeSpec>,
|
pub ovs_bridge: Option<OvsBridgeSpec>,
|
||||||
pub ethtool: Option<EthtoolSpec>,
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
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)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct IpStackSpec {
|
pub struct IpStackSpec {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub enabled: Option<bool>,
|
pub enabled: Option<bool>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub dhcp: Option<bool>,
|
pub dhcp: Option<bool>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub autoconf: Option<bool>,
|
pub autoconf: Option<bool>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub address: Option<Vec<IpAddressSpec>>,
|
pub address: Option<Vec<IpAddressSpec>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub auto_dns: Option<bool>,
|
pub auto_dns: Option<bool>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub auto_gateway: Option<bool>,
|
pub auto_gateway: Option<bool>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub auto_routes: Option<bool>,
|
pub auto_routes: Option<bool>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub dhcp_client_id: Option<String>,
|
pub dhcp_client_id: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub dhcp_duid: Option<String>,
|
pub dhcp_duid: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,8 +405,11 @@ pub struct IpAddressSpec {
|
|||||||
#[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 EthernetSpec {
|
pub struct EthernetSpec {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub speed: Option<u32>,
|
pub speed: Option<u32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub duplex: Option<String>,
|
pub duplex: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub auto_negotiation: Option<bool>,
|
pub auto_negotiation: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +417,9 @@ pub struct EthernetSpec {
|
|||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct BondSpec {
|
pub struct BondSpec {
|
||||||
pub mode: String,
|
pub mode: String,
|
||||||
|
#[serde(alias = "port")]
|
||||||
pub ports: Vec<String>,
|
pub ports: Vec<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub options: Option<BTreeMap<String, Value>>,
|
pub options: Option<BTreeMap<String, Value>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,6 +428,7 @@ pub struct BondSpec {
|
|||||||
pub struct VlanSpec {
|
pub struct VlanSpec {
|
||||||
pub base_iface: String,
|
pub base_iface: String,
|
||||||
pub id: u16,
|
pub id: u16,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub protocol: Option<String>,
|
pub protocol: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,8 +438,11 @@ pub struct VxlanSpec {
|
|||||||
pub base_iface: String,
|
pub base_iface: String,
|
||||||
pub id: u32,
|
pub id: u32,
|
||||||
pub remote: String,
|
pub remote: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub local: Option<String>,
|
pub local: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub learning: Option<bool>,
|
pub learning: Option<bool>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub destination_port: Option<u16>,
|
pub destination_port: Option<u16>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +451,7 @@ pub struct VxlanSpec {
|
|||||||
pub struct MacVtapSpec {
|
pub struct MacVtapSpec {
|
||||||
pub base_iface: String,
|
pub base_iface: String,
|
||||||
pub mode: String,
|
pub mode: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub promiscuous: Option<bool>,
|
pub promiscuous: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,6 +460,7 @@ pub struct MacVtapSpec {
|
|||||||
pub struct MacVlanSpec {
|
pub struct MacVlanSpec {
|
||||||
pub base_iface: String,
|
pub base_iface: String,
|
||||||
pub mode: String,
|
pub mode: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub promiscuous: Option<bool>,
|
pub promiscuous: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,25 +475,35 @@ pub struct InfinibandSpec {
|
|||||||
#[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 LinuxBridgeSpec {
|
pub struct LinuxBridgeSpec {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub options: Option<LinuxBridgeOptions>,
|
pub options: Option<LinuxBridgeOptions>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub ports: Option<Vec<LinuxBridgePort>>,
|
pub ports: Option<Vec<LinuxBridgePort>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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 LinuxBridgeOptions {
|
pub struct LinuxBridgeOptions {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub mac_ageing_time: Option<u32>,
|
pub mac_ageing_time: Option<u32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub multicast_snooping: Option<bool>,
|
pub multicast_snooping: Option<bool>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub stp: Option<StpOptions>,
|
pub stp: Option<StpOptions>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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 StpOptions {
|
pub struct StpOptions {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub enabled: Option<bool>,
|
pub enabled: Option<bool>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub forward_delay: Option<u16>,
|
pub forward_delay: Option<u16>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub hello_time: Option<u16>,
|
pub hello_time: Option<u16>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub max_age: Option<u16>,
|
pub max_age: Option<u16>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub priority: Option<u16>,
|
pub priority: Option<u16>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,15 +511,20 @@ pub struct StpOptions {
|
|||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct LinuxBridgePort {
|
pub struct LinuxBridgePort {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub vlan: Option<LinuxBridgePortVlan>,
|
pub vlan: Option<LinuxBridgePortVlan>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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 LinuxBridgePortVlan {
|
pub struct LinuxBridgePortVlan {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub mode: Option<String>,
|
pub mode: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub trunk_tags: Option<Vec<VlanTag>>,
|
pub trunk_tags: Option<Vec<VlanTag>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub tag: Option<u16>,
|
pub tag: Option<u16>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub enable_native: Option<bool>,
|
pub enable_native: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,6 +532,7 @@ pub struct LinuxBridgePortVlan {
|
|||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct VlanTag {
|
pub struct VlanTag {
|
||||||
pub id: u16,
|
pub id: u16,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub id_range: Option<VlanIdRange>,
|
pub id_range: Option<VlanIdRange>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,36 +546,35 @@ pub struct VlanIdRange {
|
|||||||
#[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 OvsBridgeSpec {
|
pub struct OvsBridgeSpec {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub options: Option<OvsBridgeOptions>,
|
pub options: Option<OvsBridgeOptions>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub ports: Option<Vec<OvsPortSpec>>,
|
pub ports: Option<Vec<OvsPortSpec>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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 OvsBridgeOptions {
|
pub struct OvsBridgeOptions {
|
||||||
pub stp: Option<bool>,
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub stp: Option<StpSpec>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub rstp: Option<bool>,
|
pub rstp: Option<bool>,
|
||||||
|
#[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)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct OvsPortSpec {
|
pub struct OvsPortSpec {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub link_aggregation: Option<BondSpec>,
|
pub link_aggregation: Option<BondSpec>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub vlan: Option<LinuxBridgePortVlan>,
|
pub vlan: Option<LinuxBridgePortVlan>,
|
||||||
|
#[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 {
|
|
||||||
pub auto: Option<bool>,
|
|
||||||
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,34 +35,91 @@ 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,
|
||||||
) -> Result<(), InterpretError> {
|
current_host: &usize,
|
||||||
let switch_ports = self.collect_switch_ports_for_host(topology, host).await?;
|
total_hosts: &usize,
|
||||||
if !switch_ports.is_empty() {
|
) -> Result<HostNetworkConfig, InterpretError> {
|
||||||
topology
|
if host.network.is_empty() {
|
||||||
.configure_host_network(host, HostNetworkConfig { switch_ports })
|
info!("[Host {current_host}/{total_hosts}] No interfaces to configure, skipping");
|
||||||
.await
|
return Ok(HostNetworkConfig {
|
||||||
.map_err(|e| InterpretError::new(format!("Failed to configure host: {e}")))?;
|
host_id: host.id.clone(),
|
||||||
|
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![],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
let switch_ports = self
|
||||||
|
.collect_switch_ports_for_host(topology, host, current_host, total_hosts)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let config = HostNetworkConfig {
|
||||||
|
host_id: host.id.clone(),
|
||||||
|
switch_ports,
|
||||||
|
};
|
||||||
|
|
||||||
|
if config.switch_ports.len() > 1 {
|
||||||
|
info!(
|
||||||
|
"[Host {current_host}/{total_hosts}] Found {} ports for {} interfaces",
|
||||||
|
config.switch_ports.len(),
|
||||||
|
host.network.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
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
|
||||||
|
.configure_port_channel(&config)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
InterpretError::new(format!("Failed to configure host network: {e}"))
|
||||||
|
})?;
|
||||||
|
} else if config.switch_ports.is_empty() {
|
||||||
|
info!(
|
||||||
|
"[Host {current_host}/{total_hosts}] No ports found for {} interfaces, skipping",
|
||||||
|
host.network.len()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
"[Host {current_host}/{total_hosts}] Found a single port for {} interfaces, skipping",
|
||||||
|
host.network.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn collect_switch_ports_for_host<T: Topology + Switch>(
|
async fn collect_switch_ports_for_host<T: Topology + Switch>(
|
||||||
&self,
|
&self,
|
||||||
topology: &T,
|
topology: &T,
|
||||||
host: &PhysicalHost,
|
host: &PhysicalHost,
|
||||||
|
current_host: &usize,
|
||||||
|
total_hosts: &usize,
|
||||||
) -> Result<Vec<SwitchPort>, InterpretError> {
|
) -> Result<Vec<SwitchPort>, InterpretError> {
|
||||||
let mut switch_ports = vec![];
|
let mut switch_ports = vec![];
|
||||||
|
|
||||||
|
if host.network.is_empty() {
|
||||||
|
return Ok(switch_ports);
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("[Host {current_host}/{total_hosts}] Collecting ports on switch...");
|
||||||
for network_interface in &host.network {
|
for network_interface in &host.network {
|
||||||
let mac_address = network_interface.mac_address;
|
let mac_address = network_interface.mac_address;
|
||||||
|
|
||||||
match topology.get_port_for_mac_address(&mac_address).await {
|
match topology.get_port_for_mac_address(&mac_address).await {
|
||||||
Ok(Some(port)) => {
|
Ok(Some(port)) => {
|
||||||
|
info!(
|
||||||
|
"[Host {current_host}/{total_hosts}] Found port '{port}' for '{mac_address}'"
|
||||||
|
);
|
||||||
switch_ports.push(SwitchPort {
|
switch_ports.push(SwitchPort {
|
||||||
interface: NetworkInterface {
|
interface: NetworkInterface {
|
||||||
name: network_interface.name.clone(),
|
name: network_interface.name.clone(),
|
||||||
@@ -73,7 +130,7 @@ impl HostNetworkConfigurationInterpret {
|
|||||||
port,
|
port,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(None) => debug!("No port found for host '{}', skipping", host.id),
|
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 '{}': {}",
|
||||||
@@ -85,10 +142,42 @@ impl HostNetworkConfigurationInterpret {
|
|||||||
|
|
||||||
Ok(switch_ports)
|
Ok(switch_ports)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn format_host_configuration(&self, configs: Vec<HostNetworkConfig>) -> Vec<String> {
|
||||||
|
let mut report = vec![
|
||||||
|
"Network Configuration Report".to_string(),
|
||||||
|
"------------------------------------------------------------------".to_string(),
|
||||||
|
];
|
||||||
|
|
||||||
|
for config in configs {
|
||||||
|
if config.switch_ports.is_empty() {
|
||||||
|
report.push(format!(
|
||||||
|
"⏭️ Host {}: SKIPPED (No matching switch ports found)",
|
||||||
|
config.host_id
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
let mappings: Vec<String> = config
|
||||||
|
.switch_ports
|
||||||
|
.iter()
|
||||||
|
.map(|p| format!("[{} -> {}]", p.interface.name, p.port))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
report.push(format!(
|
||||||
|
"✅ Host {}: Bonded {} port(s) {}",
|
||||||
|
config.host_id,
|
||||||
|
config.switch_ports.len(),
|
||||||
|
mappings.join(", ")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
report
|
||||||
|
.push("------------------------------------------------------------------".to_string());
|
||||||
|
report
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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")
|
||||||
}
|
}
|
||||||
@@ -114,27 +203,45 @@ impl<T: Topology + Switch> Interpret<T> for HostNetworkConfigurationInterpret {
|
|||||||
return Ok(Outcome::noop("No hosts to configure".into()));
|
return Ok(Outcome::noop("No hosts to configure".into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
info!(
|
let host_count = self.score.hosts.len();
|
||||||
"Started network configuration for {} host(s)...",
|
info!("Started network configuration for {host_count} host(s)...",);
|
||||||
self.score.hosts.len()
|
|
||||||
);
|
|
||||||
|
|
||||||
|
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...");
|
||||||
topology
|
topology
|
||||||
.setup_switch()
|
.setup_switch()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| InterpretError::new(format!("Switch setup failed: {e}")))?;
|
.map_err(|e| InterpretError::new(format!("Switch setup failed: {e}")))?;
|
||||||
|
info!("Switch ready");
|
||||||
|
|
||||||
|
let mut current_host = 1;
|
||||||
|
let mut host_configurations = vec![];
|
||||||
|
|
||||||
let mut configured_host_count = 0;
|
|
||||||
for host in &self.score.hosts {
|
for host in &self.score.hosts {
|
||||||
self.configure_network_for_host(topology, host).await?;
|
let host_configuration = self
|
||||||
configured_host_count += 1;
|
.configure_network_for_host(topology, host, ¤t_host, &host_count)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
host_configurations.push(host_configuration);
|
||||||
|
current_host += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if configured_host_count > 0 {
|
if current_host > 1 {
|
||||||
Ok(Outcome::success(format!(
|
let details = self.format_host_configuration(host_configurations);
|
||||||
"Configured {configured_host_count}/{} host(s)",
|
|
||||||
self.score.hosts.len()
|
Ok(Outcome::success_with_details(
|
||||||
)))
|
format!(
|
||||||
|
"Configured {}/{} host(s)",
|
||||||
|
current_host - 1,
|
||||||
|
self.score.hosts.len()
|
||||||
|
),
|
||||||
|
details,
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
Ok(Outcome::noop("No hosts configured".into()))
|
Ok(Outcome::noop("No hosts configured".into()))
|
||||||
}
|
}
|
||||||
@@ -150,7 +257,8 @@ mod tests {
|
|||||||
use crate::{
|
use crate::{
|
||||||
hardware::HostCategory,
|
hardware::HostCategory,
|
||||||
topology::{
|
topology::{
|
||||||
HostNetworkConfig, PreparationError, PreparationOutcome, SwitchError, SwitchPort,
|
HostNetworkConfig, NetworkError, PreparationError, PreparationOutcome, SwitchError,
|
||||||
|
SwitchPort,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
@@ -175,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(),
|
||||||
@@ -183,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]
|
||||||
@@ -198,27 +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 {
|
|
||||||
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![
|
||||||
@@ -230,10 +358,11 @@ 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(),
|
||||||
switch_ports: vec![
|
switch_ports: vec![
|
||||||
SwitchPort {
|
SwitchPort {
|
||||||
interface: EXISTING_INTERFACE.clone(),
|
interface: EXISTING_INTERFACE.clone(),
|
||||||
@@ -249,47 +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 {
|
||||||
switch_ports: vec![SwitchPort {
|
host_id: HOST_ID.clone(),
|
||||||
interface: EXISTING_INTERFACE.clone(),
|
switch_ports: vec![
|
||||||
port: PORT.clone(),
|
SwitchPort {
|
||||||
}],
|
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 {
|
||||||
switch_ports: vec![SwitchPort {
|
host_id: ANOTHER_HOST_ID.clone(),
|
||||||
interface: ANOTHER_EXISTING_INTERFACE.clone(),
|
switch_ports: vec![
|
||||||
port: ANOTHER_PORT.clone(),
|
SwitchPort {
|
||||||
}],
|
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 {
|
||||||
@@ -326,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![])),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -361,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> {
|
||||||
@@ -380,13 +683,12 @@ mod tests {
|
|||||||
Ok(Some(ports.remove(0)))
|
Ok(Some(ports.remove(0)))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn configure_host_network(
|
async fn configure_port_channel(
|
||||||
&self,
|
&self,
|
||||||
host: &PhysicalHost,
|
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((host.id.clone(), config.clone()));
|
configured_port_channels.push((config.host_id.clone(), config.clone()));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
modules::okd::{
|
modules::okd::{
|
||||||
OKDSetup01InventoryScore, OKDSetup02BootstrapScore, OKDSetup03ControlPlaneScore,
|
OKDSetup01InventoryScore, OKDSetup02BootstrapScore, OKDSetup03ControlPlaneScore,
|
||||||
OKDSetup04WorkersScore, OKDSetup05SanityCheckScore,
|
OKDSetup04WorkersScore, OKDSetup05SanityCheckScore, OKDSetupPersistNetworkBondScore,
|
||||||
bootstrap_06_installation_report::OKDSetup06InstallationReportScore,
|
bootstrap_06_installation_report::OKDSetup06InstallationReportScore,
|
||||||
},
|
},
|
||||||
score::Score,
|
score::Score,
|
||||||
@@ -65,6 +65,7 @@ impl OKDInstallationPipeline {
|
|||||||
Box::new(OKDSetup01InventoryScore::new()),
|
Box::new(OKDSetup01InventoryScore::new()),
|
||||||
Box::new(OKDSetup02BootstrapScore::new()),
|
Box::new(OKDSetup02BootstrapScore::new()),
|
||||||
Box::new(OKDSetup03ControlPlaneScore::new()),
|
Box::new(OKDSetup03ControlPlaneScore::new()),
|
||||||
|
Box::new(OKDSetupPersistNetworkBondScore::new()),
|
||||||
Box::new(OKDSetup04WorkersScore::new()),
|
Box::new(OKDSetup04WorkersScore::new()),
|
||||||
Box::new(OKDSetup05SanityCheckScore::new()),
|
Box::new(OKDSetup05SanityCheckScore::new()),
|
||||||
Box::new(OKDSetup06InstallationReportScore::new()),
|
Box::new(OKDSetup06InstallationReportScore::new()),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ mod bootstrap_05_sanity_check;
|
|||||||
mod bootstrap_06_installation_report;
|
mod bootstrap_06_installation_report;
|
||||||
pub mod bootstrap_dhcp;
|
pub mod bootstrap_dhcp;
|
||||||
pub mod bootstrap_load_balancer;
|
pub mod bootstrap_load_balancer;
|
||||||
|
mod bootstrap_persist_network_bond;
|
||||||
pub mod dhcp;
|
pub mod dhcp;
|
||||||
pub mod dns;
|
pub mod dns;
|
||||||
pub mod installation;
|
pub mod installation;
|
||||||
@@ -19,5 +20,6 @@ pub use bootstrap_03_control_plane::*;
|
|||||||
pub use bootstrap_04_workers::*;
|
pub use bootstrap_04_workers::*;
|
||||||
pub use bootstrap_05_sanity_check::*;
|
pub use bootstrap_05_sanity_check::*;
|
||||||
pub use bootstrap_06_installation_report::*;
|
pub use bootstrap_06_installation_report::*;
|
||||||
|
pub use bootstrap_persist_network_bond::*;
|
||||||
pub mod crd;
|
pub mod crd;
|
||||||
pub mod host_network;
|
pub mod host_network;
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ use crate::modules::monitoring::kube_prometheus::crd::crd_alertmanager_config::C
|
|||||||
use crate::modules::monitoring::kube_prometheus::crd::crd_default_rules::build_default_application_rules;
|
use crate::modules::monitoring::kube_prometheus::crd::crd_default_rules::build_default_application_rules;
|
||||||
use crate::modules::monitoring::kube_prometheus::crd::crd_grafana::{
|
use crate::modules::monitoring::kube_prometheus::crd::crd_grafana::{
|
||||||
Grafana, GrafanaDashboard, GrafanaDashboardSpec, GrafanaDatasource, GrafanaDatasourceConfig,
|
Grafana, GrafanaDashboard, GrafanaDashboardSpec, GrafanaDatasource, GrafanaDatasourceConfig,
|
||||||
GrafanaDatasourceSpec, GrafanaSpec,
|
GrafanaDatasourceJsonData, GrafanaDatasourceSpec, GrafanaSecretKeyRef, GrafanaSpec,
|
||||||
|
GrafanaValueFrom, GrafanaValueSource,
|
||||||
};
|
};
|
||||||
use crate::modules::monitoring::kube_prometheus::crd::crd_prometheus_rules::{
|
use crate::modules::monitoring::kube_prometheus::crd::crd_prometheus_rules::{
|
||||||
PrometheusRule, PrometheusRuleSpec, RuleGroup,
|
PrometheusRule, PrometheusRuleSpec, RuleGroup,
|
||||||
@@ -39,7 +40,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use harmony_types::id::Id;
|
use harmony_types::id::Id;
|
||||||
|
|
||||||
use super::prometheus::PrometheusApplicationMonitoring;
|
use super::prometheus::PrometheusMonitoring;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
pub struct K8sPrometheusCRDAlertingScore {
|
pub struct K8sPrometheusCRDAlertingScore {
|
||||||
@@ -49,7 +50,7 @@ pub struct K8sPrometheusCRDAlertingScore {
|
|||||||
pub prometheus_rules: Vec<RuleGroup>,
|
pub prometheus_rules: Vec<RuleGroup>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Topology + K8sclient + PrometheusApplicationMonitoring<CRDPrometheus>> Score<T>
|
impl<T: Topology + K8sclient + PrometheusMonitoring<CRDPrometheus>> Score<T>
|
||||||
for K8sPrometheusCRDAlertingScore
|
for K8sPrometheusCRDAlertingScore
|
||||||
{
|
{
|
||||||
fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> {
|
fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> {
|
||||||
@@ -75,7 +76,7 @@ pub struct K8sPrometheusCRDAlertingInterpret {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<T: Topology + K8sclient + PrometheusApplicationMonitoring<CRDPrometheus>> Interpret<T>
|
impl<T: Topology + K8sclient + PrometheusMonitoring<CRDPrometheus>> Interpret<T>
|
||||||
for K8sPrometheusCRDAlertingInterpret
|
for K8sPrometheusCRDAlertingInterpret
|
||||||
{
|
{
|
||||||
async fn execute(
|
async fn execute(
|
||||||
@@ -466,10 +467,13 @@ impl K8sPrometheusCRDAlertingInterpret {
|
|||||||
match_labels: label.clone(),
|
match_labels: label.clone(),
|
||||||
match_expressions: vec![],
|
match_expressions: vec![],
|
||||||
};
|
};
|
||||||
let mut json_data = BTreeMap::new();
|
|
||||||
json_data.insert("timeInterval".to_string(), "5s".to_string());
|
|
||||||
let namespace = self.sender.namespace.clone();
|
let namespace = self.sender.namespace.clone();
|
||||||
|
let json_data = GrafanaDatasourceJsonData {
|
||||||
|
time_interval: Some("5s".to_string()),
|
||||||
|
http_header_name1: None,
|
||||||
|
tls_skip_verify: Some(true),
|
||||||
|
oauth_pass_thru: Some(true),
|
||||||
|
};
|
||||||
let json = build_default_dashboard(&namespace);
|
let json = build_default_dashboard(&namespace);
|
||||||
|
|
||||||
let graf_data_source = GrafanaDatasource {
|
let graf_data_source = GrafanaDatasource {
|
||||||
@@ -495,7 +499,11 @@ impl K8sPrometheusCRDAlertingInterpret {
|
|||||||
"http://prometheus-operated.{}.svc.cluster.local:9090",
|
"http://prometheus-operated.{}.svc.cluster.local:9090",
|
||||||
self.sender.namespace.clone()
|
self.sender.namespace.clone()
|
||||||
),
|
),
|
||||||
|
secure_json_data: None,
|
||||||
|
is_default: None,
|
||||||
|
editable: None,
|
||||||
},
|
},
|
||||||
|
values_from: None,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -516,7 +524,9 @@ impl K8sPrometheusCRDAlertingInterpret {
|
|||||||
spec: GrafanaDashboardSpec {
|
spec: GrafanaDashboardSpec {
|
||||||
resync_period: Some("30s".to_string()),
|
resync_period: Some("30s".to_string()),
|
||||||
instance_selector: labels.clone(),
|
instance_selector: labels.clone(),
|
||||||
json,
|
json: Some(json),
|
||||||
|
grafana_com: None,
|
||||||
|
datasources: None,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,17 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait PrometheusApplicationMonitoring<S: AlertSender> {
|
pub trait PrometheusMonitoring<S: AlertSender> {
|
||||||
async fn install_prometheus(
|
async fn install_prometheus(
|
||||||
&self,
|
&self,
|
||||||
sender: &S,
|
sender: &S,
|
||||||
inventory: &Inventory,
|
inventory: &Inventory,
|
||||||
receivers: Option<Vec<Box<dyn AlertReceiver<S>>>>,
|
receivers: Option<Vec<Box<dyn AlertReceiver<S>>>>,
|
||||||
) -> Result<PreparationOutcome, PreparationError>;
|
) -> Result<PreparationOutcome, PreparationError>;
|
||||||
|
|
||||||
|
async fn ensure_prometheus_operator(
|
||||||
|
&self,
|
||||||
|
sender: &S,
|
||||||
|
inventory: &Inventory,
|
||||||
|
) -> Result<PreparationOutcome, PreparationError>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use harmony_types::id::Id;
|
use harmony_types::id::Id;
|
||||||
|
|
||||||
use super::prometheus::PrometheusApplicationMonitoring;
|
use super::prometheus::PrometheusMonitoring;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
pub struct RHOBAlertingScore {
|
pub struct RHOBAlertingScore {
|
||||||
@@ -48,8 +48,8 @@ pub struct RHOBAlertingScore {
|
|||||||
pub prometheus_rules: Vec<RuleGroup>,
|
pub prometheus_rules: Vec<RuleGroup>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Topology + K8sclient + Ingress + PrometheusApplicationMonitoring<RHOBObservability>>
|
impl<T: Topology + K8sclient + Ingress + PrometheusMonitoring<RHOBObservability>> Score<T>
|
||||||
Score<T> for RHOBAlertingScore
|
for RHOBAlertingScore
|
||||||
{
|
{
|
||||||
fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> {
|
fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> {
|
||||||
Box::new(RHOBAlertingInterpret {
|
Box::new(RHOBAlertingInterpret {
|
||||||
@@ -74,8 +74,8 @@ pub struct RHOBAlertingInterpret {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<T: Topology + K8sclient + Ingress + PrometheusApplicationMonitoring<RHOBObservability>>
|
impl<T: Topology + K8sclient + Ingress + PrometheusMonitoring<RHOBObservability>> Interpret<T>
|
||||||
Interpret<T> for RHOBAlertingInterpret
|
for RHOBAlertingInterpret
|
||||||
{
|
{
|
||||||
async fn execute(
|
async fn execute(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ pub fn init() {
|
|||||||
HarmonyEvent::HarmonyFinished => {
|
HarmonyEvent::HarmonyFinished => {
|
||||||
if !details.is_empty() {
|
if !details.is_empty() {
|
||||||
println!(
|
println!(
|
||||||
"\n{} All done! Here's what's next for you:",
|
"\n{} All done! Here's a few info for you:",
|
||||||
theme::EMOJI_SUMMARY
|
theme::EMOJI_SUMMARY
|
||||||
);
|
);
|
||||||
for detail in details.iter() {
|
for detail in details.iter() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
|
#[derive(Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
|
||||||
pub struct MacAddress(pub [u8; 6]);
|
pub struct MacAddress(pub [u8; 6]);
|
||||||
|
|
||||||
impl MacAddress {
|
impl MacAddress {
|
||||||
@@ -19,6 +19,14 @@ impl From<&MacAddress> for String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for MacAddress {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_tuple("MacAddress")
|
||||||
|
.field(&String::from(self))
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for MacAddress {
|
impl std::fmt::Display for MacAddress {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.write_str(&String::from(self))
|
f.write_str(&String::from(self))
|
||||||
|
|||||||
@@ -106,11 +106,37 @@ pub struct HAProxy {
|
|||||||
pub groups: MaybeString,
|
pub groups: MaybeString,
|
||||||
pub users: MaybeString,
|
pub users: MaybeString,
|
||||||
pub cpus: MaybeString,
|
pub cpus: MaybeString,
|
||||||
pub resolvers: MaybeString,
|
pub resolvers: HAProxyResolvers,
|
||||||
pub mailers: MaybeString,
|
pub mailers: MaybeString,
|
||||||
pub maintenance: Maintenance,
|
pub maintenance: Maintenance,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
|
||||||
|
pub struct HAProxyResolvers {
|
||||||
|
#[yaserde(rename = "resolver")]
|
||||||
|
pub resolver: Resolver,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
|
||||||
|
pub struct Resolver {
|
||||||
|
pub id: String,
|
||||||
|
pub enabled: i32,
|
||||||
|
pub name: String,
|
||||||
|
pub description: MaybeString,
|
||||||
|
pub nameservers: String,
|
||||||
|
pub parse_resolv_conf: String,
|
||||||
|
pub resolve_retries: i32,
|
||||||
|
pub timeout_resolve: String,
|
||||||
|
pub timeout_retry: String,
|
||||||
|
pub accepted_payload_size: MaybeString,
|
||||||
|
pub hold_valid: MaybeString,
|
||||||
|
pub hold_obsolete: MaybeString,
|
||||||
|
pub hold_refused: MaybeString,
|
||||||
|
pub hold_nx: MaybeString,
|
||||||
|
pub hold_timeout: MaybeString,
|
||||||
|
pub hold_other: MaybeString,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
|
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
|
||||||
pub struct Maintenance {
|
pub struct Maintenance {
|
||||||
#[yaserde(rename = "cronjobs")]
|
#[yaserde(rename = "cronjobs")]
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ pub struct Interface {
|
|||||||
pub physical_interface_name: String,
|
pub physical_interface_name: String,
|
||||||
pub descr: Option<MaybeString>,
|
pub descr: Option<MaybeString>,
|
||||||
pub mtu: Option<MaybeString>,
|
pub mtu: Option<MaybeString>,
|
||||||
pub enable: MaybeString,
|
pub enable: Option<MaybeString>,
|
||||||
pub lock: Option<MaybeString>,
|
pub lock: Option<MaybeString>,
|
||||||
#[yaserde(rename = "spoofmac")]
|
#[yaserde(rename = "spoofmac")]
|
||||||
pub spoof_mac: Option<MaybeString>,
|
pub spoof_mac: Option<MaybeString>,
|
||||||
@@ -134,19 +134,15 @@ mod test {
|
|||||||
<interfaces>
|
<interfaces>
|
||||||
<paul>
|
<paul>
|
||||||
<if></if>
|
<if></if>
|
||||||
<enable/>
|
|
||||||
</paul>
|
</paul>
|
||||||
<anotherpaul>
|
<anotherpaul>
|
||||||
<if></if>
|
<if></if>
|
||||||
<enable/>
|
|
||||||
</anotherpaul>
|
</anotherpaul>
|
||||||
<thirdone>
|
<thirdone>
|
||||||
<if></if>
|
<if></if>
|
||||||
<enable/>
|
|
||||||
</thirdone>
|
</thirdone>
|
||||||
<andgofor4>
|
<andgofor4>
|
||||||
<if></if>
|
<if></if>
|
||||||
<enable/>
|
|
||||||
</andgofor4>
|
</andgofor4>
|
||||||
</interfaces>
|
</interfaces>
|
||||||
<bar>foo</bar>
|
<bar>foo</bar>
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ pub struct System {
|
|||||||
pub maximumfrags: Option<MaybeString>,
|
pub maximumfrags: Option<MaybeString>,
|
||||||
pub aliasesresolveinterval: Option<MaybeString>,
|
pub aliasesresolveinterval: Option<MaybeString>,
|
||||||
pub maximumtableentries: Option<MaybeString>,
|
pub maximumtableentries: Option<MaybeString>,
|
||||||
pub language: String,
|
pub language: Option<String>,
|
||||||
pub dnsserver: Option<MaybeString>,
|
pub dnsserver: Option<MaybeString>,
|
||||||
pub dns1gw: Option<String>,
|
pub dns1gw: Option<String>,
|
||||||
pub dns2gw: Option<String>,
|
pub dns2gw: Option<String>,
|
||||||
@@ -1291,6 +1291,7 @@ pub struct WireguardServerItem {
|
|||||||
pub gateway: MaybeString,
|
pub gateway: MaybeString,
|
||||||
pub carp_depend_on: MaybeString,
|
pub carp_depend_on: MaybeString,
|
||||||
pub peers: String,
|
pub peers: String,
|
||||||
|
pub debug: MaybeString,
|
||||||
pub endpoint: MaybeString,
|
pub endpoint: MaybeString,
|
||||||
pub peer_dns: MaybeString,
|
pub peer_dns: MaybeString,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user