From bf6b736aef288acd087d6e4aece2d9161a20b4db Mon Sep 17 00:00:00 2001 From: catborise Date: Wed, 21 Oct 2026 20:55:12 +0300 Subject: [PATCH] fix(instances): the VM lists ask before a forced power off or power cycle In the VM lists, Force Off and Power Cycle ran on a single click, though both cut the VM off as a power failure does. Each button now asks first, naming the VM and saying that what the guest has not written to disk is lost. Co-Authored-By: Claude Opus 5.5 (1M context) --- instances/templates/instance_actions.html | 8 ++++++-- instances/tests/test_states.py | 13 +++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/instances/templates/instance_actions.html b/instances/templates/instance_actions.html index 38698843..8ee27d74 100644 --- a/instances/templates/instance_actions.html +++ b/instances/templates/instance_actions.html @@ -39,7 +39,9 @@ {% elif "force_off" in actions %}
{% csrf_token %} - +
{% else %} @@ -48,7 +50,9 @@ {% if "powercycle" in actions %}
{% csrf_token %} - +
{% else %} diff --git a/instances/tests/test_states.py b/instances/tests/test_states.py index d40b79f6..6654415d 100644 --- a/instances/tests/test_states.py +++ b/instances/tests/test_states.py @@ -13,6 +13,7 @@ from django.test import TestCase from django.urls import reverse from instances.models import Instance from instances.states import allowed_actions +from lxml import html LABELS = {0: "No state", 1: "Active", 2: "Blocked", 3: "Suspended", 4: "Shutting down", 5: "Off", 6: "Crashed", 7: "PM suspended"} # actions offered per state, for a superuser @@ -89,6 +90,18 @@ class StateTests(TestCase): with self.subTest(url=url, state=state): self.assertEqual(self.offered(page, vm), ACTIONS[state]) + def test_forced_power_actions_in_the_lists_ask_first(self): + # one click in a list row would cut the VM off as a power failure does + for url in (reverse("instances:index"), reverse("instances", args=[self.compute.id])): + page = html.fromstring(self.get(self.admin, url)) + for state, vm in self.vms.items(): + for action in {"force_off", "powercycle"} & ACTIONS[state]: + button = page.xpath(f"//form[@action='{reverse(f'instances:{action}', args=[vm.id])}']//button")[0] + with self.subTest(url=url, vm=vm.name, action=action): + self.assertEqual(button.get("onclick"), "return confirm(this.dataset.confirm)") + self.assertIn(vm.name, button.get("data-confirm")) + self.assertIn("power cut", button.get("data-confirm")) + def test_owner_is_not_offered_suspend_or_resume(self): # both views are for superusers only; a VM an administrator suspended # stays as it is for its owner, as on the VM page