# SPDX-License-Identifier: GPL-2.0-or-later # Copyright (C) 2026 Bryan Joshua Pedini import logging from django.contrib import messages from django.contrib.auth.mixins import PermissionRequiredMixin from django.shortcuts import get_object_or_404, redirect from django.views import View from core.choices import ObjectChangeActionChoices from virtualization.choices import VirtualMachineStatusChoices from virtualization.models import VirtualMachine from .proxmox import ProxmoxError, power_action logger = logging.getLogger("proxmox_power_button") VALID_ACTIONS = {"start", "stop", "reboot"} def _log_changelog_only(request, vm, message): """ Record a changelog entry for an operation that changes no field. Used for reboot: the VM stays powered on, so there is nothing to save. We build the ObjectChange by hand (pre == post, no field diff) rather than touching the object, and attribute it to the current request/user exactly like ChangeLoggingMiddleware would. """ vm.snapshot() objectchange = vm.to_objectchange(ObjectChangeActionChoices.ACTION_UPDATE) objectchange.message = message objectchange.user = request.user objectchange.request_id = request.id objectchange.save() def _save_with_changelog(request, vm, status, message): """ Apply a status change; the post_save signal writes a single changelog entry carrying our message. """ vm.snapshot() vm._changelog_message = message vm.status = status vm.save() class VMPowerActionView(PermissionRequiredMixin, View): """ POST-only action endpoint (Post/Redirect/Get) — renders no page. Performs the Proxmox power action, records exactly one changelog entry per state change, and logs every outcome (including failures). """ permission_required = "virtualization.change_virtualmachine" def get(self, request, pk, action): # Not a real page — just bounce back to the VM. return redirect("virtualization:virtualmachine", pk=pk) def post(self, request, pk, action): # Scope the lookup through NetBox's object-permission system: a user # whose change_virtualmachine permission is constraint-scoped gets a # 404 on VMs outside their scope, exactly like core NetBox views. vm = get_object_or_404( VirtualMachine.objects.restrict(request.user, "change"), pk=pk ) if action not in VALID_ACTIONS: logger.error("user=%s vm=%s: unknown power action '%s'", request.user, vm.name, action) messages.error(request, f"Unknown power action '{action}'.") return redirect("virtualization:virtualmachine", pk=vm.pk) try: result = power_action(vm, action) except ProxmoxError as exc: logger.error("user=%s vm=%s: %s failed: %s", request.user, vm.name, action, exc) # Don't echo raw Proxmox/network errors to the browser — they can # leak internal hostnames, URLs, and response bodies. Full detail # is in the log line above. messages.error( request, f"Proxmox {action} failed for {vm.name} — see the NetBox log for details.", ) return redirect("virtualization:virtualmachine", pk=vm.pk) if action == "start": if result == "running": _save_with_changelog( request, vm, VirtualMachineStatusChoices.STATUS_ACTIVE, "Powered on via Proxmox (confirmed running)", ) logger.info("user=%s vm=%s: powered on (confirmed running)", request.user, vm.name) messages.success(request, f"{vm.name} is powered on (confirmed running).") else: # Command accepted but the VM is not running yet: no state # change, so no changelog entry — log only. state = result or "unknown" logger.warning( "user=%s vm=%s: start sent but not confirmed running (state=%s)", request.user, vm.name, state, ) messages.warning( request, f"Start sent to {vm.name}, but it is not reporting 'running' yet " f"({state}); status left unchanged.", ) elif action == "stop": _save_with_changelog( request, vm, VirtualMachineStatusChoices.STATUS_OFFLINE, "Powered off via Proxmox (shutdown sent)", ) logger.info("user=%s vm=%s: stop (shutdown) sent", request.user, vm.name) messages.success(request, f"Stop sent to {vm.name}.") else: # reboot — VM stays powered on: changelog entry, but no save _log_changelog_only(request, vm, "Rebooted via Proxmox") logger.info("user=%s vm=%s: reboot sent", request.user, vm.name) messages.success(request, f"Reboot sent to {vm.name}.") return redirect("virtualization:virtualmachine", pk=vm.pk)