mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 19:26:48 +00:00
Creating each option on a RHEL qemu-kvm host showed:
- CD-ROM: the create page offered every disk bus, but the empty CD-ROM
of a new VM works only on ide, sata and scsi. fdc and virtio failed
("Invalid bus type", "does not support ejectable media"), usb failed
with a device named 'Nonea' because vol_dev_type() had no prefix for
usb, and after that libvirt refuses an empty usb disk. Only ide, sata
and scsi are offered now, and another value is refused before any
storage is allocated. vol_dev_type() names usb disks sdX, which also
fixes data volumes on the usb bus.
- USB controller: any input device added a bare <controller
type='usb'/>, which libvirt turns into piix3-uhci (USB 1.1) on q35
instead of its default qemu-xhci. The bare controller is gone; libvirt
adds the machine's default for USB inputs.
- MACs: with more networks than MAC addresses, create_instance indexed
past the list and the request failed with a 500. NICs without a given
MAC now get one from libvirt. MAC addresses are validated (format, not
more than networks) by the page and the API before anything is
created; they went into the XML unchecked.
Unit tests cover the refused values, the MAC rule and the usb prefix.
Live tests create a VM with every offered CD-ROM bus and start it,
check the USB controller with a USB input device and create a VM with
two networks and one MAC.
213 lines
5.5 KiB
Python
213 lines
5.5 KiB
Python
import random
|
|
import secrets
|
|
import re
|
|
import string
|
|
|
|
import libvirt
|
|
import lxml.etree as etree
|
|
|
|
from django.conf import settings
|
|
|
|
|
|
|
|
class OperationError(libvirt.libvirtError):
|
|
"""An error with WebVirtCloud's own message.
|
|
|
|
libvirtError(msg) reports the thread's last libvirt error instead of msg
|
|
when there is one, so an earlier, unrelated failure would be shown.
|
|
"""
|
|
|
|
def __init__(self, msg):
|
|
Exception.__init__(self, msg)
|
|
self.err = None
|
|
|
|
|
|
def is_kvm_available(xml):
|
|
kvm_domains = get_xml_path(xml, "//domain/@type='kvm'")
|
|
return kvm_domains > 0
|
|
|
|
|
|
def randomMAC():
|
|
"""Generate a random MAC address."""
|
|
# qemu MAC
|
|
mac = settings.MAC_OUI + ":%02x:%02x:%02x" % (
|
|
random.randint(0x00, 0xFF),
|
|
random.randint(0x00, 0xFF),
|
|
random.randint(0x00, 0xFF),
|
|
)
|
|
return mac
|
|
|
|
|
|
def randomUUID():
|
|
"""Generate a random UUID."""
|
|
u = [secrets.randbelow(256) for _ in range(16)]
|
|
u[6] = u[6] & 15 | 4 << 4
|
|
u[8] = u[8] & 63 | 2 << 6
|
|
return "-".join(["%02x" * 4, "%02x" * 2, "%02x" * 2, "%02x" * 2, "%02x" * 6]) % tuple(u)
|
|
|
|
|
|
def randomPasswd(length=12, alphabet=string.ascii_letters + string.digits):
|
|
"""Generate a random password"""
|
|
return "".join([secrets.choice(alphabet) for _ in range(length)])
|
|
|
|
|
|
def get_max_vcpus(conn, guest_type=None):
|
|
"""@param conn: libvirt connection to poll for max possible vcpus
|
|
@param guest_type: optional guest type (kvm, etc.)"""
|
|
if type is None:
|
|
guest_type = conn.getType()
|
|
try:
|
|
m = conn.getMaxVcpus(guest_type.lower())
|
|
except libvirt.libvirtError:
|
|
m = 32
|
|
return m
|
|
|
|
|
|
def xml_escape(xml_str):
|
|
"""Replaces chars ' " < > & with xml safe counterparts"""
|
|
if xml_str is None:
|
|
return None
|
|
|
|
xml_str = xml_str.replace("&", "&")
|
|
xml_str = xml_str.replace("'", "'")
|
|
xml_str = xml_str.replace('"', """)
|
|
xml_str = xml_str.replace("<", "<")
|
|
xml_str = xml_str.replace(">", ">")
|
|
return xml_str
|
|
|
|
|
|
def compareMAC(p, q):
|
|
"""Compare two MAC addresses"""
|
|
pa = p.split(":")
|
|
qa = q.split(":")
|
|
|
|
if len(pa) != len(qa):
|
|
if p > q:
|
|
return 1
|
|
else:
|
|
return -1
|
|
|
|
for i in range(len(pa)):
|
|
n = int(pa[i], 0x10) - int(qa[i], 0x10)
|
|
if n > 0:
|
|
return 1
|
|
elif n < 0:
|
|
return -1
|
|
return 0
|
|
|
|
|
|
def get_xml_path(xml, path=None, func=None):
|
|
"""
|
|
Return the content from the passed xml xpath, or return the result
|
|
of a passed function (receives xpathContext as its only arg)
|
|
"""
|
|
doc = None
|
|
result = None
|
|
|
|
doc = etree.fromstring(xml)
|
|
if path:
|
|
result = get_xpath(doc, path)
|
|
elif func:
|
|
result = func(doc)
|
|
|
|
else:
|
|
raise ValueError("'path' or 'func' is required.")
|
|
return result
|
|
|
|
|
|
def get_xpath(doc, path):
|
|
result = None
|
|
|
|
ret = doc.xpath(path)
|
|
if ret is not None:
|
|
if isinstance(ret, list):
|
|
if len(ret) >= 1:
|
|
result = ret[0].text if hasattr(ret[0], "text") else ret[0]
|
|
else:
|
|
result = ret
|
|
|
|
return result
|
|
|
|
|
|
def pretty_mem(val):
|
|
val = int(val)
|
|
if val > (10 * 1024 * 1024):
|
|
return "%2.2f GB" % (val / (1024.0 * 1024.0))
|
|
else:
|
|
return "%2.0f MB" % (val / 1024.0)
|
|
|
|
|
|
def pretty_bytes(val):
|
|
val = int(val)
|
|
if val > (1024 * 1024 * 1024):
|
|
return "%2.2f GB" % (val / (1024.0 * 1024.0 * 1024.0))
|
|
else:
|
|
return "%2.2f MB" % (val / (1024.0 * 1024.0))
|
|
|
|
|
|
def validate_uuid(val):
|
|
if not isinstance(val, str):
|
|
raise ValueError("UUID must be a string.")
|
|
|
|
form = re.match("[a-fA-F0-9]{8}[-]([a-fA-F0-9]{4}[-]){3}[a-fA-F0-9]{12}$", val)
|
|
if form is None:
|
|
form = re.match("[a-fA-F0-9]{32}$", val)
|
|
if form is None:
|
|
raise ValueError(
|
|
"UUID must be a 32-digit hexadecimal number. It may take "
|
|
"the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx or may "
|
|
"omit hyphens altogether."
|
|
)
|
|
|
|
else: # UUID had no dashes, so add them in
|
|
val = val[0:8] + "-" + val[8:12] + "-" + val[12:16] + "-" + val[16:20] + "-" + val[20:32]
|
|
return val
|
|
|
|
|
|
def validate_macaddr(val):
|
|
if val is None:
|
|
return
|
|
|
|
if not (isinstance(val, str) or isinstance(val, str)):
|
|
raise ValueError("MAC address must be a string.")
|
|
|
|
form = re.match("^([0-9a-fA-F]{1,2}:){5}[0-9a-fA-F]{1,2}$", val)
|
|
if form is None:
|
|
raise ValueError("MAC address must be of the format AA:BB:CC:DD:EE:FF, was {}".format(val))
|
|
|
|
|
|
# Mapping of UEFI binary names to their associated architectures. We
|
|
# only use this info to do things automagically for the user, it shouldn't
|
|
# validate anything the user explicitly enters.
|
|
UEFI_ARCH_PATTERNS = {
|
|
"i686": [
|
|
r".*ovmf-ia32.*", # fedora, gerd's firmware repo
|
|
],
|
|
"x86_64": [
|
|
r".*OVMF_CODE\.fd", # RHEL
|
|
r".*ovmf-x64/OVMF.*\.fd", # gerd's firmware repo
|
|
r".*ovmf-x86_64-.*", # SUSE
|
|
r".*ovmf.*",
|
|
".*OVMF.*", # generic attempt at a catchall
|
|
],
|
|
"aarch64": [
|
|
r".*AAVMF_CODE\.fd", # RHEL
|
|
r".*aarch64/QEMU_EFI.*", # gerd's firmware repo
|
|
r".*aarch64.*", # generic attempt at a catchall
|
|
],
|
|
"armv7l": [
|
|
r".*arm/QEMU_EFI.*", # fedora, gerd's firmware repo
|
|
],
|
|
}
|
|
|
|
|
|
def vol_dev_type(vol_bus):
|
|
if vol_bus == "ide":
|
|
return "hd"
|
|
elif vol_bus == "fdc":
|
|
return "fd"
|
|
elif vol_bus == "virtio":
|
|
return "vd"
|
|
elif vol_bus in ["scsi", "sata", "usb"]:
|
|
return "sd"
|
|
|