Skip to content

aalexeen/netdev-broker

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

netdev-broker

Persistent SSH/Telnet session API service for network automation.

Manages long-lived SSH and Telnet connections to network devices and exposes them via a lightweight HTTP API. Designed to be called from Ansible playbooks via the uri module, enabling a parallel session that is fully independent of Ansible's own network_cli connection.

Architecture

Ansible Controller
│
├── ansible-playbook ──uri──► netdev-broker :8765
│                               │
│                               └── sessions dict
│                                   ├── session_A ──SSH────► SW-01  (safety session)
│                                   └── session_B ──Telnet──► SW-02
│
└── network_cli daemon ──────────────SSH──► SW-01  (Ansible work session)

Each device has two independent sessions: one managed by Ansible (network_cli), one managed by netdev-broker. The broker session survives Ansible connection resets and can be used for rollback even if Ansible loses its own connection.

Requirements

  • Python 3.10+
  • python3-venv
  • systemd
  • Network access to target devices on port 22 (SSH) or 23 (Telnet)

Installation

sudo ./install.sh install

The installer will:

  1. Create system user netdev-broker
  2. Deploy files to /opt/netdev-broker/
  3. Create Python venv and install dependencies
  4. Prompt for configuration (bind address, port, API token, logging, web UI users)
  5. Write config to /etc/netdev-broker/config.yml (chmod 640)
  6. Register and start netdev-broker.service via systemd

After install, manage the service using the copied script:

sudo /opt/netdev-broker/netdev-broker.sh          # interactive menu
sudo /opt/netdev-broker/netdev-broker.sh start
sudo /opt/netdev-broker/netdev-broker.sh stop
sudo /opt/netdev-broker/netdev-broker.sh restart
sudo /opt/netdev-broker/netdev-broker.sh configure
sudo /opt/netdev-broker/netdev-broker.sh status
sudo /opt/netdev-broker/netdev-broker.sh logs
sudo /opt/netdev-broker/netdev-broker.sh nginx
sudo /opt/netdev-broker/netdev-broker.sh uninstall

Configuration

Config file: /etc/netdev-broker/config.yml

host: 127.0.0.1        # bind address — keep on loopback for security
port: 8765
api_token: ""          # required — set during install or via --api-token

log_level: INFO        # DEBUG | INFO | WARNING | ERROR
log_mode: stdout       # stdout | file | both
log_file: /var/log/netdev-broker/netdev-broker.log

session_timeout: 300   # seconds of idle before a session is reaped
stale_reaper: true     # background task that closes idle sessions
reaper_interval: 60    # how often the reaper runs (seconds)

cmd_log_dir: ""        # directory for per-session JSONL command logs (disabled if empty)

# Web UI (optional — omit web_users to disable)
web_secret_key: ""     # required if web_users is set
web_session_ttl: 3600  # web login cookie lifetime (seconds)
web_users:
  - username: admin
    password_hash: "$2b$12$..."   # bcrypt hash — set via installer

CLI arguments override config file values:

python -m netdev_broker --config /etc/netdev-broker/config.yml --port 9000 --log-level DEBUG

API

All endpoints except /health require the header X-API-Token: <token>.

Interactive API documentation is available at http://127.0.0.1:8765/docs when the service is running.


GET /health

No authentication required.

GET /health

200 OK
{"status": "ok", "version": "1.0.0", "sessions": 3}

SSH Sessions

POST /session

Open a new SSH session. Returns a session_id for subsequent calls.

POST /session
Content-Type: application/json
X-API-Token: <token>

{
  "host": "10.0.0.1",
  "username": "admin",
  "password": "secret",
  "device_type": "cisco_ios",   // optional, default: cisco_ios
  "port": 22,                   // optional, default: 22
  "timeout": 30,                // optional, default: 30
  "session_timeout": 600        // optional — overrides global session_timeout for this session
}

201 Created
{"session_id": "a3f2c1d0...", "host": "10.0.0.1"}

401  authentication failed
504  connection timed out
502  SSH connection error

GET /session/{session_id}/status

Check liveness and metadata of an open session.

GET /session/a3f2c1d0.../status
X-API-Token: <token>

200 OK
{
  "session_id": "a3f2c1d0...",
  "alive": true,
  "host": "10.0.0.1",
  "device_type": "cisco_ios",
  "created_at": "2026-03-12T14:30:00+00:00",
  "last_used":  "2026-03-12T14:31:45+00:00",
  "age_seconds": 105,
  "idle_seconds": 15
}

404  session not found

POST /session/{session_id}/exec

Execute one or more commands on the session. Commands run sequentially.

POST /session/a3f2c1d0.../exec
Content-Type: application/json
X-API-Token: <token>

{
  "commands": ["show version", "show ip interface brief"]
}

200 OK
{
  "outputs": {
    "show version": "Cisco IOS XE Software...",
    "show ip interface brief": "Interface  IP-Address..."
  }
}

404  session not found
410  session exists but connection is dead (re-open required)
409  session is busy (concurrent exec in progress)
500  command execution error

DELETE /session/{session_id}

Close and remove a session. Idempotent — returns 204 even if already gone.

DELETE /session/a3f2c1d0...
X-API-Token: <token>

204 No Content

Telnet Sessions

POST /telnet-session

Open a new Telnet session. Handles the login/enable flow automatically.

POST /telnet-session
Content-Type: application/json
X-API-Token: <token>

{
  "host": "10.0.0.1",
  "username": "admin",
  "password": "secret",
  "enable_password": "enable_secret",   // optional — triggers enable mode
  "port": 23,                           // optional, default: 23
  "timeout": 30                         // optional, default: 30
}

201 Created
{"session_id": "b7e9a2f1...", "host": "10.0.0.1"}

401  authentication failed
504  connection timed out
502  Telnet connection error

GET /telnet-session/{session_id}/status

Same response shape as SSH /session/{id}/status with "protocol": "telnet".

POST /telnet-session/{session_id}/exec

Execute commands on a Telnet session. Supports both plain commands and interactive commands that require answering intermediate prompts.

POST /telnet-session/b7e9a2f1.../exec
Content-Type: application/json
X-API-Token: <token>

{
  "commands": [
    "show version",
    {
      "cmd": "copy running-config startup-config",
      "answer": "\\[confirm\\]",   // regex to wait for before sending newline
      "send_newlines": 1
    }
  ]
}

200 OK
{
  "outputs": {
    "show version": "Cisco IOS XE Software...",
    "copy running-config startup-config": "..."
  }
}

Error codes are the same as for SSH exec.

DELETE /telnet-session/{session_id}

Same as SSH delete — idempotent, returns 204.


curl Examples

HOST="http://127.0.0.1:8765"
TOKEN="your_api_token_here"
DEVICE="192.168.1.1"
USER="admin"
PASS="cisco"

Health check (no token required)

curl -s "${HOST}/health" | jq .

Open an SSH session

SESSION=$(curl -s -X POST "${HOST}/session" \
  -H "X-API-Token: ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d "{\"host\":\"${DEVICE}\",\"username\":\"${USER}\",\"password\":\"${PASS}\"}" \
  | jq -r .session_id)
echo "Session: ${SESSION}"

Session status

curl -s "${HOST}/session/${SESSION}/status" \
  -H "X-API-Token: ${TOKEN}" | jq .

Execute commands

curl -s -X POST "${HOST}/session/${SESSION}/exec" \
  -H "X-API-Token: ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"commands":["show version","show ip interface brief"]}' | jq .

Close a session

curl -s -X DELETE "${HOST}/session/${SESSION}" \
  -H "X-API-Token: ${TOKEN}" -w "%{http_code}\n"
# Expected: 204

Open a Telnet session

TSESSION=$(curl -s -X POST "${HOST}/telnet-session" \
  -H "X-API-Token: ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d "{\"host\":\"${DEVICE}\",\"username\":\"${USER}\",\"password\":\"${PASS}\",\"enable_password\":\"${PASS}\"}" \
  | jq -r .session_id)

Execute interactive Telnet command

curl -s -X POST "${HOST}/telnet-session/${TSESSION}/exec" \
  -H "X-API-Token: ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"commands":[{"cmd":"copy run start","answer":"\\[confirm\\]","send_newlines":1}]}' | jq .

Error cases

# Wrong token → 401
curl -s -X POST "${HOST}/session" \
  -H "X-API-Token: wrong" \
  -H "Content-Type: application/json" \
  -d '{"host":"1.2.3.4","username":"u","password":"p"}' | jq .

# Session not found → 404
curl -s "${HOST}/session/nonexistent/status" \
  -H "X-API-Token: ${TOKEN}" | jq .

Web UI

The broker includes an optional web dashboard for monitoring active sessions. Enable it by configuring web_users in config.yml (via ./install.sh configure).

Access at http://127.0.0.1:8765/ (or via Nginx reverse proxy):

  • Login with a configured username/password (bcrypt-hashed)
  • Dashboard shows all active SSH and Telnet sessions with metadata (host, device type, age, idle time, alive/dead status)
  • Recent command history per session (last 5 commands)
  • Close individual sessions or all sessions at once
  • Auto-refreshes every 5 seconds

Command Logging

When cmd_log_dir is set, every executed command is appended to a per-session JSONL file (<session_id>.jsonl). Each line contains ts, command, output.

{"ts": "2026-03-12T14:31:45+00:00", "command": "show version", "output": "..."}

Useful for audit trails during migrations.

Ansible Integration

Example tasks for a safety session in a migration playbook:

# Open safety session before any changes
- name: Open safety session to {{ inventory_hostname }}
  ansible.builtin.uri:
    url: "http://127.0.0.1:8765/session"
    method: POST
    body_format: json
    body:
      host: "{{ ansible_host }}"
      username: "{{ local_user }}"
      password: "{{ local_pass }}"
      session_timeout: 1800   # keep alive for 30 min regardless of global timeout
    headers:
      X-API-Token: "{{ broker_token }}"
    status_code: 201
  register: safety_open
  delegate_to: localhost
  no_log: true

- name: Save safety session ID
  ansible.builtin.set_fact:
    safety_session_id: "{{ safety_open.json.session_id }}"

# ... migration tasks ...

# Rollback via safety session if needed
- name: Restore local AAA via safety session
  ansible.builtin.uri:
    url: "http://127.0.0.1:8765/session/{{ safety_session_id }}/exec"
    method: POST
    body_format: json
    body:
      commands:
        - "configure terminal"
        - "aaa authentication login default local"
        - "aaa authorization exec default local"
        - "end"
        - "write memory"
    headers:
      X-API-Token: "{{ broker_token }}"
  delegate_to: localhost

# Always close the session when done
- name: Close safety session
  ansible.builtin.uri:
    url: "http://127.0.0.1:8765/session/{{ safety_session_id }}"
    method: DELETE
    headers:
      X-API-Token: "{{ broker_token }}"
    status_code: 204
  delegate_to: localhost
  when: safety_session_id is defined

Development

# Create venv
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

# Run locally (requires a populated config.yml or --api-token)
python -m netdev_broker --api-token mytoken --log-level DEBUG

# Run tests (no real devices needed — Netmiko is mocked)
pytest tests/ -v

Telnet debugging tool (shows raw pexpect output at each auth stage):

python3 telnet_debug.py <host> <username> <password> [enable_password] [port]

Security

  • The service binds to 127.0.0.1 by default — not reachable from outside the controller.
  • All API endpoints (except /health) require X-API-Token authentication.
  • Web UI uses bcrypt password hashing and signed session cookies (itsdangerous).
  • The config file is chmod 640, readable only by root and the netdev-broker group.
  • Use no_log: true on Ansible tasks that pass credentials in the request body.
  • The API token should be stored in Ansible Vault, not in plaintext inventory.
  • Credentials are not stored after the connection object is created — only the connection is kept in memory.

Supported Device Types (SSH)

Any device type supported by Netmiko. Common values:

Value Device
cisco_ios Cisco IOS / IOS-XE (default)
cisco_nxos Cisco NX-OS
cisco_asa Cisco ASA
juniper_junos Juniper JunOS
arista_eos Arista EOS

Telnet sessions use pexpect directly and work with any device that presents a standard Username: / Password: / # or > prompt sequence.

About

Persistent SSH/Telnet session API service for network automation.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors