DEV Community

Cover image for SSH Server Management Without the Ansible Overhead
Scrooge Mcduck
Scrooge Mcduck

Posted on Edited on Originally published at vae-scrooge.github.io

SSH Server Management Without the Ansible Overhead

You SSH into 10 servers to check disk space. You write a quick for loop. A typo crashes one session. You're not sure which ones you've already checked. By the time you're done, 20 minutes are gone and you have a terminal full of scrollback you need to piece together.

There's a better way.

The Problem

Most developers SSH into servers one at a time. When you need to run the same command across multiple machines, your options are:

  1. Open 10 terminals — error-prone, slow, no audit trail
  2. Write a shell loop — for host in web-01 web-02 ...; do ssh $host "df -h"; done — works until a host is down and your loop breaks
  3. Ansible — powerful, but you need to learn YAML DSL, write playbooks, manage inventory files. Overkill for "check disk space"
  4. Fabric — good Python library, but no built-in host management or CLI

None of these give you a simple CLI that just works out of the box.

Meet remote-cmd

remote-cmd is a Python CLI + API for SSH server management. It gives you:

  • Host CRUD with tag-based grouping (production, staging, web, db)
  • One-shot command execution on single or multiple hosts
  • File upload/download via SFTP
  • A Python API for scripting automation
  • Zero setup — pip install and go

v2.11.0 Release Highlights

v2.11.0 focuses on reliability, scalability, and safer operations.

  • Reliability: strict known_hosts verification, wall-clock command timeouts, cancellation cleanup, and staged SFTP transfers.
  • Scalability: bounded batch workers, reusable TaskRunner workers, and thread-local SQLite connections.
  • Storage: SQLite schema v4 adds indexed host tags and supports transactional JSON-to-SQLite migration.
  • Batch output: use the JSON format for per-host success, exit code, output, duration, and error details. Timeout and not-attempted cases are currently described in the per-host error rather than separate outcome types.

A synthetic local benchmark with 1,000 in-flight Paramiko-style commands measured about 3,003 → 2,003 threads after removing the per-command stderr-reader thread. This is a local benchmark, not a production throughput guarantee.

Compatibility Note

JSON repository get/list now return detached snapshots (matching SQLite): changing a returned object requires an explicit save call. For example:

from remote_cmd.repository.json_host_repository import JsonHostRepository

repo = JsonHostRepository("hosts.json")
host = repo.get("web-01")
host.port = 2222
repo.save(host)
Enter fullscreen mode Exit fullscreen mode

See the migration notes for details.

Quick Demo

# Install
pip install remote-cmd-manager

# Add your first server
remote-cmd host add web-01 192.168.1.10 ubuntu --key ~/.ssh/id_rsa

# Run a command
remote-cmd run web-01 "uptime"
# → 14:32:10 up 45 days,  2:15,  1 user,  load average: 0.08, 0.03, 0.05

# Add more servers and run across all production hosts
remote-cmd host add web-02 192.168.1.11 ubuntu --key ~/.ssh/id_rsa --tag production
remote-cmd host add db-01 192.168.1.20 ubuntu --key ~/.ssh/id_rsa --tag production --tag database

remote-cmd batch-run -t production "df -h /"
# ✓ web-01  → Disk: 32G/100G (32%)
# ✓ web-02  → Disk: 45G/100G (45%)
# ✓ db-01   → Disk: 12G/50G  (24%)
Enter fullscreen mode Exit fullscreen mode

Real-World Use Cases

Incident Response

# Check errors across all web servers in 3 seconds
remote-cmd batch-run -t web "journalctl -xe -n 50 | grep -i error"
Enter fullscreen mode Exit fullscreen mode

Deploy Code

from getpass import getpass
from remote_cmd.core.host_manager import HostManager

manager = HostManager("hosts.json")
sudo_password = getpass("sudo password: ")
for host in manager.list_hosts(tag="staging"):
    with manager.connect_to_host(host.name) as client:
        client.execute("cd /app && git pull")
        client.execute("pip install -r requirements.txt")
        client.execute_sudo("systemctl restart app", password=sudo_password)
Enter fullscreen mode Exit fullscreen mode

For unattended deployments, retrieve credentials from an external secret manager or use a narrowly scoped passwordless sudo rule. Do not hardcode sudo passwords in source.

Config Update

# Upload new nginx config and reload across all web servers
remote-cmd upload web-01 ./nginx.conf /tmp/nginx.conf
remote-cmd run web-01 "sudo cp /tmp/nginx.conf /etc/nginx/nginx.conf && sudo nginx -t && sudo systemctl reload nginx"
Enter fullscreen mode Exit fullscreen mode

How It Stacks Up

remote-cmd ssh + shell Ansible Fabric
Setup time 10 seconds None (you know ssh) 30 min+ 5 min
Host management Built-in Manual Inventory YAML Manual
CLI Yes Yes ansible cmd fab cmd
Python API Yes No No (YAML) Yes
Ideal for Ad-hoc + scripts One-off Config mgmt Python scripts

Getting Started

pip install remote-cmd-manager

# Full documentation
remote-cmd --help

# GitHub
# https://github.com/Vae-Scrooge/remote-cmd
Enter fullscreen mode Exit fullscreen mode

Remote CMD is stable at v2.11.0. The core API is versioned under semantic versioning, and the project is tested on Linux, macOS, and Windows with Python 3.10+. v2.11.0 removed no CLI commands; behavioral changes are documented in the migration notes.

Roadmap

  • Observability and lightweight metrics
  • Extensible plugin architecture
  • Persistent task/result storage
  • Large-scale execution and result handling
  • API ergonomics and long-term compatibility

If you SSH into more than a couple servers regularly, give it a try. It'll save you the "which servers did I check again?" moment.

Top comments (2)

Collapse
 
nexusshell profile image
Nexus Shell •

This is a useful middle ground between a brittle shell loop and full configuration management. The tag-based inventory is especially practical for ad-hoc checks.

Before using it in production, I'd want three behaviors to be very explicit: strict host-key verification against known_hosts, per-host timeouts/cancellation with structured exit results, and credential handling that never puts sudo passwords in code or shell history. A narrowly scoped passwordless sudo rule or an external secret provider would be safer than the password literal in the deployment example.

Clear partial-failure output would also make batch runs much easier to trust: succeeded, failed, timed out, and not attempted should be distinguishable at a glance.

Collapse
 
vaescrooge profile image
Scrooge Mcduck •

Thanks, Nexus — this is a thoughtful production-readiness checklist, and I agree especially about the sudo example. Since this article was published, v2.11.0 now uses strict known_hosts verification by default, provides wall-clock command timeouts, and improves cancellation cleanup. Batch results and JSON output include per-host success, exit code, output, and error details.

Two gaps remain on my roadmap: timeout configuration is currently shared at the command/batch level rather than independently configurable per host, and timed out / not attempted are not yet separate machine-readable outcome states. Also, password="sudopass" was only a placeholder, but you’re right that it’s a poor production example. execute_sudo sends the password via stdin rather than argv or logs, but callers should retrieve it from an external secret store or use a narrowly scoped passwordless sudo rule. I’ll update the example.