proxmox_power_button
NetBox plugin that adds Start / Stop / Reboot buttons to the Virtual Machine detail page and drives the corresponding Proxmox VE VM, reflecting the power state back into the NetBox VM status.
Installation
pip install netbox-proxmox-power-button
The distribution is netbox-proxmox-power-button; the importable package — and
the name that goes into NetBox's PLUGINS — is proxmox_power_button:
# configuration/plugins.py
PLUGINS = ["proxmox_power_button"]
PLUGINS_CONFIG = {
"proxmox_power_button": {
"verify_ssl": True, # set False only for self-signed lab certs
"stop_mode": "shutdown",
"reboot_mode": "reboot",
},
}
Then run ./manage.py migrate (creates the custom fields) and restart NetBox
and its worker.
Behaviour
- Buttons live in the VM detail page's button bar, before "Add Components"
(injected via
{% plugin_buttons %}, no custom page). - Start — green,
mdi-play. Shown only when the VM is notactive. On success sets NetBox status →active. - Stop — red,
mdi-stop. Shown only when the VM isactive. On success sets NetBox status →offline. - Reboot — orange,
mdi-sync. Shown only when the VM isactive, to the left of Stop. Leaves status unchanged. - NetBox status is updated only after Proxmox confirms the command. If Proxmox is unconfigured/unreachable, the action shows an error and changes nothing (no 500).
- After a start, the plugin waits 3s and re-queries Proxmox; the status is
set to
activeonly if the VM reportsrunning. Stop/reboot are not verified inline (they take too long to settle).
Auditing
One changelog entry per state change, each carrying a readable message
(visible in the VM's Changelog tab and in /core/changelog/):
| operation | changelog entry | object saved |
|---|---|---|
| start (confirmed running) | "Powered on via Proxmox (confirmed running)" | yes (status) |
| stop | "Powered off via Proxmox (shutdown sent)" | yes (status) |
| reboot | "Rebooted via Proxmox" | no — entry written directly |
| start sent, not yet running | none (no state change) | no |
| failure | none (no state change) | no |
Every outcome — including the two "none" rows above — is written to the
proxmox_power_button logger (visible in docker compose logs netbox), e.g.:
INFO … proxmox_power_button user=admin vm=jellyfin: powered on (confirmed running)
ERROR … proxmox_power_button user=admin vm=jellyfin: start failed: VMID 20100 not found …
Data model (custom fields, auto-created by migration)
VirtualMachine.vmid— integer, required, min 100. Unique within a cluster (enforced on create, on VMID change, and when moving the VM to another cluster — a move into a cluster that already has that VMID is rejected).Cluster.endpoint— text:host,host:port, orhttps://host:port.Cluster.token— text:user@realm!tokenid=secret.
The client resolves the target VM by vmid via cluster/resources, so it works
for both QEMU and LXC.
Settings (PLUGINS_CONFIG["proxmox_power_button"])
| key | default | meaning |
|---|---|---|
verify_ssl |
True |
verify Proxmox TLS cert (set False only for self-signed lab certs) |
stop_mode |
"shutdown" |
shutdown (graceful ACPI) or stop (hard) |
reboot_mode |
"reboot" |
reboot (graceful) or reset (hard) |
Security
- Token is stored in a plain-text custom field. It is visible to anyone who
can view the cluster — in the UI, in REST API responses for the cluster,
and in change-log data. NetBox has no "secret" custom-field type, so:
- restrict Cluster view permissions to administrators only;
- use a least-privilege Proxmox API token: only
VM.Audit+VM.PowerMgmt, scoped to the specific VMs or pool this plugin should manage — never aroot@pamtoken with datacenter-wide rights. Anyone who can read the field can extract the token and drive the hypervisor directly.
- Permissions. The buttons render only for users with
virtualization.change_virtualmachine, and the action endpoint enforces NetBox's object-scoped permissions: a user whose change permission is constrained (e.g. to one tenant's VMs) gets a 404 on any VM outside that scope — the same behaviour as core NetBox object views. - TLS verification is on by default. The Proxmox API token crosses this
connection on every power action; leaving
verify_sslatTrueprevents token theft via MitM. Only set it toFalsefor self-signed lab certificates. - Error detail goes to the log, not the browser. Failed actions show a
generic message; the full Proxmox/network error (which may contain internal
hostnames, URLs, or response bodies) is written to the
proxmox_power_buttonlogger.
Notes / caveats
- Requires NetBox ≥ 4.5.0. The data migration depends on
extras.0134_ownerandvirtualization.0052_gfk_indexes, which first appear in 4.5.0; on 4.4 or oldermigratefails withNodeNotFoundError. Bumpmin_versiontogether with those pins if you ever retarget them. Verified against 4.5.8. - Making
vmidrequired means existing VMs without a VMID will fail validation on their next edit until one is set.
Releasing to PyPI
Everything runs through the makefile:
make venv # one-off: build+twine in .venv-publish (PEP 668-safe)
make bump V=0.2.0 # writes the version to BOTH places, then verifies
make build # clean + sdist + wheel
make check # lists both artifacts, asserts contents, twine check
make testpypi # optional dry run against TestPyPI
make publish # build + check + upload (asks you to type the version)
make tag # git tag <version>
Uploads authenticate with username __token__ and a pypi-… API token
(~/.pypirc, or TWINE_USERNAME/TWINE_PASSWORD). A version number is burned
permanently on upload — it can never be reused, even after deleting the
release; hence publish refuses to run without a passing check and a typed
confirmation.
The version lives in two places: pyproject.toml → version, and
proxmox_power_button/__init__.py → ProxmoxPowerButtonConfig.version. They are
not single-sourced on purpose — importing the package to read a version would
drag in netbox, absent in a build environment. make bump writes both and
make version fails loudly if they ever drift.
make check asserts three things that a broken build would otherwise hide until
someone installs the package: the wheel carries the button template (templates/
has no __init__.py, so without [tool.setuptools.package-data] +MANIFEST.in
you get a wheel that raises TemplateDoesNotExist on every VM page), the
migration, and the licence.
Licence
GPL-2.0-or-later. See LICENSE for the full text; every source file carries an
SPDX-License-Identifier: GPL-2.0-or-later header, which is what expresses the
"or later" option (the GPL-2 text alone does not).