A Rust replacement for Juniper PyEZ — async-first Junos device automation built on rustnetconf.
PyEZ is the de facto Python library for Junos automation. It works, but:
- Slow at scale — synchronous, single-threaded. Managing hundreds of devices is painful
- Runtime errors — dynamic typing means bugs surface in production, not at compile time
- No real concurrency — threading is bolted on, not native
rustEZ gives you the same Junos automation capabilities with:
- 10-100x faster — async Rust with tokio for parallel operations across thousands of devices
- Compile-time safety — typed RPCs, typed facts, typed configs. Wrong RPC? The compiler tells you
- Native async concurrency —
tokio::join!across 1000 devices is one line of code
rustez/ Core library — Device, Facts, Config, RPC, operational data
rustez-cli/ CLI binary — Junos automation from the terminal
rustez-py/ Python bindings via PyO3 — pip install rustez
Built on rustnetconf for NETCONF transport, SSH (via russh), connection pooling, vendor profiles, and event notifications (RFC 5277).
use rustez::{Device, ConfigPayload};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Connect and gather facts
let mut dev = Device::connect("10.0.0.1")
.username("admin")
.password("secret")
.open()
.await?;
let facts = dev.facts().await?;
println!("{} running Junos {}", facts.hostname, facts.version);
// Push a config change
let mut config = dev.config()?;
config.lock().await?;
config.load(ConfigPayload::Text(
"system { host-name new-hostname; }".into()
)).await?;
if let Some(diff) = config.diff().await? {
println!("Changes:\n{diff}");
config.commit().await?;
}
config.unlock().await?;
// Run an operational RPC
let output = dev.cli("show interfaces terse").await?;
println!("{output}");
dev.close().await?;
Ok(())
}Some Junos platforms limit the number of concurrent NETCONF sessions. Exceeding the limit causes connection resets.
| Platform | Max Concurrent Sessions |
|---|---|
| vSRX | 3 |
| SRX (branch) | 3 |
| MX / EX / QFX | 8+ (varies by model) |
When automating multiple operations against the same device, keep your
concurrent connections within these limits. The v0.3 DevicePool will
auto-detect platform personality and enforce the correct ceiling
automatically.
# Gather device facts
rustez facts 10.0.0.1 -u admin -p secret
# Run a show command
rustez rpc 10.0.0.1 "show interfaces terse" -u admin
# Push a config
rustez config apply 10.0.0.1 -f config.set -u adminfrom rustez import Device
async def main():
dev = await Device.connect("10.0.0.1", username="admin", password="secret")
facts = await dev.facts()
print(f"{facts.hostname} running Junos {facts.version}")
await dev.close()PyPI wheels are published for Linux x86_64 only:
| Platform | Wheel | Status |
|---|---|---|
| Linux x86_64 (glibc) | manylinux |
Supported |
| Linux x86_64 (musl/Alpine) | musllinux_1_2 |
Supported |
| Linux aarch64 | — | Not supported |
| macOS / Windows | — | Not supported |
For unsupported platforms, build from source with maturin:
pip install maturin
git clone https://github.com/fastrevmd-lab/rustEZ.git
cd rustEZ && maturin build --release -m rustez-py/Cargo.toml
pip install target/wheels/*.whlVerified on a real device with all integration tests passing:
| Platform | Junos Version | NETCONF | Tests |
|---|---|---|---|
| vSRX | 24.4R1.9 | 1.0 (EOM) | connect, facts, cli, config load/diff/commit/rollback, RFC 5277 event notifications |
| Phase | Version | Scope |
|---|---|---|
| 1 | v0.1 | Device, Facts, RPC, Config (load/diff/commit/rollback) |
| 2 | v0.2 | Typed operational data (interfaces, routes, ARP, LLDP), CLI |
| 3 | v0.3 | Software management, filesystem, shell, SCP, DevicePool with per-platform session limits |
| 4 | v0.4 | Python bindings via PyO3 |
| 5 | v1.0 | YANG codegen, TUI, config drift detection, 1000+ device scale |
| Feature | PyEZ | rustEZ |
|---|---|---|
| Language | Python | Rust (with Python bindings) |
| Concurrency | Threading (painful) | Async/await (native) |
| Type safety | Runtime errors | Compile-time checks |
| NETCONF library | ncclient | rustnetconf (async, pure Rust) |
| SSH library | paramiko (OpenSSL) | russh (pure Rust) |
| Config templating | Jinja2 | Tera |
| Operational data | YAML Tables/Views | Typed Rust structs (serde) |
| Multi-vendor | No (Junos only) | No (Junos only) |
| Crate | Version | Purpose |
|---|---|---|
| rustnetconf | 0.10 | NETCONF client (SSH transport, RFC 6241/5277) |
| tokio | 1.50 | Async runtime |
| quick-xml | 0.37 | XML parsing |
| thiserror | 2.0 | Error derive macros |
| tracing | 0.1 | Structured logging |
| serial_test | 3.4 | Sequential integration tests (dev only) |
| Crate | Version | Purpose |
|---|---|---|
| pyo3 | 0.24 | Python FFI bindings |
| rustez | 0.9.0 | Core library |
| rustnetconf | 0.10 | NETCONF client |
| tokio | 1.50 | Async runtime |
Python runtime dependency: lxml >= 4.9.0
Last audited: 2026-05-06 via cargo audit (runs in CI on every PR)
| Severity | Crate | Advisory | Description | Fix Available |
|---|---|---|---|---|
| Medium (5.9) | rsa 0.10.0-rc.16 |
RUSTSEC-2023-0071 | Marvin Attack — potential key recovery through timing sidechannels | No upstream fix yet |
Transitive dependency through russh (used by rustnetconf for SSH transport). Not directly exploitable in rustEZ's use case — connections are to managed network devices, not public-facing services. Will resolve when upstream russh updates its dependency tree. Ignored in CI via cargo audit --ignore RUSTSEC-2023-0071.
Run cargo audit to check for the latest advisories.
Dual-licensed under MIT or Apache-2.0, at your option.