network-device 0.1.0

A strongly typed eDSL and IaC contracts for network device configuration
Documentation
  • Coverage
  • 100%
    53 out of 53 items documented1 out of 1 items with examples
  • Size
  • Source code size: 46.0 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 916.5 kB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 4s Average build duration of successful builds.
  • all releases: 2s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • kamyar1979/network-device-rust
    0 0 0
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • kamyar1979

network-device

network-device is a dependency-free Rust core for two distinct ways to express network configuration:

  • Operational eDSL: typed imperative operations executed immediately by a selected device driver.
  • Desired state: declarative resource values planned and applied by an infrastructure-as-code (IaC) backend.

The crate contains neutral domain values, standard operation/resource types, and open extension contracts. It contains no protocol clients, vendor payload or path abstraction, persistence, Terraform/Pulumi dependencies, or runtime.

Operational eDSL

Implement Driver and Execute<Operation> for each supported operation. The operation types are owned by the caller and consumed exactly once. The eDSL composes these operations with statically typed then; it executes in order, stops at the first error, and returns the last operation's output.

use network_device::{
    AdministrativeState, Driver, Execute, InterfaceName, Run, SetInterfaceState,
    operation,
};
use std::convert::Infallible;
use std::future::Future;

struct Device;

impl Driver for Device {
    type Error = Infallible;
}

impl Execute<SetInterfaceState> for Device {
    type Output = ();

    fn execute<'a>(
        &'a self,
        operation: SetInterfaceState,
    ) -> impl Future<Output = Result<Self::Output, Self::Error>> + Send + 'a {
        async move {
            let _interface = operation.interface;
            let _state = operation.state;
            Ok(())
        }
    }
}

async fn enable() -> Result<(), Infallible> {
    let interface = InterfaceName::try_from("Ethernet1/1").expect("valid interface name");
    operation(SetInterfaceState::new(interface, AdministrativeState::Enabled))
        .run(&Device)
        .await
}

Sequential composition is static and has no boxing or dynamic dispatch:

operation(SetInterfaceDescription::new(interface.clone(), "uplink"))
    .then(operation(SetInterfaceState::new(interface, AdministrativeState::Enabled)))
    .run(&driver)
    .await?;

Each downstream crate can define its own operation and implement Execute<CustomOperation> for a driver it owns, or implement a standard operation for its own driver. New operation kinds do not require changes to this crate.

Desired State and IaC

Desired-state resources are not imperative commands. For example, InterfaceConfiguration describes a target configuration, while SetInterfaceState asks a driver to perform an operation now. Both can use the same neutral values such as InterfaceName and AdministrativeState.

IacBackend<Resource> is generic over the resource type, not a closed enum. Each backend chooses its own plan and apply result types and implements only the resource types it supports:

use network_device::{
    AdministrativeState, IacBackend, InterfaceConfiguration, InterfaceName,
};
use std::convert::Infallible;
use std::future::Future;

struct ExampleBackend;

impl IacBackend<InterfaceConfiguration> for ExampleBackend {
    type Error = Infallible;
    type Plan = String;
    type ApplyResult = ();

    fn plan<'a>(
        &'a self,
        desired: &'a InterfaceConfiguration,
    ) -> impl Future<Output = Result<Self::Plan, Self::Error>> + Send + 'a {
        async move { Ok(format!("configure {}", desired.name)) }
    }

    fn apply<'a>(
        &'a self,
        _desired: &'a InterfaceConfiguration,
    ) -> impl Future<Output = Result<Self::ApplyResult, Self::Error>> + Send + 'a {
        async { Ok(()) }
    }
}

async fn apply() -> Result<(), Infallible> {
    let desired = InterfaceConfiguration::new(
        InterfaceName::try_from("Ethernet1/1").expect("valid interface name"),
    )
    .administrative_state(AdministrativeState::Enabled)
    .description("uplink");
    let backend = ExampleBackend;
    let _plan = backend.plan(&desired).await?;
    backend.apply(&desired).await
}

Potential implementation crates include network-device-nxapi, network-device-netconf, network-device-terraform, network-device-opentofu, and network-device-pulumi. Those names describe future integrations only; this crate implements none of them. Terraform and Pulumi have different semantics, so the shared IaC contract deliberately stops at generic plan and apply operations with backend-specific result types.

Base Types

  • InterfaceName rejects empty names and control characters but preserves vendor-specific spelling.
  • IpPrefix validates IPv4 and IPv6 prefix lengths.
  • StaticRoute validates that its destination and next hop use the same IP family.
  • Standard operations: SetInterfaceState, SetInterfaceDescription, EnsureStaticRoute, and RemoveStaticRoute.
  • Standard desired resources: InterfaceConfiguration and StaticRouteConfiguration.

These are shared semantic types, not a claim that every device protocol maps them to identical native paths or payloads.