fix(host_network): retrieve proper hostname and next available bond id #182
@@ -1,10 +1,15 @@
|
|||||||
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 kube::api::ObjectMeta;
|
use k8s_openapi::api::core::v1::Node;
|
||||||
|
use kube::{
|
||||||
|
ResourceExt,
|
||||||
|
api::{ObjectList, ObjectMeta},
|
||||||
|
};
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use log::info;
|
use log::info;
|
||||||
|
|
||||||
@@ -22,7 +27,7 @@ use super::{
|
|||||||
Topology, k8s::K8sClient,
|
Topology, k8s::K8sClient,
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::{BTreeMap, HashSet};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -63,7 +68,7 @@ impl K8sclient for HAClusterTopology {
|
|||||||
K8sClient::try_default().await.map_err(|e| e.to_string())?,
|
K8sClient::try_default().await.map_err(|e| e.to_string())?,
|
||||||
)),
|
)),
|
||||||
Some(kubeconfig) => {
|
Some(kubeconfig) => {
|
||||||
let Some(client) = K8sClient::from_kubeconfig(&kubeconfig).await else {
|
let Some(client) = K8sClient::from_kubeconfig(kubeconfig).await else {
|
||||||
return Err("Failed to create k8s client".to_string());
|
return Err("Failed to create k8s client".to_string());
|
||||||
};
|
};
|
||||||
Ok(Arc::new(client))
|
Ok(Arc::new(client))
|
||||||
@@ -143,7 +148,10 @@ impl HAClusterTopology {
|
|||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
debug!("Creating NMState: {nmstate:#?}");
|
debug!(
|
||||||
|
"Creating NMState:\n{}",
|
||||||
|
serde_yaml::to_string(&nmstate).unwrap()
|
||||||
|
);
|
||||||
k8s_client
|
k8s_client
|
||||||
.apply(&nmstate, None)
|
.apply(&nmstate, None)
|
||||||
.await
|
.await
|
||||||
@@ -152,10 +160,6 @@ impl HAClusterTopology {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_next_bond_id(&self) -> u8 {
|
|
||||||
42 // FIXME: Find a better way to declare the bond id
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn configure_bond(&self, config: &HostNetworkConfig) -> Result<(), SwitchError> {
|
async fn configure_bond(&self, config: &HostNetworkConfig) -> Result<(), SwitchError> {
|
||||||
self.ensure_nmstate_operator_installed()
|
self.ensure_nmstate_operator_installed()
|
||||||
.await
|
.await
|
||||||
@@ -165,9 +169,23 @@ impl HAClusterTopology {
|
|||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let bond_config = self.create_bond_configuration(config);
|
let hostname = self.get_hostname(&config.host_id).await.map_err(|e| {
|
||||||
|
SwitchError::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| {
|
||||||
|
SwitchError::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!(
|
debug!(
|
||||||
"Applying NMState bond config for host {}: {bond_config:#?}",
|
"Applying NMState bond config for host {}:\n{}",
|
||||||
|
serde_yaml::to_string(&bond_config).unwrap(),
|
||||||
config.host_id
|
config.host_id
|
||||||
);
|
);
|
||||||
self.k8s_client()
|
self.k8s_client()
|
||||||
@@ -182,26 +200,24 @@ impl HAClusterTopology {
|
|||||||
|
|
||||||
fn create_bond_configuration(
|
fn create_bond_configuration(
|
||||||
&self,
|
&self,
|
||||||
|
host: &str,
|
||||||
|
bond_name: &str,
|
||||||
config: &HostNetworkConfig,
|
config: &HostNetworkConfig,
|
||||||
) -> NodeNetworkConfigurationPolicy {
|
) -> NodeNetworkConfigurationPolicy {
|
||||||
let host_name = &config.host_id;
|
info!("Configuring bond '{bond_name}' for host '{host}'...");
|
||||||
let bond_id = self.get_next_bond_id();
|
|
||||||
let bond_name = format!("bond{bond_id}");
|
|
||||||
|
|
||||||
info!("Configuring bond '{bond_name}' for host '{host_name}'...");
|
|
||||||
|
|
||||||
let mut bond_mtu: Option<u32> = None;
|
let mut bond_mtu: Option<u32> = None;
|
||||||
let mut copy_mac_from: Option<String> = None;
|
let mut copy_mac_from: Option<String> = None;
|
||||||
let mut bond_ports = Vec::new();
|
let mut bond_ports = Vec::new();
|
||||||
let mut interfaces: Vec<nmstate::InterfaceSpec> = Vec::new();
|
let mut interfaces: Vec<nmstate::Interface> = Vec::new();
|
||||||
|
|
||||||
for switch_port in &config.switch_ports {
|
for switch_port in &config.switch_ports {
|
||||||
let interface_name = switch_port.interface.name.clone();
|
let interface_name = switch_port.interface.name.clone();
|
||||||
|
|
||||||
interfaces.push(nmstate::InterfaceSpec {
|
interfaces.push(nmstate::Interface {
|
||||||
name: interface_name.clone(),
|
name: interface_name.clone(),
|
||||||
description: Some(format!("Member of bond {bond_name}")),
|
description: Some(format!("Member of bond {bond_name}")),
|
||||||
r#type: "ethernet".to_string(),
|
r#type: nmstate::InterfaceType::Ethernet,
|
||||||
state: "up".to_string(),
|
state: "up".to_string(),
|
||||||
mtu: Some(switch_port.interface.mtu),
|
mtu: Some(switch_port.interface.mtu),
|
||||||
mac_address: Some(switch_port.interface.mac_address.to_string()),
|
mac_address: Some(switch_port.interface.mac_address.to_string()),
|
||||||
@@ -228,10 +244,10 @@ impl HAClusterTopology {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interfaces.push(nmstate::InterfaceSpec {
|
interfaces.push(nmstate::Interface {
|
||||||
name: bond_name.clone(),
|
name: bond_name.to_string(),
|
||||||
description: Some(format!("Network bond for host {host_name}")),
|
description: Some(format!("Network bond for host {host}")),
|
||||||
r#type: "bond".to_string(),
|
r#type: nmstate::InterfaceType::Bond,
|
||||||
state: "up".to_string(),
|
state: "up".to_string(),
|
||||||
copy_mac_from,
|
copy_mac_from,
|
||||||
ipv4: Some(nmstate::IpStackSpec {
|
ipv4: Some(nmstate::IpStackSpec {
|
||||||
@@ -255,19 +271,80 @@ impl HAClusterTopology {
|
|||||||
|
|
||||||
NodeNetworkConfigurationPolicy {
|
NodeNetworkConfigurationPolicy {
|
||||||
metadata: ObjectMeta {
|
metadata: ObjectMeta {
|
||||||
name: Some(format!("{host_name}-bond-config")),
|
name: Some(format!("{host}-bond-config")),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
spec: NodeNetworkConfigurationPolicySpec {
|
spec: NodeNetworkConfigurationPolicySpec {
|
||||||
node_selector: Some(BTreeMap::from([(
|
node_selector: Some(BTreeMap::from([(
|
||||||
"kubernetes.io/hostname".to_string(),
|
"kubernetes.io/hostname".to_string(),
|
||||||
host_name.to_string(),
|
host.to_string(),
|
||||||
)])),
|
)])),
|
||||||
desired_state: nmstate::DesiredStateSpec { interfaces },
|
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()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.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()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.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}"))
|
||||||
|
}
|
||||||
|
|
||||||
async fn configure_port_channel(&self, config: &HostNetworkConfig) -> Result<(), SwitchError> {
|
async fn configure_port_channel(&self, config: &HostNetworkConfig) -> Result<(), SwitchError> {
|
||||||
debug!("Configuring port channel: {config:#?}");
|
debug!("Configuring port channel: {config:#?}");
|
||||||
let switch_ports = config.switch_ports.iter().map(|s| s.port.clone()).collect();
|
let switch_ports = config.switch_ports.iter().map(|s| s.port.clone()).collect();
|
||||||
@@ -458,16 +535,14 @@ impl HttpServer for HAClusterTopology {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Switch for HAClusterTopology {
|
impl Switch for HAClusterTopology {
|
||||||
async fn setup_switch(&self) -> Result<(), SwitchError> {
|
async fn setup_switch(&self) -> Result<(), SwitchError> {
|
||||||
self.switch_client.setup().await?;
|
self.switch_client.setup().await.map(|_| ())
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_port_for_mac_address(
|
async fn get_port_for_mac_address(
|
||||||
&self,
|
&self,
|
||||||
mac_address: &MacAddress,
|
mac_address: &MacAddress,
|
||||||
) -> Result<Option<PortLocation>, SwitchError> {
|
) -> Result<Option<PortLocation>, SwitchError> {
|
||||||
let port = self.switch_client.find_port(mac_address).await?;
|
self.switch_client.find_port(mac_address).await
|
||||||
Ok(port)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn configure_host_network(&self, config: &HostNetworkConfig) -> Result<(), SwitchError> {
|
async fn configure_host_network(&self, config: &HostNetworkConfig) -> Result<(), SwitchError> {
|
||||||
|
|||||||
@@ -5,13 +5,15 @@ use k8s_openapi::{
|
|||||||
ClusterResourceScope, NamespaceResourceScope,
|
ClusterResourceScope, NamespaceResourceScope,
|
||||||
api::{
|
api::{
|
||||||
apps::v1::Deployment,
|
apps::v1::Deployment,
|
||||||
core::v1::{Pod, ServiceAccount},
|
core::v1::{Node, Pod, ServiceAccount},
|
||||||
},
|
},
|
||||||
apimachinery::pkg::version::Info,
|
apimachinery::pkg::version::Info,
|
||||||
};
|
};
|
||||||
use kube::{
|
use kube::{
|
||||||
Client, Config, Discovery, Error, Resource,
|
Client, Config, Discovery, Error, Resource,
|
||||||
api::{Api, AttachParams, DeleteParams, ListParams, Patch, PatchParams, ResourceExt},
|
api::{
|
||||||
|
Api, AttachParams, DeleteParams, ListParams, ObjectList, Patch, PatchParams, ResourceExt,
|
||||||
|
},
|
||||||
config::{KubeConfigOptions, Kubeconfig},
|
config::{KubeConfigOptions, Kubeconfig},
|
||||||
core::ErrorResponse,
|
core::ErrorResponse,
|
||||||
discovery::{ApiCapabilities, Scope},
|
discovery::{ApiCapabilities, Scope},
|
||||||
@@ -564,7 +566,58 @@ impl K8sClient {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn from_kubeconfig(path: &str) -> Option<K8sClient> {
|
/// Gets a single named resource of a specific type `K`.
|
||||||
|
///
|
||||||
|
/// This function uses the `ApplyStrategy` trait to correctly determine
|
||||||
|
/// whether to look in a specific namespace or in the entire cluster.
|
||||||
|
///
|
||||||
|
/// Returns `Ok(None)` if the resource is not found (404).
|
||||||
|
pub async fn get_resource<K>(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
namespace: Option<&str>,
|
||||||
|
) -> Result<Option<K>, Error>
|
||||||
|
where
|
||||||
|
K: Resource + Clone + std::fmt::Debug + DeserializeOwned,
|
||||||
|
<K as Resource>::Scope: ApplyStrategy<K>,
|
||||||
|
<K as kube::Resource>::DynamicType: Default,
|
||||||
|
{
|
||||||
|
let api: Api<K> =
|
||||||
|
<<K as Resource>::Scope as ApplyStrategy<K>>::get_api(&self.client, namespace);
|
||||||
|
|
||||||
|
api.get_opt(name).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lists all resources of a specific type `K`.
|
||||||
|
///
|
||||||
|
/// This function uses the `ApplyStrategy` trait to correctly determine
|
||||||
|
/// whether to list from a specific namespace or from the entire cluster.
|
||||||
|
pub async fn list_resources<K>(
|
||||||
|
&self,
|
||||||
|
namespace: Option<&str>,
|
||||||
|
list_params: Option<ListParams>,
|
||||||
|
) -> Result<ObjectList<K>, Error>
|
||||||
|
where
|
||||||
|
K: Resource + Clone + std::fmt::Debug + DeserializeOwned,
|
||||||
|
<K as Resource>::Scope: ApplyStrategy<K>,
|
||||||
|
<K as kube::Resource>::DynamicType: Default,
|
||||||
|
{
|
||||||
|
let api: Api<K> =
|
||||||
|
<<K as Resource>::Scope as ApplyStrategy<K>>::get_api(&self.client, namespace);
|
||||||
|
|
||||||
|
let list_params = list_params.unwrap_or_default();
|
||||||
|
api.list(&list_params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches a list of all Nodes in the cluster.
|
||||||
|
pub async fn get_nodes(
|
||||||
|
&self,
|
||||||
|
list_params: Option<ListParams>,
|
||||||
|
) -> Result<ObjectList<Node>, Error> {
|
||||||
|
self.list_resources(None, list_params).await
|
||||||
|
letian marked this conversation as resolved
|
|||||||
|
}
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
|||||||
@@ -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()
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use kube::CustomResource;
|
use k8s_openapi::{ClusterResourceScope, Resource};
|
||||||
|
use kube::{CustomResource, api::ObjectMeta};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
@@ -47,28 +48,223 @@ pub struct ProbeDns {
|
|||||||
group = "nmstate.io",
|
group = "nmstate.io",
|
||||||
version = "v1",
|
version = "v1",
|
||||||
kind = "NodeNetworkConfigurationPolicy",
|
kind = "NodeNetworkConfigurationPolicy",
|
||||||
namespaced
|
namespaced = false
|
||||||
)]
|
)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct NodeNetworkConfigurationPolicySpec {
|
pub struct NodeNetworkConfigurationPolicySpec {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub node_selector: Option<BTreeMap<String, String>>,
|
pub node_selector: Option<BTreeMap<String, String>>,
|
||||||
pub desired_state: DesiredStateSpec,
|
pub desired_state: NetworkState,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Currently, kube-rs derive doesn't support resources without a `spec` field, so we have
|
||||||
|
// to implement it ourselves.
|
||||||
|
//
|
||||||
|
// Ref:
|
||||||
|
// - https://github.com/kube-rs/kube/issues/1763
|
||||||
|
// - https://github.com/kube-rs/kube/discussions/1762
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct NodeNetworkState {
|
||||||
|
metadata: ObjectMeta,
|
||||||
|
pub status: NodeNetworkStateStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Resource for NodeNetworkState {
|
||||||
|
const API_VERSION: &'static str = "nmstate.io/v1beta1";
|
||||||
|
const GROUP: &'static str = "nmstate.io";
|
||||||
|
const VERSION: &'static str = "v1beta1";
|
||||||
|
const KIND: &'static str = "NodeNetworkState";
|
||||||
|
const URL_PATH_SEGMENT: &'static str = "nodenetworkstates";
|
||||||
|
type Scope = ClusterResourceScope;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl k8s_openapi::Metadata for NodeNetworkState {
|
||||||
|
type Ty = ObjectMeta;
|
||||||
|
|
||||||
|
fn metadata(&self) -> &Self::Ty {
|
||||||
|
&self.metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
fn metadata_mut(&mut self) -> &mut Self::Ty {
|
||||||
|
&mut self.metadata
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct NodeNetworkStateStatus {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub current_state: Option<NetworkState>,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub handler_nmstate_version: Option<String>,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub host_network_manager_version: Option<String>,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub last_successful_update_time: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The NetworkState is the top-level struct, representing the entire
|
||||||
|
/// desired or current network state.
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct DesiredStateSpec {
|
#[serde(deny_unknown_fields)]
|
||||||
pub interfaces: Vec<InterfaceSpec>,
|
pub struct NetworkState {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub hostname: Option<HostNameState>,
|
||||||
|
#[serde(rename = "dns-resolver", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub dns: Option<DnsState>,
|
||||||
|
#[serde(rename = "route-rules", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub rules: Option<RouteRuleState>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub routes: Option<RouteState>,
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub interfaces: Vec<Interface>,
|
||||||
|
#[serde(rename = "ovs-db", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub ovsdb: Option<OvsDbGlobalConfig>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub ovn: Option<OvnConfiguration>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct InterfaceSpec {
|
pub struct HostNameState {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub running: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub config: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct DnsState {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub running: Option<DnsResolverConfig>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub config: Option<DnsResolverConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct DnsResolverConfig {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub search: Option<Vec<String>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub server: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct RouteRuleState {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub config: Option<Vec<RouteRule>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub running: Option<Vec<RouteRule>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct RouteState {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub config: Option<Vec<Route>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub running: Option<Vec<Route>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct RouteRule {
|
||||||
|
#[serde(rename = "ip-from", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub ip_from: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub priority: Option<u32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub route_table: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct Route {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub destination: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub metric: Option<u32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub next_hop_address: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub next_hop_interface: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub table_id: Option<u32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub mtu: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct OvsDbGlobalConfig {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub external_ids: Option<BTreeMap<String, String>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub other_config: Option<BTreeMap<String, String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct OvnConfiguration {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub bridge_mappings: Option<Vec<OvnBridgeMapping>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct OvnBridgeMapping {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub localnet: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub bridge: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub enum StpSpec {
|
||||||
|
Bool(bool),
|
||||||
|
Options(StpOptions),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct LldpState {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub enabled: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct OvsDb {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub external_ids: Option<BTreeMap<String, String>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub other_config: Option<BTreeMap<String, String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct PatchState {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub peer: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct Interface {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub r#type: String,
|
pub r#type: InterfaceType,
|
||||||
pub state: String,
|
pub state: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub mac_address: Option<String>,
|
pub mac_address: Option<String>,
|
||||||
@@ -99,9 +295,81 @@ pub struct InterfaceSpec {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub linux_bridge: Option<LinuxBridgeSpec>,
|
pub linux_bridge: Option<LinuxBridgeSpec>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[serde(alias = "bridge")]
|
||||||
pub ovs_bridge: Option<OvsBridgeSpec>,
|
pub ovs_bridge: Option<OvsBridgeSpec>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub ethtool: Option<EthtoolSpec>,
|
pub ethtool: Option<Value>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub accept_all_mac_addresses: Option<bool>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub identifier: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub lldp: Option<LldpState>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub permanent_mac_address: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub max_mtu: Option<u32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub min_mtu: Option<u32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub mptcp: Option<Value>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub profile_name: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub wait_ip: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub ovs_db: Option<OvsDb>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub driver: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub patch: Option<PatchState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, JsonSchema)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub enum InterfaceType {
|
||||||
|
#[serde(rename = "unknown")]
|
||||||
|
Unknown,
|
||||||
|
#[serde(rename = "dummy")]
|
||||||
|
Dummy,
|
||||||
|
#[serde(rename = "loopback")]
|
||||||
|
Loopback,
|
||||||
|
#[serde(rename = "linux-bridge")]
|
||||||
|
LinuxBridge,
|
||||||
|
#[serde(rename = "ovs-bridge")]
|
||||||
|
OvsBridge,
|
||||||
|
#[serde(rename = "ovs-interface")]
|
||||||
|
OvsInterface,
|
||||||
|
#[serde(rename = "bond")]
|
||||||
|
Bond,
|
||||||
|
#[serde(rename = "ipvlan")]
|
||||||
|
IpVlan,
|
||||||
|
#[serde(rename = "vlan")]
|
||||||
|
Vlan,
|
||||||
|
#[serde(rename = "vxlan")]
|
||||||
|
Vxlan,
|
||||||
|
#[serde(rename = "mac-vlan")]
|
||||||
|
Macvlan,
|
||||||
|
#[serde(rename = "mac-vtap")]
|
||||||
|
Macvtap,
|
||||||
|
#[serde(rename = "ethernet")]
|
||||||
|
Ethernet,
|
||||||
|
#[serde(rename = "infiniband")]
|
||||||
|
Infiniband,
|
||||||
|
#[serde(rename = "vrf")]
|
||||||
|
Vrf,
|
||||||
|
#[serde(rename = "veth")]
|
||||||
|
Veth,
|
||||||
|
#[serde(rename = "ipsec")]
|
||||||
|
Ipsec,
|
||||||
|
#[serde(rename = "hsr")]
|
||||||
|
Hrs,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for InterfaceType {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Loopback
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||||
@@ -287,11 +555,15 @@ pub struct OvsBridgeSpec {
|
|||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct OvsBridgeOptions {
|
pub struct OvsBridgeOptions {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub stp: Option<bool>,
|
pub stp: Option<StpSpec>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub rstp: Option<bool>,
|
pub rstp: Option<bool>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub mcast_snooping_enable: Option<bool>,
|
pub mcast_snooping_enable: Option<bool>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub datapath: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub fail_mode: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
||||||
@@ -305,18 +577,3 @@ pub struct OvsPortSpec {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub r#type: Option<String>,
|
pub r#type: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
|
||||||
#[serde(rename_all = "kebab-case")]
|
|
||||||
pub struct EthtoolSpec {
|
|
||||||
// TODO: Properly describe this spec (https://nmstate.io/devel/yaml_api.html#ethtool)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
|
|
||||||
#[serde(rename_all = "kebab-case")]
|
|
||||||
pub struct EthtoolFecSpec {
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub auto: Option<bool>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub mode: Option<String>,
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user
Rust is so beautiful. Type K infered from return type, completely transparent, super powerful, super readable, super intuitive.