Skip to content

labrouss/DeviceFirmwareManager

Repository files navigation

Device Firmware Manager (DFM)

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.

What it does

  • 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, supportSave bundles) — 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.

What it explicitly does not do

  • 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.

Requirements

  • Python 3.11+ (developed/tested primarily on 3.12–3.13)
  • See requirements.txt. Notably paramiko==2.12.0 is pinned deliberately (see above) — pip install will 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.

Running from source

pip install -r requirements.txt
python main.py

On 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.

Building a standalone executable

A PyInstaller spec file is included:

pip install pyinstaller
pyinstaller DeviceFirmwareManager.spec

Use 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.

Project layout

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

Command registry syntax

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.

Known limitations / honest caveats

  • 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 classic LIST command (deliberately not MLSD, which many older/ embedded FTP servers never implemented) and parses the common Unix ls -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 structured listdir_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.

About

Device Firmware Manager - A firmware manager platform with workflows for several devices

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages