rneter 0.4.5

SSH connection manager for network devices with intelligent state machine handling
Documentation

rneter

Crates.io Documentation License: MIT

中文文档

rneter is a Rust library for managing SSH connections to network devices and Linux hosts with an explicit prompt-state-machine execution model. Its design is inspired by libraries such as Netmiko and Scrapli, and it serves a similar problem space, while focusing more heavily on formal state transitions, reusable interactive flows, transactions, and replayable automation workflows.

Table of Contents

Features

  • Connection Pooling: Automatically caches and reuses SSH connections for better performance
  • State Machine Management: Intelligent device state tracking and automatic transitions
  • Prompt Detection: Automatic prompt recognition and handling across different device types
  • Mode Switching: Seamless transitions between device modes (user mode, enable mode, config mode, etc.)
  • Lifecycle Hooks: Declarative setup and cleanup operations after connect, before disconnect, and around state transitions
  • Template Autodetect: Rank built-in templates by scored probe matches before creating a full state-machine session
  • SFTP File Uploads: Upload local files to remote hosts that expose the SSH sftp subsystem
  • Built-in Copy Flow Templates: Reuse structured templates for Cisco-like interactive copy workflows
  • Maximum Compatibility: Supports a wide range of SSH algorithms including legacy protocols for older devices
  • Async/Await: Built on Tokio for high-performance asynchronous operations
  • Error Handling: Comprehensive error types with detailed context

Installation

Add this to your Cargo.toml:

[dependencies]
rneter = "0.4.4"

Quick Start

use rneter::session::{ConnectionRequest, ExecutionContext, MANAGER, Command, CmdJob};
use rneter::templates;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Use a predefined device template (e.g., Cisco)
    let handler = templates::cisco()?;

    // Get a connection from the manager
    let sender = MANAGER
        .get_with_context(
            ConnectionRequest::new(
                "admin".to_string(),
                "192.168.1.1".to_string(),
                22,
                "password".to_string(),
                None,
                handler,
            ),
            ExecutionContext::default(),
        )
        .await?;

    // Execute a command
    let (tx, rx) = tokio::sync::oneshot::channel();
    let cmd = CmdJob {
        data: Command {
            mode: "Enable".to_string(), // Cisco template uses "Enable" mode
            command: "show version".to_string(),
            timeout: Some(60),
            ..Command::default()
        },
        sys: None,
        responder: tx,
    };

    sender.send(cmd).await?;
    let output = rx.await??;

    println!("Command successful: {}", output.success);
    println!("Output: {}", output.content);
    Ok(())
}

Linux Server Management

rneter supports Linux server management with flexible privilege escalation:

use rneter::session::{ConnectionRequest, ExecutionContext, MANAGER, Command, CmdJob};
use rneter::templates;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Configure Linux template with sudo password
    let mut handler = templates::linux()?;
    handler.dyn_param.insert(
        "SudoPassword".to_string(),
        "your_sudo_password".to_string()
    );

    // Connect to Linux server
    let sender = MANAGER
        .get_with_context(
            ConnectionRequest::new(
                "user".to_string(),
                "192.168.1.100".to_string(),
                22,
                "ssh_password".to_string(),
                None,
                handler,
            ),
            ExecutionContext::default(),
        )
        .await?;

    // Execute command as regular user
    let (tx, rx) = tokio::sync::oneshot::channel();
    sender.send(CmdJob {
        data: Command {
            mode: "User".to_string(),
            command: "ls -la /home".to_string(),
            timeout: Some(30),
            ..Command::default()
        },
        sys: None,
        responder: tx,
    }).await?;
    let output = rx.await??;
    println!("Output: {}", output.content);

    // Execute command with sudo (single command privilege escalation)
    let (tx, rx) = tokio::sync::oneshot::channel();
    sender.send(CmdJob {
        data: Command {
            mode: "User".to_string(),
            command: "sudo systemctl status nginx".to_string(),
            timeout: Some(30),
            ..Command::default()
        },
        sys: None,
        responder: tx,
    }).await?;
    let output = rx.await??;
    println!("Nginx status: {}", output.content);

    // Switch to persistent root shell
    let (tx, rx) = tokio::sync::oneshot::channel();
    sender.send(CmdJob {
        data: Command {
            mode: "Root".to_string(),  // Automatically executes sudo -i
            command: "systemctl restart nginx".to_string(),
            timeout: Some(30),
            ..Command::default()
        },
        sys: None,
        responder: tx,
    }).await?;
    let output = rx.await??;
    println!("Restart result: {}", output.content);

    Ok(())
}

LinuxTemplateConfig.shell_flavor defaults to DeviceShellFlavor::Posix. If the remote login shell is fish, set it explicitly to DeviceShellFlavor::Fish.

Custom Configuration:

use rneter::device::DeviceShellFlavor;
use rneter::templates::{linux_with_config, LinuxTemplateConfig, SudoMode, CustomPrompts};

// Use sudo -s instead of sudo -i
let config = LinuxTemplateConfig {
    sudo_mode: SudoMode::SudoShell,
    sudo_password: Some("password".to_string()),
    custom_prompts: None,
    ..LinuxTemplateConfig::default()
};
let handler = linux_with_config(config)?;

// Custom prompt patterns
let config = LinuxTemplateConfig {
    sudo_mode: SudoMode::SudoInteractive,
    sudo_password: Some("password".to_string()),
    custom_prompts: Some(CustomPrompts {
        user_prompts: vec![r"^myuser@myhost\$\s*$"],
        root_prompts: vec![r"^root@myhost#\s*$"],
    }),
    ..LinuxTemplateConfig::default()
};
let handler = linux_with_config(config)?;

// Force fish-compatible exit-status capture
let config = LinuxTemplateConfig {
    shell_flavor: DeviceShellFlavor::Fish,
    ..LinuxTemplateConfig::default()
};
let handler = linux_with_config(config)?;

File Uploads

If the remote host enables the SSH sftp subsystem, rneter can upload local files over the same authenticated SSH connection:

use rneter::session::{ConnectionRequest, ExecutionContext, FileUploadRequest, MANAGER};
use rneter::templates;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let handler = templates::linux()?;

    MANAGER
        .upload_file_with_context(
            ConnectionRequest::new(
                "user".to_string(),
                "192.168.1.100".to_string(),
                22,
                "ssh_password".to_string(),
                None,
                handler,
            ),
            FileUploadRequest::new(
                "./artifacts/config.backup".to_string(),
                "/tmp/config.backup".to_string(),
            )
            .with_timeout_secs(30)
            .with_buffer_size(16 * 1024)
            .with_progress_reporting(true),
            ExecutionContext::default(),
        )
        .await?;

    Ok(())
}

This path requires SFTP support on the remote host. For devices that only expose CLI-driven transfer commands such as copy scp: or copy tftp:, build a transfer flow from templates and execute it through the generic command-flow API.

Network Device SCP/TFTP Transfers

For Cisco-like CLIs, rneter ships a built-in reusable copy template. Render it with runtime variables, then execute the resulting CommandFlow through the generic command-flow API:

use rneter::session::{ConnectionRequest, ExecutionContext, MANAGER};
use rneter::templates::{self, cisco_like_copy_template, CommandFlowTemplateRuntime};
use serde_json::json;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let flow = cisco_like_copy_template().to_command_flow(
        &CommandFlowTemplateRuntime::new()
            .with_default_mode("Enable")
            .with_vars(json!({
                "command": "copy scp: flash:/image.bin",
                "server_addr": "198.51.100.20",
                "remote_path": "/pub/image.bin",
                "transfer_username": "deploy",
                "transfer_password": "secret",
            })),
    )?;

    let result = MANAGER
        .execute_command_flow_with_context(
            ConnectionRequest::new(
                "admin".to_string(),
                "192.168.1.1".to_string(),
                22,
                "password".to_string(),
                None,
                templates::cisco()?,
            ),
            flow,
            ExecutionContext::default(),
        )
        .await?;

    if let Some(last) = result.outputs.last() {
        println!("Transfer output: {}", last.content);
    }
    Ok(())
}

This built-in template matches the prompt style used by cisco, cisco_asa, cisco_nxos, arista, aruba_aoscx, chaitin, dell_os10, maipu, ruijie, venustech, and zte_zxros. If a vendor wizard differs, build another CommandFlowTemplate on top of the same abstraction. The template intentionally avoids input-side conditional branches: pass the exact command plus shared prompt vars (server_addr, remote_path, and optional credentials).

Structured Command-Flow Templates

If you want a less hard-coded workflow, build a reusable CommandFlowTemplate in Rust. The current model is intentionally linear: each step sends one command, answers any expected prompts, and then continues to the next step. There is no output-side branching layer to maintain.

use rneter::templates::{
    CommandFlowTemplate, CommandFlowTemplatePrompt, CommandFlowTemplateRuntime,
    CommandFlowTemplateStep, CommandFlowTemplateVar,
};
use serde_json::json;

let template = CommandFlowTemplate::new(
    "copy_with_verify",
    vec![
        CommandFlowTemplateStep::from_template("copy {{protocol}}: {{device_path}}")
            .with_prompts(vec![
                CommandFlowTemplatePrompt::from_template(
                    vec![r"(?i)^Address or name of remote host.*\?\s*$".to_string()],
                    "{{server_addr}}",
                )
                .with_append_newline(true),
                CommandFlowTemplatePrompt::from_template(
                    vec![r"(?i)^Source (?:file ?name|filename).*\?\s*$".to_string()],
                    "{{remote_path}}",
                )
                .with_append_newline(true),
            ]),
        CommandFlowTemplateStep::from_template("verify /md5 {{device_path}}"),
    ],
)
.with_default_mode("Enable")
.with_vars(vec![
    CommandFlowTemplateVar::new("protocol")
        .with_label("Transfer Protocol")
        .with_description("Transfer protocol used by the device-side copy workflow.")
        .with_required(true)
        .with_options(["scp", "tftp"]),
    CommandFlowTemplateVar::new("server_addr")
        .with_label("Server Address")
        .with_description("SCP/TFTP server reachable from the target device.")
        .with_required(true),
    CommandFlowTemplateVar::new("remote_path")
        .with_label("Remote Path")
        .with_description("Remote file path that the device should fetch.")
        .with_required(true),
    CommandFlowTemplateVar::new("device_path")
        .with_label("Device Path")
        .with_description("Destination path on the target device.")
        .with_required(true),
]);

let flow = template.to_command_flow(
    &CommandFlowTemplateRuntime::new()
        .with_default_mode("Enable")
        .with_vars(json!({
            "protocol": "scp",
            "server_addr": "198.51.100.20",
            "remote_path": "/pub/image.bin",
            "device_path": "flash:/image.bin",
        })),
)?;

The built-in cisco_like_copy_template() uses the same abstraction, so future http, ftp, or vendor-specific copy wizards can stay in one structured template layer instead of adding more one-off Rust structs.

Custom Interactive Command Flows

If a device workflow needs multiple commands or prompt patterns that are not baked into a template, build a CommandFlow directly and attach runtime PromptResponseRules to each step:

use rneter::session::{
    Command, CommandFlow, CommandInteraction, ConnectionRequest, ExecutionContext, MANAGER,
    PromptResponseRule,
};
use rneter::templates;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let flow = CommandFlow::new(vec![Command {
        mode: "Enable".to_string(),
        command: "copy http: flash:/image.bin".to_string(),
        timeout: Some(600),
        interaction: CommandInteraction::default()
            .push_prompt(PromptResponseRule::new(
                vec![r"(?i)^Address or name of remote host.*\?\s*$".to_string()],
                "203.0.113.10\n".to_string(),
            ))
            .push_prompt(PromptResponseRule::new(
                vec![r"(?i)^Source (?:file ?name|filename).*\?\s*$".to_string()],
                "/pub/image.bin\n".to_string(),
            ))
            .push_prompt(
                PromptResponseRule::new(
                    vec![r"(?i)^Destination (?:file ?name|filename).*\?\s*$".to_string()],
                    "\n".to_string(),
                )
                .with_record_input(true),
            ),
        ..Command::default()
    },
    Command {
        mode: "Enable".to_string(),
        command: "verify /md5 flash:/image.bin".to_string(),
        timeout: Some(300),
        ..Command::default()
    }]);

    let result = MANAGER
        .execute_command_flow_with_context(
            ConnectionRequest::new(
                "admin".to_string(),
                "192.168.1.1".to_string(),
                22,
                "password".to_string(),
                None,
                templates::cisco()?,
            ),
            flow,
            ExecutionContext::default(),
        )
        .await?;

    if let Some(last) = result.outputs.last() {
        println!("Last step output: {}", last.content);
    }
    Ok(())
}

Runtime prompt-response rules are evaluated before template static input rules, so new SCP/TFTP/HTTP style wizards can usually be added without changing the underlying template definition. Each flow then continues step-by-step in declaration order, which keeps device copy workflows predictable and easier to review.

Security Levels

rneter now supports secure defaults and configurable SSH security levels when connecting:

use rneter::session::{
    ConnectionRequest, ConnectionSecurityOptions, ExecutionContext, MANAGER,
};
use rneter::templates;

// Secure by default (uses known_hosts verification + strict algorithms)
let _sender = MANAGER
    .get_with_context(
        ConnectionRequest::new(
            "admin".to_string(),
            "192.168.1.1".to_string(),
            22,
            "password".to_string(),
            None,
            templates::cisco()?,
        ),
        ExecutionContext::default(),
    )
    .await?;

// Explicitly choose a security profile
let _sender = MANAGER
    .get_with_context(
        ConnectionRequest::new(
            "admin".to_string(),
            "192.168.1.1".to_string(),
            22,
            "password".to_string(),
            None,
            templates::cisco()?,
        ),
        ExecutionContext::new()
            .with_security_options(ConnectionSecurityOptions::legacy_compatible()),
    )
    .await?;

Session Recording and Replay

use rneter::session::{
    ConnectionRequest, ExecutionContext, MANAGER, SessionRecordLevel, SessionReplayer,
};
use rneter::templates;

let (sender, recorder) = MANAGER
    .get_with_recording_level_and_context(
        ConnectionRequest::new(
            "admin".to_string(),
            "192.168.1.1".to_string(),
            22,
            "password".to_string(),
            None,
            templates::cisco()?,
        ),
        ExecutionContext::default(),
        SessionRecordLevel::Full,
    )
    .await?;

// Subscribe to future recorder events in real time
let mut rx = recorder.subscribe();
tokio::spawn(async move {
    while let Ok(entry) = rx.recv().await {
        println!("live event: {:?}", entry.event);
    }
});

// Or record key events only (no raw shell chunks)
let (_sender2, _recorder2) = MANAGER
    .get_with_recording_level_and_context(
        ConnectionRequest::new(
            "admin".to_string(),
            "192.168.1.1".to_string(),
            22,
            "password".to_string(),
            None,
            templates::cisco()?,
        ),
        ExecutionContext::default(),
        SessionRecordLevel::KeyEventsOnly,
    )
    .await?;

// ...send CmdJob through `sender`...

// Export recording as JSONL
let jsonl = recorder.to_jsonl()?;

// Restore and replay offline
let restored = rneter::session::SessionRecorder::from_jsonl(&jsonl)?;
let mut replayer = SessionReplayer::from_recorder(&restored);
let replayed_output = replayer.replay_next("show version")?;
println!("Replayed output: {}", replayed_output.content);

// Offline command-flow testing without real SSH
let script = vec![
    rneter::session::Command {
        mode: "Enable".to_string(),
        command: "terminal length 0".to_string(),
        timeout: None,
        ..rneter::session::Command::default()
    },
    rneter::session::Command {
        mode: "Enable".to_string(),
        command: "show version".to_string(),
        timeout: None,
        ..rneter::session::Command::default()
    },
];
let outputs = replayer.replay_script(&script)?;
assert_eq!(outputs.len(), 2);

Transactional Command Blocks

For mutable workflows, execute a block with explicit RollbackPolicy:

use rneter::session::{
    Command, CommandFlow, ConnectionRequest, ExecutionContext, MANAGER,
    RollbackPolicy, SessionOperation, TxBlock, TxStep,
};
use rneter::templates::{self, cisco_like_copy_template, CommandFlowTemplateRuntime};

let block = TxBlock {
    name: "addr-create".to_string(),
    rollback_policy: RollbackPolicy::WholeResource {
        rollback: Box::new(
            Command {
                mode: "Config".to_string(),
                command: "no object network WEB01".to_string(),
                timeout: Some(30),
                ..Command::default()
            }
            .into(),
        ),
        trigger_step_index: 0,
    },
    steps: vec![
        TxStep::new(Command {
            mode: "Config".to_string(),
            command: "object network WEB01".to_string(),
            timeout: Some(30),
            ..Command::default()
        }),
        TxStep::new(CommandFlow::new(vec![
            Command {
                mode: "Config".to_string(),
                command: "host 10.0.0.10".to_string(),
                timeout: Some(30),
                ..Command::default()
            },
            Command {
                mode: "Config".to_string(),
                command: "description WEB01".to_string(),
                timeout: Some(30),
                ..Command::default()
            },
        ])),
    ],
    fail_fast: true,
};

let result = MANAGER
    .execute_tx_block_with_context(
        ConnectionRequest::new(
            "admin".to_string(),
            "192.168.1.1".to_string(),
            22,
            "password".to_string(),
            None,
            templates::cisco()?,
        ),
        block,
        ExecutionContext::default(),
    )
    .await?;
println!(
    "committed={}, rollback_succeeded={}",
    result.committed, result.rollback_succeeded
);

TxStep::new(...) now accepts any SessionOperation, so a workflow step can be a single command, a multi-step CommandFlow, or a reusable template invocation:

let copy_step = TxStep::new(SessionOperation::template(
    cisco_like_copy_template(),
    CommandFlowTemplateRuntime::new().with_vars(serde_json::json!({
        "command": "copy scp: flash:/fw.bin",
        "server_addr": "192.168.1.100",
        "remote_path": "/srv/images/fw.bin",
        "transfer_username": "deploy",
        "transfer_password": "secret",
    })),
));

let summary = copy_step.run.summary()?;
println!(
    "kind={} mode={} steps={} desc={}",
    summary.kind, summary.mode, summary.step_count, summary.description
);

For multi-block all-or-nothing workflows (for example addresses -> services -> policy):

use rneter::session::{TxWorkflow, TxWorkflowResult};

let workflow = TxWorkflow {
    name: "fw-policy-publish".to_string(),
    blocks: vec![addr_block, svc_block, policy_block],
    fail_fast: true,
};

let workflow_result: TxWorkflowResult = MANAGER
    .execute_tx_workflow_with_context(
        ConnectionRequest::new(
            "admin".to_string(),
            "192.168.1.1".to_string(),
            22,
            "password".to_string(),
            None,
            templates::cisco()?,
        ),
        workflow,
        ExecutionContext::default(),
    )
    .await?;

for block in &workflow_result.block_results {
    for step in &block.step_results {
        println!(
            "step[{}] op={} execution={:?} rollback={:?}",
            step.step_index,
            step.operation_summary,
            step.execution_state,
            step.rollback_state
        );
        for child in &step.forward_operation_steps {
            println!(
                "  forward_step[{}] op={} success={}",
                child.step_index, child.operation_summary, child.success
            );
        }
        for child in &step.rollback_operation_steps {
            println!(
                "  rollback_step[{}] op={} success={}",
                child.step_index, child.operation_summary, child.success
            );
        }
    }
    if let Some(block_rollback) = &block.block_rollback_operation_summary {
        println!("block_rollback={block_rollback}");
        for child in &block.block_rollback_steps {
            println!(
                "  block_rollback_step[{}] op={} success={}",
                child.step_index, child.operation_summary, child.success
            );
        }
    }
}

You can also build blocks from template strategies:

let cmds = vec![
    "object network WEB01".to_string(),
    "host 10.0.0.10".to_string(),
];
let block = templates::build_tx_block(
    "cisco",
    "addr-create",
    "Config",
    &cmds,
    Some(30),
    Some("no object network WEB01".to_string()), // whole-resource rollback
)?;

For CI-style offline tests, you can store JSONL recordings under tests/fixtures/ and replay them in integration tests (see tests/replay_fixtures.rs).

To normalize noisy online recordings into stable fixtures:

cargo run --example normalize_fixture -- raw_session.jsonl tests/fixtures/session_new.jsonl

Template and State-Machine Ecosystem

You can manage built-in templates as a catalog and run state-graph diagnostics:

use rneter::templates;

let names = templates::available_templates();
assert!(names.contains(&"cisco"));

let _handler = templates::by_name("juniper")?; // case-insensitive

let report = templates::diagnose_template("cisco")?;
println!("has issues: {}", report.has_issues());
println!("dead ends: {:?}", report.dead_end_states);

let catalog = templates::template_catalog();
println!("template count: {}", catalog.len());

let all_json = templates::diagnose_all_templates_json()?;
println!("all diagnostics json bytes: {}", all_json.len());

You can also export a built-in template configuration, extend it, and build your own handler:

use rneter::device::prompt_rule;
use rneter::templates;

let mut config = templates::by_name_config("cisco")?;
config
    .prompt
    .push(prompt_rule("CustomMode", &[r"^custom>\s*$"]));

let handler = config.build()?;
assert!(handler.states().iter().any(|state| state == "custommode"));

New recording/replay capabilities:

  • Prompt tracking: each command_output now records both prompt_before/prompt_after
  • FSM prompt tracking: each event can include fsm_prompt_before/fsm_prompt_after
  • Output prompt: command/replay results now include Output.prompt
  • Transaction lifecycle recording: tx_block_started, tx_step_succeeded, tx_step_failed, tx_rollback_started, tx_rollback_step_succeeded, tx_rollback_step_failed, tx_block_finished
  • Schema compatibility: legacy connection_established fields (prompt/state) remain readable
  • Fixture quality workflow: tests/fixtures/ includes success/failure/state-switch samples and snapshot checks in tests/replay_fixtures.rs

Example command_output event shape:

{
  "kind": "command_output",
  "command": "show version",
  "mode": "Enable",
  "prompt_before": "router#",
  "prompt_after": "router#",
  "fsm_prompt_before": "enable",
  "fsm_prompt_after": "enable",
  "success": true,
  "content": "Version 1.0",
  "all": "show version\nVersion 1.0\nrouter#"
}

Example transaction lifecycle event shape:

{
  "kind": "tx_block_finished",
  "block_name": "addr-create",
  "committed": false,
  "rollback_attempted": true,
  "rollback_succeeded": true
}

Architecture

Connection Management

The SshConnectionManager provides a singleton connection pool accessible via the MANAGER constant. It automatically:

  • Caches connections for 5 minutes of inactivity
  • Reconnects on connection failure
  • Manages up to 100 concurrent connections

State Machine

The DeviceHandler implements a finite state machine that:

  • Tracks the current device state using regex patterns
  • Finds optimal paths between states using BFS
  • Handles automatic state transitions
  • Supports system-specific states (e.g., different VRFs or contexts)

Design Rationale

The state machine is designed around two stable facts in network-device automation:

  1. Prompts are more reliable than command text for identifying current mode.
  2. Transition paths vary by vendor/model, so pathfinding must be data-driven.

Core design choices:

  • Normalize states to lowercase and map prompt regex matches to state indexes for fast lookups.
  • Separate prompt detection (read_prompt) from state update (read) to keep command loops predictable.
  • Model transitions as a directed graph (edges) and use BFS to find shortest valid mode switch path.
  • Keep dynamic input handling (read_need_write) independent from command logic, so password/confirm flows are reusable.
  • Track both CLI prompt text and FSM prompt (state name) to support online diagnostics and offline replay assertions.

Benefits:

  • Better portability: vendor-specific behavior is mostly data configuration, not hard-coded branches.
  • Better resilience: command execution relies on prompt/state convergence instead of fixed output formats.
  • Better testability: record/replay can validate state transitions and prompt evolution without real SSH sessions.

State Transition Model

flowchart LR
    O["Output"] --> L["Login Prompt"]
    L -->|enable| E["Enable Prompt"]
    E -->|configure terminal| C["Config Prompt"]
    C -->|exit| E
    E -->|exit| L
    E -->|show ...| E
    C -->|show ... / set ...| C

Command Execution Flow (State-Aware)

flowchart TD
    A["Receive Command(mode, command, timeout)"] --> B["Read current FSM prompt/state"]
    B --> C["BFS transition planning: trans_state_write(target_mode)"]
    C --> D["Execute transition commands sequentially"]
    D --> E["Execute target command"]
    E --> F["Read stream chunks -> update handler.read(line)"]
    F --> G{"Prompt matched?"}
    G -->|No| F
    G -->|Yes| H["Build Output(success, content, all, prompt)"]
    H --> I["Record event: prompt_before/after + fsm_prompt_before/after"]

Command Execution

Commands are executed through an async channel-based architecture:

  1. Submit a CmdJob to the connection sender
  2. The library automatically transitions to the target state if needed
  3. Executes the command and waits for the prompt
  4. Returns the output with success status

Mode names supplied by callers are normalized to lowercase internally, so "Enable", "enable", and "ENABLE" target the same FSM state.

Lifecycle Hooks

rneter now supports declarative lifecycle hooks through DeviceHandlerConfig.hooks:

  • after_connect
  • before_disconnect
  • after_enter_state
  • before_exit_state

Hooks reuse SessionOperation, so they can run either a single command or a command flow. In 0.4.4, connection-level hooks are template-scoped so they remain stable under connection caching, while state-scoped hooks are normalized against the internal lowercase FSM state names.

Built-in templates can ship sensible defaults. For example:

  • Cisco/ASA runs terminal pager 0 after connect
  • Juniper runs set cli screen-length 0 after connect

Hook output does not get merged into the parent command result, but hook lifecycle events are recorded by the session recorder.

Template Autodetect

rneter can now score built-in templates before you commit to a concrete DeviceHandler.

The autodetect result is a ranked report, not a single opaque answer:

  • best_match
  • candidates
  • raw_facts

This makes it easier to understand why a device looks like Cisco, Juniper, Huawei, H3C, Linux, Arista, Aruba AOS-CX, Cisco ASA/NX-OS, Dell OS10, Ruijie, ZTE ZXROS, Fortinet, Palo Alto, or Check Point, and to debug ambiguous results in mixed environments.

Current scope:

  • SSH only
  • built-in templates currently covered: cisco, juniper, huawei, h3c, linux, hillstone, arista, aruba_aoscx, cisco_asa, cisco_nxos, dell_os10, fortinet, paloalto, ruijie, zte_zxros, checkpoint
  • cisco_asa is exposed as a distinct template name and autodetect target, but it currently reuses the proven cisco handler behavior
  • probe-driven scoring using initial prompt/output plus cached read-only probe commands

How to read the diagnostics:

  • raw_facts now includes both positive matches and probe-level error matches.
  • A positive fact means a prompt or probe output matched a scoring regex and contributed weight.
  • An error fact means the probe output matched an invalid-command pattern such as Invalid input, Unrecognized command, or command not found; that probe is then ignored for scoring, similar to Netmiko's autodetect behavior.
  • This makes it easier to tell the difference between "this device does not look like Cisco" and "the Cisco probe command was not accepted here".

Example shape:

use rneter::session::{DetectRequest, ExecutionContext};
use rneter::templates::autodetect_with_context;

# async fn demo() -> Result<(), Box<dyn std::error::Error>> {
let report = autodetect_with_context(
    DetectRequest::new(
        "admin".to_string(),
        "192.168.1.1".to_string(),
        22,
        "password".to_string(),
    ),
    ExecutionContext::default(),
)
.await?;

if let Some(best) = &report.best_match {
    println!("best template: {} ({:?}, score={})", best.template_name, best.confidence, best.score);
}

for candidate in &report.candidates {
    println!("candidate: {} score={}", candidate.template_name, candidate.score);
}
# Ok(())
# }

You can also continue directly into a live connection when the best candidate meets a minimum confidence threshold:

use rneter::session::{ExecutionContext, DetectRequest};
use rneter::templates::{
    autodetect_and_connect_with_context, DetectConnectPolicy,
};

# async fn demo() -> Result<(), Box<dyn std::error::Error>> {
let connected = autodetect_and_connect_with_context(
    DetectRequest::new(
        "admin".to_string(),
        "192.168.1.1".to_string(),
        22,
        "password".to_string(),
    ),
    None,
    ExecutionContext::default(),
    DetectConnectPolicy::default(), // default minimum confidence = Medium
)
.await?;

println!("connected with template: {}", connected.template_name);
# Ok(())
# }

Comparison With Netmiko And Scrapli

If you are coming from Netmiko or Scrapli, the biggest difference is where rneter puts its abstraction boundary.

  • Netmiko is primarily a device session toolkit built around prompt-driven command execution.
  • Scrapli is primarily a transport/channel/driver toolkit built around prompt patterns and privilege levels.
  • rneter is primarily a prompt-state-machine execution engine built around explicit states, transitions, and reusable operations.

At a high level:

  • In Netmiko, prompt detection is mainly used to know when command output is complete.
  • In Scrapli, prompt detection and privilege levels are used to keep the channel aligned with the expected operating mode.
  • In rneter, prompt detection is used to update a formal state machine, and command execution is a state-convergence process.

Mechanism Comparison

Dimension rneter Netmiko Scrapli What This Means
Core abstraction DeviceHandler as a finite state machine with prompt rules, input rules, and transition edges BaseConnection as a prompt-driven session object Driver + Channel + Transport with platform privilege levels rneter models device behavior more explicitly; the others emphasize session interaction first
Prompt role Prompt is a state event and command completion signal Prompt is mainly a command completion signal Prompt is mainly a channel alignment and completion signal rneter treats prompt text as control-plane data, not just output framing
Mode switching Automatic BFS pathfinding over explicit edges Usually explicit helper methods such as enable() / config_mode() / exit_config_mode() Privilege-level acquisition/transition in the driver rneter can generalize arbitrary mode graphs more naturally
Interactive input Prompt/input rules are part of the runtime FSM and can be extended per command flow Usually handled through timing/expect workflows such as send_command_timing() / send_multiline() Usually handled through interactive channel operations and explicit prompt expectations rneter is better suited to reusable interactive device wizards
Multi-line / noisy prompt handling Shared stream normalization, prompt prefix buffering, fragment merge, and prompt matching ANSI/backspace stripping plus prompt reads Prompt pattern search depth and explicit prompt reads in channel operations rneter spends more machinery on difficult prompts such as themed shells or JunOS context prompts
Error handling Error lines can map into FSM error state and can also be selectively ignored Mostly command-method or output-pattern based Mostly response / failed-when / parser-layer handling rneter can fold error semantics into execution flow more directly
Output model Output.success, content, all, prompt, optional exit code, recorder events Primarily processed string output, plus helper parsing paths Response objects with raw/processed output and driver/channel metadata rneter is oriented toward orchestration and replay, not only interactive use
Linux support Linux is handled through the same stateful execution engine, including shell exit-status capture Not a primary design center Supported, but still channel/prompt-centric rneter can treat network devices and Linux hosts more uniformly
Transactions / rollback Built-in TxBlock, TxWorkflow, rollback policies, recorded child-step results Caller-managed Caller-managed This is one of the biggest architectural differences in favor of rneter for automation platforms
Replay / fixture testing Built-in session recording and replay Not a core architectural feature Not a core architectural feature rneter is designed to support offline testing of CLI automation behavior

Same Task, Different Mental Model

Task Netmiko mental model Scrapli mental model rneter mental model
Run show version Send a command and read until prompt Send a command through the channel and read until prompt pattern Converge to target mode, execute command, and update FSM from returned prompt
Send config commands Enter config mode, send commands, optionally exit Acquire config privilege level, send configs, later return to desired privilege Treat config as a named state and route execution there through transition edges
Handle copy scp: prompts Use timing / multiline helpers with expected follow-up prompts Use interactive send/read operations with explicit prompt expectations Model the interaction as a reusable CommandFlow or CommandFlowTemplate
Handle [edit] + user@host# Tune prompt logic for this platform Tune prompt pattern / channel read behavior Model [edit] as a prompt prefix and merge it into the next prompt candidate

Why This Matters

For a Netmiko user, rneter will feel less like “a better send_command” and more like “a reusable execution engine that knows what state the device is in”.

For a Scrapli user, rneter will feel less like “a better driver/channel stack” and more like “a higher-level state graph built on prompt parsing”.

That is why rneter is especially strong when you need:

  • multi-step command workflows,
  • vendor-specific interactive wizards,
  • transaction-style rollback,
  • prompt-aware replayable tests,
  • or one orchestration layer that spans both network devices and Linux servers.

The tradeoff is that rneter asks the caller to think in terms of states, transitions, and execution models more often than Netmiko or Scrapli.

Supported Device Types

The library is designed to work with any SSH-enabled network device and Linux servers. It's particularly well-suited for:

Network Devices:

Template name Vendor / platform Primary modes Notes
cisco Cisco IOS / IOS-XE Login, Enable, Config Also used as the proven handler behavior for cisco_asa
cisco_asa Cisco ASA Login, Enable, Config Distinct template name and autodetect target; reuses cisco handler behavior
cisco_nxos Cisco NX-OS Login, Enable, Config Cisco-like mode transitions with NX-OS paging defaults
juniper Juniper JunOS Enable, Config Supports JunOS edit prompt prefix handling
arista Arista EOS Login, Enable, Config Cisco-like template for EOS
aruba_aoscx Aruba AOS-CX Login, Enable, Config Uses AOS-CX paging defaults
dell_os10 Dell OS10 Login, Enable, Config Cisco-like template for Dell OS10
ruijie Ruijie RGOS Login, Enable, Config Includes password-change decline prompt handling
zte_zxros ZTE ZXROS Login, Enable, Config Cisco-like template for ZTE ZXROS
huawei Huawei VRP Enable, Config Uses system-view / return transitions
h3c H3C Comware Enable, Config Comware-style angle/square-bracket prompts
hillstone Hillstone SG / StoneOS Enable, Config Includes save confirmation prompts
array Array Networks APV Login, Enable, Config, vsite modes Supports system/context mode variants
fortinet Fortinet FortiGate Enable, vdom modes Basic FortiGate / VDOM-oriented state model
paloalto Palo Alto Networks PAN-OS Enable, Config Operational and config prompts
checkpoint Check Point Gaia Enable Read/operational template
topsec Topsec NGFW Enable Basic operational template
venustech Venustech USG Login, Enable, Config Cisco-like firewall template
dptech DPTech firewall Enable, Config H3C-like prompt style
chaitin Chaitin SafeLine Login, Enable, Config Cisco-like gateway template
qianxin QiAnXin NSG Enable, Config Security gateway template
maipu Maipu network devices Login, Enable, Config Cisco-like template for Maipu devices

Linux Servers:

Template name Scope Notes
linux Generic Linux distributions Ubuntu, Debian, CentOS, RHEL, and other shell-based Linux hosts
linux Privilege escalation Supports sudo -i, sudo -s, su, and direct root sessions
linux Prompt handling Supports intelligent prompt detection with customizable patterns
linux Transactions Supports transaction-based configuration management with rollback

Configuration

SSH Algorithm Support

rneter includes comprehensive SSH algorithm support in the config module:

  • Key exchange: Curve25519, DH groups, ECDH
  • Ciphers: AES (CTR/CBC/GCM), ChaCha20-Poly1305
  • MAC: HMAC-SHA1/256/512 with ETM variants
  • Host keys: Ed25519, ECDSA, RSA, DSA (for legacy devices)

This ensures maximum compatibility with both modern and legacy network equipment.

Error Handling

The library provides detailed error types through ConnectError:

  • UnreachableState: Target state cannot be reached from current state
  • TargetStateNotExistError: Requested state doesn't exist in configuration
  • ChannelDisconnectError: SSH channel disconnected unexpectedly
  • ExecTimeout: Command execution exceeded timeout
  • And more...

For operation-level APIs such as execute_operation_with_context(...), failures now return SessionOperationExecutionError, which preserves partial_output() for already completed child steps.

Documentation

For detailed API documentation, visit docs.rs/rneter.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Author

demohiiiii