network-device 0.2.0

A strongly typed eDSL and IaC contracts for network device configuration
Documentation
  • Coverage
  • 100%
    59 out of 59 items documented2 out of 2 items with examples
  • Size
  • Source code size: 53.7 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 954.5 kB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 1s 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
}

Connecting and authenticating a driver

Implement DriverConnector in the protocol/provider integration crate. The connector defines its own target, authentication, driver, and connection error types, and consumes the authentication value while constructing an authenticated driver. It does not retrieve credentials.

An external integration can use its existing CredentialProvider (for example, oss-providers::CredentialProvider) to resolve a CredentialMaterial, convert that material to the selected connector's Authentication type, and then call connect:

let credential = credential_provider.credential(query).await?;
let authentication = ProviderAuthentication::try_from(credential.material)?;
let driver = connector.connect(&target, authentication).await?;

operation(provider_operation).run(&driver).await?;

This keeps secret retrieval, leases, and renewal in the external credential integration while keeping protocol-specific authentication in the provider. network-device itself has no dependency on oss-providers, Vault, or any credential backend. The associated types and returned driver use static dispatch; the connector contract adds no Box, trait object, or shared-state wrapper.

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.