1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00
webvirtcloud/instances/utils.py
catborise 792487e65e fix(instances): failures reach the user; no redirect to other sites
Web views:
- 34 views returned to request.META["HTTP_REFERER"] + "#tab": without
  a Referer they failed with a TypeError (500), and a Referer from
  another site was followed (open redirect). They return through
  _back(), which keeps a same-site Referer and otherwise opens the VM
  page at the tab. get_safe_redirect uses the same check. Rejected disk
  options also return to the VM's disks tab instead of the VM list.
- migrate_instance returned silently when the target was the current
  compute or unreachable, so the web view logged "Instance is migrated"
  and the API answered "instance migrate is started". It now raises;
  the view reports the error and logs only a completed migration, and
  an unknown compute id is a 404 instead of a 500. The automatic
  migration after a clone skips a target that is the current compute.
- add_owner and del_owner failed with a 500 without their ids, add_owner
  also when the user was already an owner, and del_owner deleted the
  given ownership record even when it belonged to another VM. Ids are
  validated, a record of another VM is a 404, and a repeated owner is
  reported.
- Broad handlers that hid programming errors are narrowed: clone catches
  libvirt, value and OS errors (a lock timeout included), ownership
  lookups catch DoesNotExist. The DRBD status no longer needs its bare
  except: the VM name is escaped in the regex, which was the only thing
  that could fail there.

API:
- A libvirt error under /api/ is JSON with status 400, and a lock
  timeout 409, instead of the HTML error page. This lives in the
  existing ExceptionMiddleware, so existing settings need no change.
- Volume create/delete on an inactive pool answer 409, and invalid
  volume data 400 with the serializer errors, instead of 200.

The new tests cover every view that returns with _back() without a
Referer and with a foreign one, the migrate failures on web and API,
the owner ids, the API error format and the volume statuses; nine of
the ten fail on the previous code.
2026-10-04 12:23:54 +03:00

286 lines
9.5 KiB
Python

import os
import random
import re
import string
from accounts.models import UserInstance, UserAttributes
from appsettings.settings import app_settings
from computes.utils import libvirt_compute_lock
from django.conf import settings
from django.db import transaction
from django.utils.translation import gettext_lazy as _
from vrtManager import util
from vrtManager.connection import connection_manager
from vrtManager.instance import wvmInstance, wvmInstances
from .models import Instance
def can_open_console(user, instance):
"""
The single rule for console access (noVNC page, novncd, the virt-viewer
.vv file and the VDI URL): an active superuser or an active owner of the
VM. The global view_instances permission is read-only and does not grant
an interactive console.
"""
if not user.is_active:
return False
if user.is_superuser:
return True
return UserInstance.objects.filter(instance=instance, user=user).exists()
def get_clone_free_names(size=10):
prefix = app_settings.CLONE_INSTANCE_DEFAULT_PREFIX
free_names = []
existing_names = [i.name for i in Instance.objects.filter(name__startswith=prefix)]
index = 1
while len(free_names) < size:
new_name = prefix + str(index)
if new_name not in existing_names:
free_names.append(new_name)
index += 1
return free_names
def check_user_quota(user, instance, cpu, memory, disk_size):
ua, attributes_created = UserAttributes.objects.get_or_create(user=user)
msg = ""
if user.is_superuser:
return msg
quota_debug = app_settings.QUOTA_DEBUG
user_instances = UserInstance.objects.filter(user=user, instance__is_template=False)
instance += user_instances.count()
for usr_inst in user_instances:
if connection_manager.host_is_up(
usr_inst.instance.compute.type,
usr_inst.instance.compute.hostname,
):
conn = wvmInstance(
usr_inst.instance.compute.hostname,
usr_inst.instance.compute.login,
usr_inst.instance.compute.password,
usr_inst.instance.compute.type,
usr_inst.instance.name,
uuid=usr_inst.instance.uuid,
)
cpu += int(conn.get_vcpu())
memory += int(conn.get_memory())
for disk in conn.get_disk_devices():
if disk["size"]:
disk_size += int(disk["size"]) >> 30
if ua.max_instances > 0 and instance > ua.max_instances:
msg = "instance"
if quota_debug:
msg += f" ({instance} > {ua.max_instances})"
if ua.max_cpus > 0 and cpu > ua.max_cpus:
msg = "cpu"
if quota_debug:
msg += f" ({cpu} > {ua.max_cpus})"
if ua.max_memory > 0 and memory > ua.max_memory:
msg = "memory"
if quota_debug:
msg += f" ({memory} > {ua.max_memory})"
if ua.max_disk_size > 0 and disk_size > ua.max_disk_size:
msg = "disk"
if quota_debug:
msg += f" ({disk_size} > {ua.max_disk_size})"
return msg
def get_new_disk_dev(media, disks, bus):
existing_disk_devs = []
existing_media_devs = []
if bus == "virtio":
dev_base = "vd"
elif bus == "ide":
dev_base = "hd"
elif bus == "fdc":
dev_base = "fd"
else:
dev_base = "sd"
if disks:
existing_disk_devs = [disk["dev"] for disk in disks]
# cd-rom bus could be virtio/sata, because of that we should check it also
if media:
existing_media_devs = [m["dev"] for m in media]
for al in string.ascii_lowercase:
dev = dev_base + al
if dev not in existing_disk_devs and dev not in existing_media_devs:
return dev
raise Exception(_("None available device name"))
def get_network_tuple(network_source_str):
network_source_pack = network_source_str.split(":", 1)
if len(network_source_pack) > 1:
return network_source_pack[1], network_source_pack[0]
else:
return network_source_pack[0], "net"
def migrate_instance(
new_compute,
instance,
user,
live=False,
unsafe=False,
xml_del=False,
offline=False,
autoconverge=False,
compress=False,
postcopy=False,
):
if new_compute == instance.compute:
raise util.OperationError(_("The instance is already on %(compute)s") % {"compute": new_compute.name})
if not connection_manager.host_is_up(new_compute.type, new_compute.hostname):
raise util.OperationError(_("%(compute)s is not reachable") % {"compute": new_compute.name})
c1, c2 = (
(instance.compute, new_compute)
if instance.compute.id < new_compute.id
else (new_compute, instance.compute)
)
with libvirt_compute_lock(c1):
with libvirt_compute_lock(c2):
conn_migrate = None
try:
conn_migrate = wvmInstances(
new_compute.hostname,
new_compute.login,
new_compute.password,
new_compute.type,
)
autostart = instance.autostart
conn_migrate.moveto(
instance.proxy,
instance.name,
live,
unsafe,
xml_del,
offline,
autoconverge,
compress,
postcopy,
)
finally:
if conn_migrate is not None:
conn_migrate.close()
conn_new = None
try:
conn_new = wvmInstance(
new_compute.hostname,
new_compute.login,
new_compute.password,
new_compute.type,
instance.name,
uuid=instance.uuid,
)
if autostart:
conn_new.set_autostart(1)
finally:
if conn_new is not None:
conn_new.close()
with transaction.atomic():
target_inst = Instance.objects.filter(
compute=new_compute, uuid=instance.uuid
).first()
if target_inst and target_inst.id != instance.id:
for ui in UserInstance.objects.filter(instance=instance):
existing_ui = UserInstance.objects.filter(
instance=target_inst, user=ui.user
).first()
if not existing_ui:
ui.instance = target_inst
ui.save()
else:
updated = False
if ui.is_change and not existing_ui.is_change:
existing_ui.is_change = True
updated = True
if ui.is_delete and not existing_ui.is_delete:
existing_ui.is_delete = True
updated = True
if ui.is_vnc and not existing_ui.is_vnc:
existing_ui.is_vnc = True
updated = True
if updated:
existing_ui.save()
ui.delete()
instance.delete()
instance.id = target_inst.id
instance.compute = new_compute
else:
instance.compute = new_compute
instance.save()
def refr(compute):
from computes.utils import refresh_instance_database
refresh_instance_database(compute)
def get_dhcp_mac_address(vname):
dhcp_file = str(settings.BASE_DIR) + "/dhcpd.conf"
mac = ""
if os.path.isfile(dhcp_file):
with open(dhcp_file, "r") as f:
name_found = False
for line in f:
if "host %s." % vname in line:
name_found = True
if name_found and "hardware ethernet" in line:
mac = line.split(" ")[-1].strip().strip(";")
break
return mac
def get_random_mac_address():
mac = settings.MAC_OUI + ":%02x:%02x:%02x" % (
random.randint(0x00, 0xFF),
random.randint(0x00, 0xFF),
random.randint(0x00, 0xFF),
)
return mac
def get_clone_disk_name(disk, prefix, clone_name=""):
if not disk["image"]:
return None
if disk["image"].startswith(prefix) and clone_name:
suffix = disk["image"][len(prefix) :]
image = f"{clone_name}{suffix}"
elif "." in disk["image"] and len(disk["image"].rsplit(".", 1)[1]) <= 7:
name, suffix = disk["image"].rsplit(".", 1)
image = f"{name}-clone.{suffix}"
else:
image = f"{disk['image']}-clone"
# Derived names go into volume XML and paths: keep them to a safe charset.
image = re.sub(r"[^A-Za-z0-9_.+-]+", "-", image).lstrip("_.+-")
return image or None
def can_manage_console(user, instance):
"""
Who may see and change a VM's console settings, including its VNC
password: an active superuser, or an active owner with both is_change and
is_vnc. is_staff and the global view_instances permission do not count.
"""
if not user.is_active:
return False
if user.is_superuser:
return True
return UserInstance.objects.filter(
instance=instance, user=user, is_change=True, is_vnc=True
).exists()