netplan-types 0.8.0

Netplan configuration types as Rust structs and enums
Documentation
//! netplan-types
//!
//! ## Motivation
//! This crate attempts to map the entire netplan configuration into Rust structs and enums.
//! The 'layout' is as close to what you'd write in your netplan YAML files.
//!
//! The intented use of this crate is to allow for easy editing of the network configuration via the netplan
//! configuration files from a Rust program.
//!
//! Based on the documentation from netplan, which can be found [here](https://netplan.io/reference/)
//! Please note that I do not check the docs often for updates, if anything is missing or incorrect in the future,
//! please open an issue or a pull-request so the issue can be addressed.
//!
//! ## Features
//! - `serde`: \[Default\] Add serde support
//! - `derive_builder` Enable the derive_builder crate for an automatically generated builder pattern API
//! - `schemars`: Enable the schemars crate for generating a JSON schema from the structs

#[cfg(feature = "serde")]
mod bool;

mod netplan;
pub use netplan::*;

use std::collections::HashMap;

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

#[cfg(feature = "derive_builder")]
use derive_builder::Builder;

#[derive(Default, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "derive_builder", derive(Builder))]
#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct NetplanConfig {
    pub network: NetworkConfig,
}

#[derive(Default, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "derive_builder", derive(Builder))]
#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct NetworkConfig {
    pub version: u8,
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub renderer: Option<Renderer>,
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub ethernets: Option<HashMap<String, EthernetConfig>>,
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub modems: Option<HashMap<String, ModemConfig>>,
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub wifis: Option<HashMap<String, WifiConfig>>,
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub bonds: Option<HashMap<String, BondConfig>>,
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub bridges: Option<HashMap<String, BridgeConfig>>,
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub vlans: Option<HashMap<String, VlanConfig>>,
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub tunnels: Option<HashMap<String, TunnelConfig>>,
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub vrfs: Option<HashMap<String, VrfsConfig>>,
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub dummy_devices: Option<HashMap<String, DummyDeviceConfig>>,
}

/// Use the given networking backend for this definition. Currently supported are
/// networkd and NetworkManager. This property can be specified globally
/// in network:, for a device type (in e. g. ethernets:) or
/// for a particular device definition. Default is networkd.
///
/// (Since 0.99) The renderer property has one additional acceptable value for vlan
/// objects (i. e. defined in vlans:): sriov. If a vlan is defined with the
/// sriov renderer for an SR-IOV Virtual Function interface, this causes netplan to
/// set up a hardware VLAN filter for it. There can be only one defined per VF.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum Renderer {
    #[cfg_attr(feature = "serde", serde(rename = "networkd"))]
    Networkd,
    #[cfg_attr(feature = "serde", serde(rename = "NetworkManager"))]
    NetworkManager,
    #[cfg_attr(feature = "serde", serde(rename = "sriov"))]
    Sriov,
}

/// Takes a boolean, or the special value “route”. When true, the domain
/// name received from the DHCP server will be used as DNS search domain
/// over this link, similar to the effect of the Domains= setting. If set
/// to “route”, the domain name received from the DHCP server will be
/// used for routing DNS queries only, but not for searching, similar to
/// the effect of the Domains= setting when the argument is prefixed with
/// “~”.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UseDomains {
    Boolean(bool),
    Route,
}

#[cfg(feature = "serde")]
impl Serialize for UseDomains {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        match self {
            UseDomains::Boolean(b) => serializer.serialize_bool(*b),
            UseDomains::Route => serializer.serialize_str("route"),
        }
    }
}

#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for UseDomains {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        struct UseDomainsVisitor;

        impl serde::de::Visitor<'_> for UseDomainsVisitor {
            type Value = UseDomains;

            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                formatter.write_str("a boolean, or the string \"route\"")
            }

            fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                Ok(UseDomains::Boolean(v))
            }

            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                match v.to_lowercase().as_str() {
                    "true" | "yes" | "on" | "y" => Ok(UseDomains::Boolean(true)),
                    "false" | "no" | "off" | "n" => Ok(UseDomains::Boolean(false)),
                    "route" => Ok(UseDomains::Route),
                    _ => Err(serde::de::Error::unknown_variant(
                        v,
                        &["true", "false", "yes", "no", "on", "off", "y", "n", "route"],
                    )),
                }
            }
        }

        deserializer.deserialize_any(UseDomainsVisitor)
    }
}

#[cfg(feature = "schemars")]
impl schemars::JsonSchema for UseDomains {
    fn schema_name() -> std::borrow::Cow<'static, str> {
        "UseDomains".into()
    }

    fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
        schemars::json_schema!({
            "anyOf": [
                { "type": "boolean" },
                { "type": "string", "enum": ["route"] }
            ]
        })
    }
}

#[cfg(test)]
mod test {
    use crate::NetplanConfig;

    #[test]
    fn yaml_booleans() {
        let input = r#"
            network:
              version: 2
              ethernets:
                nics:
                  match:
                    name: ens*
                  dhcp4: on
                  dhcp6: N
            "#;

        let netplan_config: NetplanConfig = serde_yaml::from_str(&input).unwrap();
        let ethernets = netplan_config.network.ethernets.unwrap();
        let ethernet = ethernets.values().next().unwrap();

        assert!(ethernet.common_all.is_some());

        let common = ethernet.common_all.as_ref().unwrap();

        assert_eq!(common.dhcp4, Some(true));
    }
}