# 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.
```rust
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`:
```rust,ignore
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:
```rust,ignore
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:
```rust
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.