You've already forked netbox-proxmox-power-button
159 lines
7.3 KiB
Markdown
159 lines
7.3 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
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`:
|
|
|
|
```python
|
|
# 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 *not* `active`.
|
|
On success sets NetBox status → `active`.
|
|
- **Stop** — red, `mdi-stop`. Shown only when the VM is `active`.
|
|
On success sets NetBox status → `offline`.
|
|
- **Reboot** — orange, `mdi-sync`. Shown only when the VM is `active`, 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 `active` only if the VM reports `running`. 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`, or `https://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 a
|
|
`root@pam` token 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_ssl` at `True` prevents
|
|
token theft via MitM. Only set it to `False` for 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_button` logger.
|
|
|
|
## Notes / caveats
|
|
- **Requires NetBox ≥ 4.5.0.** The data migration depends on
|
|
`extras.0134_owner` and `virtualization.0052_gfk_indexes`, which first appear
|
|
in 4.5.0; on 4.4 or older `migrate` fails with `NodeNotFoundError`. Bump
|
|
`min_version` together with those pins if you ever retarget them.
|
|
Verified against 4.5.8.
|
|
- Making `vmid` required 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`:
|
|
|
|
```bash
|
|
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).
|