1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00
webvirtcloud/instances/views.py
catborise 479faaa4ca fix(instances): allow editing disks that have no driver format
A disk without driver/@type could not be edited: the empty format was
rejected, and edit_disk would have written type=''. An empty format is
now accepted and edit_disk leaves the type attribute out.
2026-10-02 23:58:55 +03:00

2239 lines
81 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.utils import libvirt_compute_lock
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import permission_required
from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied
from django.http import Http404, HttpResponse, JsonResponse
from django.db.models import prefetch_related_objects
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.http import url_has_allowed_host_and_scheme
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_NVRAM,
VIR_DOMAIN_START_PAUSED,
libvirtError)
from logs.views import addlogmsg
from vrtManager import util
from vrtManager.create import wvmCreate
from vrtManager.instance import wvmInstances
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
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")
if request.user.is_superuser or request.user.has_perm("instances.view_instances"):
instances = Instance.objects.all().prefetch_related("userinstance_set")
else:
instances = Instance.objects.filter(
userinstance__user=request.user
).prefetch_related("userinstance_set")
return render(
request, "allinstances.html", {"computes": computes, "instances": instances}
)
def instance(request, pk):
instance: Instance = get_instance(request.user, pk)
compute: Compute = 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
console_types = AppSettings.objects.get(
key="QEMU_CONSOLE_DEFAULT_TYPE"
).choices_as_list()
# 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={
"type": instance.console_type,
"listen_on": instance.console_listener_address,
"password": instance.console_passwd,
"keymap": instance.console_keymap,
}
)
console_listener_addresses = settings.QEMU_CONSOLE_LISTENER_ADDRESSES
bottom_bar = app_settings.VIEW_INSTANCE_DETAIL_BOTTOM_BAR
allow_admin_or_not_template = (
request.user.is_superuser or request.user.is_staff or not instance.is_template
)
try:
userinstance = UserInstance.objects.get(
instance__compute_id=compute.id,
instance__name=instance.name,
user__id=request.user.id,
)
except UserInstance.DoesNotExist:
userinstance = None
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()
user_quota_msg = utils.check_user_quota(request.user, 0, 0, 0, 0)
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")
allow_admin_or_not_template = (
request.user.is_superuser or request.user.is_staff or not instance.is_template
)
# 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 drbd_status(request, pk):
instance = get_instance(request.user, pk)
result = "None DRBD"
if instance.compute.type == 2:
conn = instance.compute.login + "@" + instance.compute.hostname
remoteDrbdStatus = subprocess.run(
["ssh", conn, "sudo", "/usr/sbin/drbdadm", "status", "&&", "exit"],
stdout=subprocess.PIPE,
text=True,
)
if remoteDrbdStatus.stdout:
try:
instanceFindDrbd = re.compile(
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
except:
print("Error to get drbd role and status")
return result
def stats(request, pk):
instance = get_instance(request.user, pk)
json_blk = []
json_net = []
# TODO: stats are inaccurate
cpu_usage = instance.proxy.cpu_usage()
mem_usage = instance.proxy.mem_usage()
blk_usage = instance.proxy.disk_usage()
net_usage = instance.proxy.net_usage()
current_time = time.strftime("%H:%M:%S")
for blk in blk_usage:
json_blk.append(
{
"dev": blk["dev"],
"data": [int(blk["rd"]) / 1048576, int(blk["wr"]) / 1048576],
}
)
for net in net_usage:
json_net.append(
{
"dev": net["dev"],
"data": [int(net["rx"]) / 1048576, int(net["tx"]) / 1048576],
}
)
return JsonResponse(
{
"cpudata": int(cpu_usage["cpu"]),
"memdata": mem_usage,
"blkdata": json_blk,
"netdata": json_net,
"timeline": current_time,
}
)
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 = "/srv/webvirtcloud/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 get_safe_redirect(request, default=None):
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 redirect(referer)
if default:
return redirect(default)
return redirect("instances:index")
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)
"""
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":
if not (user_inst and user_inst.is_change):
raise PermissionDenied
elif perm_type == "delete":
if not (user_inst and user_inst.is_delete):
raise PermissionDenied
else:
raise PermissionDenied
return instance
def serialize_instance_mutation(func):
"""
Decorator that ensures any view mutating an instance's libvirt state
acquires libvirt_compute_lock for the instance's compute.
Works re-entrantly with inner calls such as refresh_instance_database.
"""
@functools.wraps(func)
def wrapper(request, pk, *args, **kwargs):
try:
inst = Instance.objects.only("compute_id").get(pk=pk)
with libvirt_compute_lock(inst.compute):
return func(request, pk, *args, **kwargs)
except Instance.DoesNotExist:
return func(request, pk, *args, **kwargs)
return wrapper
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, invalid):
messages.error(
request,
_("Invalid disk options: %(options)s") % {"options": ", ".join(invalid)},
)
return redirect(request.META.get("HTTP_REFERER") + "#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")
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])
)
def destroy(request, pk):
if request.method in ["POST", "DELETE"]:
instance = get_instance(request.user, pk, perm_type="delete")
with libvirt_compute_lock(instance.compute):
if instance.proxy.get_status() == 1:
instance.proxy.force_shutdown()
if request.POST.get("delete_disk", ""):
snapshots = sorted(
instance.proxy.get_snapshot(), reverse=True, key=lambda k: k["date"]
)
for snapshot in snapshots:
instance.proxy.snapshot_delete(snapshot["name"])
instance.proxy.delete_all_disks()
if request.POST.get("delete_nvram", ""):
instance.proxy.delete(VIR_DOMAIN_UNDEFINE_NVRAM)
else:
instance.proxy.delete(VIR_DOMAIN_UNDEFINE_KEEP_NVRAM)
instance.delete()
addlogmsg(
request.user.username, instance.compute.name, instance.name, _("Destroy")
)
return redirect(reverse("instances:index"))
instance = get_instance(request.user, pk, perm_type="delete")
try:
userinstance = instance.userinstance_set.get(user=request.user)
except Exception:
userinstance = UserInstance(
is_delete=request.user.is_superuser
)
return render(
request,
"instances/destroy_instance_form.html",
{
"instance": instance,
"userinstance": userinstance,
},
)
@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)
xml_del = request.POST.get("xml_delete", False)
offline = request.POST.get("offline_migrate", False)
autoconverge = request.POST.get("autoconverge", False)
compress = request.POST.get("compress", False)
postcopy = request.POST.get("postcopy", False)
current_host = instance.compute.hostname
target_host = Compute.objects.get(id=compute_id)
try:
utils.migrate_instance(
target_host,
instance,
request.user,
live,
unsafe,
xml_del,
offline,
autoconverge,
compress,
postcopy,
)
except libvirtError as err:
messages.error(request, err)
migration_method = "live" if live is True else "offline"
msg = _("Instance is migrated(%(method)s) to %(hostname)s") % {
"hostname": target_host.hostname,
"method": migration_method,
}
addlogmsg(request.user.username, current_host, instance.name, msg)
return get_safe_redirect(
request, default=reverse("instances:instance", args=[instance.id])
)
GSTFSD_PORT = 16510
GSTFSD_TIMEOUT = 120 # gstfsd starts a guestfs appliance, which takes a while
def gstfsd_request(hostname, data):
"""Send one request to gstfsd and return its JSON reply, or an error reply."""
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(GSTFSD_TIMEOUT)
try:
s.connect((hostname, GSTFSD_PORT))
s.send(json.dumps(data).encode())
return json.loads(s.recv(1024).strip())
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))
data = {"action": "password", "passwd": passwd_hash, "vname": 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", "")
publickey = get_object_or_404(UserSSHKey, id=sshkeyid, user=request.user)
data = {
"action": "publickey",
"key": publickey.keypublic,
"vname": 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_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", "")
quota_msg = utils.check_user_quota(
request.user, 0, int(new_vcpu) - vcpu, 0, 0
)
if not request.user.is_superuser and quota_msg:
msg = _(
"User %(quota_msg)s quota reached, cannot resize CPU of '%(instance_name)s'!"
) % {
"quota_msg": quota_msg,
"instance_name": instance.name,
}
messages.error(request, msg)
else:
cur_vcpu = new_cur_vcpu
vcpu = new_vcpu
instance.proxy.resize_cpu(cur_vcpu, vcpu)
msg = _("CPU is resized: %(old)s to %(new)s") % {
"old": cur_vcpu,
"new": vcpu,
}
addlogmsg(
request.user.username, instance.compute.name, instance.name, msg
)
messages.success(request, msg)
return redirect(reverse("instances:instance", args=[instance.id]) + "#resize")
@require_POST
@serialize_instance_mutation
def resize_memory(request, pk):
instance = get_instance(request.user, pk, 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
quota_msg = utils.check_user_quota(
request.user, 0, 0, int(new_memory) - memory, 0
)
if not request.user.is_superuser and quota_msg:
msg = _(
"User %(quota_msg)s quota reached, cannot resize memory of '%(instance_name)s'!"
) % {
"quota_msg": quota_msg,
"instance_name": instance.name,
}
messages.error(request, msg)
else:
instance.proxy.resize_mem(new_cur_memory, new_memory)
msg = _(
"Memory is resized: current/max: %(old_cur)s/%(old_max)s to %(new_cur)s/%(new_max)s"
) % {
"old_cur": cur_memory,
"old_max": memory,
"new_cur": new_cur_memory,
"new_max": new_memory,
}
addlogmsg(
request.user.username, instance.compute.name, instance.name, msg
)
messages.success(request, msg)
return redirect(reverse("instances:instance", args=[instance.id]) + "#resize")
@require_POST
@serialize_instance_mutation
def resize_disk(request, pk):
instance = get_instance(request.user, pk, 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 = _(
"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")
media = instance.proxy.get_media_devices()
disks = instance.proxy.get_disk_devices()
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, invalid)
conn_create = wvmCreate(
instance.compute.hostname,
instance.compute.login,
instance.compute.password,
instance.compute.type,
)
target_dev = utils.get_new_disk_dev(media, 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))
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 redirect(request.META.get("HTTP_REFERER") + "#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, invalid)
media = instance.proxy.get_media_devices()
disks = instance.proxy.get_disk_devices()
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, ["vols"])
format_type = conn_create.get_volume_format_type(name)
disk_type = conn_create.get_volume_type(name)
pool_type = conn_create.get_type()
if pool_type == "rbd":
source_info = conn_create.get_rbd_source()
path = conn_create.get_source_name()
else:
source_info = None
path = conn_create.get_target_path()
target_dev = utils.get_new_disk_dev(media, disks, bus)
source = f"{path}/{name}"
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 redirect(request.META.get("HTTP_REFERER") + "#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))
disk_type = request.POST.get("vol_type", "")
bus = request.POST.get("vol_bus_old", "")
new_bus = request.POST.get("vol_bus", 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"):
current = next((d for d in instance.disks if d["dev"] == target_dev), {})
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 target_dev not in [disk["dev"] for disk in instance.disks]:
invalid.append("dev")
if invalid:
return reject_disk_options(request, invalid)
new_target_dev = utils.get_new_disk_dev(instance.media, instance.disks, new_bus)
if new_bus != bus:
instance.proxy.change_disk_bus(target_dev, new_target_dev, new_bus)
target_dev = new_target_dev
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 redirect(request.META.get("HTTP_REFERER") + "#disks")
@require_POST
@superuser_only
@serialize_instance_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 redirect(request.META.get("HTTP_REFERER") + "#disks")
conn_delete = wvmStorage(
instance.compute.hostname,
instance.compute.login,
instance.compute.password,
instance.compute.type,
disk["storage"],
)
msg = _("Delete disk: %(dev)s") % {"dev": dev}
instance.proxy.detach_disk(dev)
conn_delete.del_volume(disk["image"])
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
return redirect(request.META.get("HTTP_REFERER") + "#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 redirect(request.META.get("HTTP_REFERER") + "#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, invalid)
target = utils.get_new_disk_dev(instance.media, instance.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 redirect(request.META.get("HTTP_REFERER") + "#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 redirect(request.META.get("HTTP_REFERER") + "#disks")
@require_POST
@superuser_only
@serialize_instance_mutation
def unmount_iso(request, pk):
instance = get_instance(request.user, pk, perm_type="change")
image = request.POST.get("path", "")
dev = request.POST.get("umount_iso", "")
instance.proxy.umount_iso(dev, image)
msg = _("Mount media: %(dev)s") % {"dev": dev}
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
return redirect(request.META.get("HTTP_REFERER") + "#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", "")
instance.proxy.mount_iso(dev, image)
msg = _("Unmount media: %(dev)s") % {"dev": dev}
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
return redirect(request.META.get("HTTP_REFERER") + "#disks")
@require_POST
@serialize_instance_mutation
def snapshot(request, pk):
instance = get_instance(request.user, pk, perm_type="change")
allow_admin_or_not_template = (
request.user.is_superuser or request.user.is_staff or not instance.is_template
)
if allow_admin_or_not_template and request.user.has_perm(
"instances.snapshot_instances"
):
name = request.POST.get("name", "")
desc = request.POST.get("description", "")
instance.proxy.create_snapshot(name, desc)
msg = _("Create snapshot: %(snap)s") % {"snap": name}
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
return redirect(request.META.get("HTTP_REFERER") + "#managesnapshot")
@require_POST
@serialize_instance_mutation
def delete_snapshot(request, pk):
instance = get_instance(request.user, pk, perm_type="change")
allow_admin_or_not_template = (
request.user.is_superuser or request.user.is_staff or not instance.is_template
)
if allow_admin_or_not_template and request.user.has_perm(
"instances.snapshot_instances"
):
snap_name = request.POST.get("name", "")
instance.proxy.snapshot_delete(snap_name)
msg = _("Delete snapshot: %(snap)s") % {"snap": snap_name}
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
return redirect(request.META.get("HTTP_REFERER") + "#managesnapshot")
@require_POST
@serialize_instance_mutation
def revert_snapshot(request, pk):
instance = get_instance(request.user, pk, perm_type="change")
allow_admin_or_not_template = (
request.user.is_superuser or request.user.is_staff or not instance.is_template
)
if allow_admin_or_not_template and request.user.has_perm(
"instances.snapshot_instances"
):
snap_name = request.POST.get("name", "")
instance.proxy.snapshot_revert(snap_name)
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 redirect(request.META.get("HTTP_REFERER") + "#managesnapshot")
@require_POST
@serialize_instance_mutation
def create_external_snapshot(request, pk):
instance = get_instance(request.user, pk, perm_type="change")
allow_admin_or_not_template = (
request.user.is_superuser or request.user.is_staff or not instance.is_template
)
if allow_admin_or_not_template and request.user.has_perm(
"instances.snapshot_instances"
):
name = request.POST.get("name", "")
desc = request.POST.get("description", "")
instance.proxy.create_external_snapshot("s1." + name, instance, desc=desc)
msg = _("Create external snapshot: %(snap)s") % {"snap": name}
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
return redirect(request.META.get("HTTP_REFERER") + "#managesnapshot")
def get_external_snapshots(request, pk):
instance = get_instance(request.user, pk)
allow_admin_or_not_template = (
request.user.is_superuser or request.user.is_staff or not instance.is_template
)
if allow_admin_or_not_template and request.user.has_perm(
"instances.snapshot_instances"
):
external_snapshots = instance.proxy.get_external_snapshots()
return external_snapshots
@require_POST
@serialize_instance_mutation
def revert_external_snapshot(request, pk):
instance = get_instance(request.user, pk, perm_type="change")
allow_admin_or_not_template = (
request.user.is_superuser or request.user.is_staff or not instance.is_template
)
if allow_admin_or_not_template and request.user.has_perm(
"instances.snapshot_instances"
):
instance_state = True if instance.proxy.get_status() != 5 else False
name = request.POST.get("name", "")
date = request.POST.get("date", "")
desc = request.POST.get("desc", "")
instance.proxy.force_shutdown() if instance_state else None
instance.proxy.revert_external_snapshot(name, date, desc)
instance.proxy.start() if instance_state else None
msg = _("Revert external snapshot: %(snap)s") % {"snap": name}
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
return redirect(request.META.get("HTTP_REFERER") + "#managesnapshot")
@require_POST
@serialize_instance_mutation
def delete_external_snapshot(request, pk):
instance = get_instance(request.user, pk, perm_type="change")
instance_state = True if instance.proxy.get_status() == 5 else False
allow_admin_or_not_template = (
request.user.is_superuser or request.user.is_staff or not instance.is_template
)
if allow_admin_or_not_template and request.user.has_perm(
"instances.snapshot_instances"
):
name = request.POST.get("name", "")
instance.proxy.start(VIR_DOMAIN_START_PAUSED) if instance_state else None
try:
instance.proxy.delete_external_snapshot(name)
msg = _("Delete external snapshot: %(snap)s") % {"snap": name}
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
finally:
instance.proxy.force_shutdown() if instance_state else None
return redirect(request.META.get("HTTP_REFERER") + "#managesnapshot")
@require_POST
@superuser_only
@serialize_instance_mutation
def set_vcpu(request, pk):
instance = get_instance(request.user, pk)
id = request.POST.get("id", "")
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 redirect(request.META.get("HTTP_REFERER") + "#resize")
@require_POST
@superuser_only
@serialize_instance_mutation
def set_vcpu_hotplug(request, pk):
instance = get_instance(request.user, pk)
status = True if request.POST.get("vcpu_hotplug", "False") == "True" else False
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 redirect(request.META.get("HTTP_REFERER") + "#resize")
@require_POST
@superuser_only
@serialize_instance_mutation
def set_autostart(request, pk):
instance = get_instance(request.user, pk)
instance.proxy.set_autostart(1)
msg = _("Set autostart")
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
return redirect(request.META.get("HTTP_REFERER") + "#boot_opt")
@require_POST
@superuser_only
@serialize_instance_mutation
def unset_autostart(request, pk):
instance = get_instance(request.user, pk)
instance.proxy.set_autostart(0)
msg = _("Unset autostart")
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
return redirect(request.META.get("HTTP_REFERER") + "#boot_opt")
@require_POST
@superuser_only
@serialize_instance_mutation
def set_bootmenu(request, pk):
instance = get_instance(request.user, pk)
instance.proxy.set_bootmenu(1)
msg = _("Enable boot menu")
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
return redirect(request.META.get("HTTP_REFERER") + "#boot_opt")
@require_POST
@superuser_only
@serialize_instance_mutation
def unset_bootmenu(request, pk):
instance = get_instance(request.user, pk)
instance.proxy.set_bootmenu(0)
msg = _("Disable boot menu")
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
return redirect(request.META.get("HTTP_REFERER") + "#boot_opt")
@require_POST
@superuser_only
@serialize_instance_mutation
def set_bootorder(request, pk):
instance = get_instance(request.user, pk)
bootorder = request.POST.get("bootorder", "")
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}
instance.proxy.set_bootorder(order_list)
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 redirect(request.META.get("HTTP_REFERER") + "#boot_opt")
@require_POST
@superuser_only
@serialize_instance_mutation
def change_xml(request, pk):
instance = get_instance(request.user, pk)
new_xml = request.POST.get("inst_xml", "")
if new_xml:
instance.proxy._defineXML(new_xml)
msg = _("Change instance XML")
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
return redirect(request.META.get("HTTP_REFERER") + "#xmledit")
@require_POST
@superuser_only
@serialize_instance_mutation
def set_guest_agent(request, pk):
instance = get_instance(request.user, pk)
status = request.POST.get("guest_agent")
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 redirect(request.META.get("HTTP_REFERER") + "#options")
@require_POST
@superuser_only
@serialize_instance_mutation
def set_video_model(request, pk):
instance = get_instance(request.user, pk)
video_model = request.POST.get("video_model", "vga")
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 redirect(request.META.get("HTTP_REFERER") + "#options")
@require_POST
@superuser_only
@serialize_instance_mutation
def change_network(request, pk):
instance = get_instance(request.user, pk)
msg = _("Change network")
network_data = {}
for post in request.POST:
if post.startswith("net-source-"):
(source, source_type) = utils.get_network_tuple(request.POST.get(post))
network_data[post] = source
network_data[post + "-type"] = source_type
if source_type == "iface":
iface = wvmInterface(
instance.compute.hostname,
instance.compute.login,
instance.compute.password,
instance.compute.type,
source,
)
network_data[post + "-type"] = iface.get_type()
elif post.startswith("net-"):
network_data[post] = request.POST.get(post, "")
instance.proxy.change_network(network_data)
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(request.META.get("HTTP_REFERER") + "#network")
@require_POST
@superuser_only
@serialize_instance_mutation
def add_network(request, pk):
instance = get_instance(request.user, pk)
mac = request.POST.get("add-net-mac")
nwfilter = request.POST.get("add-net-nwfilter")
(source, source_type) = utils.get_network_tuple(request.POST.get("add-net-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 redirect(request.META.get("HTTP_REFERER") + "#network")
@require_POST
@superuser_only
@serialize_instance_mutation
def delete_network(request, pk):
instance = get_instance(request.user, pk)
mac_address = request.POST.get("delete_network", "")
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 redirect(request.META.get("HTTP_REFERER") + "#network")
@require_POST
@superuser_only
@serialize_instance_mutation
def set_link_state(request, pk):
instance = get_instance(request.user, pk)
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 redirect(request.META.get("HTTP_REFERER") + "#network")
@require_POST
@superuser_only
@serialize_instance_mutation
def set_qos(request, pk):
instance = get_instance(request.user, pk)
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
keys = request.POST.keys()
mac_key = [key for key in keys if "mac" in key]
if mac_key:
mac = request.POST.get(mac_key[0])
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 redirect(request.META.get("HTTP_REFERER") + "#network")
@require_POST
@superuser_only
@serialize_instance_mutation
def unset_qos(request, pk):
instance = get_instance(request.user, pk)
qos_dir = request.POST.get("qos_direction", "")
mac = request.POST.get("net-mac")
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 redirect(request.META.get("HTTP_REFERER") + "#network")
@require_POST
@superuser_only
def add_owner(request, pk):
instance = get_instance(request.user, pk)
user_id = request.POST.get("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:
add_user_inst = UserInstance(instance=instance, user_id=user_id)
add_user_inst.save()
user = User.objects.get(id=user_id)
msg = _("Add owner: %(user)s") % {"user": user}
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
return redirect(request.META.get("HTTP_REFERER") + "#users")
@require_POST
@superuser_only
def del_owner(request, pk):
instance = get_instance(request.user, pk)
userinstance_id = int(request.POST.get("userinstance", ""))
userinstance = UserInstance.objects.get(pk=userinstance_id)
userinstance.delete()
msg = _("Delete owner: %(userinstance_id)s ") % {"userinstance_id": userinstance_id}
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
return redirect(request.META.get("HTTP_REFERER") + "#users")
MAC_ADDRESS_RE = re.compile(r"^([0-9A-F]{2})(:?[0-9A-F]{2}){5}$", re.IGNORECASE)
CLONE_POST_KEY_RE = re.compile(r"^(clone-net-mac-\d+|disk-[a-z0-9]+|meta-[a-z0-9]+)$")
@require_POST
@permission_required("instances.clone_instances", raise_exception=True)
def clone(request, 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(request.user, pk)
if not instance.is_template:
instance = get_instance(request.user, pk, perm_type="change")
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()
disk_sum = sum([disk["size"] >> 30 for disk in instance.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.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.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.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 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 = _(
"User '%(quota_msg)s' quota reached, cannot create '%(clone_name)s'!"
) % {
"quota_msg": quota_msg,
"clone_name": clone_data["name"],
}
messages.error(request, msg)
elif check_instance:
msg = _("Instance '%(clone_name)s' already exists!") % {
"clone_name": clone_data["name"]
}
messages.error(request, msg)
elif 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"]
}
messages.error(request, msg)
elif "clone-net-mac-0" not in clone_data or invalid_macs:
msg = _("Instance MAC '%(clone_mac)s' invalid format!") % {
"clone_mac": ", ".join(invalid_macs)
}
messages.error(request, msg)
elif invalid_disks:
msg = _("Disk name '%(disk_name)s' contains invalid characters!") % {
"disk_name": ", ".join(str(v) for v in invalid_disks)
}
messages.error(request, msg)
else:
try:
with libvirt_compute_lock(instance.compute):
new_uuid = instance.proxy.clone_instance(clone_data)
new_instance = Instance.objects.get_or_create(
compute=instance.compute,
uuid=new_uuid,
defaults={"name": clone_data["name"]},
)[0]
if new_instance.name != clone_data["name"]:
new_instance.name = clone_data["name"]
new_instance.save(update_fields=["name"])
UserInstance.objects.get_or_create(
instance_id=new_instance.id,
user_id=request.user.id,
defaults={"is_delete": True, "is_change": True, "is_vnc": True},
)
msg = _("Create a clone of '%(instance_name)s'") % {
"instance_name": instance.name
}
messages.success(request, msg)
addlogmsg(
request.user.username, instance.compute.name, new_instance.name, msg
)
if app_settings.CLONE_INSTANCE_AUTO_MIGRATE == "True":
new_compute = Compute.objects.order_by("?").first()
utils.migrate_instance(
new_compute, new_instance, request.user, xml_del=True, offline=True
)
return redirect(reverse("instances:instance", args=[new_instance.id]))
except Exception as e:
messages.error(request, e)
return redirect(request.META.get("HTTP_REFERER") + "#clone")
@require_POST
@serialize_instance_mutation
def update_console(request, pk):
instance = get_instance(request.user, pk, perm_type="change")
if utils.can_manage_console(request.user, instance):
form = ConsoleForm(request.POST or None)
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"]
if 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 "type" in form.changed_data:
instance.proxy.set_console_type(form.cleaned_data["type"])
msg = _("Set VNC type")
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
)
return get_safe_redirect(
request, default=reverse("instances:instance", args=[instance.id]) + "#vncsettings"
)
@require_POST
@serialize_instance_mutation
def change_options(request, pk):
instance = get_instance(request.user, pk, perm_type="change")
try:
userinstance = instance.userinstance_set.get(user=request.user)
except Exception:
userinstance = UserInstance(is_change=False)
if request.user.is_superuser or userinstance.is_change:
# Only superusers and staff may (un)mark templates (ROADMAP S-22). 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:
instance.is_template = bool(request.POST.get("is_template", False))
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
conn = wvmInstances(
instance.compute.hostname,
instance.compute.login,
instance.compute.password,
instance.compute.type,
)
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",
)
response.writelines("[virt-viewer]\n")
response.writelines("type=" + conn.graphics_type(instance.name) + "\n")
if conn.graphics_listen(instance.name) == "0.0.0.0":
response.writelines("host=" + conn.host + "\n")
else:
response.writelines("host=" + conn.graphics_listen(instance.name) + "\n")
response.writelines("port=" + conn.graphics_port(instance.name) + "\n")
response.writelines("title=" + conn.domain_name(instance.name) + "\n")
response.writelines("password=" + conn.graphics_passwd(instance.name) + "\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.refresh_instance_database(compute)
instance = compute.instance_set.get(name=name)
return redirect(reverse("instances:instance", args=[instance.id]))
return render(request, "create_instance_w1.html", locals())
@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)
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_graphics = app_settings.QEMU_CONSOLE_DEFAULT_TYPE
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)
hv_supports_uefi = conn.supports_uefi_xml(dom_caps["loader_enums"])
# 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, dom_caps["loaders"])
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 libvirtError(_("You haven't defined any storage pools"))
if not networks:
raise libvirtError(_("You haven't defined any network pools"))
if request.method == "POST":
if "create" in request.POST:
firmware = dict()
volume_list = list()
is_disk_created = False
clone_path = ""
form = NewVMForm(request.POST)
if form.is_valid():
data = form.cleaned_data
if data["meta_prealloc"]:
meta_prealloc = True
if instances:
if data["name"] in instances:
raise libvirtError(
_("A virtual machine with this name already exists")
)
if Instance.objects.filter(name__exact=data["name"]):
raise libvirtError(
_(
"There is an instance with same name. Remove it and try again!"
)
)
if data["hdd_size"]:
if not data["mac"]:
raise libvirtError(
_("No Virtual Machine MAC has been entered")
)
else:
path = conn.create_volume(
data["storage"],
data["name"],
data["hdd_size"],
default_disk_format,
meta_prealloc,
default_disk_owner_uid,
default_disk_owner_gid,
)
volume = dict()
volume["device"] = "disk"
volume["path"] = path
volume["type"] = conn.get_volume_format_type(path)
volume["cache_mode"] = data["cache_mode"]
volume["bus"] = default_bus
if volume["bus"] == "scsi":
volume["scsi_model"] = default_scsi_disk_model
volume["discard_mode"] = default_discard
volume["detect_zeroes_mode"] = default_zeroes
volume["io_mode"] = default_io
volume_list.append(volume)
is_disk_created = True
elif data["template"]:
templ_path = conn.get_volume_path(data["template"])
dest_vol = conn.get_volume_path(
data["name"] + ".img", data["storage"]
)
if dest_vol:
raise libvirtError(
_(
"Image has already exist. Please check volumes or change instance name"
)
)
else:
clone_path = conn.clone_from_template(
data["name"],
templ_path,
data["storage"],
meta_prealloc,
default_disk_owner_uid,
default_disk_owner_gid,
)
volume = dict()
volume["path"] = clone_path
volume["type"] = conn.get_volume_format_type(clone_path)
volume["device"] = "disk"
volume["cache_mode"] = data["cache_mode"]
volume["bus"] = default_bus
if volume["bus"] == "scsi":
volume["scsi_model"] = default_scsi_disk_model
volume["discard_mode"] = default_discard
volume["detect_zeroes_mode"] = default_zeroes
volume["io_mode"] = default_io
volume_list.append(volume)
is_disk_created = True
else:
if not data["images"]:
raise libvirtError(
_("First you need to create or select an image")
)
else:
for idx, vol in enumerate(data["images"].split(",")):
path = conn.get_volume_path(vol)
volume = dict()
volume["path"] = path
volume["type"] = conn.get_volume_format_type(path)
volume["device"] = request.POST.get(
"device" + str(idx), ""
)
volume["bus"] = request.POST.get(
"bus" + str(idx), ""
)
if volume["bus"] == "scsi":
volume["scsi_model"] = default_scsi_disk_model
volume["cache_mode"] = data["cache_mode"]
volume["discard_mode"] = default_discard
volume["detect_zeroes_mode"] = default_zeroes
volume["io_mode"] = default_io
volume_list.append(volume)
if data["cache_mode"] not in conn.get_cache_modes():
error_msg = _("Invalid cache mode")
raise libvirtError
if "UEFI" in data["firmware"]:
firmware["loader"] = data["firmware"].split(":")[1].strip()
firmware["secure"] = "no"
firmware["readonly"] = "yes"
firmware["type"] = "pflash"
if "secboot" in firmware["loader"] and 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"
uuid = util.randomUUID()
try:
with libvirt_compute_lock(compute):
conn.create_instance(
name=data["name"],
memory=data["memory"],
vcpu=data["vcpu"],
vcpu_mode=data["vcpu_mode"],
uuid=uuid,
arch=arch,
machine=machine,
firmware=firmware,
volumes=volume_list,
networks=data["networks"],
virtio=data["virtio"],
listener_addr=data["listener_addr"],
nwfilter=data["nwfilter"],
net_model=data["net_model"],
graphics=data["graphics"],
video=data["video"],
console_pass=data["console_pass"],
mac=data["mac"],
qemu_ga=data["qemu_ga"],
add_cdrom=data["add_cdrom"],
add_input=data["add_input"],
)
create_instance = Instance.objects.get_or_create(
compute_id=compute_id, uuid=uuid, defaults={"name": data["name"]}
)[0]
if create_instance.name != data["name"]:
create_instance.name = data["name"]
create_instance.save(update_fields=["name"])
msg = _("Instance is created")
messages.success(request, msg)
addlogmsg(
request.user.username,
create_instance.compute.name,
create_instance.name,
msg,
)
return redirect(
reverse("instances:instance", args=[create_instance.id])
)
except libvirtError as lib_err:
if data["hdd_size"] or len(volume_list) > 0:
if is_disk_created:
for vol in volume_list:
conn.delete_volume(vol["path"])
messages.error(request, lib_err)
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:flavors"))
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:flavors"))
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:flavors"))
return render(
request,
"common/confirm_delete.html",
{"object": flavor},
)