from computes.models import Compute from django.db import models from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ from libvirt import ( VIR_DOMAIN_SNAPSHOT_LIST_EXTERNAL, VIR_DOMAIN_SNAPSHOT_LIST_INTERNAL, libvirtError, ) from operations import claims from operations.models import Claim from vrtManager.instance import PERSISTENT_XML, wvmInstance class Flavor(models.Model): label = models.CharField(_("label"), max_length=12, unique=True) memory = models.IntegerField(_("memory")) vcpu = models.IntegerField(_("vcpu")) disk = models.IntegerField(_("disk")) def __str__(self): return self.label class InstanceManager(models.Manager): def get_queryset(self): return super().get_queryset().select_related("compute") class Instance(models.Model): compute = models.ForeignKey(Compute, on_delete=models.CASCADE) name = models.CharField(_("name"), max_length=120, db_index=True) uuid = models.CharField(_("uuid"), max_length=36, db_index=True) is_template = models.BooleanField(_("is template"), default=False) created = models.DateTimeField(_("created"), auto_now_add=True) drbd = models.CharField(_("drbd"), max_length=24, default="None") objects = InstanceManager() class Meta: constraints = [ models.UniqueConstraint(fields=["compute", "uuid"], name="unique_compute_uuid") ] def __str__(self): return f"{self.compute}/{self.name}" @cached_property def proxy(self): return wvmInstance( self.compute.hostname, self.compute.login, self.compute.password, self.compute.type, self.name, uuid=self.uuid, ) @cached_property def media(self) -> list[dict]: return self.proxy.get_media_devices() @cached_property def media_iso(self) -> list[str]: return sorted(self.proxy.get_iso_media()) @cached_property def disks(self) -> list[dict]: return self.proxy.get_disk_devices() @cached_property def info(self) -> list | None: """virDomainGetInfo, read once: state, max memory KiB, memory KiB, vCPUs, CPU time. None if the VM cannot be read (host down, VM gone): lists show it as unknown.""" try: return self.proxy.instance.info() except libvirtError: return None @cached_property def status(self) -> int: return self.proxy.get_status() @cached_property def autostart(self) -> int: return self.proxy.get_autostart() @cached_property def bootmenu(self) -> bool: return self.proxy.get_bootmenu() @cached_property def boot_order(self) -> dict: return self.proxy.get_bootorder() @cached_property def arch(self) -> str: return self.proxy.get_arch() @cached_property def machine(self) -> str: return self.proxy.get_machine_type() @cached_property def firmware(self) -> dict: return self.proxy.get_loader() @cached_property def nvram(self) -> str | None: return self.proxy.get_nvram() @cached_property def vcpu(self) -> int: return self.proxy.get_vcpu() @cached_property def vcpu_range(self): """The vCPU counts the Resize tab offers, the VM's own maximum among them.""" return self.proxy.get_max_cpus(at_least=self.config_vcpu) @cached_property def cur_vcpu(self): return self.proxy.get_cur_vcpu() @cached_property def vcpus(self): return self.proxy.get_vcpus() @cached_property def vcpu_cells(self) -> list[dict]: """One cell per vCPU up to the maximum, as the Resize tab draws them: its id, whether it is on, and whether it can be turned off and on (a VM with hot-pluggable vCPUs lists them; vCPU 0 never is).""" if self.vcpus: return [{"id": id, "on": v["enabled"] == "yes", "fixed": v["hotpluggable"] != "yes"} for id, v in self.vcpus.items()] return [{"id": str(i), "on": i < self.cur_vcpu, "fixed": True} for i in range(self.vcpu)] @cached_property def get_uuid(self): return self.proxy.get_uuid() @cached_property def memory(self) -> int: return self.proxy.get_memory() @cached_property def cur_memory(self): return self.proxy.get_cur_memory() @cached_property def title(self) -> str: return self.proxy.get_title() @cached_property def description(self) -> str: return self.proxy.get_description() @cached_property def networks(self) -> list[dict]: return self.proxy.get_net_devices() @cached_property def config_networks(self) -> list[dict]: """NICs of the persistent definition, which the edit and clone forms change; target is the running VM's device name, if any.""" live_targets = {net["mac"]: net.get("target", "") for net in self.networks} networks = self.proxy.get_net_devices(config=True) for net in networks: net["target"] = live_targets.get(net["mac"], "") return networks @cached_property def pcie_slots(self) -> tuple[int, int] | None: """(PCIe slots, free ones) the VM has now; None: not a PCIe machine.""" return self.proxy.get_pcie_slots() @cached_property def config_pcie_slots(self) -> tuple[int, int] | None: """The same at its next start, which added slots wait for while it runs.""" return self.proxy.get_pcie_slots(config=True) @cached_property def config_disks(self) -> list[dict]: """Disks of the persistent definition, which the disk list, its edit form and a clone use: on a running VM a pending change shows, and an edit does not post the live value back over it.""" return self.proxy.get_disk_devices(config=True) @cached_property def config_vcpu(self) -> int: """vCPUs of the persistent definition, which the CPU form changes.""" return self.proxy.get_vcpu(config=True) @cached_property def config_cur_vcpu(self) -> int: return self.proxy.get_cur_vcpu(config=True) @cached_property def config_memory(self) -> int: """Memory (MiB) of the persistent definition, which a clone starts from.""" return self.proxy.get_memory(config=True) @cached_property def config_title(self) -> str: """Title and description of the persistent definition, which the options form changes.""" return self.proxy.get_title(config=True) @cached_property def config_description(self) -> str: return self.proxy.get_description(config=True) @cached_property def config_media(self) -> list[dict]: """CD-ROMs, boot menu and boot order of the persistent definition, which the boot order editor changes.""" return self.proxy.get_media_devices(config=True) @cached_property def config_bootmenu(self) -> bool: return self.proxy.get_bootmenu(config=True) @cached_property def config_boot_order(self) -> dict: return self.proxy.get_bootorder(config=True) @cached_property def config_boot_devices(self) -> list[dict]: """The disks, CD-ROM drives and NICs the boot order editor lists, those in the boot order first in their position; value is as posted, as in "disk:vda".""" order = [f"{d['dev']}:{d['target']}" for position, d in sorted(self.config_boot_order.items())] devices = ( [{"value": f"disk:{d['dev']}", "name": d["dev"], "kind": _("Disk"), "source": d["image"], "icon": "device-hdd"} for d in self.config_disks] + [{"value": f"cdrom:{d['dev']}", "name": d["dev"], "kind": _("CD-ROM"), "source": d["image"], "icon": "disc"} for d in self.config_media] + [{"value": f"network:{n['mac']}", "name": n["mac"], "kind": _("NIC"), "source": n["nic"], "icon": "ethernet"} for n in self.config_networks] ) for device in devices: device["checked"] = device["value"] in order return sorted(devices, key=lambda d: order.index(d["value"]) if d["checked"] else len(order)) @cached_property def config_boot_types(self) -> str: """The device types the VM boots by, in turn ("disk, cdrom"), when its order names types rather than devices; "" otherwise.""" if any(d["checked"] for d in self.config_boot_devices): return "" return ", ".join(b["target"] for position, b in sorted(self.config_boot_order.items())) @cached_property def config_video_model(self) -> str | None: """Video model and console settings of the persistent definition, which their forms change.""" return self.proxy.get_video_model(config=True) @cached_property def config_console_type(self) -> str | None: return self.proxy.get_console_type(config=True) @cached_property def config_console_listener_address(self) -> str: return self.proxy.get_console_listener_addr(config=True) @cached_property def config_console_passwd(self): return self.proxy.get_console_passwd(config=True) @cached_property def config_console_keymap(self) -> str: return self.proxy.get_console_keymap(config=True) @cached_property def telnet_port(self): return self.proxy.get_telnet_port() @cached_property def console_type(self) -> str | None: return self.proxy.get_console_type() @cached_property def console_port(self) -> str | None: return self.proxy.get_console_port() @cached_property def console_keymap(self) -> str: return self.proxy.get_console_keymap() @cached_property def console_listener_address(self) -> str: return self.proxy.get_console_listener_addr() @cached_property def guest_agent(self): return False if self.proxy.get_guest_agent() is None else True @cached_property def config_guest_agent(self) -> bool: """Whether the next start has the agent channel; the agent form shows it.""" return self.proxy.get_guest_agent(config=True) is not None @cached_property def config_vcpu_hotplug(self) -> bool: """Whether the next start has hotpluggable vCPUs; the hot plug form shows it.""" return any(vcpu["hotpluggable"] == "yes" for vcpu in self.proxy.get_vcpus(config=True).values()) @cached_property def config_rng(self) -> str | None: return self.proxy.get_rng() @cached_property def config_tpm(self) -> str | None: return self.proxy.get_tpm() @cached_property def config_on_crash(self) -> str: return self.proxy.get_on_crash() @cached_property def guest_agent_ready(self) -> bool: return self.proxy.is_agent_ready() @cached_property def video_model(self) -> str | None: return self.proxy.get_video_model() @cached_property def video_models(self): return self.proxy.get_video_models(self.arch, self.machine) def _snapshots(self, flag): external = flag == VIR_DOMAIN_SNAPSHOT_LIST_EXTERNAL return sorted(({**snap, "external": external} for snap in self.proxy.get_snapshot(flag)), reverse=True, key=lambda k: k["date"]) @cached_property def snapshots(self) -> list[dict]: """Both kinds, the newest first; "external" tells them apart.""" return sorted(self.internal_snapshots + self.external_snapshots, reverse=True, key=lambda k: k["date"]) @cached_property def newer_than_current_snapshot(self) -> bool: """Snapshots newer than the one reverted to: an external snapshot waits until they are deleted.""" return any(snap["current"] and snap["children"] for snap in self.external_snapshots) @cached_property def internal_snapshots(self) -> list[dict]: return self._snapshots(VIR_DOMAIN_SNAPSHOT_LIST_INTERNAL) @cached_property def external_snapshots(self) -> list[dict]: return self._snapshots(VIR_DOMAIN_SNAPSHOT_LIST_EXTERNAL) @cached_property def snapshot_jobs(self) -> list[dict]: # only for the page; create, revert and delete query the jobs again and refuse if that fails try: return self.proxy.get_snapshot_jobs() except libvirtError: return [] @cached_property def held_by_job(self) -> bool: # a queued job has no libvirt block job yet; a job's claim still holds the VM return Claim.objects.filter(resource=claims.instance_key(self.uuid), operation__isnull=False).exists() @cached_property def inst_xml(self): # the definition for the next start, which is the current one while the VM is off return self.proxy._XMLDesc(PERSISTENT_XML) @cached_property def has_managed_save_image(self): return self.proxy.get_managed_save_image() @cached_property def console_passwd(self): return self.proxy.get_console_passwd() @cached_property def cache_modes(self): return sorted(self.proxy.get_cache_modes().items()) @cached_property def io_modes(self): return sorted(self.proxy.get_io_modes().items()) @cached_property def discard_modes(self): return sorted(self.proxy.get_discard_modes().items()) @cached_property def detect_zeroes_modes(self): return sorted(self.proxy.get_detect_zeroes_modes().items()) @cached_property def formats(self): return self.proxy.get_image_formats() class InstanceTombstone(models.Model): """Ownership of a VM that disappeared from its compute. Kept for INSTANCE_OWNERSHIP_RETENTION_DAYS so that the owners come back when the same UUID shows up again, on this or another compute. """ uuid = models.CharField(max_length=36, db_index=True) name = models.CharField(max_length=120) host = models.CharField(max_length=64, blank=True) # the compute's name: the compute may be deleted is_template = models.BooleanField(default=False) owners = models.JSONField(default=list) # [{"user": id, "is_change": .., "is_delete": .., "is_vnc": ..}] removed = models.DateTimeField(auto_now_add=True) def __str__(self): return "%s (%s)" % (self.name, self.uuid) class PermissionSet(models.Model): """ Dummy model for holding set of permissions we need to be automatically added by Django """ class Meta: default_permissions = () permissions = [ ("clone_instances", "Can clone instances"), ("passwordless_console", _("Can access console without password")), ("view_instances", "Can view instances"), ("snapshot_instances", "Can snapshot instances"), ] managed = False