From 73d8fd71fb3174eb78450f3076da00638d600229 Mon Sep 17 00:00:00 2001 From: catborise Date: Mon, 28 Sep 2026 14:56:19 +0300 Subject: [PATCH] fix(instances): guard mutations and refresh lists after sync --- .../templates/instances/settings_tab.html | 12 +- instances/test_create_instance.py | 64 +++ instances/test_index.py | 46 ++ instances/test_root_pass.py | 6 +- instances/test_security_mutations.py | 294 ++++++++++ instances/tests.py | 9 + instances/views.py | 517 +++++++++++------- 7 files changed, 741 insertions(+), 207 deletions(-) create mode 100644 instances/test_create_instance.py create mode 100644 instances/test_index.py diff --git a/instances/templates/instances/settings_tab.html b/instances/templates/instances/settings_tab.html index 5d91ba59..74ae23ed 100644 --- a/instances/templates/instances/settings_tab.html +++ b/instances/templates/instances/settings_tab.html @@ -81,9 +81,13 @@

{% trans "Autostart your instance when host server is power on " %} {% if instance.autostart == 0 %} - {% trans "Enable" %} +

{% csrf_token %} + +
{% else %} - {% trans "Disable" %} +
{% csrf_token %} + +
{% endif %}

@@ -198,9 +202,9 @@ {% endif %} {% if instance.status == 5 and allow_admin_or_not_template %} - + {% endif %} diff --git a/instances/test_create_instance.py b/instances/test_create_instance.py new file mode 100644 index 00000000..c981a2d9 --- /dev/null +++ b/instances/test_create_instance.py @@ -0,0 +1,64 @@ +"""Regression tests for the instance creation request.""" + +from unittest.mock import patch + +from computes.models import Compute +from django.contrib.auth import get_user_model +from django.test import TestCase +from django.urls import reverse +from instances.models import Instance + + +class CreateInstanceViewTests(TestCase): + @patch("instances.views.wvmCreate") + @patch("instances.views.NewVMForm") + def test_successful_create_redirects_after_saving_instance(self, form_class, connection_class): + user = get_user_model().objects.create_superuser( + username="create-test-admin", password="password", email="create@example.com" + ) + self.client.force_login(user) + compute = Compute.objects.create( + name="create-test-compute", hostname="localhost", type=4, + login="", password="", + ) + conn = connection_class.return_value + conn.get_instances.return_value = [] + conn.get_video_models.return_value = ["vga"] + conn.get_cache_modes.return_value = {"default": "Default"} + conn.get_disk_device_types.return_value = ["disk"] + conn.get_disk_bus_types.return_value = ["virtio"] + conn.get_networks.return_value = ["default"] + conn.get_nwfilters.return_value = [] + conn.get_network_models.return_value = ["virtio"] + conn.get_storages.return_value = ["default"] + conn.get_dom_capabilities.return_value = {"loader_enums": [], "loaders": []} + conn.get_capabilities.return_value = {} + conn.label_for_firmware_path.return_value = None + conn.find_uefi_path_for_arch.return_value = None + conn.get_volume_path.return_value = "/var/lib/libvirt/images/base.qcow2" + conn.get_volume_format_type.return_value = "qcow2" + + form = form_class.return_value + form.is_valid.return_value = True + form.cleaned_data = { + "name": "new-vm", "meta_prealloc": False, "hdd_size": 0, + "template": "", "images": "base.qcow2", "cache_mode": "default", + "firmware": "BIOS", "net_model": "virtio", "memory": 1024, + "vcpu": 1, "vcpu_mode": "host-model", "networks": "default", + "virtio": True, "listener_addr": "0.0.0.0", "nwfilter": "", + "graphics": "vnc", "video": "vga", "console_pass": "", + "mac": "52:54:00:00:00:01", "qemu_ga": False, + "add_cdrom": "sata", "add_input": "default", + } + + response = self.client.post( + reverse("instances:create_instance", args=[compute.id, "x86_64", "q35"]), + {"create": "1"}, + ) + + instance = Instance.objects.get(compute=compute, name="new-vm") + self.assertRedirects( + response, reverse("instances:instance", args=[instance.id]), + fetch_redirect_response=False, + ) + conn.create_instance.assert_called_once() diff --git a/instances/test_index.py b/instances/test_index.py new file mode 100644 index 00000000..8f8b2f5e --- /dev/null +++ b/instances/test_index.py @@ -0,0 +1,46 @@ +"""Tests for the instances list after out-of-band libvirt changes.""" + +from unittest.mock import PropertyMock, patch + +from computes.models import Compute +from django.contrib.auth import get_user_model +from django.test import TestCase +from django.urls import reverse +from instances.models import Instance + + +class InstancesIndexTests(TestCase): + def test_removed_domain_is_not_rendered_from_prefetch_cache(self): + user = get_user_model().objects.create_superuser( + username="instances-index-admin", password="password", email="index@example.com" + ) + self.client.force_login(user) + compute = Compute.objects.create( + name="index-compute", hostname="localhost", type=4, + login="", password="", + ) + instance = Instance.objects.create( + compute=compute, name="test4", uuid="cb2a2697-0945-408a-ac6b-c3e07a280851" + ) + self.assertEqual(Compute.objects.count(), 1) + + def synchronize(_compute): + Instance.objects.filter(pk=instance.pk).delete() + self.assertFalse(Instance.objects.filter(pk=instance.pk).exists()) + + with patch("instances.views.utils.refr", side_effect=synchronize), patch.object( + Compute, "status", new_callable=PropertyMock, return_value=True + ), patch.object( + Compute, "cpu_count", new_callable=PropertyMock, return_value=1 + ), patch.object( + Compute, "ram_size", new_callable=PropertyMock, return_value=1024 + ), patch.object( + Compute, "ram_usage", new_callable=PropertyMock, return_value=0 + ), patch.object( + Compute, "cpu_usage", new_callable=PropertyMock, return_value=0 + ): + response = self.client.get(reverse("instances:index")) + + self.assertEqual(response.status_code, 200) + self.assertNotContains(response, "test4") + self.assertEqual(list(response.context["computes"][0].instance_set.all()), []) diff --git a/instances/test_root_pass.py b/instances/test_root_pass.py index 9003feb4..fccd601f 100644 --- a/instances/test_root_pass.py +++ b/instances/test_root_pass.py @@ -46,10 +46,10 @@ class SetRootPassTestCase(TestCase): # SHA-512 crypt starts with $6$kgPoiREy$ self.assertTrue(data["passwd"].startswith("$6$kgPoiREy$")) - def test_set_root_pass_get_redirects(self): + def test_set_root_pass_get_not_allowed(self): response = self.client.get(reverse("instances:rootpasswd", args=[self.instance.id])) - self.assertEqual(response.status_code, 302) + self.assertEqual(response.status_code, 405) def test_set_root_pass_not_found(self): - response = self.client.get(reverse("instances:rootpasswd", args=[99999])) + response = self.client.post(reverse("instances:rootpasswd", args=[99999]), {"passwd": "secret"}) self.assertEqual(response.status_code, 404) diff --git a/instances/test_security_mutations.py b/instances/test_security_mutations.py index 7fd6f25f..0b047d2a 100644 --- a/instances/test_security_mutations.py +++ b/instances/test_security_mutations.py @@ -1,12 +1,15 @@ from unittest.mock import MagicMock, patch +from django.core.exceptions import PermissionDenied from django.contrib.auth import get_user_model from django.contrib.auth.models import Permission +from django.http import Http404 from django.test import TestCase from django.urls import reverse from accounts.models import UserInstance from computes.models import Compute from instances.models import Instance +from instances.views import get_instance class InstanceSecurityMutationsTestCase(TestCase): @@ -105,3 +108,294 @@ class InstanceSecurityMutationsTestCase(TestCase): url = f"/api/v1/computes/{other_compute.id}/instances/{self.instance.id}/" res = self.client.get(url) self.assertEqual(res.status_code, 404) + + def test_disk_media_and_snapshots_reject_get_requests(self): + self.client.force_login(self.change_user) + endpoints = [ + reverse("instances:add_new_vol", args=[self.instance.id]), + reverse("instances:add_existing_vol", args=[self.instance.id]), + reverse("instances:edit_volume", args=[self.instance.id]), + reverse("instances:delete_vol", args=[self.instance.id]), + reverse("instances:detach_vol", args=[self.instance.id]), + reverse("instances:add_cdrom", args=[self.instance.id]), + reverse("instances:detach_cdrom", args=[self.instance.id, "hda"]), + reverse("instances:mount_iso", args=[self.instance.id]), + reverse("instances:unmount_iso", args=[self.instance.id]), + reverse("instances:snapshot", args=[self.instance.id]), + reverse("instances:delete_snapshot", args=[self.instance.id]), + reverse("instances:revert_snapshot", args=[self.instance.id]), + ] + for url in endpoints: + res = self.client.get(url) + self.assertEqual(res.status_code, 405, f"GET {url} should return 405") + + def test_disk_and_media_mutations_require_is_change(self): + # Viewer (global read-only) -> 403 + self.client.force_login(self.viewer) + urls_to_test = [ + reverse("instances:add_new_vol", args=[self.instance.id]), + reverse("instances:detach_cdrom", args=[self.instance.id, "hda"]), + reverse("instances:mount_iso", args=[self.instance.id]), + reverse("instances:delete_vol", args=[self.instance.id]), + ] + for url in urls_to_test: + res = self.client.post(url, {}) + self.assertEqual(res.status_code, 403, f"Viewer POST {url} should be 403") + + # Owner without is_change (is_change=False, is_delete=False) -> 403 + self.client.force_login(self.owner) + for url in urls_to_test: + res = self.client.post(url, {}) + self.assertEqual(res.status_code, 403, f"Owner without is_change POST {url} should be 403") + + def test_resize_views_reject_get_requests(self): + self.client.force_login(self.change_user) + endpoints = [ + reverse("instances:resize_disk", args=[self.instance.id]), + reverse("instances:resizevm_cpu", args=[self.instance.id]), + reverse("instances:resize_memory", args=[self.instance.id]), + ] + for url in endpoints: + res = self.client.get(url) + self.assertEqual(res.status_code, 405, f"GET {url} should return 405") + + def test_resize_views_require_is_change(self): + self.client.force_login(self.viewer) + endpoints = [ + reverse("instances:resize_disk", args=[self.instance.id]), + reverse("instances:resizevm_cpu", args=[self.instance.id]), + reverse("instances:resize_memory", args=[self.instance.id]), + ] + for url in endpoints: + res = self.client.post(url, {}) + self.assertEqual(res.status_code, 403, f"Viewer POST {url} should return 403") + + self.client.force_login(self.owner) + for url in endpoints: + res = self.client.post(url, {}) + self.assertEqual(res.status_code, 403, f"Owner without is_change POST {url} should return 403") + + def test_get_instance_fail_closed_for_all_users_on_invalid_perm(self): + with self.assertRaises(PermissionDenied): + get_instance(self.superuser, self.instance.pk, perm_type="unknown_perm") + + with self.assertRaises(PermissionDenied): + get_instance(self.owner, self.instance.pk, perm_type="unknown_perm") + + def test_get_instance_staff_user_access(self): + User = get_user_model() + staff_user = User.objects.create_user(username="sec_staff", password="password", is_staff=True) + # Staff user without ownership or view_instances permission cannot view instance + with self.assertRaises(Http404): + get_instance(staff_user, self.instance.pk, perm_type="view") + + # Staff user with explicit view_instances permission can view instance + view_perm = Permission.objects.get(codename="view_instances") + staff_user.user_permissions.add(view_perm) + staff_user = User.objects.get(pk=staff_user.pk) + inst = get_instance(staff_user, self.instance.pk, perm_type="view") + self.assertEqual(inst.pk, self.instance.pk) + + # Staff user with view permission but without ownership cannot perform power, change, or delete operations + for perm in ["power", "change", "delete"]: + with self.assertRaises(PermissionDenied): + get_instance(staff_user, self.instance.pk, perm_type=perm) + + def test_migrate_endpoint_requires_post(self): + self.client.force_login(self.superuser) + res = self.client.get(reverse("instances:migrate", args=[self.instance.id])) + self.assertEqual(res.status_code, 405) + + def test_post_only_mutation_endpoints(self): + self.client.force_login(self.change_user) + # All mutation endpoints reject GET with 405 Method Not Allowed + endpoints_405 = ["change_options", "update_console", "rootpasswd", "add_public_key"] + for ep in endpoints_405: + res = self.client.get(reverse(f"instances:{ep}", args=[self.instance.id])) + self.assertEqual(res.status_code, 405, f"{ep} allowed GET request instead of 405") + + def test_opposite_direction_migrations_no_deadlock(self): + import threading + import time + from instances.utils import migrate_instance + + c1 = MagicMock() + c1.id = 101 + c1.pk = 101 + c1.hostname = "127.0.0.1" + c1.type = 1 + c1.login = "root" + c1.password = "" + + c2 = MagicMock() + c2.id = 102 + c2.pk = 102 + c2.hostname = "127.0.0.2" + c2.type = 1 + c2.login = "root" + c2.password = "" + + inst1 = MagicMock() + inst1.compute = c1 + inst1.name = "inst1" + inst1.autostart = False + inst1.proxy = MagicMock() + inst1.uuid = "u1" + + inst2 = MagicMock() + inst2.compute = c2 + inst2.name = "inst2" + inst2.autostart = False + inst2.proxy = MagicMock() + inst2.uuid = "u2" + + barrier = threading.Barrier(2) + errors = [] + + def worker_1_to_2(): + try: + barrier.wait(timeout=5) + migrate_instance(c2, inst1, self.superuser) + except Exception as e: + errors.append(("worker_1_to_2", e)) + + def worker_2_to_1(): + try: + barrier.wait(timeout=5) + migrate_instance(c1, inst2, self.superuser) + except Exception as e: + errors.append(("worker_2_to_1", e)) + + def slow_moveto(*args, **kwargs): + time.sleep(0.05) + + with patch("instances.utils.connection_manager.host_is_up", return_value=True), \ + patch("instances.utils.wvmInstances") as mock_instances_cls, \ + patch("instances.utils.wvmInstance") as mock_instance_cls, \ + patch("instances.utils.transaction.atomic"), \ + patch("instances.utils.Instance") as mock_instance_model: + + mock_instances_cls.return_value.moveto.side_effect = slow_moveto + mock_instance_model.objects.filter.return_value.first.return_value = None + + t1 = threading.Thread(target=worker_1_to_2) + t2 = threading.Thread(target=worker_2_to_1) + t1.start() + t2.start() + t1.join(timeout=10) + t2.join(timeout=10) + + self.assertFalse(t1.is_alive(), "Worker 1 to 2 timed out / deadlocked") + self.assertFalse(t2.is_alive(), "Worker 2 to 1 timed out / deadlocked") + self.assertEqual(errors, [], f"Unexpected errors during concurrent migrations: {errors}") + + def test_resize_disk_empty_selection_warns_cleanly(self): + self.client.force_login(self.change_user) + with patch("instances.models.wvmInstance") as mock_wvm_cls: + mock_wvm_cls.return_value.get_disk_devices.return_value = [] + res = self.client.post(reverse("instances:resize_disk", args=[self.instance.id]), {}) + self.assertEqual(res.status_code, 302) + self.assertIn(f"/instances/{self.instance.id}/#resize", res.url) + + def test_subresource_mutation_allowed_with_is_change(self): + # change_user has is_change=True, is_delete=False + self.client.force_login(self.change_user) + with patch("instances.models.wvmInstance"), patch("instances.views.wvmStorage"): + # delete_vol uses perm_type="change", so it passes get_instance without 403 + res = self.client.post( + reverse("instances:delete_vol", args=[self.instance.id]), + {"storage": "default", "dev": "vda", "path": "/path"}, + HTTP_REFERER=f"/instances/{self.instance.id}/", + ) + self.assertEqual(res.status_code, 302) + + def test_destroy_vm_authorization(self): + # 1. User with is_change=True but is_delete=False gets 403 when trying to destroy VM + self.client.force_login(self.change_user) + res = self.client.post(reverse("instances:destroy", args=[self.instance.id]), {}) + self.assertEqual(res.status_code, 403) + + # 2. Staff user without ownership or is_delete cannot destroy VM + User = get_user_model() + staff_user = User.objects.create_user(username="staff_destroyer", password="password", is_staff=True) + self.client.force_login(staff_user) + res_staff = self.client.post(reverse("instances:destroy", args=[self.instance.id]), {}) + self.assertEqual(res_staff.status_code, 404) + + # 2b. Staff user with view_instances permission but is_delete=False gets 403 + view_perm = Permission.objects.get(codename="view_instances") + staff_user.user_permissions.add(view_perm) + res_staff_view = self.client.post(reverse("instances:destroy", args=[self.instance.id]), {}) + self.assertEqual(res_staff_view.status_code, 403) + + # 3. Superuser can destroy VM + self.client.force_login(self.superuser) + with patch("instances.models.wvmInstance") as mock_wvm_cls: + mock_wvm_cls.return_value.get_status.return_value = 5 # shutoff + res = self.client.post(reverse("instances:destroy", args=[self.instance.id]), {}) + self.assertEqual(res.status_code, 302) + self.assertFalse(Instance.objects.filter(id=self.instance.id).exists()) + + def test_migration_deduplicate_instances_logic(self): + import importlib + migration_mod = importlib.import_module("instances.migrations.0012_instance_unique_compute_uuid") + dedup_fn = getattr(migration_mod, "deduplicate_instances") + + mock_apps = MagicMock() + mock_instance_model = MagicMock() + mock_userinstance_model = MagicMock() + + def get_model(app_label, model_name): + if model_name == "Instance": + return mock_instance_model + elif model_name == "UserInstance": + return mock_userinstance_model + return MagicMock() + + mock_apps.get_model = get_model + + empty_inst = MagicMock(uuid="") + primary_inst = MagicMock(id=1) + dup_inst = MagicMock(id=2) + + def mock_inst_filter(*args, **kwargs): + if args: + # Called for empty_uuid_instances query + return [empty_inst] + # Called with compute_id and uuid + ret = MagicMock() + ret.order_by.return_value = [primary_inst, dup_inst] + return ret + + mock_instance_model.objects.filter.side_effect = mock_inst_filter + mock_instance_model.objects.exclude.return_value.exclude.return_value.values.return_value.annotate.return_value.filter.return_value = [ + {"compute_id": 1, "uuid": "test-uuid-1234"} + ] + + primary_ui = MagicMock(is_change=False, is_delete=False, is_vnc=False) + dup_ui = MagicMock(user="user1", is_change=True, is_delete=False, is_vnc=True) + + mock_userinstance_model.objects.filter.side_effect = lambda **kwargs: ( + [dup_ui] if kwargs.get("instance") == dup_inst else + MagicMock(first=lambda: primary_ui) + ) + + dedup_fn(mock_apps, None) + + self.assertTrue(len(empty_inst.uuid) > 0) + empty_inst.save.assert_called_once_with(update_fields=['uuid']) + dup_inst.delete.assert_called_once() + dup_ui.delete.assert_called_once() + self.assertTrue(primary_ui.is_change) + self.assertTrue(primary_ui.is_vnc) + self.assertFalse(primary_ui.is_delete) + primary_ui.save.assert_called_once() + + def test_unique_compute_uuid_db_constraint_enforced(self): + from django.db import IntegrityError + with self.assertRaises(IntegrityError): + Instance.objects.create( + compute=self.compute, + name="duplicate_vm", + uuid=self.instance.uuid, + ) diff --git a/instances/tests.py b/instances/tests.py index 878b8d0a..4ff0c96b 100644 --- a/instances/tests.py +++ b/instances/tests.py @@ -81,6 +81,15 @@ class InstancesTestCase(TestCase): cls.instance: Instance = Instance.objects.filter(compute=cls.compute).first() except Exception as e: cls.instance = None + if getattr(cls, "compute", None): + try: + cls.compute.delete() + except Exception: + pass + try: + super().tearDownClass() + except Exception: + pass raise unittest.SkipTest(f"Live libvirt host not available for instances tests: {e}") @classmethod diff --git a/instances/views.py b/instances/views.py index a5c862a7..1c45f9d3 100755 --- a/instances/views.py +++ b/instances/views.py @@ -2,6 +2,7 @@ try: import crypt_r as crypt except ImportError: import crypt +import functools import json import os import re @@ -15,12 +16,14 @@ from admin.decorators import superuser_only from appsettings.models import AppSettings from appsettings.settings import app_settings from computes.models import Compute +from computes.utils import libvirt_compute_lock from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import permission_required from django.contrib.auth.models import User from django.core.exceptions import PermissionDenied from django.http import Http404, HttpResponse, JsonResponse +from django.db.models import prefetch_related_objects from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils.http import url_has_allowed_host_and_scheme @@ -48,14 +51,10 @@ from .models import Flavor def index(request): instances = None - computes = ( - Compute.objects.all() - .order_by("name") - .prefetch_related("instance_set") - .prefetch_related("instance_set__userinstance_set") - ) + computes = list(Compute.objects.all().order_by("name")) for compute in computes: utils.refr(compute) + prefetch_related_objects(computes, "instance_set__userinstance_set") if request.user.is_superuser or request.user.has_perm("instances.view_instances"): instances = Instance.objects.all().prefetch_related("userinstance_set") @@ -341,6 +340,10 @@ def get_instance(user, pk, perm_type="view"): - 'change': superuser or (UserInstance owner and is_change) - 'delete': superuser or (UserInstance owner and is_delete) """ + valid_perms = {"view", "power", "change", "delete"} + if perm_type not in valid_perms: + raise PermissionDenied + instance = get_object_or_404(Instance, pk=pk) user_instances = user.userinstance_set.filter(instance=instance) has_owner_rel = user_instances.exists() @@ -365,11 +368,32 @@ def get_instance(user, pk, perm_type="view"): elif perm_type == "delete": if not (user_inst and user_inst.is_delete): raise PermissionDenied + else: + raise PermissionDenied return instance +def serialize_instance_mutation(func): + """ + Decorator that ensures any view mutating an instance's libvirt state + acquires libvirt_compute_lock for the instance's compute. + Works re-entrantly with inner calls such as refresh_instance_database. + """ + @functools.wraps(func) + def wrapper(request, pk, *args, **kwargs): + try: + inst = Instance.objects.only("compute_id").get(pk=pk) + with libvirt_compute_lock(inst.compute): + return func(request, pk, *args, **kwargs) + except Instance.DoesNotExist: + return func(request, pk, *args, **kwargs) + + return wrapper + + @require_POST +@serialize_instance_mutation def poweron(request, pk): instance = get_instance(request.user, pk, perm_type="power") if instance.is_template: @@ -386,6 +410,7 @@ def poweron(request, pk): @require_POST +@serialize_instance_mutation def powercycle(request, pk): instance = get_instance(request.user, pk, perm_type="power") instance.proxy.force_shutdown() @@ -399,6 +424,7 @@ def powercycle(request, pk): @require_POST +@serialize_instance_mutation def poweroff(request, pk): instance = get_instance(request.user, pk, perm_type="power") instance.proxy.shutdown() @@ -413,6 +439,7 @@ def poweroff(request, pk): @require_POST @superuser_only +@serialize_instance_mutation def suspend(request, pk): instance = get_instance(request.user, pk) instance.proxy.suspend() @@ -424,6 +451,7 @@ def suspend(request, pk): @require_POST @superuser_only +@serialize_instance_mutation def resume(request, pk): instance = get_instance(request.user, pk) instance.proxy.resume() @@ -434,6 +462,7 @@ def resume(request, pk): @require_POST +@serialize_instance_mutation def force_off(request, pk): instance = get_instance(request.user, pk, perm_type="power") instance.proxy.force_shutdown() @@ -446,37 +475,39 @@ def force_off(request, pk): def destroy(request, pk): - instance = get_instance(request.user, pk) - try: - userinstance = instance.userinstance_set.get(user=request.user) - except Exception: - userinstance = UserInstance(is_delete=request.user.is_superuser) - if request.method in ["POST", "DELETE"]: - if not userinstance.is_delete: - raise PermissionDenied - if instance.proxy.get_status() == 1: - instance.proxy.force_shutdown() + instance = get_instance(request.user, pk, perm_type="delete") + with libvirt_compute_lock(instance.compute): + if instance.proxy.get_status() == 1: + instance.proxy.force_shutdown() - if request.POST.get("delete_disk", ""): - snapshots = sorted( - instance.proxy.get_snapshot(), reverse=True, key=lambda k: k["date"] - ) - for snapshot in snapshots: - instance.proxy.snapshot_delete(snapshot["name"]) - instance.proxy.delete_all_disks() + if request.POST.get("delete_disk", ""): + snapshots = sorted( + instance.proxy.get_snapshot(), reverse=True, key=lambda k: k["date"] + ) + for snapshot in snapshots: + instance.proxy.snapshot_delete(snapshot["name"]) + instance.proxy.delete_all_disks() - if request.POST.get("delete_nvram", ""): - instance.proxy.delete(VIR_DOMAIN_UNDEFINE_NVRAM) - else: - instance.proxy.delete(VIR_DOMAIN_UNDEFINE_KEEP_NVRAM) + if request.POST.get("delete_nvram", ""): + instance.proxy.delete(VIR_DOMAIN_UNDEFINE_NVRAM) + else: + instance.proxy.delete(VIR_DOMAIN_UNDEFINE_KEEP_NVRAM) - instance.delete() + instance.delete() addlogmsg( request.user.username, instance.compute.name, instance.name, _("Destroy") ) return redirect(reverse("instances:index")) + instance = get_instance(request.user, pk, perm_type="delete") + try: + userinstance = instance.userinstance_set.get(user=request.user) + except Exception: + userinstance = UserInstance( + is_delete=request.user.is_superuser + ) + return render( request, "instances/destroy_instance_form.html", @@ -487,6 +518,7 @@ def destroy(request, pk): ) +@require_POST @superuser_only def migrate(request, pk): instance = get_instance(request.user, pk) @@ -531,6 +563,8 @@ def migrate(request, pk): ) +@require_POST +@serialize_instance_mutation def set_root_pass(request, pk): instance = get_instance(request.user, pk, perm_type="change") @@ -558,11 +592,16 @@ def set_root_pass(request, pk): else: msg = _("Please shutdown down your instance and then try again") messages.error(request, msg) - return redirect(reverse("instances:instance", args=[instance.id]) + "#access") + return get_safe_redirect( + request, default=reverse("instances:instance", args=[instance.id]) + "#access" + ) +@require_POST +@serialize_instance_mutation def add_public_key(request, pk): instance = get_instance(request.user, pk, perm_type="change") + if request.method == "POST": sshkeyid = request.POST.get("sshkeyid", "") publickey = get_object_or_404(UserSSHKey, id=sshkeyid, user=request.user) @@ -593,142 +632,138 @@ def add_public_key(request, pk): else: msg = _("Please shutdown down your instance and then try again") messages.error(request, msg) - return redirect(reverse("instances:instance", args=[instance.id]) + "#access") + return get_safe_redirect( + request, default=reverse("instances:instance", args=[instance.id]) + "#access" + ) +@require_POST +@serialize_instance_mutation def resizevm_cpu(request, pk): - instance = get_instance(request.user, pk) - try: - userinstance = instance.userinstance_set.get(user=request.user) - except Exception: - userinstance = UserInstance(is_change=False) + instance = get_instance(request.user, pk, perm_type="change") vcpu = instance.proxy.get_vcpu() - if request.method == "POST": - if request.user.is_superuser or request.user.is_staff or userinstance.is_change: - new_vcpu = request.POST.get("vcpu", "") - new_cur_vcpu = request.POST.get("cur_vcpu", "") - quota_msg = utils.check_user_quota( - request.user, 0, int(new_vcpu) - vcpu, 0, 0 - ) - if not request.user.is_superuser and quota_msg: - msg = _( - "User %(quota_msg)s quota reached, cannot resize CPU of '%(instance_name)s'!" - ) % { - "quota_msg": quota_msg, - "instance_name": instance.name, - } - messages.error(request, msg) - else: - cur_vcpu = new_cur_vcpu - vcpu = new_vcpu - instance.proxy.resize_cpu(cur_vcpu, vcpu) - msg = _("CPU is resized: %(old)s to %(new)s") % { - "old": cur_vcpu, - "new": vcpu, - } - addlogmsg( - request.user.username, instance.compute.name, instance.name, msg - ) - messages.success(request, msg) + new_vcpu = request.POST.get("vcpu", "") + new_cur_vcpu = request.POST.get("cur_vcpu", "") + + quota_msg = utils.check_user_quota( + request.user, 0, int(new_vcpu) - vcpu, 0, 0 + ) + if not request.user.is_superuser and quota_msg: + msg = _( + "User %(quota_msg)s quota reached, cannot resize CPU of '%(instance_name)s'!" + ) % { + "quota_msg": quota_msg, + "instance_name": instance.name, + } + messages.error(request, msg) + else: + cur_vcpu = new_cur_vcpu + vcpu = new_vcpu + instance.proxy.resize_cpu(cur_vcpu, vcpu) + msg = _("CPU is resized: %(old)s to %(new)s") % { + "old": cur_vcpu, + "new": vcpu, + } + addlogmsg( + request.user.username, instance.compute.name, instance.name, msg + ) + messages.success(request, msg) return redirect(reverse("instances:instance", args=[instance.id]) + "#resize") +@require_POST +@serialize_instance_mutation def resize_memory(request, pk): - instance = get_instance(request.user, pk) - try: - userinstance = instance.userinstance_set.get(user=request.user) - except Exception: - userinstance = UserInstance(is_change=False) - + instance = get_instance(request.user, pk, perm_type="change") memory = instance.proxy.get_memory() cur_memory = instance.proxy.get_cur_memory() - if request.method == "POST": - if request.user.is_superuser or request.user.is_staff or userinstance.is_change: - new_memory = request.POST.get("memory", "") - new_memory_custom = request.POST.get("memory_custom", "") - if new_memory_custom: - new_memory = new_memory_custom - new_cur_memory = request.POST.get("cur_memory", "") - new_cur_memory_custom = request.POST.get("cur_memory_custom", "") - if new_cur_memory_custom: - new_cur_memory = new_cur_memory_custom - quota_msg = utils.check_user_quota( - request.user, 0, 0, int(new_memory) - memory, 0 - ) - if not request.user.is_superuser and quota_msg: - msg = _( - "User %(quota_msg)s quota reached, cannot resize memory of '%(instance_name)s'!" - ) % { - "quota_msg": quota_msg, - "instance_name": instance.name, - } - messages.error(request, msg) - else: - instance.proxy.resize_mem(new_cur_memory, new_memory) - msg = _( - "Memory is resized: current/max: %(old_cur)s/%(old_max)s to %(new_cur)s/%(new_max)s" - ) % { - "old_cur": cur_memory, - "old_max": memory, - "new_cur": new_cur_memory, - "new_max": new_memory, - } - addlogmsg( - request.user.username, instance.compute.name, instance.name, msg - ) - messages.success(request, msg) + new_memory = request.POST.get("memory", "") + new_memory_custom = request.POST.get("memory_custom", "") + if new_memory_custom: + new_memory = new_memory_custom + new_cur_memory = request.POST.get("cur_memory", "") + new_cur_memory_custom = request.POST.get("cur_memory_custom", "") + if new_cur_memory_custom: + new_cur_memory = new_cur_memory_custom + quota_msg = utils.check_user_quota( + request.user, 0, 0, int(new_memory) - memory, 0 + ) + if not request.user.is_superuser and quota_msg: + msg = _( + "User %(quota_msg)s quota reached, cannot resize memory of '%(instance_name)s'!" + ) % { + "quota_msg": quota_msg, + "instance_name": instance.name, + } + messages.error(request, msg) + else: + instance.proxy.resize_mem(new_cur_memory, new_memory) + msg = _( + "Memory is resized: current/max: %(old_cur)s/%(old_max)s to %(new_cur)s/%(new_max)s" + ) % { + "old_cur": cur_memory, + "old_max": memory, + "new_cur": new_cur_memory, + "new_max": new_memory, + } + addlogmsg( + request.user.username, instance.compute.name, instance.name, msg + ) + messages.success(request, msg) return redirect(reverse("instances:instance", args=[instance.id]) + "#resize") +@require_POST +@serialize_instance_mutation def resize_disk(request, pk): - instance = get_instance(request.user, pk) - - try: - userinstance = instance.userinstance_set.get(user=request.user) - except Exception: - userinstance = UserInstance(is_change=False) - + instance = get_instance(request.user, pk, perm_type="change") disks = instance.proxy.get_disk_devices() - if request.method == "POST": - if request.user.is_superuser or request.user.is_staff or userinstance.is_change: - disks_new = list() - for disk in disks: - input_disk_size = ( - int(request.POST.get("disk_size_" + disk["dev"], "0")) * 1073741824 - ) - if input_disk_size > disk["size"] + (64 << 20): - disk["size_new"] = input_disk_size - disks_new.append(disk) - disk_sum = sum([disk["size"] >> 30 for disk in disks_new]) - disk_new_sum = sum([disk["size_new"] >> 30 for disk in disks_new]) - quota_msg = utils.check_user_quota( - request.user, 0, 0, 0, disk_new_sum - disk_sum - ) - if not request.user.is_superuser and quota_msg: - msg = _( - "User %(quota_msg)s quota reached, cannot resize disks of '%(instance_name)s'!" - ) % { - "quota_msg": quota_msg, - "instance_name": instance.name, - } - messages.error(request, msg) - else: - instance.proxy.resize_disk(disks_new) - msg = _("Disk is resized: %(dev)s") % {"dev": disk["dev"]} - addlogmsg( - request.user.username, instance.compute.name, instance.name, msg - ) - messages.success(request, msg) + disks_new = list() + for disk in disks: + input_disk_size = ( + int(request.POST.get("disk_size_" + disk["dev"], "0")) * 1073741824 + ) + if input_disk_size > disk["size"] + (64 << 20): + disk["size_new"] = input_disk_size + disks_new.append(disk) + + if not disks_new: + messages.warning(request, _("No disks were selected or valid for resizing.")) + return redirect(reverse("instances:instance", args=[instance.id]) + "#resize") + + disk_sum = sum([disk["size"] >> 30 for disk in disks_new]) + disk_new_sum = sum([disk["size_new"] >> 30 for disk in disks_new]) + quota_msg = utils.check_user_quota( + request.user, 0, 0, 0, disk_new_sum - disk_sum + ) + if not request.user.is_superuser and quota_msg: + msg = _( + "User %(quota_msg)s quota reached, cannot resize disks of '%(instance_name)s'!" + ) % { + "quota_msg": quota_msg, + "instance_name": instance.name, + } + messages.error(request, msg) + else: + instance.proxy.resize_disk(disks_new) + devs_str = ", ".join([d["dev"] for d in disks_new]) + msg = _("Disk is resized: %(dev)s") % {"dev": devs_str} + addlogmsg( + request.user.username, instance.compute.name, instance.name, msg + ) + messages.success(request, msg) return redirect(reverse("instances:instance", args=[instance.id]) + "#resize") +@require_POST +@serialize_instance_mutation def add_new_vol(request, pk): - instance = get_instance(request.user, pk) + instance = get_instance(request.user, pk, perm_type="change") allow_admin_or_not_template = ( request.user.is_superuser or request.user.is_staff or not instance.is_template ) @@ -795,8 +830,10 @@ def add_new_vol(request, pk): return redirect(request.META.get("HTTP_REFERER") + "#disks") +@require_POST +@serialize_instance_mutation def add_existing_vol(request, pk): - instance = get_instance(request.user, pk) + instance = get_instance(request.user, pk, perm_type="change") allow_admin_or_not_template = ( request.user.is_superuser or request.user.is_staff or not instance.is_template ) @@ -845,8 +882,10 @@ def add_existing_vol(request, pk): return redirect(request.META.get("HTTP_REFERER") + "#disks") +@require_POST +@serialize_instance_mutation def edit_volume(request, pk): - instance = get_instance(request.user, pk) + instance = get_instance(request.user, pk, perm_type="change") allow_admin_or_not_template = ( request.user.is_superuser or request.user.is_staff or not instance.is_template ) @@ -919,8 +958,10 @@ def edit_volume(request, pk): return redirect(request.META.get("HTTP_REFERER") + "#disks") +@require_POST +@serialize_instance_mutation def delete_vol(request, pk): - instance = get_instance(request.user, pk) + instance = get_instance(request.user, pk, perm_type="change") allow_admin_or_not_template = ( request.user.is_superuser or request.user.is_staff or not instance.is_template ) @@ -945,8 +986,10 @@ def delete_vol(request, pk): return redirect(request.META.get("HTTP_REFERER") + "#disks") +@require_POST +@serialize_instance_mutation def detach_vol(request, pk): - instance = get_instance(request.user, pk) + instance = get_instance(request.user, pk, perm_type="change") allow_admin_or_not_template = ( request.user.is_superuser or request.user.is_staff or not instance.is_template ) @@ -961,8 +1004,10 @@ def detach_vol(request, pk): return redirect(request.META.get("HTTP_REFERER") + "#disks") +@require_POST +@serialize_instance_mutation def add_cdrom(request, pk): - instance = get_instance(request.user, pk) + instance = get_instance(request.user, pk, perm_type="change") allow_admin_or_not_template = ( request.user.is_superuser or request.user.is_staff or not instance.is_template ) @@ -983,8 +1028,10 @@ def add_cdrom(request, pk): return redirect(request.META.get("HTTP_REFERER") + "#disks") +@require_POST +@serialize_instance_mutation def detach_cdrom(request, pk, dev): - instance = get_instance(request.user, pk) + instance = get_instance(request.user, pk, perm_type="change") allow_admin_or_not_template = ( request.user.is_superuser or request.user.is_staff or not instance.is_template ) @@ -998,8 +1045,10 @@ def detach_cdrom(request, pk, dev): return redirect(request.META.get("HTTP_REFERER") + "#disks") +@require_POST +@serialize_instance_mutation def unmount_iso(request, pk): - instance = get_instance(request.user, pk) + instance = get_instance(request.user, pk, perm_type="change") allow_admin_or_not_template = ( request.user.is_superuser or request.user.is_staff or not instance.is_template ) @@ -1013,8 +1062,10 @@ def unmount_iso(request, pk): return redirect(request.META.get("HTTP_REFERER") + "#disks") +@require_POST +@serialize_instance_mutation def mount_iso(request, pk): - instance = get_instance(request.user, pk) + instance = get_instance(request.user, pk, perm_type="change") allow_admin_or_not_template = ( request.user.is_superuser or request.user.is_staff or not instance.is_template ) @@ -1028,8 +1079,10 @@ def mount_iso(request, pk): return redirect(request.META.get("HTTP_REFERER") + "#disks") +@require_POST +@serialize_instance_mutation def snapshot(request, pk): - instance = get_instance(request.user, pk) + instance = get_instance(request.user, pk, perm_type="change") allow_admin_or_not_template = ( request.user.is_superuser or request.user.is_staff or not instance.is_template ) @@ -1045,8 +1098,10 @@ def snapshot(request, pk): return redirect(request.META.get("HTTP_REFERER") + "#managesnapshot") +@require_POST +@serialize_instance_mutation def delete_snapshot(request, pk): - instance = get_instance(request.user, pk) + instance = get_instance(request.user, pk, perm_type="change") allow_admin_or_not_template = ( request.user.is_superuser or request.user.is_staff or not instance.is_template ) @@ -1060,8 +1115,10 @@ def delete_snapshot(request, pk): return redirect(request.META.get("HTTP_REFERER") + "#managesnapshot") +@require_POST +@serialize_instance_mutation def revert_snapshot(request, pk): - instance = get_instance(request.user, pk) + instance = get_instance(request.user, pk, perm_type="change") allow_admin_or_not_template = ( request.user.is_superuser or request.user.is_staff or not instance.is_template ) @@ -1078,8 +1135,10 @@ def revert_snapshot(request, pk): return redirect(request.META.get("HTTP_REFERER") + "#managesnapshot") +@require_POST +@serialize_instance_mutation def create_external_snapshot(request, pk): - instance = get_instance(request.user, pk) + instance = get_instance(request.user, pk, perm_type="change") allow_admin_or_not_template = ( request.user.is_superuser or request.user.is_staff or not instance.is_template ) @@ -1108,8 +1167,10 @@ def get_external_snapshots(request, pk): return external_snapshots +@require_POST +@serialize_instance_mutation def revert_external_snapshot(request, pk): - instance = get_instance(request.user, pk) + instance = get_instance(request.user, pk, perm_type="change") allow_admin_or_not_template = ( request.user.is_superuser or request.user.is_staff or not instance.is_template ) @@ -1129,8 +1190,10 @@ def revert_external_snapshot(request, pk): return redirect(request.META.get("HTTP_REFERER") + "#managesnapshot") +@require_POST +@serialize_instance_mutation def delete_external_snapshot(request, pk): - instance = get_instance(request.user, pk) + instance = get_instance(request.user, pk, perm_type="change") instance_state = True if instance.proxy.get_status() == 5 else False allow_admin_or_not_template = ( request.user.is_superuser or request.user.is_staff or not instance.is_template @@ -1153,7 +1216,9 @@ def delete_external_snapshot(request, pk): return redirect(request.META.get("HTTP_REFERER") + "#managesnapshot") +@require_POST @superuser_only +@serialize_instance_mutation def set_vcpu(request, pk): instance = get_instance(request.user, pk) id = request.POST.get("id", "") @@ -1167,7 +1232,9 @@ def set_vcpu(request, pk): return redirect(request.META.get("HTTP_REFERER") + "#resize") +@require_POST @superuser_only +@serialize_instance_mutation def set_vcpu_hotplug(request, pk): instance = get_instance(request.user, pk) status = True if request.POST.get("vcpu_hotplug", "False") == "True" else False @@ -1177,7 +1244,9 @@ def set_vcpu_hotplug(request, pk): return redirect(request.META.get("HTTP_REFERER") + "#resize") +@require_POST @superuser_only +@serialize_instance_mutation def set_autostart(request, pk): instance = get_instance(request.user, pk) instance.proxy.set_autostart(1) @@ -1186,7 +1255,9 @@ def set_autostart(request, pk): return redirect(request.META.get("HTTP_REFERER") + "#boot_opt") +@require_POST @superuser_only +@serialize_instance_mutation def unset_autostart(request, pk): instance = get_instance(request.user, pk) instance.proxy.set_autostart(0) @@ -1195,7 +1266,9 @@ def unset_autostart(request, pk): return redirect(request.META.get("HTTP_REFERER") + "#boot_opt") +@require_POST @superuser_only +@serialize_instance_mutation def set_bootmenu(request, pk): instance = get_instance(request.user, pk) instance.proxy.set_bootmenu(1) @@ -1204,7 +1277,9 @@ def set_bootmenu(request, pk): return redirect(request.META.get("HTTP_REFERER") + "#boot_opt") +@require_POST @superuser_only +@serialize_instance_mutation def unset_bootmenu(request, pk): instance = get_instance(request.user, pk) instance.proxy.set_bootmenu(0) @@ -1213,7 +1288,9 @@ def unset_bootmenu(request, pk): return redirect(request.META.get("HTTP_REFERER") + "#boot_opt") +@require_POST @superuser_only +@serialize_instance_mutation def set_bootorder(request, pk): instance = get_instance(request.user, pk) bootorder = request.POST.get("bootorder", "") @@ -1239,7 +1316,9 @@ def set_bootorder(request, pk): return redirect(request.META.get("HTTP_REFERER") + "#boot_opt") +@require_POST @superuser_only +@serialize_instance_mutation def change_xml(request, pk): instance = get_instance(request.user, pk) new_xml = request.POST.get("inst_xml", "") @@ -1250,7 +1329,9 @@ def change_xml(request, pk): return redirect(request.META.get("HTTP_REFERER") + "#xmledit") +@require_POST @superuser_only +@serialize_instance_mutation def set_guest_agent(request, pk): instance = get_instance(request.user, pk) status = request.POST.get("guest_agent") @@ -1264,7 +1345,9 @@ def set_guest_agent(request, pk): return redirect(request.META.get("HTTP_REFERER") + "#options") +@require_POST @superuser_only +@serialize_instance_mutation def set_video_model(request, pk): instance = get_instance(request.user, pk) video_model = request.POST.get("video_model", "vga") @@ -1274,7 +1357,9 @@ def set_video_model(request, pk): return redirect(request.META.get("HTTP_REFERER") + "#options") +@require_POST @superuser_only +@serialize_instance_mutation def change_network(request, pk): instance = get_instance(request.user, pk) @@ -1307,7 +1392,9 @@ def change_network(request, pk): return redirect(request.META.get("HTTP_REFERER") + "#network") +@require_POST @superuser_only +@serialize_instance_mutation def add_network(request, pk): instance = get_instance(request.user, pk) @@ -1332,7 +1419,9 @@ def add_network(request, pk): return redirect(request.META.get("HTTP_REFERER") + "#network") +@require_POST @superuser_only +@serialize_instance_mutation def delete_network(request, pk): instance = get_instance(request.user, pk) mac_address = request.POST.get("delete_network", "") @@ -1343,7 +1432,9 @@ def delete_network(request, pk): return redirect(request.META.get("HTTP_REFERER") + "#network") +@require_POST @superuser_only +@serialize_instance_mutation def set_link_state(request, pk): instance = get_instance(request.user, pk) @@ -1356,7 +1447,9 @@ def set_link_state(request, pk): return redirect(request.META.get("HTTP_REFERER") + "#network") +@require_POST @superuser_only +@serialize_instance_mutation def set_qos(request, pk): instance = get_instance(request.user, pk) @@ -1387,7 +1480,9 @@ def set_qos(request, pk): return redirect(request.META.get("HTTP_REFERER") + "#network") +@require_POST @superuser_only +@serialize_instance_mutation def unset_qos(request, pk): instance = get_instance(request.user, pk) qos_dir = request.POST.get("qos_direction", "") @@ -1410,6 +1505,7 @@ def unset_qos(request, pk): return redirect(request.META.get("HTTP_REFERER") + "#network") +@require_POST @superuser_only def add_owner(request, pk): instance = get_instance(request.user, pk) @@ -1433,6 +1529,7 @@ def add_owner(request, pk): return redirect(request.META.get("HTTP_REFERER") + "#users") +@require_POST @superuser_only def del_owner(request, pk): instance = get_instance(request.user, pk) @@ -1444,6 +1541,7 @@ def del_owner(request, pk): return redirect(request.META.get("HTTP_REFERER") + "#users") +@require_POST @permission_required("instances.clone_instances", raise_exception=True) def clone(request, pk): instance = get_instance(request.user, pk) @@ -1500,15 +1598,22 @@ def clone(request, pk): } messages.error(request, msg) else: - new_instance = Instance(compute=instance.compute, name=clone_data["name"]) try: - new_uuid = instance.proxy.clone_instance(clone_data) - new_instance.uuid = new_uuid - new_instance.save() - user_instance = UserInstance( - instance_id=new_instance.id, user_id=request.user.id, is_delete=True - ) - user_instance.save() + with libvirt_compute_lock(instance.compute): + new_uuid = instance.proxy.clone_instance(clone_data) + new_instance = Instance.objects.get_or_create( + compute=instance.compute, + uuid=new_uuid, + defaults={"name": clone_data["name"]}, + )[0] + if new_instance.name != clone_data["name"]: + new_instance.name = clone_data["name"] + new_instance.save(update_fields=["name"]) + UserInstance.objects.get_or_create( + instance_id=new_instance.id, + user_id=request.user.id, + defaults={"is_delete": True, "is_change": True, "is_vnc": True}, + ) msg = _("Create a clone of '%(instance_name)s'") % { "instance_name": instance.name } @@ -1530,14 +1635,16 @@ def clone(request, pk): return redirect(request.META.get("HTTP_REFERER") + "#clone") +@require_POST +@serialize_instance_mutation def update_console(request, pk): - instance = get_instance(request.user, pk) + instance = get_instance(request.user, pk, perm_type="change") try: userinstance = instance.userinstance_set.get(user=request.user) except Exception: userinstance = UserInstance(is_vnc=False) - if request.user.is_superuser or request.user.is_staff or userinstance.is_vnc: + if request.user.is_superuser or userinstance.is_vnc: form = ConsoleForm(request.POST or None) if form.is_valid(): if ( @@ -1589,18 +1696,22 @@ def update_console(request, pk): request.user.username, instance.compute.name, instance.name, msg ) - return redirect(request.META.get("HTTP_REFERER") + "#vncsettings") + return get_safe_redirect( + request, default=reverse("instances:instance", args=[instance.id]) + "#vncsettings" + ) +@require_POST +@serialize_instance_mutation def change_options(request, pk): - instance = get_instance(request.user, pk) + instance = get_instance(request.user, pk, perm_type="change") try: userinstance = instance.userinstance_set.get(user=request.user) except Exception: userinstance = UserInstance(is_change=False) - if request.user.is_superuser or request.user.is_staff or userinstance.is_change: - instance.is_template = request.POST.get("is_template", False) + if request.user.is_superuser or userinstance.is_change: + instance.is_template = bool(request.POST.get("is_template", False)) instance.save() options = {} @@ -1611,7 +1722,9 @@ def change_options(request, pk): msg = _("Edit options") addlogmsg(request.user.username, instance.compute.name, instance.name, msg) - return redirect(request.META.get("HTTP_REFERER") + "#options") + return get_safe_redirect( + request, default=reverse("instances:instance", args=[instance.id]) + "#options" + ) def getvvfile(request, pk): @@ -1695,9 +1808,10 @@ def create_instance_select_type(request, compute_id): error_msg = _("A virtual machine with this name already exists") messages.error(request, error_msg) else: - conn._defineXML(xml) - utils.refr(compute) - instance = compute.instance_set.get(name=name) + with libvirt_compute_lock(compute): + conn._defineXML(xml) + utils.refresh_instance_database(compute) + instance = compute.instance_set.get(name=name) return redirect(reverse("instances:instance", args=[instance.id])) return render(request, "create_instance_w1.html", locals()) @@ -1919,33 +2033,36 @@ def create_instance(request, compute_id, arch, machine): uuid = util.randomUUID() try: - conn.create_instance( - name=data["name"], - memory=data["memory"], - vcpu=data["vcpu"], - vcpu_mode=data["vcpu_mode"], - uuid=uuid, - arch=arch, - machine=machine, - firmware=firmware, - volumes=volume_list, - networks=data["networks"], - virtio=data["virtio"], - listener_addr=data["listener_addr"], - nwfilter=data["nwfilter"], - net_model=data["net_model"], - graphics=data["graphics"], - video=data["video"], - console_pass=data["console_pass"], - mac=data["mac"], - qemu_ga=data["qemu_ga"], - add_cdrom=data["add_cdrom"], - add_input=data["add_input"], - ) - create_instance = Instance( - compute_id=compute_id, name=data["name"], uuid=uuid - ) - create_instance.save() + with libvirt_compute_lock(compute): + conn.create_instance( + name=data["name"], + memory=data["memory"], + vcpu=data["vcpu"], + vcpu_mode=data["vcpu_mode"], + uuid=uuid, + arch=arch, + machine=machine, + firmware=firmware, + volumes=volume_list, + networks=data["networks"], + virtio=data["virtio"], + listener_addr=data["listener_addr"], + nwfilter=data["nwfilter"], + net_model=data["net_model"], + graphics=data["graphics"], + video=data["video"], + console_pass=data["console_pass"], + mac=data["mac"], + qemu_ga=data["qemu_ga"], + add_cdrom=data["add_cdrom"], + add_input=data["add_input"], + ) + create_instance = Instance.objects.get_or_create( + compute_id=compute_id, uuid=uuid, defaults={"name": data["name"]} + )[0] + if create_instance.name != data["name"]: + create_instance.name = data["name"] + create_instance.save(update_fields=["name"]) msg = _("Instance is created") messages.success(request, msg) addlogmsg(