Metadata-Version: 2.4
Name: netbox-proxmox-power-button
Version: 0.1.1
Summary: NetBox plugin adding Start/Stop/Reboot buttons that drive Proxmox VE VMs
Author: Bryan Joshua Pedini
License-Expression: GPL-2.0-or-later
Project-URL: Homepage, https://git.bjphoster.com/source/netbox-proxmox-power-button
Project-URL: Repository, https://git.bjphoster.com/source/netbox-proxmox-power-button
Project-URL: Issues, https://git.bjphoster.com/source/netbox-proxmox-power-button/issues
Keywords: netbox,netbox-plugin,proxmox,virtualization
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Intended Audience :: System Administrators
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: System :: Systems Administration
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: proxmoxer>=2.0
Requires-Dist: requests
Dynamic: license-file

# 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).
