mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 19:26:48 +00:00
fix(instances): guard mutations and refresh lists after sync
This commit is contained in:
parent
2cdf5dfca1
commit
73d8fd71fb
7 changed files with 741 additions and 207 deletions
|
|
@ -81,9 +81,13 @@
|
|||
<div class="col-sm-12 text-center">
|
||||
<p>{% trans "Autostart your instance when host server is power on " %}
|
||||
{% if instance.autostart == 0 %}
|
||||
<a class="btn btn-success" href="{% url 'instances:set_autostart' instance.id %}">{% trans "Enable" %}</a>
|
||||
<form action="{% url 'instances:set_autostart' instance.id %}" method="post" class="d-inline">{% csrf_token %}
|
||||
<button type="submit" class="btn btn-success">{% trans "Enable" %}</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<a class="btn btn-danger" href="{% url 'instances:unset_autostart' instance.id %}">{% trans "Disable" %}</a>
|
||||
<form action="{% url 'instances:unset_autostart' instance.id %}" method="post" class="d-inline">{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger">{% trans "Disable" %}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -198,9 +202,9 @@
|
|||
<button class="btn btn-sm btn-success disabled">{% trans "Mount" %}</button>
|
||||
{% endif %}
|
||||
{% if instance.status == 5 and allow_admin_or_not_template %}
|
||||
<a href="{% url 'instances:detach_cdrom' instance.id cd.dev %}" class="btn btn-sm btn-danger float-end" title="{% trans "Detach CD-ROM (remove device)" %}">
|
||||
<button type="submit" class="btn btn-sm btn-danger float-end" formaction="{% url 'instances:detach_cdrom' instance.id cd.dev %}" formmethod="post" title="{% trans "Detach CD-ROM (remove device)" %}">
|
||||
{% bs_icon 'dash-circle' %}
|
||||
</a>
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
|
|
|||
64
instances/test_create_instance.py
Normal file
64
instances/test_create_instance.py
Normal file
|
|
@ -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()
|
||||
46
instances/test_index.py
Normal file
46
instances/test_index.py
Normal file
|
|
@ -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()), [])
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue