mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 19:26:48 +00:00
After a snapshot revert or delete, files libvirt could not remove are named in a warning that says nothing uses them and asks to remove them by hand. A file another VM on the same host uses (as a disk, a backing file or a snapshot file) is no longer named: removing it would break that VM. The warning now says no VM on this host uses the files.
2462 lines
94 KiB
Python
Executable file
2462 lines
94 KiB
Python
Executable file
try:
|
|
import crypt_r as crypt
|
|
except ImportError:
|
|
import crypt
|
|
import functools
|
|
import json
|
|
import os
|
|
import re
|
|
import socket
|
|
import subprocess
|
|
import time
|
|
from bisect import insort
|
|
|
|
from accounts.models import UserInstance, UserSSHKey
|
|
from admin.decorators import superuser_only
|
|
from appsettings.models import AppSettings
|
|
from appsettings.settings import app_settings
|
|
from computes.models import Compute
|
|
from computes.validators import validate_hostname, validate_login
|
|
from computes.utils import libvirt_compute_lock, libvirt_instance_lock, user_quota_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 import signing
|
|
from django.core.exceptions import PermissionDenied, ValidationError
|
|
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
|
|
from django.utils.translation import gettext_noop as _
|
|
from django.views.decorators.http import require_POST
|
|
from libvirt import (VIR_DOMAIN_UNDEFINE_KEEP_NVRAM,
|
|
VIR_DOMAIN_UNDEFINE_MANAGED_SAVE,
|
|
VIR_DOMAIN_UNDEFINE_SNAPSHOTS_METADATA,
|
|
VIR_DOMAIN_UNDEFINE_NVRAM,
|
|
libvirtError)
|
|
from logs.views import addlogmsg
|
|
from webvirtcloud.middleware import error_text
|
|
from vrtManager import util
|
|
from vrtManager.create import wvmCreate
|
|
from vrtManager.instance import USAGE_MAX_WINDOW
|
|
from vrtManager.interface import wvmInterface
|
|
from vrtManager.storage import wvmStorage
|
|
from vrtManager.util import randomPasswd
|
|
|
|
from instances.models import Instance
|
|
|
|
from . import utils
|
|
from .forms import ConsoleForm, FlavorForm, NewVMForm
|
|
from .models import Flavor
|
|
|
|
# the VM page waits this long (seconds) for the compute's drbdadm status
|
|
DRBD_STATUS_TIMEOUT = 10
|
|
# the stats tab sends the sample of each answer back with its next poll
|
|
STATS_SAMPLE_SIGNER = signing.TimestampSigner(salt="instances.stats-sample")
|
|
|
|
|
|
def index(request):
|
|
instances = None
|
|
|
|
computes = list(Compute.objects.all().order_by("name"))
|
|
for compute in computes:
|
|
utils.refr(compute)
|
|
prefetch_related_objects(computes, "instance_set__userinstance_set")
|
|
|
|
show_hosts = request.user.is_superuser or request.user.has_perm("instances.view_instances")
|
|
own_hosts_down = False
|
|
if show_hosts:
|
|
instances = Instance.objects.all().prefetch_related("userinstance_set")
|
|
else:
|
|
instances = Instance.objects.filter(
|
|
userinstance__user=request.user
|
|
).prefetch_related("userinstance_set")
|
|
# other users get no host names or connection errors, only whether
|
|
# a host of their own VMs is down
|
|
own = set(instances.values_list("compute_id", flat=True))
|
|
own_hosts_down = any(c.status is not True for c in computes if c.pk in own)
|
|
|
|
return render(
|
|
request,
|
|
"allinstances.html",
|
|
{"computes": computes, "instances": instances, "show_hosts": show_hosts, "own_hosts_down": own_hosts_down},
|
|
)
|
|
|
|
|
|
def instance(request, pk):
|
|
instance: Instance = get_instance(request.user, pk)
|
|
compute: Compute = instance.compute
|
|
# the page reads the VM from libvirt throughout; without the host it would
|
|
# be the error page, with a connection error that names the host
|
|
if compute.status is not True:
|
|
if request.user.is_superuser or request.user.has_perm("instances.view_instances"):
|
|
msg = _("The host %(host)s of '%(name)s' cannot be reached: %(error)s") % {
|
|
"host": compute.name,
|
|
"name": instance.name,
|
|
"error": compute.connection_error,
|
|
}
|
|
else:
|
|
msg = _("The host of '%(name)s' cannot be reached right now.") % {"name": instance.name}
|
|
messages.error(request, msg)
|
|
return redirect(reverse("instances:index"))
|
|
# The page only reads the VM, and its getters read the same XML some 30
|
|
# times: libvirt is asked once per kind of XML.
|
|
with instance.proxy.cached_reads():
|
|
return _instance_page(request, pk, instance, compute)
|
|
|
|
|
|
def _instance_page(request, pk, instance, compute):
|
|
computes = Compute.objects.all().order_by("name")
|
|
computes_count = computes.count()
|
|
users = User.objects.all().order_by("username")
|
|
publickeys = UserSSHKey.objects.filter(user_id=request.user.id)
|
|
keymaps = settings.QEMU_KEYMAPS
|
|
# The console form renders the VNC password, so only users who may manage
|
|
# console settings get the form (and the password) at all.
|
|
can_manage_console = utils.can_manage_console(request.user, instance)
|
|
console_form = None
|
|
if can_manage_console:
|
|
console_form = ConsoleForm(
|
|
initial={
|
|
"listen_on": instance.config_console_listener_address,
|
|
"password": instance.config_console_passwd,
|
|
"keymap": instance.config_console_keymap,
|
|
},
|
|
can_set_listener=request.user.is_superuser,
|
|
)
|
|
console_listener_addresses = settings.QEMU_CONSOLE_LISTENER_ADDRESSES
|
|
bottom_bar = app_settings.VIEW_INSTANCE_DETAIL_BOTTOM_BAR
|
|
try:
|
|
userinstance = UserInstance.objects.get(
|
|
instance__compute_id=compute.id,
|
|
instance__name=instance.name,
|
|
user__id=request.user.id,
|
|
)
|
|
except UserInstance.DoesNotExist:
|
|
userinstance = None
|
|
# The page offers only what the backend allows this user (get_instance
|
|
# perm_type="change", utils.can_open_console); is_staff and the global
|
|
# view_instances permission are read-only.
|
|
can_open_console = utils.can_open_console(request.user, instance)
|
|
can_power = request.user.is_superuser or userinstance is not None
|
|
# get_instance(perm_type="delete"): a template only for superusers and staff owners
|
|
can_delete = request.user.is_superuser or bool(
|
|
userinstance and userinstance.is_delete and (request.user.is_staff or not instance.is_template)
|
|
)
|
|
# get_instance(perm_type="change"): a template only for superusers and staff owners
|
|
can_change = request.user.is_superuser or bool(
|
|
userinstance and userinstance.is_change and (request.user.is_staff or not instance.is_template)
|
|
)
|
|
can_clone = request.user.has_perm("instances.clone_instances") and (instance.is_template or can_change)
|
|
can_snapshot = can_change and request.user.has_perm("instances.snapshot_instances")
|
|
show_settings = (
|
|
request.user.is_superuser or can_manage_console or can_clone or can_change or instance.guest_agent_ready
|
|
)
|
|
|
|
memory_range = [256, 512, 768, 1024, 2048, 3072, 4096, 6144, 8192, 16384]
|
|
if instance.memory not in memory_range:
|
|
insort(memory_range, instance.memory)
|
|
if instance.cur_memory not in memory_range:
|
|
insort(memory_range, instance.cur_memory)
|
|
clone_free_names = utils.get_clone_free_names()
|
|
|
|
default_bus = app_settings.INSTANCE_VOLUME_DEFAULT_BUS
|
|
default_io = app_settings.INSTANCE_VOLUME_DEFAULT_IO
|
|
default_discard = app_settings.INSTANCE_VOLUME_DEFAULT_DISCARD
|
|
default_zeroes = app_settings.INSTANCE_VOLUME_DEFAULT_DETECT_ZEROES
|
|
default_cache = app_settings.INSTANCE_VOLUME_DEFAULT_CACHE
|
|
default_format = app_settings.INSTANCE_VOLUME_DEFAULT_FORMAT
|
|
# default_disk_owner_uid = int(app_settings.INSTANCE_VOLUME_DEFAULT_OWNER_UID)
|
|
# default_disk_owner_gid = int(app_settings.INSTANCE_VOLUME_DEFAULT_OWNER_GID)
|
|
|
|
# clone_instance_auto_name = app_settings.CLONE_INSTANCE_AUTO_NAME
|
|
|
|
# try:
|
|
# instance = Instance.objects.get(compute=compute, name=vname)
|
|
# if instance.uuid != uuid:
|
|
# instance.uuid = uuid
|
|
# instance.save()
|
|
# msg = _(f"Fixing UUID {uuid}")
|
|
# addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
# except Instance.DoesNotExist:
|
|
# instance = Instance(compute=compute, name=vname, uuid=uuid)
|
|
# instance.save()
|
|
# msg = _("Instance does not exist: Creating new instance")
|
|
# addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
|
|
# userinstances = UserInstance.objects.filter(instance=instance).order_by('user__username')
|
|
userinstances = instance.userinstance_set.order_by("user__username")
|
|
# Host resources
|
|
vcpu_host = len(instance.vcpu_range)
|
|
memory_host = instance.proxy.get_max_memory()
|
|
bus_host = instance.proxy.get_disk_bus_types(instance.arch, instance.machine)
|
|
networks_host = sorted(instance.proxy.get_networks())
|
|
interfaces_host = sorted(instance.proxy.get_ifaces())
|
|
nwfilters_host = instance.proxy.get_nwfilters()
|
|
storages_host = sorted(instance.proxy.get_storages(True))
|
|
net_models_host = instance.proxy.get_network_models()
|
|
|
|
if app_settings.VM_DRBD_STATUS == "True":
|
|
instance.drbd = drbd_status(request, pk)
|
|
instance.save()
|
|
|
|
return render(request, "instance.html", locals(),)
|
|
|
|
|
|
def status(request, pk):
|
|
instance = get_instance(request.user, pk)
|
|
return JsonResponse({"status": instance.proxy.get_status()})
|
|
|
|
|
|
def _valid_ssh_target(compute):
|
|
"""The login and hostname reach ssh as a command-line argument; a compute
|
|
whose values would inject an ssh option (an existing or bypassed row that
|
|
the field validators did not cover) is skipped rather than run."""
|
|
try:
|
|
validate_login(compute.login)
|
|
validate_hostname(compute.hostname)
|
|
except ValidationError:
|
|
return False
|
|
return True
|
|
|
|
|
|
def drbd_status(request, pk):
|
|
instance = get_instance(request.user, pk)
|
|
result = "None DRBD"
|
|
|
|
if instance.compute.type == 2 and _valid_ssh_target(instance.compute):
|
|
conn = instance.compute.login + "@" + instance.compute.hostname
|
|
try:
|
|
remoteDrbdStatus = subprocess.run(
|
|
["ssh", "--", conn, "sudo", "/usr/sbin/drbdadm", "status"],
|
|
stdout=subprocess.PIPE,
|
|
text=True,
|
|
timeout=DRBD_STATUS_TIMEOUT,
|
|
)
|
|
except subprocess.TimeoutExpired:
|
|
return result
|
|
|
|
if remoteDrbdStatus.stdout:
|
|
instanceFindDrbd = re.compile(
|
|
re.escape(instance.name) + "[_]*[A-Z]* role:(.+?)\n disk:(.+?)\n",
|
|
re.IGNORECASE,
|
|
)
|
|
instanceDrbd = instanceFindDrbd.findall(remoteDrbdStatus.stdout)
|
|
|
|
primaryCount = 0
|
|
secondaryCount = 0
|
|
statusDisk = "OK"
|
|
|
|
for disk in instanceDrbd:
|
|
if disk[0] == "Primary":
|
|
primaryCount = primaryCount + 1
|
|
elif disk[0] == "Secondary":
|
|
secondaryCount = secondaryCount + 1
|
|
if disk[1] != "UpToDate":
|
|
statusDisk = "NOK"
|
|
|
|
if primaryCount > 0 and secondaryCount > 0:
|
|
statusRole = "NOK"
|
|
else:
|
|
if primaryCount > secondaryCount:
|
|
statusRole = "Primary"
|
|
else:
|
|
statusRole = "Secondary"
|
|
|
|
result = statusRole + "/" + statusDisk
|
|
|
|
return result
|
|
|
|
|
|
def _stats_sample(instance, token):
|
|
"""The usage sample in a token of this VM's stats on its current compute,
|
|
or None when there is none, it is altered or too old."""
|
|
if not token:
|
|
return None
|
|
try:
|
|
data = STATS_SAMPLE_SIGNER.unsign_object(token, max_age=USAGE_MAX_WINDOW)
|
|
except signing.BadSignature:
|
|
return None
|
|
if data.get("vm") != [instance.id, instance.compute_id, instance.uuid]:
|
|
return None
|
|
return data["sample"]
|
|
|
|
|
|
def stats(request, pk):
|
|
instance = get_instance(request.user, pk)
|
|
usage = instance.proxy.usage(_stats_sample(instance, request.GET.get("sample")))
|
|
mem_usage = instance.proxy.mem_usage()
|
|
|
|
def mega(value):
|
|
return None if value is None else value / 10**6
|
|
|
|
result = {
|
|
"cpudata": None if usage["cpu"] is None else round(usage["cpu"], 1),
|
|
"memdata": mem_usage,
|
|
# MB/s and Mbit/s
|
|
"blkdata": [{"dev": disk["dev"], "data": [mega(disk["rd"]), mega(disk["wr"])]} for disk in usage["disks"]],
|
|
"netdata": [{"dev": nic["dev"], "data": [mega(nic["rx"]), mega(nic["tx"])]} for nic in usage["nics"]],
|
|
"timeline": time.strftime("%H:%M:%S"),
|
|
}
|
|
if "sample" in usage:
|
|
result["window"] = None if usage["window"] is None else round(usage["window"], 1)
|
|
result["sample"] = STATS_SAMPLE_SIGNER.sign_object(
|
|
{"vm": [instance.id, instance.compute_id, instance.uuid], "sample": usage["sample"]}, compress=True
|
|
)
|
|
return JsonResponse(result)
|
|
|
|
|
|
def osinfo(request, pk):
|
|
instance = get_instance(request.user, pk)
|
|
results = instance.proxy.osinfo()
|
|
|
|
return JsonResponse(results)
|
|
|
|
|
|
@superuser_only
|
|
def guess_mac_address(request, vname):
|
|
data = {"vname": vname}
|
|
mac = utils.get_dhcp_mac_address(vname)
|
|
if not mac:
|
|
mac = utils.get_random_mac_address()
|
|
data["mac"] = mac
|
|
return JsonResponse(data)
|
|
|
|
|
|
@superuser_only
|
|
def random_mac_address(request):
|
|
data = dict()
|
|
data["mac"] = utils.get_random_mac_address()
|
|
return JsonResponse(data)
|
|
|
|
|
|
@superuser_only
|
|
def guess_clone_name(request):
|
|
dhcp_file = utils.dhcpd_conf()
|
|
prefix = app_settings.CLONE_INSTANCE_DEFAULT_PREFIX
|
|
if os.path.isfile(dhcp_file):
|
|
instance_names = [
|
|
i.name for i in Instance.objects.filter(name__startswith=prefix)
|
|
]
|
|
with open(dhcp_file, "r") as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if f"host {prefix}" in line:
|
|
fqdn = line.split(" ")[1]
|
|
hostname = fqdn.split(".")[0]
|
|
if hostname.startswith(prefix) and hostname not in instance_names:
|
|
return JsonResponse({"name": hostname})
|
|
return JsonResponse({})
|
|
|
|
|
|
def sshkeys(request, pk):
|
|
"""
|
|
:param request:
|
|
:param vname:
|
|
:return:
|
|
"""
|
|
instance = get_instance(request.user, pk)
|
|
instance_keys = []
|
|
userinstances = UserInstance.objects.filter(instance=instance)
|
|
|
|
for ui in userinstances:
|
|
keys = UserSSHKey.objects.filter(user=ui.user)
|
|
for k in keys:
|
|
instance_keys.append(k.keypublic)
|
|
if request.GET.get("plain", ""):
|
|
response = "\n".join(instance_keys)
|
|
response += "\n"
|
|
return HttpResponse(response, content_type="text/plain; charset=utf-8")
|
|
return JsonResponse(instance_keys, safe=False)
|
|
|
|
|
|
def _same_origin_referer(request, default):
|
|
"""The Referer without its fragment if it is on this site, else default."""
|
|
referer = request.META.get("HTTP_REFERER")
|
|
if referer and url_has_allowed_host_and_scheme(
|
|
url=referer, allowed_hosts={request.get_host()}, require_https=request.is_secure()
|
|
):
|
|
return referer.split("#")[0]
|
|
return default
|
|
|
|
|
|
def get_safe_redirect(request, default=None):
|
|
return redirect(_same_origin_referer(request, default or reverse("instances:index")))
|
|
|
|
|
|
def _is_int(value):
|
|
try:
|
|
int(value)
|
|
except (TypeError, ValueError):
|
|
return False
|
|
return True
|
|
|
|
|
|
def _back(request, pk, tab):
|
|
"""Back to the page the form was on, at the given tab; the VM page if
|
|
the Referer is missing or from another site."""
|
|
return redirect(_same_origin_referer(request, reverse("instances:instance", args=[pk])) + "#" + tab)
|
|
|
|
|
|
def get_instance(user, pk, perm_type="view"):
|
|
"""
|
|
Check that instance is available for user, if not raise 404 or PermissionDenied.
|
|
perm_type:
|
|
- 'view': superuser, has_perm("instances.view_instances"), or UserInstance owner
|
|
- 'power': superuser or UserInstance owner
|
|
- 'change': superuser or (UserInstance owner and is_change)
|
|
- 'delete': superuser or (UserInstance owner and is_delete)
|
|
A template is changed or deleted only by superusers and staff owners.
|
|
"""
|
|
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()
|
|
|
|
if not (
|
|
user.is_superuser
|
|
or user.has_perm("instances.view_instances")
|
|
or has_owner_rel
|
|
):
|
|
raise Http404()
|
|
|
|
if user.is_superuser or perm_type == "view":
|
|
return instance
|
|
|
|
user_inst = user_instances.first()
|
|
if perm_type == "power":
|
|
if not has_owner_rel:
|
|
raise PermissionDenied
|
|
elif perm_type == "change":
|
|
# A template is changed only by superusers (above) and staff owners
|
|
if not (user_inst and user_inst.is_change) or (instance.is_template and not user.is_staff):
|
|
raise PermissionDenied
|
|
elif perm_type == "delete":
|
|
if not (user_inst and user_inst.is_delete) or (instance.is_template and not user.is_staff):
|
|
raise PermissionDenied
|
|
else:
|
|
raise PermissionDenied
|
|
|
|
return instance
|
|
|
|
|
|
def _busy(request, pk):
|
|
if hasattr(request, "accepted_renderer"):
|
|
return JsonResponse({"detail": "Instance operation is busy. Please retry."}, status=409)
|
|
messages.error(request, _("Instance operation is busy. Please retry."))
|
|
return get_safe_redirect(request, default=reverse("instances:instance", args=[pk]))
|
|
|
|
|
|
def _moved_meanwhile(pk, inst):
|
|
"""A migration may have moved the VM while the request waited for its
|
|
compute's lock: the lock held is then the old compute's. The retry locks
|
|
the new one (or finds the record merged away)."""
|
|
return not Instance.objects.filter(pk=pk, compute_id=inst.compute_id).exists()
|
|
|
|
|
|
def serialize_instance_mutation(func):
|
|
"""Serialize one VM, after checking visibility, without blocking sibling VMs."""
|
|
@functools.wraps(func)
|
|
def wrapper(request, pk, *args, **kwargs):
|
|
inst = get_instance(request.user, pk)
|
|
try:
|
|
with libvirt_instance_lock(inst):
|
|
if _moved_meanwhile(pk, inst):
|
|
return _busy(request, pk)
|
|
return func(request, pk, *args, **kwargs)
|
|
except TimeoutError:
|
|
return _busy(request, pk)
|
|
|
|
return wrapper
|
|
|
|
|
|
def quota_refused(quota_msg, refused):
|
|
"""The message for a refused change: refused is the action's own
|
|
'User %(quota_msg)s quota reached, ...' text, already formatted."""
|
|
if quota_msg == utils.QUOTA_UNVERIFIED:
|
|
return _("The quota cannot be checked because a host of your virtual machines cannot be reached. Nothing was changed.")
|
|
return refused
|
|
|
|
|
|
def serialize_user_quota(func):
|
|
"""Outermost lock for quota-checked changes: concurrent requests of one
|
|
user (also on different computes) must not pass the check together."""
|
|
@functools.wraps(func)
|
|
def wrapper(request, pk, *args, **kwargs):
|
|
if request.user.is_superuser: # quotas do not apply
|
|
return func(request, pk, *args, **kwargs)
|
|
try:
|
|
with user_quota_lock(request.user):
|
|
return func(request, pk, *args, **kwargs)
|
|
except TimeoutError:
|
|
return _busy(request, pk)
|
|
|
|
return wrapper
|
|
|
|
|
|
def serialize_compute_mutation(func):
|
|
"""Hold the whole compute: disk deletions must not race another VM attaching that disk."""
|
|
@functools.wraps(func)
|
|
def wrapper(request, pk, *args, **kwargs):
|
|
inst = get_instance(request.user, pk)
|
|
if request.method in ("GET", "HEAD"): # e.g. the destroy confirmation page
|
|
return func(request, pk, *args, **kwargs)
|
|
try:
|
|
with libvirt_compute_lock(inst.compute):
|
|
if _moved_meanwhile(pk, inst):
|
|
return _busy(request, pk)
|
|
return func(request, pk, *args, **kwargs)
|
|
except TimeoutError:
|
|
return _busy(request, pk)
|
|
|
|
return wrapper
|
|
|
|
|
|
DISK_FORMAT_RE = re.compile(r"^[a-z0-9]+$")
|
|
DISK_SERIAL_RE = re.compile(r"^[A-Za-z0-9_.+-]*$")
|
|
VOLUME_NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.+-]*$")
|
|
|
|
|
|
def invalid_disk_options(
|
|
instance,
|
|
bus=None,
|
|
cache=None,
|
|
io=None,
|
|
discard=None,
|
|
zeroes=None,
|
|
format=None,
|
|
serial=None,
|
|
):
|
|
"""
|
|
Return the names of the given disk options whose values are not allowed.
|
|
These values end up in libvirt disk XML, so they must come from the
|
|
hypervisor's own lists (or match a strict pattern). None means "not given".
|
|
"""
|
|
proxy = instance.proxy
|
|
checks = {
|
|
"bus": (
|
|
bus,
|
|
lambda v: v in proxy.get_disk_bus_types(instance.arch, instance.machine),
|
|
),
|
|
"cache": (cache, lambda v: v in proxy.get_cache_modes()),
|
|
"io": (io, lambda v: v in proxy.get_io_modes()),
|
|
"discard": (discard, lambda v: v in proxy.get_discard_modes()),
|
|
"detect_zeroes": (zeroes, lambda v: v in proxy.get_detect_zeroes_modes()),
|
|
"format": (format, lambda v: bool(DISK_FORMAT_RE.fullmatch(v))),
|
|
"serial": (serial, lambda v: bool(DISK_SERIAL_RE.fullmatch(v))),
|
|
}
|
|
return [
|
|
name
|
|
for name, (value, is_valid) in checks.items()
|
|
if value is not None and not is_valid(value)
|
|
]
|
|
|
|
|
|
def reject_disk_options(request, pk, invalid):
|
|
messages.error(
|
|
request,
|
|
_("Invalid disk options: %(options)s") % {"options": ", ".join(invalid)},
|
|
)
|
|
return _back(request, pk, "disks")
|
|
|
|
|
|
@require_POST
|
|
@serialize_instance_mutation
|
|
def poweron(request, pk):
|
|
instance = get_instance(request.user, pk, perm_type="power")
|
|
if instance.is_template:
|
|
messages.warning(request, _("Templates cannot be started."))
|
|
else:
|
|
instance.proxy.start()
|
|
addlogmsg(
|
|
request.user.username, instance.compute.name, instance.name, _("Power On")
|
|
)
|
|
|
|
return get_safe_redirect(
|
|
request, default=reverse("instances:instance", args=[instance.id])
|
|
)
|
|
|
|
|
|
@require_POST
|
|
@serialize_instance_mutation
|
|
def powercycle(request, pk):
|
|
instance = get_instance(request.user, pk, perm_type="power")
|
|
if instance.is_template:
|
|
messages.warning(request, _("Templates cannot be started."))
|
|
else:
|
|
instance.proxy.force_shutdown()
|
|
instance.proxy.start()
|
|
addlogmsg(
|
|
request.user.username, instance.compute.name, instance.name, _("Power Cycle")
|
|
)
|
|
return get_safe_redirect(
|
|
request, default=reverse("instances:instance", args=[instance.id])
|
|
)
|
|
|
|
|
|
@require_POST
|
|
@serialize_instance_mutation
|
|
def poweroff(request, pk):
|
|
instance = get_instance(request.user, pk, perm_type="power")
|
|
instance.proxy.shutdown()
|
|
addlogmsg(
|
|
request.user.username, instance.compute.name, instance.name, _("Power Off")
|
|
)
|
|
|
|
return get_safe_redirect(
|
|
request, default=reverse("instances:instance", args=[instance.id])
|
|
)
|
|
|
|
|
|
@require_POST
|
|
@superuser_only
|
|
@serialize_instance_mutation
|
|
def suspend(request, pk):
|
|
instance = get_instance(request.user, pk)
|
|
instance.proxy.suspend()
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, _("Suspend"))
|
|
return get_safe_redirect(
|
|
request, default=reverse("instances:instance", args=[instance.id])
|
|
)
|
|
|
|
|
|
@require_POST
|
|
@superuser_only
|
|
@serialize_instance_mutation
|
|
def resume(request, pk):
|
|
instance = get_instance(request.user, pk)
|
|
instance.proxy.resume()
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, _("Resume"))
|
|
return get_safe_redirect(
|
|
request, default=reverse("instances:instance", args=[instance.id])
|
|
)
|
|
|
|
|
|
@require_POST
|
|
@serialize_instance_mutation
|
|
def force_off(request, pk):
|
|
instance = get_instance(request.user, pk, perm_type="power")
|
|
instance.proxy.force_shutdown()
|
|
addlogmsg(
|
|
request.user.username, instance.compute.name, instance.name, _("Force Off")
|
|
)
|
|
return get_safe_redirect(
|
|
request, default=reverse("instances:instance", args=[instance.id])
|
|
)
|
|
|
|
|
|
@serialize_compute_mutation
|
|
def destroy(request, pk):
|
|
if request.method in ["POST", "DELETE"]:
|
|
instance = get_instance(request.user, pk, perm_type="delete")
|
|
with libvirt_instance_lock(instance):
|
|
proxy = instance.proxy
|
|
# Paused and other active states must stop too, or the VM keeps
|
|
# running as a transient domain after the undefine.
|
|
if proxy.instance.isActive():
|
|
proxy.force_shutdown()
|
|
|
|
to_delete, shared = [], []
|
|
if request.POST.get("delete_disk", ""):
|
|
to_delete, shared = proxy.split_disk_paths_by_use()
|
|
|
|
# Undefine before deleting any disk: if it fails, nothing is lost.
|
|
# Snapshot metadata goes with the domain; internal snapshot data
|
|
# stays in the disk images that are kept.
|
|
flags = VIR_DOMAIN_UNDEFINE_MANAGED_SAVE | VIR_DOMAIN_UNDEFINE_SNAPSHOTS_METADATA
|
|
if request.POST.get("delete_nvram", ""):
|
|
flags |= VIR_DOMAIN_UNDEFINE_NVRAM
|
|
else:
|
|
flags |= VIR_DOMAIN_UNDEFINE_KEEP_NVRAM
|
|
proxy.delete(flags)
|
|
instance.delete()
|
|
|
|
for path in to_delete:
|
|
try:
|
|
proxy.get_volume_by_path(path).delete(0)
|
|
except libvirtError as err:
|
|
messages.error(request, _("Disk %(path)s was not deleted: %(err)s") % {"path": path, "err": error_text(request, err)})
|
|
for path in shared:
|
|
messages.warning(request, _("Disk %(path)s is used by another VM and was kept") % {"path": path})
|
|
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 UserInstance.DoesNotExist:
|
|
userinstance = UserInstance(
|
|
is_delete=request.user.is_superuser
|
|
)
|
|
|
|
return render(
|
|
request,
|
|
"instances/destroy_instance_form.html",
|
|
{
|
|
"instance": instance,
|
|
"userinstance": userinstance,
|
|
# get_instance(perm_type="delete") above already refused the rest
|
|
"can_delete": True,
|
|
},
|
|
)
|
|
|
|
|
|
@require_POST
|
|
@superuser_only
|
|
def migrate(request, pk):
|
|
instance = get_instance(request.user, pk)
|
|
|
|
compute_id = request.POST.get("compute_id", "")
|
|
live = request.POST.get("live_migrate", False)
|
|
unsafe = request.POST.get("unsafe_migrate", False)
|
|
offline = request.POST.get("offline_migrate", False)
|
|
autoconverge = request.POST.get("autoconverge", False)
|
|
compress = request.POST.get("compress", False)
|
|
|
|
if not compute_id.isdigit():
|
|
raise Http404
|
|
target_host = get_object_or_404(Compute, pk=compute_id)
|
|
|
|
try:
|
|
# migrate_instance logs the outcome
|
|
utils.migrate_instance(target_host, instance, request.user, live, unsafe, offline, autoconverge, compress)
|
|
except (libvirtError, OSError) as err: # OSError includes a lock timeout
|
|
messages.error(request, err)
|
|
|
|
return get_safe_redirect(
|
|
request, default=reverse("instances:instance", args=[instance.id])
|
|
)
|
|
|
|
|
|
GSTFSD_PORT = 16510
|
|
GSTFSD_TIMEOUT = 120 # gstfsd answers within its 100 s limit for a request (GSTFSD_DEADLINE)
|
|
|
|
|
|
def gstfsd_request(hostname, data):
|
|
"""Send one request to gstfsd and return its JSON reply, or an error reply."""
|
|
try:
|
|
# the host name may resolve to IPv4 or IPv6
|
|
s = socket.create_connection((hostname, GSTFSD_PORT), timeout=GSTFSD_TIMEOUT)
|
|
except OSError as err:
|
|
return {"return": "error", "message": _("gstfsd error: %(err)s") % {"err": err}}
|
|
try:
|
|
s.sendall(json.dumps(data).encode())
|
|
# gstfsd sends one reply and closes the connection
|
|
return json.loads(b"".join(iter(lambda: s.recv(4096), b"")))
|
|
except (OSError, ValueError) as err:
|
|
return {"return": "error", "message": _("gstfsd error: %(err)s") % {"err": err}}
|
|
finally:
|
|
s.close()
|
|
|
|
|
|
@require_POST
|
|
@serialize_instance_mutation
|
|
def set_root_pass(request, pk):
|
|
instance = get_instance(request.user, pk, perm_type="change")
|
|
|
|
if request.method == "POST":
|
|
passwd = request.POST.get("passwd", None)
|
|
if passwd:
|
|
passwd_hash = crypt.crypt(passwd, crypt.mksalt(crypt.METHOD_SHA512))
|
|
# gstfsd opens the domain by name: send the host's current one.
|
|
data = {"action": "password", "passwd": passwd_hash, "vname": instance.proxy.instance.name()}
|
|
|
|
if instance.proxy.get_status() == 5:
|
|
result = gstfsd_request(instance.compute.hostname, data)
|
|
if result["return"] == "success":
|
|
msg = _("Reset root password")
|
|
addlogmsg(
|
|
request.user.username, instance.compute.name, instance.name, msg
|
|
)
|
|
messages.success(request, msg)
|
|
else:
|
|
messages.error(request, result["message"])
|
|
else:
|
|
msg = _("Please shutdown down your instance and then try again")
|
|
messages.error(request, msg)
|
|
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", "")
|
|
if not _is_int(sshkeyid):
|
|
messages.error(request, _("Select an SSH public key"))
|
|
return get_safe_redirect(
|
|
request, default=reverse("instances:instance", args=[instance.id]) + "#access"
|
|
)
|
|
publickey = get_object_or_404(UserSSHKey, id=sshkeyid, user=request.user)
|
|
data = {
|
|
"action": "publickey",
|
|
"key": publickey.keypublic,
|
|
"vname": instance.proxy.instance.name(),
|
|
}
|
|
|
|
if instance.proxy.get_status() == 5:
|
|
result = gstfsd_request(instance.compute.hostname, data)
|
|
if result["return"] == "error":
|
|
msg = result["message"]
|
|
else:
|
|
msg = _("Installed new SSH public key %(keyname)s") % {
|
|
"keyname": publickey.keyname
|
|
}
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
|
|
if result["return"] == "success":
|
|
messages.success(request, msg)
|
|
else:
|
|
messages.error(request, msg)
|
|
else:
|
|
msg = _("Please shutdown down your instance and then try again")
|
|
messages.error(request, msg)
|
|
return get_safe_redirect(
|
|
request, default=reverse("instances:instance", args=[instance.id]) + "#access"
|
|
)
|
|
|
|
|
|
@require_POST
|
|
@serialize_user_quota
|
|
@serialize_instance_mutation
|
|
def resizevm_cpu(request, pk):
|
|
instance = get_instance(request.user, pk, perm_type="change")
|
|
vcpu = instance.proxy.get_vcpu()
|
|
|
|
new_vcpu = request.POST.get("vcpu", "")
|
|
new_cur_vcpu = request.POST.get("cur_vcpu", "")
|
|
if not _is_int(new_vcpu) or not _is_int(new_cur_vcpu):
|
|
messages.error(request, _("Enter a whole number of vCPUs"))
|
|
return redirect(reverse("instances:instance", args=[instance.id]) + "#resize")
|
|
|
|
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 = quota_refused(quota_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_user_quota
|
|
@serialize_instance_mutation
|
|
def resize_memory(request, pk):
|
|
instance = get_instance(request.user, pk, perm_type="change")
|
|
memory = instance.proxy.get_memory()
|
|
cur_memory = instance.proxy.get_cur_memory()
|
|
|
|
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
|
|
# The quota charges the maximum. An active VM keeps it (resize_mem changes
|
|
# only the current memory there); pass the current maximum, so a VM that
|
|
# shuts down meanwhile does not get an unchecked one.
|
|
if instance.proxy.instance.isActive():
|
|
new_memory = memory
|
|
if not _is_int(new_memory) or not _is_int(new_cur_memory):
|
|
messages.error(request, _("Enter a whole number for memory"))
|
|
return redirect(reverse("instances:instance", args=[instance.id]) + "#resize")
|
|
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 = quota_refused(quota_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_user_quota
|
|
@serialize_instance_mutation
|
|
def resize_disk(request, pk):
|
|
instance = get_instance(request.user, pk, perm_type="change")
|
|
disks = instance.proxy.get_disk_devices()
|
|
|
|
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 = quota_refused(quota_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
|
|
@superuser_only
|
|
@serialize_instance_mutation
|
|
def add_new_vol(request, pk):
|
|
instance = get_instance(request.user, pk, perm_type="change")
|
|
storage = request.POST.get("storage", "")
|
|
name = request.POST.get("name", "")
|
|
format = request.POST.get("format", app_settings.INSTANCE_VOLUME_DEFAULT_FORMAT)
|
|
size = request.POST.get("size", 0)
|
|
meta_prealloc = True if request.POST.get("meta_prealloc", False) else False
|
|
bus = request.POST.get("bus", app_settings.INSTANCE_VOLUME_DEFAULT_BUS)
|
|
cache = request.POST.get("cache", app_settings.INSTANCE_VOLUME_DEFAULT_CACHE)
|
|
|
|
invalid = invalid_disk_options(instance, bus=bus, cache=cache, format=format)
|
|
if not VOLUME_NAME_RE.fullmatch(name):
|
|
invalid.append("name")
|
|
if invalid:
|
|
return reject_disk_options(request, pk, invalid)
|
|
|
|
conn_create = wvmCreate(
|
|
instance.compute.hostname,
|
|
instance.compute.login,
|
|
instance.compute.password,
|
|
instance.compute.type,
|
|
)
|
|
# a name either definition uses is taken: on an active VM a disk can be
|
|
# pending in the persistent one only
|
|
target_dev = utils.get_new_disk_dev(
|
|
instance.media + instance.config_media, instance.disks + instance.config_disks, bus
|
|
)
|
|
|
|
source = conn_create.create_volume(
|
|
storage,
|
|
name,
|
|
size,
|
|
format,
|
|
meta_prealloc,
|
|
int(app_settings.INSTANCE_VOLUME_DEFAULT_OWNER_UID),
|
|
int(app_settings.INSTANCE_VOLUME_DEFAULT_OWNER_GID),
|
|
)
|
|
|
|
conn_pool = wvmStorage(
|
|
instance.compute.hostname,
|
|
instance.compute.login,
|
|
instance.compute.password,
|
|
instance.compute.type,
|
|
storage,
|
|
)
|
|
|
|
pool_type = conn_pool.get_type()
|
|
disk_type = conn_pool.get_volume_type(os.path.basename(source))
|
|
# the pool decides: only a dir pool keeps the asked format, the others create raw
|
|
format = util.vol_driver_type(conn_pool.get_volume_format_type(os.path.basename(source)), pool_type)
|
|
|
|
if pool_type == "rbd":
|
|
source_info = conn_pool.get_rbd_source()
|
|
else: # add more disk types to handle different pool and disk types
|
|
source_info = None
|
|
|
|
instance.proxy.attach_disk(
|
|
target_dev,
|
|
source,
|
|
source_info=source_info,
|
|
pool_type=pool_type,
|
|
disk_type=disk_type,
|
|
target_bus=bus,
|
|
format_type=format,
|
|
cache_mode=cache,
|
|
)
|
|
msg = _("Attach new disk: %(name)s (%(format)s)") % {
|
|
"name": name,
|
|
"format": format,
|
|
}
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
return _back(request, pk, "disks")
|
|
|
|
|
|
@require_POST
|
|
@superuser_only
|
|
@serialize_instance_mutation
|
|
def add_existing_vol(request, pk):
|
|
instance = get_instance(request.user, pk, perm_type="change")
|
|
storage = request.POST.get("selected_storage", "")
|
|
name = request.POST.get("vols", "")
|
|
bus = request.POST.get("bus", app_settings.INSTANCE_VOLUME_DEFAULT_BUS)
|
|
cache = request.POST.get("cache", app_settings.INSTANCE_VOLUME_DEFAULT_CACHE)
|
|
|
|
invalid = invalid_disk_options(instance, bus=bus, cache=cache)
|
|
if invalid:
|
|
return reject_disk_options(request, pk, invalid)
|
|
|
|
conn_create = wvmStorage(
|
|
instance.compute.hostname,
|
|
instance.compute.login,
|
|
instance.compute.password,
|
|
instance.compute.type,
|
|
storage,
|
|
)
|
|
|
|
# Only volumes that really exist in the selected pool; this also rules
|
|
# out path traversal through the volume name.
|
|
if name not in conn_create.get_volumes():
|
|
return reject_disk_options(request, pk, ["vols"])
|
|
|
|
pool_type = conn_create.get_type()
|
|
format_type = util.vol_driver_type(conn_create.get_volume_format_type(name), pool_type)
|
|
disk_type = conn_create.get_volume_type(name)
|
|
if pool_type == "rbd":
|
|
source_info = conn_create.get_rbd_source()
|
|
path = conn_create.get_source_name()
|
|
source = f"{path}/{name}"
|
|
else:
|
|
source_info = None
|
|
# an iSCSI volume's path is not the pool target plus its name
|
|
source = conn_create.get_volume(name).path()
|
|
|
|
target_dev = utils.get_new_disk_dev(
|
|
instance.media + instance.config_media, instance.disks + instance.config_disks, bus
|
|
)
|
|
|
|
instance.proxy.attach_disk(
|
|
target_dev,
|
|
source,
|
|
source_info=source_info,
|
|
pool_type=pool_type,
|
|
disk_type=disk_type,
|
|
target_bus=bus,
|
|
format_type=format_type,
|
|
cache_mode=cache,
|
|
)
|
|
msg = _("Attach Existing disk: %(target_dev)s") % {"target_dev": target_dev}
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
return _back(request, pk, "disks")
|
|
|
|
|
|
@require_POST
|
|
@superuser_only
|
|
@serialize_instance_mutation
|
|
def edit_volume(request, pk):
|
|
instance = get_instance(request.user, pk, perm_type="change")
|
|
if "edit_volume" in request.POST:
|
|
target_dev = request.POST.get("dev", "")
|
|
|
|
new_path = request.POST.get("vol_path", "")
|
|
shareable = bool(request.POST.get("vol_shareable", False))
|
|
readonly = bool(request.POST.get("vol_readonly", False))
|
|
disks = instance.proxy.get_disk_devices(config=True)
|
|
current = next((disk for disk in disks if disk["dev"] == target_dev), {})
|
|
new_bus = request.POST.get("vol_bus", current.get("bus"))
|
|
serial = request.POST.get("vol_serial", "")
|
|
format = request.POST.get("vol_format", "")
|
|
cache = request.POST.get(
|
|
"vol_cache", app_settings.INSTANCE_VOLUME_DEFAULT_CACHE
|
|
)
|
|
io = request.POST.get("vol_io_mode", app_settings.INSTANCE_VOLUME_DEFAULT_IO)
|
|
discard = request.POST.get(
|
|
"vol_discard_mode", app_settings.INSTANCE_VOLUME_DEFAULT_DISCARD
|
|
)
|
|
zeroes = request.POST.get(
|
|
"vol_detect_zeroes", app_settings.INSTANCE_VOLUME_DEFAULT_DETECT_ZEROES
|
|
)
|
|
# The form shows "None" (or nothing) for a disk without driver type.
|
|
if format in ("", "None"):
|
|
format = current.get("format") or ""
|
|
|
|
invalid = invalid_disk_options(
|
|
instance,
|
|
bus=new_bus,
|
|
cache=cache,
|
|
io=io,
|
|
discard=discard,
|
|
zeroes=zeroes,
|
|
# "": the disk has no driver type and keeps having none.
|
|
format=format or None,
|
|
serial=serial,
|
|
)
|
|
if not current:
|
|
invalid.append("dev")
|
|
if invalid:
|
|
return reject_disk_options(request, pk, invalid)
|
|
|
|
instance.proxy.edit_disk(
|
|
target_dev,
|
|
new_path,
|
|
readonly,
|
|
shareable,
|
|
new_bus,
|
|
serial,
|
|
format,
|
|
cache,
|
|
io,
|
|
discard,
|
|
zeroes,
|
|
)
|
|
|
|
if not instance.proxy.get_status() == 5:
|
|
messages.success(
|
|
request,
|
|
_(
|
|
"Volume changes are applied. "
|
|
+ "But it will be activated after shutdown"
|
|
),
|
|
)
|
|
else:
|
|
messages.success(request, _("Volume is changed successfully."))
|
|
msg = _("Edit disk: %(target_dev)s") % {"target_dev": target_dev}
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
|
|
return _back(request, pk, "disks")
|
|
|
|
|
|
@require_POST
|
|
@superuser_only
|
|
@serialize_compute_mutation
|
|
def delete_vol(request, pk):
|
|
instance = get_instance(request.user, pk, perm_type="change")
|
|
dev = request.POST.get("dev", "")
|
|
|
|
# Resolve the volume from the VM's own XML; never trust the posted
|
|
# storage/name, which could point at any volume on the compute.
|
|
disk = next((d for d in instance.disks if d["dev"] == dev), None)
|
|
if disk is None or not disk["storage"] or not disk["image"]:
|
|
messages.error(
|
|
request,
|
|
_("Disk %(dev)s is not a storage volume of this instance") % {"dev": dev},
|
|
)
|
|
return _back(request, pk, "disks")
|
|
# the page lists the persistent disks: delete only a volume both definitions use there
|
|
if not any(d["dev"] == dev and d["path"] == disk["path"] for d in instance.config_disks):
|
|
messages.error(
|
|
request,
|
|
_("Disk %(dev)s has a pending change; delete it once the VM is shut down") % {"dev": dev},
|
|
)
|
|
return _back(request, pk, "disks")
|
|
|
|
conn_delete = wvmStorage(
|
|
instance.compute.hostname,
|
|
instance.compute.login,
|
|
instance.compute.password,
|
|
instance.compute.type,
|
|
disk["storage"],
|
|
)
|
|
|
|
if disk["path"] in instance.proxy.paths_used_by_other_domains():
|
|
messages.error(
|
|
request,
|
|
_("Volume %(vol)s is used by another VM; detach it instead") % {"vol": disk["image"]},
|
|
)
|
|
return _back(request, pk, "disks")
|
|
|
|
msg = _("Delete disk: %(dev)s") % {"dev": dev}
|
|
instance.proxy.detach_disk(dev)
|
|
# Never delete a volume the guest may still be writing to.
|
|
if not instance.proxy.wait_disk_detached(dev):
|
|
messages.warning(
|
|
request,
|
|
_("The guest has not released disk %(dev)s yet; volume %(vol)s was kept. "
|
|
"Delete it from the storage pool once the VM is shut down.")
|
|
% {"dev": dev, "vol": disk["image"]},
|
|
)
|
|
return _back(request, pk, "disks")
|
|
try:
|
|
conn_delete.del_volume(disk["image"])
|
|
except util.OperationError as err: # a file of the VM's own snapshots
|
|
messages.warning(
|
|
request,
|
|
_("Disk %(dev)s was detached, but its volume was kept: %(err)s") % {"dev": dev, "err": err},
|
|
)
|
|
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
return _back(request, pk, "disks")
|
|
|
|
|
|
@require_POST
|
|
@superuser_only
|
|
@serialize_instance_mutation
|
|
def detach_vol(request, pk):
|
|
instance = get_instance(request.user, pk, perm_type="change")
|
|
dev = request.POST.get("dev", "")
|
|
instance.proxy.detach_disk(dev)
|
|
msg = _("Detach disk: %(dev)s") % {"dev": dev}
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
|
|
return _back(request, pk, "disks")
|
|
|
|
|
|
@require_POST
|
|
@superuser_only
|
|
@serialize_instance_mutation
|
|
def add_cdrom(request, pk):
|
|
instance = get_instance(request.user, pk, perm_type="change")
|
|
bus = request.POST.get("bus", "ide" if instance.machine == "pc" else "sata")
|
|
invalid = invalid_disk_options(instance, bus=bus)
|
|
if invalid:
|
|
return reject_disk_options(request, pk, invalid)
|
|
|
|
target = utils.get_new_disk_dev(
|
|
instance.media + instance.config_media, instance.disks + instance.config_disks, bus
|
|
)
|
|
instance.proxy.attach_disk(
|
|
target,
|
|
"",
|
|
disk_device="cdrom",
|
|
cache_mode="none",
|
|
target_bus=bus,
|
|
readonly=True,
|
|
)
|
|
msg = _("Add CD-ROM: %(target)s") % {"target": target}
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
|
|
return _back(request, pk, "disks")
|
|
|
|
|
|
@require_POST
|
|
@superuser_only
|
|
@serialize_instance_mutation
|
|
def detach_cdrom(request, pk, dev):
|
|
instance = get_instance(request.user, pk, perm_type="change")
|
|
# dev = request.POST.get('detach_cdrom', '')
|
|
instance.proxy.detach_disk(dev)
|
|
msg = _("Detach CD-ROM: %(dev)s") % {"dev": dev}
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
|
|
return _back(request, pk, "disks")
|
|
|
|
|
|
@require_POST
|
|
@superuser_only
|
|
@serialize_instance_mutation
|
|
def unmount_iso(request, pk):
|
|
instance = get_instance(request.user, pk, perm_type="change")
|
|
dev = request.POST.get("umount_iso", "")
|
|
try:
|
|
instance.proxy.umount_iso(dev)
|
|
except libvirtError as err:
|
|
messages.error(request, err)
|
|
else:
|
|
msg = _("Unmount media: %(dev)s") % {"dev": dev}
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
return redirect(_same_origin_referer(request, reverse("instances:instance", args=[pk])) + "#disks")
|
|
|
|
|
|
@require_POST
|
|
@superuser_only
|
|
@serialize_instance_mutation
|
|
def mount_iso(request, pk):
|
|
instance = get_instance(request.user, pk, perm_type="change")
|
|
image = request.POST.get("media", "")
|
|
dev = request.POST.get("mount_iso", "")
|
|
try:
|
|
instance.proxy.mount_iso(dev, image)
|
|
except libvirtError as err:
|
|
messages.error(request, err)
|
|
else:
|
|
msg = _("Mount media: %(dev)s") % {"dev": dev}
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
return redirect(_same_origin_referer(request, reverse("instances:instance", args=[pk])) + "#disks")
|
|
|
|
|
|
@require_POST
|
|
@serialize_instance_mutation
|
|
def snapshot(request, pk):
|
|
instance = get_instance(request.user, pk, perm_type="change")
|
|
if not request.user.has_perm("instances.snapshot_instances"):
|
|
raise PermissionDenied
|
|
name = request.POST.get("name", "")
|
|
desc = request.POST.get("description", "")
|
|
try:
|
|
instance.proxy.create_snapshot(name, desc)
|
|
except libvirtError as err:
|
|
# e.g. UEFI firmware whose NVRAM format does not allow internal snapshots
|
|
messages.error(request, _("Snapshot was not created: %(err)s") % {"err": error_text(request, err)})
|
|
else:
|
|
msg = _("Create snapshot: %(snap)s") % {"snap": name}
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
return _back(request, pk, "managesnapshot")
|
|
|
|
|
|
@require_POST
|
|
@serialize_instance_mutation
|
|
def delete_snapshot(request, pk):
|
|
instance = get_instance(request.user, pk, perm_type="change")
|
|
if not request.user.has_perm("instances.snapshot_instances"):
|
|
raise PermissionDenied
|
|
snap_name = request.POST.get("name", "")
|
|
try:
|
|
instance.proxy.snapshot_delete(snap_name)
|
|
except libvirtError as err:
|
|
messages.error(request, _("Snapshot was not deleted: %(err)s") % {"err": error_text(request, err)})
|
|
return _back(request, pk, "managesnapshot")
|
|
msg = _("Delete snapshot: %(snap)s") % {"snap": snap_name}
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
return _back(request, pk, "managesnapshot")
|
|
|
|
|
|
@require_POST
|
|
@serialize_instance_mutation
|
|
def revert_snapshot(request, pk):
|
|
instance = get_instance(request.user, pk, perm_type="change")
|
|
if not request.user.has_perm("instances.snapshot_instances"):
|
|
raise PermissionDenied
|
|
snap_name = request.POST.get("name", "")
|
|
try:
|
|
instance.proxy.snapshot_revert(snap_name)
|
|
except libvirtError as err:
|
|
messages.error(request, _("Snapshot was not reverted: %(err)s") % {"err": error_text(request, err)})
|
|
return _back(request, pk, "managesnapshot")
|
|
msg = _("Successful revert snapshot: ")
|
|
msg += snap_name
|
|
messages.success(request, msg)
|
|
msg = _("Revert snapshot: %(snap)s") % {"snap": snap_name}
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
return _back(request, pk, "managesnapshot")
|
|
|
|
|
|
def _warn_left_behind(request, files):
|
|
"""libvirt only logs it when it cannot remove a snapshot file."""
|
|
if files:
|
|
messages.warning(
|
|
request,
|
|
_(
|
|
"These snapshot files were not removed, although no VM on this host uses them any more: %(files)s. "
|
|
"On NFS libvirt removes them as the qemu user, who needs write access to their directory. "
|
|
"Remove them by hand."
|
|
)
|
|
% {"files": ", ".join(files)},
|
|
)
|
|
|
|
|
|
@require_POST
|
|
@serialize_instance_mutation
|
|
def create_external_snapshot(request, pk):
|
|
instance = get_instance(request.user, pk, perm_type="change")
|
|
if not request.user.has_perm("instances.snapshot_instances"):
|
|
raise PermissionDenied
|
|
name = request.POST.get("name", "")
|
|
desc = request.POST.get("description", "")
|
|
memory = request.POST.get("memory") == "on"
|
|
try:
|
|
instance.proxy.create_external_snapshot(name, desc, memory)
|
|
except libvirtError as err:
|
|
messages.error(request, _("Snapshot was not created: %(err)s") % {"err": error_text(request, err)})
|
|
else:
|
|
msg = _("Create external snapshot: %(snap)s") % {"snap": name}
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
return _back(request, pk, "managesnapshot")
|
|
|
|
|
|
@require_POST
|
|
@serialize_instance_mutation
|
|
def revert_external_snapshot(request, pk):
|
|
instance = get_instance(request.user, pk, perm_type="change")
|
|
if not request.user.has_perm("instances.snapshot_instances"):
|
|
raise PermissionDenied
|
|
name = request.POST.get("name", "")
|
|
try:
|
|
left = instance.proxy.revert_external_snapshot(name)
|
|
except libvirtError as err:
|
|
messages.error(request, _("Snapshot was not reverted: %(err)s") % {"err": error_text(request, err)})
|
|
else:
|
|
messages.success(request, _("Successful revert snapshot: ") + name)
|
|
_warn_left_behind(request, left)
|
|
msg = _("Revert external snapshot: %(snap)s") % {"snap": name}
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
return _back(request, pk, "managesnapshot")
|
|
|
|
|
|
@require_POST
|
|
@serialize_instance_mutation
|
|
def delete_external_snapshot(request, pk):
|
|
instance = get_instance(request.user, pk, perm_type="change")
|
|
if not request.user.has_perm("instances.snapshot_instances"):
|
|
raise PermissionDenied
|
|
name = request.POST.get("name", "")
|
|
try:
|
|
left = instance.proxy.delete_external_snapshot(name)
|
|
except libvirtError as err:
|
|
messages.error(request, _("Snapshot was not deleted: %(err)s") % {"err": error_text(request, err)})
|
|
else:
|
|
_warn_left_behind(request, left)
|
|
msg = _("Delete external snapshot: %(snap)s") % {"snap": name}
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
return _back(request, pk, "managesnapshot")
|
|
|
|
|
|
@require_POST
|
|
@superuser_only
|
|
@serialize_instance_mutation
|
|
def set_vcpu(request, pk):
|
|
instance = get_instance(request.user, pk)
|
|
id = request.POST.get("id", "")
|
|
enabled = request.POST.get("set_vcpu", "")
|
|
if enabled == "True":
|
|
instance.proxy.set_vcpu(id, 1)
|
|
else:
|
|
instance.proxy.set_vcpu(id, 0)
|
|
msg = _("VCPU %(id)s is enabled=%(enabled)s") % {"id": id, "enabled": enabled}
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
return _back(request, pk, "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
|
|
msg = _("VCPU Hot-plug is enabled=%(status)s") % {"status": status}
|
|
instance.proxy.set_vcpu_hotplug(status)
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
return _back(request, pk, "resize")
|
|
|
|
|
|
@require_POST
|
|
@superuser_only
|
|
@serialize_instance_mutation
|
|
def set_autostart(request, pk):
|
|
instance = get_instance(request.user, pk)
|
|
instance.proxy.set_autostart(1)
|
|
msg = _("Set autostart")
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
return _back(request, pk, "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)
|
|
msg = _("Unset autostart")
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
return _back(request, pk, "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)
|
|
msg = _("Enable boot menu")
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
return _back(request, pk, "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)
|
|
msg = _("Disable boot menu")
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
return _back(request, pk, "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", "")
|
|
if bootorder:
|
|
order_list = {}
|
|
for idx, val in enumerate(bootorder.split(",")):
|
|
dev_type, dev = val.split(":", 1)
|
|
order_list[idx] = {"type": dev_type, "dev": dev}
|
|
try:
|
|
instance.proxy.set_bootorder(order_list)
|
|
except util.OperationError as err:
|
|
messages.error(request, err)
|
|
return _back(request, pk, "boot_opt")
|
|
msg = _("Set boot order")
|
|
|
|
if not instance.proxy.get_status() == 5:
|
|
messages.success(
|
|
request,
|
|
_(
|
|
"Boot menu changes applied. "
|
|
+ "But it will be activated after shutdown"
|
|
),
|
|
)
|
|
else:
|
|
messages.success(request, _("Boot order changed successfully."))
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
return _back(request, pk, "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", "")
|
|
# The editor is filled while the VM is shut off; once it runs, the posted
|
|
# XML would overwrite changes made since.
|
|
if new_xml and instance.proxy.get_status() != 5:
|
|
messages.error(request, _("Power off the instance to change its XML"))
|
|
elif new_xml:
|
|
instance.proxy._defineXML(new_xml)
|
|
msg = _("Change instance XML")
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
return _back(request, pk, "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")
|
|
if status == "True":
|
|
instance.proxy.add_guest_agent()
|
|
if status == "False":
|
|
instance.proxy.remove_guest_agent()
|
|
|
|
msg = _("Set Guest Agent: %(status)s") % {"status": status}
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
return _back(request, pk, "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")
|
|
instance.proxy.set_video_model(video_model)
|
|
msg = _("Set Video Model: %(model)s") % {"model": video_model}
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
return _back(request, pk, "options")
|
|
|
|
|
|
@require_POST
|
|
@superuser_only
|
|
@serialize_instance_mutation
|
|
def change_network(request, pk):
|
|
instance = get_instance(request.user, pk)
|
|
back = _same_origin_referer(request, reverse("instances:instance", args=[pk])) + "#network"
|
|
|
|
# Each NIC has its own form; its fields end with that NIC's index.
|
|
nums = [key[len("net-old-mac-"):] for key in request.POST if key.startswith("net-old-mac-")]
|
|
if len(nums) != 1:
|
|
messages.error(request, _("Select one network interface to change"))
|
|
return redirect(back)
|
|
num = nums[0]
|
|
old_mac = request.POST[f"net-old-mac-{num}"]
|
|
mac = request.POST.get(f"net-mac-{num}", "").strip() or old_mac
|
|
try:
|
|
util.validate_macaddr(mac)
|
|
except ValueError as err:
|
|
messages.error(request, err)
|
|
return redirect(back)
|
|
|
|
(source, source_type) = utils.get_network_tuple(request.POST.get(f"net-source-{num}", ""))
|
|
if source_type == "iface":
|
|
iface = wvmInterface(
|
|
instance.compute.hostname,
|
|
instance.compute.login,
|
|
instance.compute.password,
|
|
instance.compute.type,
|
|
source,
|
|
)
|
|
source_type = iface.get_type()
|
|
|
|
try:
|
|
instance.proxy.change_network(
|
|
old_mac,
|
|
mac,
|
|
source,
|
|
source_type,
|
|
request.POST.get(f"net-model-{num}"),
|
|
request.POST.get(f"net-nwfilter-{num}", ""),
|
|
)
|
|
except libvirtError as err:
|
|
messages.error(request, err)
|
|
return redirect(back)
|
|
|
|
msg = _("Change network: %(mac)s") % {"mac": old_mac}
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
msg = _("Network Device Config is changed. Please shutdown instance to activate.")
|
|
if instance.proxy.get_status() != 5:
|
|
messages.success(request, msg)
|
|
return redirect(back)
|
|
|
|
|
|
@require_POST
|
|
@superuser_only
|
|
@serialize_instance_mutation
|
|
def add_network(request, pk):
|
|
instance = get_instance(request.user, pk)
|
|
|
|
network = request.POST.get("add-net-network")
|
|
if not network:
|
|
messages.error(request, _("No network selected"))
|
|
return _back(request, pk, "network")
|
|
mac = request.POST.get("add-net-mac")
|
|
nwfilter = request.POST.get("add-net-nwfilter")
|
|
(source, source_type) = utils.get_network_tuple(network)
|
|
model = request.POST.get("add-net-model")
|
|
|
|
if source_type == "iface":
|
|
iface = wvmInterface(
|
|
instance.compute.hostname,
|
|
instance.compute.login,
|
|
instance.compute.password,
|
|
instance.compute.type,
|
|
source,
|
|
)
|
|
source_type = iface.get_type()
|
|
|
|
instance.proxy.add_network(mac, source, source_type, model=model, nwfilter=nwfilter)
|
|
msg = _("Add network: %(mac)s") % {"mac": mac}
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
return _back(request, pk, "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", "")
|
|
|
|
instance.proxy.delete_network(mac_address)
|
|
msg = _("Delete Network: %(mac)s") % {"mac": mac_address}
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
return _back(request, pk, "network")
|
|
|
|
|
|
@require_POST
|
|
@superuser_only
|
|
@serialize_instance_mutation
|
|
def set_link_state(request, pk):
|
|
instance = get_instance(request.user, pk)
|
|
|
|
mac_address = request.POST.get("mac", "")
|
|
state = request.POST.get("set_link_state")
|
|
state = "down" if state == "up" else "up"
|
|
instance.proxy.set_link_state(mac_address, state)
|
|
msg = _("Set Link State: %(state)s") % {"state": state}
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
return _back(request, pk, "network")
|
|
|
|
|
|
@require_POST
|
|
@superuser_only
|
|
@serialize_instance_mutation
|
|
def set_qos(request, pk):
|
|
instance = get_instance(request.user, pk)
|
|
|
|
qos_dir = request.POST.get("qos_direction", "")
|
|
average = request.POST.get("qos_average") or 0
|
|
peak = request.POST.get("qos_peak") or 0
|
|
burst = request.POST.get("qos_burst") or 0
|
|
mac_key = [key for key in request.POST if "mac" in key]
|
|
mac = request.POST.get(mac_key[0]) if mac_key else None
|
|
if not mac or qos_dir not in ("inbound", "outbound"):
|
|
messages.error(request, _("Select a network interface and a QoS direction"))
|
|
return _back(request, pk, "network")
|
|
if not all(_is_int(rate) for rate in (average, peak, burst)):
|
|
messages.error(request, _("QoS rates must be whole numbers"))
|
|
return _back(request, pk, "network")
|
|
|
|
instance.proxy.set_qos(mac, qos_dir, average, peak, burst)
|
|
if instance.proxy.get_status() == 5:
|
|
messages.success(
|
|
request, _("%(qos_dir)s QoS is set") % {"qos_dir": qos_dir.capitalize()}
|
|
)
|
|
else:
|
|
messages.success(
|
|
request,
|
|
_(
|
|
"%(qos_dir)s QoS is set. Network XML is changed. \
|
|
Stop and start network to activate new config."
|
|
)
|
|
% {"qos_dir": qos_dir.capitalize()},
|
|
)
|
|
|
|
return _back(request, pk, "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", "")
|
|
mac = request.POST.get("net-mac")
|
|
if not mac or qos_dir not in ("inbound", "outbound"):
|
|
messages.error(request, _("Select a network interface and a QoS direction"))
|
|
return _back(request, pk, "network")
|
|
instance.proxy.unset_qos(mac, qos_dir)
|
|
|
|
if instance.proxy.get_status() == 5:
|
|
messages.success(
|
|
request, _("%(qos_dir)s QoS is deleted") % {"qos_dir": qos_dir.capitalize()}
|
|
)
|
|
else:
|
|
messages.success(
|
|
request,
|
|
_(
|
|
"%(qos_dir)s QoS is deleted. Network XML is changed. \
|
|
Stop and start network to activate new config."
|
|
)
|
|
% {"qos_dir": qos_dir.capitalize()},
|
|
)
|
|
return _back(request, pk, "network")
|
|
|
|
|
|
@require_POST
|
|
@superuser_only
|
|
def add_owner(request, pk):
|
|
instance = get_instance(request.user, pk)
|
|
user_id = request.POST.get("user_id", "")
|
|
if not user_id.isdigit():
|
|
raise Http404
|
|
user = get_object_or_404(User, pk=user_id)
|
|
|
|
check_inst = 0
|
|
|
|
if app_settings.ALLOW_INSTANCE_MULTIPLE_OWNER == "False":
|
|
check_inst = UserInstance.objects.filter(instance=instance).count()
|
|
|
|
if check_inst > 0:
|
|
messages.error(
|
|
request, _("Only one owner is allowed and the one already added")
|
|
)
|
|
else:
|
|
if UserInstance.objects.get_or_create(instance=instance, user=user)[1]:
|
|
msg = _("Add owner: %(user)s") % {"user": user}
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
else:
|
|
messages.warning(request, _("%(user)s is already an owner") % {"user": user})
|
|
return _back(request, pk, "users")
|
|
|
|
|
|
@require_POST
|
|
@superuser_only
|
|
def del_owner(request, pk):
|
|
instance = get_instance(request.user, pk)
|
|
userinstance_id = request.POST.get("userinstance", "")
|
|
if not userinstance_id.isdigit():
|
|
raise Http404
|
|
userinstance = get_object_or_404(UserInstance, pk=userinstance_id, instance=instance)
|
|
userinstance.delete()
|
|
msg = _("Delete owner: %(userinstance_id)s ") % {"userinstance_id": userinstance_id}
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
return _back(request, pk, "users")
|
|
|
|
|
|
# Buses for the empty CD-ROM of a new VM: fdc and virtio have no ejectable
|
|
# media, and libvirt refuses an empty usb disk.
|
|
CDROM_BUSES = ("ide", "sata", "scsi")
|
|
CLONE_POST_KEY_RE = re.compile(r"^(clone-net-mac-\d+|disk-[a-z0-9]+|meta-[a-z0-9]+)$")
|
|
|
|
|
|
def _clone_source(user, pk):
|
|
"""Cloning copies the source disks, so it needs change permission on the
|
|
source VM. Templates are meant to be deployed from, so viewing is enough."""
|
|
instance = get_instance(user, pk)
|
|
if not instance.is_template:
|
|
instance = get_instance(user, pk, perm_type="change")
|
|
return instance
|
|
|
|
|
|
def _clone_plan(request, instance):
|
|
"""The clone's settings from the request and the source as it is now,
|
|
and why it cannot be made, or None."""
|
|
clone_data = dict()
|
|
clone_data["name"] = request.POST.get("name", "").strip()
|
|
clone_data["clone-title"] = request.POST.get("clone-title", "").strip()
|
|
clone_data["clone-description"] = request.POST.get("clone-description", "").strip()
|
|
|
|
# the clone copies the disks of the persistent definition
|
|
disk_sum = sum(int(disk.get("size") or 0) >> 30 for disk in instance.config_disks)
|
|
quota_msg = utils.check_user_quota(
|
|
request.user, 1, instance.vcpu, instance.memory, disk_sum
|
|
)
|
|
|
|
clone_data["disk_owner_uid"] = int(app_settings.INSTANCE_VOLUME_DEFAULT_OWNER_UID)
|
|
clone_data["disk_owner_gid"] = int(app_settings.INSTANCE_VOLUME_DEFAULT_OWNER_GID)
|
|
|
|
# Only superusers may choose disk names and MAC addresses; the form only
|
|
# shows those fields to them.
|
|
if request.user.is_superuser:
|
|
for key, value in request.POST.items():
|
|
if CLONE_POST_KEY_RE.fullmatch(key):
|
|
clone_data[key] = value.strip()
|
|
|
|
if app_settings.CLONE_INSTANCE_AUTO_NAME == "True" and not clone_data["name"]:
|
|
auto_vname = utils.get_clone_free_names()[0]
|
|
clone_data["name"] = auto_vname
|
|
clone_data["clone-net-mac-0"] = utils.get_dhcp_mac_address(auto_vname)
|
|
for disk in instance.config_disks:
|
|
disk_dev = f"disk-{disk['dev']}"
|
|
disk_name = utils.get_clone_disk_name(disk, instance.name, auto_vname)
|
|
clone_data[disk_dev] = disk_name
|
|
|
|
if not request.user.is_superuser:
|
|
for disk in instance.config_disks:
|
|
clone_data[f"disk-{disk['dev']}"] = utils.get_clone_disk_name(
|
|
disk, instance.name, clone_data["name"]
|
|
)
|
|
for num in range(max(1, len(instance.config_networks))):
|
|
key = f"clone-net-mac-{num}"
|
|
if not clone_data.get(key):
|
|
clone_data[key] = (
|
|
utils.get_dhcp_mac_address(clone_data["name"]) if num == 0 else ""
|
|
) or utils.get_random_mac_address()
|
|
|
|
check_instance = Instance.objects.filter(name=clone_data["name"])
|
|
invalid_macs = [
|
|
value
|
|
for key, value in clone_data.items()
|
|
if key.startswith("clone-net-mac-") and not utils.MAC_ADDRESS_RE.fullmatch(value)
|
|
]
|
|
invalid_disks = [
|
|
value
|
|
for key, value in clone_data.items()
|
|
# None: a disk without a volume, which clone_instance does not copy.
|
|
if key.startswith("disk-")
|
|
and value is not None
|
|
and not VOLUME_NAME_RE.fullmatch(value)
|
|
]
|
|
|
|
if not request.user.is_superuser and quota_msg:
|
|
msg = quota_refused(quota_msg, _(
|
|
"User '%(quota_msg)s' quota reached, cannot create '%(clone_name)s'!"
|
|
) % {
|
|
"quota_msg": quota_msg,
|
|
"clone_name": clone_data["name"],
|
|
})
|
|
return clone_data, msg
|
|
if check_instance:
|
|
msg = _("Instance '%(clone_name)s' already exists!") % {
|
|
"clone_name": clone_data["name"]
|
|
}
|
|
return clone_data, msg
|
|
if not re.match(r"^[a-zA-Z0-9-]+$", clone_data["name"]):
|
|
msg = _("Instance name '%(clone_name)s' contains invalid characters!") % {
|
|
"clone_name": clone_data["name"]
|
|
}
|
|
return clone_data, msg
|
|
if "clone-net-mac-0" not in clone_data or invalid_macs:
|
|
msg = _("Instance MAC '%(clone_mac)s' invalid format!") % {
|
|
"clone_mac": ", ".join(invalid_macs)
|
|
}
|
|
return clone_data, msg
|
|
if invalid_disks:
|
|
msg = _("Disk name '%(disk_name)s' contains invalid characters!") % {
|
|
"disk_name": ", ".join(str(v) for v in invalid_disks)
|
|
}
|
|
return clone_data, msg
|
|
return clone_data, None
|
|
|
|
|
|
@require_POST
|
|
@permission_required("instances.clone_instances", raise_exception=True)
|
|
@serialize_user_quota
|
|
def clone(request, pk):
|
|
instance = _clone_source(request.user, pk)
|
|
try:
|
|
# The whole compute: the destination name and disk names must not be
|
|
# taken by a concurrent clone of another VM while volumes are copied.
|
|
with libvirt_compute_lock(instance.compute):
|
|
# While this request waited, a migration may have moved the
|
|
# source, or its template mark, size or the user's rights changed:
|
|
# the clone is checked against the source as it is now.
|
|
if _moved_meanwhile(pk, instance):
|
|
return _busy(request, pk)
|
|
instance = _clone_source(request.user, pk)
|
|
clone_data, refusal = _clone_plan(request, instance)
|
|
if refusal:
|
|
messages.error(request, refusal)
|
|
return get_safe_redirect(request, default=reverse("instances:instance", args=[pk]))
|
|
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
|
|
}
|
|
messages.success(request, msg)
|
|
addlogmsg(
|
|
request.user.username, instance.compute.name, new_instance.name, msg
|
|
)
|
|
|
|
return redirect(reverse("instances:instance", args=[new_instance.id]))
|
|
except (libvirtError, ValueError, OSError) as e: # OSError includes a lock timeout
|
|
messages.error(request, error_text(request, e))
|
|
|
|
return get_safe_redirect(request, default=reverse("instances:instance", args=[pk]))
|
|
|
|
|
|
@require_POST
|
|
@serialize_instance_mutation
|
|
def update_console(request, pk):
|
|
instance = get_instance(request.user, pk, perm_type="change")
|
|
|
|
if not utils.can_manage_console(request.user, instance):
|
|
raise PermissionDenied
|
|
form = ConsoleForm(request.POST or None, can_set_listener=request.user.is_superuser)
|
|
if form.is_valid():
|
|
if (
|
|
"generate_password" in form.changed_data
|
|
or "clear_password" in form.changed_data
|
|
or "password" in form.changed_data
|
|
):
|
|
if form.cleaned_data["generate_password"]:
|
|
password = randomPasswd()
|
|
elif form.cleaned_data["clear_password"]:
|
|
password = ""
|
|
else:
|
|
password = form.cleaned_data["password"]
|
|
|
|
# A console that listens beyond localhost (set by an
|
|
# administrator) keeps its password unless an administrator
|
|
# removes it. The definition being changed is the persistent one.
|
|
if (
|
|
not password
|
|
and not request.user.is_superuser
|
|
and not instance.proxy.console_listens_locally()
|
|
):
|
|
messages.error(
|
|
request,
|
|
_("The console listens beyond localhost; only an administrator can remove its password."),
|
|
)
|
|
elif not instance.proxy.set_console_passwd(password):
|
|
msg = _(
|
|
"Error setting console password. "
|
|
+ "You should check that your instance have an graphic device."
|
|
)
|
|
messages.error(request, msg)
|
|
else:
|
|
msg = _("Set VNC password")
|
|
addlogmsg(
|
|
request.user.username, instance.compute.name, instance.name, msg
|
|
)
|
|
|
|
if "keymap" in form.changed_data or "clear_keymap" in form.changed_data:
|
|
if form.cleaned_data["clear_keymap"]:
|
|
instance.proxy.set_console_keymap("")
|
|
else:
|
|
instance.proxy.set_console_keymap(form.cleaned_data["keymap"])
|
|
|
|
msg = _("Set VNC keymap")
|
|
addlogmsg(
|
|
request.user.username, instance.compute.name, instance.name, msg
|
|
)
|
|
|
|
if "listen_on" in form.changed_data:
|
|
instance.proxy.set_console_listener_addr(form.cleaned_data["listen_on"])
|
|
msg = _("Set VNC listen address")
|
|
addlogmsg(
|
|
request.user.username, instance.compute.name, instance.name, msg
|
|
)
|
|
else:
|
|
messages.error(request, _("Console settings were not saved: %(errors)s") % {"errors": form.errors.as_text()})
|
|
|
|
return get_safe_redirect(
|
|
request, default=reverse("instances:instance", args=[instance.id]) + "#vncsettings"
|
|
)
|
|
|
|
|
|
@require_POST
|
|
@serialize_user_quota
|
|
@serialize_instance_mutation
|
|
def change_options(request, pk):
|
|
instance = get_instance(request.user, pk, perm_type="change")
|
|
try:
|
|
userinstance = instance.userinstance_set.get(user=request.user)
|
|
except UserInstance.DoesNotExist:
|
|
userinstance = UserInstance(is_change=False)
|
|
|
|
if request.user.is_superuser or userinstance.is_change:
|
|
# Only superusers and staff may (un)mark templates. The
|
|
# checkbox is disabled for everyone else, and a disabled checkbox is
|
|
# not submitted, so for them the flag must be left alone.
|
|
if request.user.is_superuser or request.user.is_staff:
|
|
is_template = bool(request.POST.get("is_template", False))
|
|
# A template is not counted and cannot be started: making it a
|
|
# VM again adds it to the owner's usage, so the quota applies.
|
|
if instance.is_template and not is_template and not request.user.is_superuser:
|
|
disk_sum = sum(int(disk.get("size") or 0) >> 30 for disk in instance.disks)
|
|
quota_msg = utils.check_user_quota(request.user, 1, instance.vcpu, instance.memory, disk_sum)
|
|
if quota_msg:
|
|
messages.error(request, quota_refused(quota_msg, _(
|
|
"User %(quota_msg)s quota reached, '%(instance_name)s' stays a template!"
|
|
) % {"quota_msg": quota_msg, "instance_name": instance.name}))
|
|
is_template = True
|
|
instance.is_template = is_template
|
|
instance.save(update_fields=["is_template"])
|
|
|
|
options = {}
|
|
for post in request.POST:
|
|
if post in ["title", "description"]:
|
|
options[post] = request.POST.get(post, "")
|
|
instance.proxy.set_options(options)
|
|
|
|
msg = _("Edit options")
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
return get_safe_redirect(
|
|
request, default=reverse("instances:instance", args=[instance.id]) + "#options"
|
|
)
|
|
|
|
|
|
def getvvfile(request, pk):
|
|
instance = get_instance(request.user, pk)
|
|
# The .vv file contains the VNC password: same rule as the console.
|
|
if not utils.can_open_console(request.user, instance):
|
|
raise PermissionDenied
|
|
msg = _("Send console.vv file")
|
|
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
|
|
response = HttpResponse(
|
|
content="",
|
|
content_type="application/x-virt-viewer",
|
|
status=200,
|
|
reason=None,
|
|
charset="utf-8",
|
|
)
|
|
# the VM found by its UUID; the stored name may be stale
|
|
with instance.proxy.cached_reads() as proxy:
|
|
listen = proxy.get_console_listener_addr()
|
|
response.writelines("[virt-viewer]\n")
|
|
response.writelines(f"type={proxy.get_console_type()}\n")
|
|
response.writelines(f"host={proxy.host if listen == '0.0.0.0' else listen}\n")
|
|
response.writelines(f"port={proxy.get_console_port()}\n")
|
|
response.writelines(f"title={proxy.instance.name()}\n")
|
|
response.writelines(f"password={proxy.get_console_passwd() or ''}\n")
|
|
response.writelines("enable-usbredir=1\n")
|
|
response.writelines("disable-effects=all\n")
|
|
response.writelines("secure-attention=ctrl+alt+ins\n")
|
|
response.writelines("release-cursor=ctrl+alt\n")
|
|
response.writelines("fullscreen=1\n")
|
|
response.writelines("delete-this-file=1\n")
|
|
response["Content-Disposition"] = 'attachment; filename="console.vv"'
|
|
return response
|
|
|
|
|
|
@superuser_only
|
|
def create_instance_select_type(request, compute_id):
|
|
"""
|
|
:param request:
|
|
:param compute_id:
|
|
:return:
|
|
"""
|
|
|
|
conn = None
|
|
storages = list()
|
|
networks = list()
|
|
hypervisors = list()
|
|
meta_prealloc = False
|
|
compute = get_object_or_404(Compute, pk=compute_id)
|
|
|
|
conn = wvmCreate(compute.hostname, compute.login, compute.password, compute.type)
|
|
instances = conn.get_instances()
|
|
all_hypervisors = conn.get_hypervisors_machines()
|
|
|
|
# Supported hypervisors by webvirtcloud: i686, x86_64(for now)
|
|
supported_arch = [
|
|
"x86_64",
|
|
"i686",
|
|
"aarch64",
|
|
"armv7l",
|
|
"ppc64",
|
|
"ppc64le",
|
|
"s390x",
|
|
]
|
|
hypervisors = [hpv for hpv in all_hypervisors.keys() if hpv in supported_arch]
|
|
default_machine = app_settings.INSTANCE_MACHINE_DEFAULT_TYPE
|
|
default_arch = app_settings.INSTANCE_ARCH_DEFAULT_TYPE
|
|
|
|
if request.method == "POST":
|
|
if "create_xml" in request.POST:
|
|
xml = request.POST.get("dom_xml", "")
|
|
try:
|
|
name = util.get_xml_path(xml, "/domain/name")
|
|
except util.etree.Error:
|
|
name = None
|
|
if name in instances:
|
|
error_msg = _("A virtual machine with this name already exists")
|
|
messages.error(request, error_msg)
|
|
else:
|
|
with libvirt_compute_lock(compute):
|
|
conn._defineXML(xml)
|
|
utils.refr(compute)
|
|
instance = compute.instance_set.get(name=name)
|
|
return redirect(reverse("instances:instance", args=[instance.id]))
|
|
|
|
return render(request, "create_instance_w1.html", locals())
|
|
|
|
|
|
@superuser_only
|
|
def create_instance(request, compute_id, arch, machine):
|
|
"""
|
|
:param request:
|
|
:param compute_id:
|
|
:param arch:
|
|
:param machine:
|
|
:return:
|
|
"""
|
|
|
|
conn = None
|
|
storages = list()
|
|
networks = list()
|
|
hypervisors = list()
|
|
firmwares = list()
|
|
meta_prealloc = False
|
|
compute = get_object_or_404(Compute, pk=compute_id)
|
|
flavors = Flavor.objects.filter().order_by("id")
|
|
appsettings = AppSettings.objects.all()
|
|
|
|
try:
|
|
conn = wvmCreate(
|
|
compute.hostname,
|
|
compute.login,
|
|
compute.password,
|
|
compute.type
|
|
)
|
|
|
|
default_firmware = app_settings.INSTANCE_FIRMWARE_DEFAULT_TYPE
|
|
default_cpu_mode = app_settings.INSTANCE_CPU_DEFAULT_MODE
|
|
instances = conn.get_instances()
|
|
videos = conn.get_video_models(arch, machine)
|
|
default_video = app_settings.INSTANCE_VIDEO_DEFAULT_TYPE
|
|
cache_modes = sorted(conn.get_cache_modes().items())
|
|
default_cache = app_settings.INSTANCE_VOLUME_DEFAULT_CACHE
|
|
default_io = app_settings.INSTANCE_VOLUME_DEFAULT_IO
|
|
default_zeroes = app_settings.INSTANCE_VOLUME_DEFAULT_DETECT_ZEROES
|
|
default_discard = app_settings.INSTANCE_VOLUME_DEFAULT_DISCARD
|
|
default_disk_format = app_settings.INSTANCE_VOLUME_DEFAULT_FORMAT
|
|
default_disk_owner_uid = int(app_settings.INSTANCE_VOLUME_DEFAULT_OWNER_UID)
|
|
default_disk_owner_gid = int(app_settings.INSTANCE_VOLUME_DEFAULT_OWNER_GID)
|
|
default_scsi_disk_model = app_settings.INSTANCE_VOLUME_DEFAULT_SCSI_CONTROLLER
|
|
listener_addr = settings.QEMU_CONSOLE_LISTENER_ADDRESSES
|
|
mac_auto = util.randomMAC()
|
|
disk_devices = conn.get_disk_device_types(arch, machine)
|
|
disk_buses = conn.get_disk_bus_types(arch, machine)
|
|
cdrom_buses = [bus for bus in disk_buses if bus in CDROM_BUSES]
|
|
default_bus = app_settings.INSTANCE_VOLUME_DEFAULT_BUS
|
|
networks = sorted(conn.get_networks())
|
|
nwfilters = conn.get_nwfilters()
|
|
net_models_host = conn.get_network_models()
|
|
default_nic_type = app_settings.INSTANCE_NIC_DEFAULT_TYPE
|
|
storages = sorted(conn.get_storages(only_actives=True))
|
|
default_cdrom = app_settings.INSTANCE_CDROM_ADD
|
|
input_device_buses = ["default", "virtio", "usb"]
|
|
default_input_device_bus = app_settings.INSTANCE_INPUT_DEFAULT_DEVICE
|
|
|
|
dom_caps = conn.get_dom_capabilities(arch, machine)
|
|
caps = conn.get_capabilities(arch)
|
|
|
|
virtio_support = conn.is_supports_virtio(arch, machine)
|
|
# Add BIOS
|
|
label = conn.label_for_firmware_path(arch, None)
|
|
if label:
|
|
firmwares.append(label)
|
|
# Add UEFI
|
|
loader_path = conn.find_uefi_path_for_arch(arch, machine)
|
|
label = conn.label_for_firmware_path(arch, loader_path)
|
|
if label:
|
|
firmwares.append(label)
|
|
firmwares = list(set(firmwares))
|
|
|
|
flavor_form = FlavorForm()
|
|
|
|
if conn:
|
|
if not storages:
|
|
raise util.OperationError(_("You haven't defined any storage pools"))
|
|
if not networks:
|
|
raise util.OperationError(_("You haven't defined any network pools"))
|
|
|
|
if request.method == "POST":
|
|
if "create" in request.POST:
|
|
form = NewVMForm(request.POST)
|
|
if form.is_valid():
|
|
data = form.cleaned_data
|
|
meta_prealloc = bool(data["meta_prealloc"])
|
|
|
|
# Everything is checked before any storage is allocated.
|
|
if data["name"] in instances:
|
|
raise util.OperationError(_("A virtual machine with this name already exists"))
|
|
if Instance.objects.filter(name__exact=data["name"]):
|
|
raise util.OperationError(
|
|
_("There is an instance with same name. Remove it and try again!")
|
|
)
|
|
if data["hdd_size"] and not data["mac"]:
|
|
raise util.OperationError(_("No Virtual Machine MAC has been entered"))
|
|
if not (data["hdd_size"] or data["template"] or data["images"]):
|
|
raise util.OperationError(_("First you need to create or select an image"))
|
|
if (
|
|
not data["hdd_size"]
|
|
and data["template"]
|
|
and conn.get_volume_path(data["name"] + ".img", data["storage"])
|
|
):
|
|
raise util.OperationError(
|
|
_("Image has already exist. Please check volumes or change instance name")
|
|
)
|
|
if data["cache_mode"] not in conn.get_cache_modes():
|
|
raise util.OperationError(_("Invalid cache mode"))
|
|
if data["add_cdrom"] != "None" and data["add_cdrom"] not in cdrom_buses:
|
|
raise util.OperationError(
|
|
_("A CD-ROM cannot use the %(bus)s bus") % {"bus": data["add_cdrom"]}
|
|
)
|
|
try:
|
|
utils.nic_macs(data["mac"], data["networks"])
|
|
except ValueError as err:
|
|
raise util.OperationError(str(err))
|
|
|
|
firmware = dict()
|
|
if "UEFI" in data["firmware"]:
|
|
if ":" not in data["firmware"]:
|
|
raise util.OperationError(_("Invalid firmware"))
|
|
firmware["loader"] = data["firmware"].split(":")[1].strip()
|
|
firmware["secure"] = "no"
|
|
firmware["readonly"] = "yes"
|
|
firmware["type"] = "pflash"
|
|
if "secboot" in firmware["loader"]:
|
|
# A secure boot loader needs secure='yes' and SMM, on q35 only
|
|
if machine != "q35":
|
|
messages.warning(
|
|
request,
|
|
"Changing machine type from '%s' to 'q35' "
|
|
"which is required for UEFI secure boot." % machine,
|
|
)
|
|
machine = "q35"
|
|
firmware["secure"] = "yes"
|
|
|
|
if data["net_model"] == "default":
|
|
data["net_model"] = "virtio"
|
|
|
|
def volume(path, device="disk", bus=default_bus):
|
|
return {
|
|
"path": path,
|
|
"type": conn.get_volume_format_type(path),
|
|
"device": device,
|
|
"bus": bus,
|
|
"scsi_model": default_scsi_disk_model if bus == "scsi" else None,
|
|
"cache_mode": data["cache_mode"],
|
|
"discard_mode": default_discard,
|
|
"detect_zeroes_mode": default_zeroes,
|
|
"io_mode": default_io,
|
|
}
|
|
|
|
created = [] # volumes this request allocated
|
|
defined = False
|
|
volume_list = []
|
|
uuid = util.randomUUID()
|
|
try:
|
|
if data["hdd_size"]:
|
|
path = conn.create_volume(
|
|
data["storage"],
|
|
data["name"],
|
|
data["hdd_size"],
|
|
default_disk_format,
|
|
meta_prealloc,
|
|
default_disk_owner_uid,
|
|
default_disk_owner_gid,
|
|
)
|
|
created.append(path)
|
|
volume_list.append(volume(path))
|
|
elif data["template"]:
|
|
path = conn.clone_from_template(
|
|
data["name"],
|
|
conn.get_volume_path(data["template"]),
|
|
data["storage"],
|
|
meta_prealloc,
|
|
default_disk_owner_uid,
|
|
default_disk_owner_gid,
|
|
)
|
|
created.append(path)
|
|
volume_list.append(volume(path))
|
|
else:
|
|
for idx, vol in enumerate(data["images"].split(",")):
|
|
volume_list.append(volume(
|
|
conn.get_volume_path(vol),
|
|
request.POST.get("device" + str(idx), ""),
|
|
request.POST.get("bus" + str(idx), ""),
|
|
))
|
|
|
|
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"],
|
|
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"],
|
|
)
|
|
defined = True # from here on the VM uses the volumes
|
|
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"])
|
|
except Exception as err:
|
|
leftovers = []
|
|
for path in [] if defined else created:
|
|
try:
|
|
conn.delete_volume(path)
|
|
except libvirtError:
|
|
leftovers.append(path)
|
|
if leftovers:
|
|
messages.error(
|
|
request,
|
|
_("Remove these volumes by hand: %(paths)s") % {"paths": ", ".join(leftovers)},
|
|
)
|
|
if not isinstance(err, libvirtError):
|
|
raise # unexpected: the volumes are gone, the error stays visible
|
|
messages.error(request, err)
|
|
else:
|
|
msg = _("Instance is created")
|
|
messages.success(request, msg)
|
|
addlogmsg(
|
|
request.user.username,
|
|
create_instance.compute.name,
|
|
create_instance.name,
|
|
msg,
|
|
)
|
|
return redirect(reverse("instances:instance", args=[create_instance.id]))
|
|
conn.close()
|
|
except libvirtError as lib_err:
|
|
messages.error(request, lib_err)
|
|
|
|
return render(request, "create_instance_w2.html", locals())
|
|
|
|
|
|
@superuser_only
|
|
def flavor_create(request):
|
|
form = FlavorForm(request.POST or None)
|
|
if form.is_valid():
|
|
form.save()
|
|
messages.success(request, _("Flavor Created"))
|
|
return get_safe_redirect(request, default=reverse("instances:index"))
|
|
|
|
return render(
|
|
request,
|
|
"common/form.html",
|
|
{"form": form, "title": _("Create Flavor")},
|
|
)
|
|
|
|
|
|
@superuser_only
|
|
def flavor_update(request, pk):
|
|
flavor = get_object_or_404(Flavor, pk=pk)
|
|
form = FlavorForm(request.POST or None, instance=flavor)
|
|
if form.is_valid():
|
|
form.save()
|
|
messages.success(request, _("Flavor Updated"))
|
|
return get_safe_redirect(request, default=reverse("instances:index"))
|
|
|
|
return render(
|
|
request,
|
|
"common/form.html",
|
|
{"form": form, "title": _("Update Flavor")},
|
|
)
|
|
|
|
|
|
@superuser_only
|
|
def flavor_delete(request, pk):
|
|
flavor = get_object_or_404(Flavor, pk=pk)
|
|
if request.method == "POST":
|
|
flavor.delete()
|
|
messages.success(request, _("Flavor Deleted"))
|
|
return get_safe_redirect(request, default=reverse("instances:index"))
|
|
|
|
return render(
|
|
request,
|
|
"common/confirm_delete.html",
|
|
{"object": flavor},
|
|
)
|