Skip to content

feat: Network device simulation infrastructure (containerlab + praxis + rust)#5

Closed
kayodebristol wants to merge 3 commits into
mainfrom
feature/netops-simulation-infra
Closed

feat: Network device simulation infrastructure (containerlab + praxis + rust)#5
kayodebristol wants to merge 3 commits into
mainfrom
feature/netops-simulation-infra

Conversation

@kayodebristol

Copy link
Copy Markdown
Contributor

Overview

Praxis-first architecture for realistic network device simulation and testing.

The Problem

  • Parsers break on firmware they haven't seen
  • Mock fixtures are hand-crafted and diverge from reality
  • No way to test advanced features (config push, topology discovery, rollback)
  • SSHDetect behavior varies by vendor and isn't tested

The Solution

Containerlab provides the ground truth (real NOS containers), fixture-recorder captures real responses, .px rules drive fast simulation, Rust actors handle IO.

What's included

Layer Files
Praxis logic netops-detect.px, netops-simulate.px, netops-scan.px, netops-record.px
Rust actors ssh_connect, device_command, snmp_query, ping, fixture_recorder, mock_ssh_server
Containerlab Nokia SRL + Arista cEOS + Cisco XRd + Juniper cRPD mesh topology
Docs docs/netops-simulation.md

Two test tiers

  1. Fast CI (every push): Praxis simulation rules replay recorded fixtures — seconds
  2. Full integration (nightly): Containerlab real NOS containers — minutes

Next steps

  • Wire actors into Cargo.toml (add ssh2, russh dependencies)
  • Implement mock_ssh_server handle_client (russh crate)
  • Record first fixtures from Nokia SR Linux (free image)
  • CI workflow for tier-1 tests
  • Record from nrush's Brocade MLXe for legacy support

… actors

Praxis-first architecture for network device testing:

.px files (logic):
  - netops-detect.px: vendor identification rules
  - netops-simulate.px: device simulation personalities
  - netops-scan.px: scan orchestration pipeline
  - netops-record.px: fixture recording workflow from containerlab/real devices

Rust actors (side effects only):
  - ssh_connect.rs: TCP + SSH handshake
  - device_command.rs: send/receive CLI commands
  - snmp_query.rs: SNMP GET/WALK
  - ping.rs: ICMP reachability
  - fixture_recorder.rs: save captured output to disk
  - mock_ssh_server.rs: simulated device SSH responder

Containerlab integration:
  - clab/netops-fixtures.yml: topology with Nokia SRL, Arista cEOS,
    Cisco XRd, Juniper cRPD in a mesh
  - configs/ for each device type
  - fixture-record command records real responses for simulation

Two test tiers:
  1. Fast CI: praxis simulation rules + fixture replay (seconds)
  2. Full integration: containerlab real NOS containers (minutes)

No hallucinated fixtures — everything recorded from real devices.
Copilot AI review requested due to automatic review settings May 21, 2026 18:00

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Introduces an initial “praxis-first” network device simulation/recording stack: Praxis rules for scan/detect/simulate/record flows, Rust-side IO actors (SSH, device command, ping, SNMP, fixture recording, mock SSH server skeleton), plus containerlab topology/config and documentation to support fixture capture + fast simulation-based testing.

Changes:

  • Added Praxis procedures/rules for device scanning, vendor detection, fixture recording, and SSH-driven simulation personalities.
  • Added Rust actor modules for SSH connect, command execution, ping sweep, SNMP querying (stub), fixture recording, and a mock SSH server (skeleton).
  • Added containerlab topology + per-vendor minimal configs, and docs describing the intended two-tier test approach.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 19 comments.

Show a summary per file
File Description
src-tauri/src/actors/ssh_connect.rs New SSH connect actor (TCP+SSH handshake/auth + banner/prompt capture).
src-tauri/src/actors/device_command.rs New “send command/read until prompt” SSH channel helper.
src-tauri/src/actors/snmp_query.rs New SNMP actor placeholder (currently stubbed).
src-tauri/src/actors/ping.rs New ping + ping sweep implementation using system ping.
src-tauri/src/actors/fixture_recorder.rs New fixture writer + fixture listing helpers.
src-tauri/src/actors/mock_ssh_server.rs New mock SSH server scaffolding + personality loader.
src-tauri/src/actors/mod.rs New actors module declaration and module-level documentation.
praxis/netops-simulate.px Simulation routing rules + personality definitions.
praxis/netops-scan.px Scan orchestration + SSH deep-scan procedure.
praxis/netops-record.px Fixture recording orchestration + containerlab batch recording procedure.
praxis/netops-detect.px Vendor/model/version/serial extraction rules from CLI output.
docs/netops-simulation.md Documentation for architecture + intended workflows.
clab/netops-fixtures.yml Containerlab topology for fixture recording/integration testing.
clab/configs/nokia-srl.cfg Minimal SR Linux config enabling SSH/SNMP/LLDP.
clab/configs/juniper-crpd.cfg Minimal Juniper cRPD config enabling SSH/SNMP/LLDP.
clab/configs/cisco-xrd.cfg Minimal Cisco XRd config enabling SSH/SNMP/LLDP.
clab/configs/arista-eos.cfg Minimal Arista cEOS config enabling SNMP/LLDP (and mgmt API).

Comment on lines +30 to +40
pub fn ping_sweep(hosts: &[String], concurrency: usize, timeout: Duration) -> Vec<String> {
use std::sync::mpsc;
use std::thread;

let (tx, rx) = mpsc::channel();
let hosts: Vec<String> = hosts.to_vec();

// Simple thread pool
let chunk_size = (hosts.len() + concurrency - 1) / concurrency;
for chunk in hosts.chunks(chunk_size) {
let chunk = chunk.to_vec();
Comment on lines +12 to +17
let count_flag = if cfg!(windows) { "-n" } else { "-c" };
let timeout_flag = if cfg!(windows) { "-w" } else { "-W" };
let timeout_val = if cfg!(windows) {
format!("{}", timeout.as_millis())
} else {
format!("{}", timeout.as_secs())
Comment on lines +45 to +48
let tcp = TcpStream::connect_timeout(
&format!("{}:{}", params.host, params.port).parse()?,
params.timeout,
)?;
Comment thread src-tauri/src/actors/ssh_connect.rs Outdated
Comment on lines +74 to +85
// Open channel and capture banner
let mut channel = session.channel_session()?;
channel.request_pty("xterm", None, None)?;
channel.shell()?;

// Read initial banner/prompt
let mut banner = String::new();
let mut buf = [0u8; 4096];
std::thread::sleep(Duration::from_millis(500));
if let Ok(n) = channel.read(&mut buf) {
banner = String::from_utf8_lossy(&buf[..n]).to_string();
}
Comment thread src-tauri/src/actors/ssh_connect.rs Outdated
Comment on lines +56 to +68
// Enable legacy algorithms for old devices (Brocade, old Cisco)
// ssh2 crate uses libssh2 which still supports group14-sha1
session.handshake()?;

// Authenticate
if !params.use_keys {
session.userauth_password(&params.username, &params.password)?;
} else {
// Try key-based auth first, fall back to password
if session.userauth_agent(&params.username).is_err() {
session.userauth_password(&params.username, &params.password)?;
}
}
Comment thread praxis/netops-scan.px
Comment on lines +60 to +73
step compile_inventory {
for host in reachable_hosts {
let device = merge(
snmp_results[host] ?? {},
enriched_devices[host] ?? {},
);
// Run detection rules against the raw output
let detection = invoke netops.detect.detect_vendor(output: device.raw_output ?? "");
if detection.vendor != "unknown" {
device.vendor = detection.vendor;
}
}
emit inventory = devices;
}
Comment thread praxis/netops-simulate.px
Comment on lines +41 to +69
// Unknown command — vendor-appropriate error
otherwise {
emit response = format_error(input.personality.vendor, input.command);
emit prompt = input.personality.prompt;
}
}

// --- Error response formatting (vendor-specific) ---

rule format_error {
when input.vendor_family == "cisco" {
emit error = "% Invalid input detected at '^' marker.";
}
when input.vendor_family == "brocade" {
emit error = "Invalid input -> " + input.command;
}
when input.vendor_family == "juniper" {
emit error = "unknown command.";
}
when input.vendor_family == "nokia" {
emit error = "Error: Bad command.";
}
when input.vendor_family == "arista" {
emit error = "% Invalid input";
}
otherwise {
emit error = "% Unknown command: " + input.command;
}
}
Comment thread docs/netops-simulation.md
Comment on lines +71 to +101
# Record fixtures from all devices
cargo run -- fixture-record --topology clab/netops-fixtures.yml

# Destroy lab when done
clab destroy -t clab/netops-fixtures.yml
```

### Record from real devices (legacy gear)

```bash
# Record from nrush's Brocade MLXe
cargo run -- fixture-record \
--host 10.221.0.185 \
--user admin \
--vendor-hint brocade_fastiron \
--output fixtures/brocade-ironware-6.3.0e/
```

### Run fast CI tests (no containers needed)

```bash
cargo test --workspace
```

### Run full integration tests (containerlab required)

```bash
clab deploy -t clab/netops-fixtures.yml
cargo test --features integration
clab destroy -t clab/netops-fixtures.yml
```
Comment thread clab/netops-fixtures.yml
Comment on lines +13 to +16
# Usage:
# clab deploy -t clab/netops-fixtures.yml
# pares-bastion fixture-record --topology clab/netops-fixtures.yml
# clab destroy -t clab/netops-fixtures.yml
Comment on lines +24 to +28
/// Result of a successful SSH connection.
pub struct SshConnection {
pub session: ssh2::Session,
pub banner: String,
pub prompt: String,
Added dependencies:
  - ssh2 0.9 (SSH client for device connections)
  - russh 0.46 + russh-keys 0.46 (async SSH server for mock devices)
  - chrono 0.4 (timestamp fixtures)

All actors compile cleanly:
  - ssh_connect.rs: full SSH client with legacy KEX support
  - device_command.rs: send command, read until prompt
  - snmp_query.rs: shell-out to snmpget (portable)
  - ping.rs: ICMP via system ping command
  - fixture_recorder.rs: write/load fixture files
  - mock_ssh_server.rs: personality loading + command matching (server impl pending)

cargo check passes (0 new errors, 8 pre-existing warnings).
- WORK_PLAN.md: full architecture, naming conventions, dev standards
- 5 fixture files from nrush's real devices + netops-toolkit test suite
- Ready for parallel subagent work streams
@kayodebristol

Copy link
Copy Markdown
Contributor Author

Closing: CI blocked on unpublished @plures/design-dojo ESLint plugin. This feature branch has been stale since May 21. Will recreate when the shared ESLint config is published.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants