A desktop application for managing SAN, network, and embedded Linux devices over SSH, Telnet, and FTP — pulling configs and logs, running diagnostic commands, and pushing firmware, including to legacy hardware that modern SSH clients have stopped supporting by default.
This is a Python/PySide6 rewrite of an earlier .NET/WinForms tool of the
same purpose. The rewrite exists because of one specific, recurring
problem: very old switch firmware (e.g. Brocade FOS 6.2.2f) only offers
ssh-dss host keys and diffie-hellman-group1-sha1 key exchange — both of
which SSH.NET, OpenSSH 10+, and even recent paramiko releases have removed
outright on security grounds. paramiko==2.12.0 is the last release that
still implements both and enables them by default, which is what makes
plain SSH to this hardware work again without any extra configuration. See
requirements.txt for the full rationale; do not casually bump this
pin without checking whether the device types you actually use still need
it.
- Connects to devices over SSH (paramiko) or, for hardware that genuinely can't be reached over SSH at all, Telnet/FTP as a fallback.
- Runs an inbound SSH/SCP server so devices can push files to the app
(firmware downloads,
supportSavebundles) — including from clients that only offer legacy KEX/host-key algorithms, which was the actual wall the previous .NET version hit and couldn't get past. - Drives device actions from a JSON command registry
(
config/DeviceCommandRegistry.json) rather than hardcoded per-vendor logic, so new device types are data, not code. Out of the box it covers: Ubuntu-based sensor nodes, Cisco IOS-XE switches, Yocto-based embedded Linux, Brocade Fabric OS, HPE Comware, Aruba (ArubaOS-CX), Mellanox/NVIDIA Onyx, and OpenWrt. - Supports plain commands, pulling a file or a command's output back from a device, pushing a local file to a device, and multi-step workflows (run/wait/poll sequences — e.g. "start a firmware download, poll status until it reports success, then verify").
- Lets you export/import the device list and the command registry as JSON, so a registry or device set can be shared between machines. Exported device files never include passwords — they're encrypted locally with a key tied to the machine that encrypted them, so they wouldn't be usable elsewhere anyway, and a portable export with embedded plaintext credentials is a worse outcome than asking you to re-enter them.
- It is not a general-purpose network management platform. The command registry is intentionally simple (string templates with token substitution), not a full automation/orchestration engine.
- The Telnet/FTP fallback is unencrypted. It exists only for hardware that cannot be reached over SSH at all; it is not a recommended default for anything that can use SSH instead.
- The four newer registry entries (HPE Comware, Aruba CX, Mellanox Onyx, OpenWrt) were written against documented CLI syntax for each platform but have not all been exercised against every real device family they claim to cover — treat the first run against a given device type as a verification pass, the same way you would manually testing a new script against a switch for the first time.
- Python 3.11+ (developed/tested primarily on 3.12–3.13)
- See
requirements.txt. Notablyparamiko==2.12.0is pinned deliberately (see above) —pip installwill respect the pin, but be aware if you're merging this into an environment that already has a different paramiko version for some other tool.
pip install -r requirements.txt
python main.pyOn first run, the app creates its data directory (settings, the encrypted
device list, the SSH host key, logs) under the OS-appropriate per-user
location, and a DeviceFirmwareManager folder under your Documents folder
for collected files (pulled configs, firmware artifacts, etc.) — created
automatically if it doesn't already exist. Both are configurable afterward
via Settings.
A PyInstaller spec file is included:
pip install pyinstaller
pyinstaller DeviceFirmwareManager.specUse the spec file rather than a raw pyinstaller main.py command — it
bundles the device command registry correctly (PyInstaller does not bundle
non-Python data files automatically) and excludes PyQt5/PyQt6/PySide2 so an
unrelated Qt-bindings package elsewhere in your environment can't abort the
build (PyInstaller refuses to bundle two conflicting Qt bindings into one
app). The built executable lands in dist/.
CI builds for Linux, Windows, and macOS (both Apple Silicon and Intel) on
every version tag — see .github/workflows/build-and-release.yml.
main.py Entry point — wires every layer together
app_paths.py Cross-platform data/config/Documents paths
password_helper.py Local Fernet-based password encryption
config/DeviceCommandRegistry.json Device types and their command templates
models/ device_models.py Device list (encrypted-at-rest store)
settings/ app_settings.py App settings (port, storage location, etc.)
registry/ device_command_registry.py Loads/resolves command templates
net/ ssh_client.py, telnet_client.py, ftp_helper.py Outbound protocol clients
server/ scp_server.py Inbound SSH/SCP server (paramiko server mode)
orchestration/ device_coordinator.py Dispatch engine tying everything together
ui/ PySide6 windows/dialogs
See the "Syntax help" button inside the Registry Editor (Edit Registry →
❓ Syntax help) for the full reference on template patterns (__PULL__,
__PULL_BROWSE__, __PULL_CMD__, __PUSH__, __FILE__, __SERVE_FROM__,
__WORKFLOW__) and available tokens. The same reference is also worth
reading before adding a new device type.
__PULL_BROWSE__:/starting/dir is worth calling out specifically: use it
instead of __PULL__ whenever the exact remote filename isn't known
ahead of time (a license file, a backup whose name includes a timestamp,
a generated report). It opens a file browser dialog connected to the
device — one persistent SFTP/FTP session reused for every navigation
within that dialog, not a fresh reconnect per click — and pulls back
whatever the user picks.
__SERVE_FROM__:rest-of-template is the other direction: some firmware
delivery mechanisms have the DEVICE pull several files from us on its own
schedule, rather than accepting one file we push. Brocade FOS's
firmwaredownload is the motivating example — it expects an
already-extracted release directory, not a single archive, and fetches
individual files (manifest, kernel/boot images, etc.) over the course of
the upgrade. __SERVE_FROM__ opens a folder picker, registers that exact
folder as servable via SCP source mode for the duration of whatever
follows (typically a __WORKFLOW__), and unregisters it automatically
once that finishes or fails. Nothing is copied or extracted by the app —
point it at wherever you already extracted the release.
- Closing the app cancels in-flight actions and stops the inbound server, but a network call stuck in a blocking OS-thread read has no safe cross-thread interrupt — if it's genuinely stuck (e.g. a switch stopped responding mid-command), that thread keeps running until it returns or the process exits, same as killing any SSH client mid-command.
- The inbound SCP server's SINK mode (receiving files pushed by a device —
supportSave, config backups, etc.) implements the classic single-file
protocol only; recursive/directory transfers and the SFTP subsystem are
not supported, by design — device-pushed bundles are always single
files. SOURCE mode (serving files out to a device, via
__SERVE_FROM__) is single-file-per-request too, but a device can make as many separate requests as it wants against a registered folder over the course of one action — that's the whole point for firmwaredownload-style transfers. __SERVE_FROM__exposes every file under the chosen folder, recursively, to whichever device is running that action, for as long as the action takes — lookup is by filename match anywhere in the tree, not a strict path check. Don't point it at a folder containing anything beyond the firmware release itself. The registration is scoped to one device ID at a time and is cleared automatically when the action ends, so it can't outlive the operation that requested it or leak into an unrelated later action against the same device.__PULL_BROWSE__'s directory listing on Telnet/FTP devices uses the classicLISTcommand (deliberately notMLSD, which many older/ embedded FTP servers never implemented) and parses the common Unixls -l-style output. There's no standardized LIST format, so this is a best-effort parse — lines it doesn't recognize are silently skipped rather than crashing the browse, but an unusual server could in theory list fewer files than actually exist. SSH/SFTP-routed devices use paramiko's structuredlistdir_attr()instead and aren't affected.- Telnet has no real notion of a command exit code; success/failure for Telnet-routed actions is inferred from output content, not a status code.