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).
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters