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.
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.
- Python 3.10+
python3-venvsystemd- Network access to target devices on port 22 (SSH) or 23 (Telnet)
sudo ./install.sh installThe installer will:
- Create system user
netdev-broker - Deploy files to
/opt/netdev-broker/ - Create Python venv and install dependencies
- Prompt for configuration (bind address, port, API token, logging, web UI users)
- Write config to
/etc/netdev-broker/config.yml(chmod 640) - Register and start
netdev-broker.servicevia 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 uninstallConfig 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 installerCLI arguments override config file values:
python -m netdev_broker --config /etc/netdev-broker/config.yml --port 9000 --log-level DEBUGAll 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.
No authentication required.
GET /health
200 OK
{"status": "ok", "version": "1.0.0", "sessions": 3}
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
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
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
Close and remove a session. Idempotent — returns 204 even if already gone.
DELETE /session/a3f2c1d0...
X-API-Token: <token>
204 No Content
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
Same response shape as SSH /session/{id}/status with "protocol": "telnet".
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.
Same as SSH delete — idempotent, returns 204.
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: 204Open 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 .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
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.
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# 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/ -vTelnet debugging tool (shows raw pexpect output at each auth stage):
python3 telnet_debug.py <host> <username> <password> [enable_password] [port]- The service binds to
127.0.0.1by default — not reachable from outside the controller. - All API endpoints (except
/health) requireX-API-Tokenauthentication. - Web UI uses bcrypt password hashing and signed session cookies (
itsdangerous). - The config file is
chmod 640, readable only by root and thenetdev-brokergroup. - Use
no_log: trueon 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.
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.