mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 11:16:47 +00:00
A job stored its error and its result's warning as English text, much of it built with the values in it (volume paths, a disk, libvirt's error), so no catalog could translate it, and the job pages and panel showed English whatever the language. A job now stores them as messages: our English text, marked for translation, and its values; libvirt's own text is a message without values and is shown as it is. The jobs API and pages put them in the reader's language. OperationError keeps its text and values apart for the same reason, so a refusal raised while a job is submitted is shown translated too. Messages one after another still read as sentences. Errors stored before are kept as one message each, shown as they were.
226 lines
6.3 KiB
Python
226 lines
6.3 KiB
Python
import ipaddress
|
|
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: text, with %(name)s for each
|
|
of values. str() is the English message; a page or a job shows text
|
|
translated, with the values (operations.utils.said).
|
|
|
|
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, text, **values):
|
|
Exception.__init__(self, text % values if values else text)
|
|
self.text, self.values = str(text), values
|
|
self.err = None
|
|
|
|
|
|
class ConnectionFailed(OperationError):
|
|
"""The host could not be connected to; the message may name it and how it
|
|
is reached."""
|
|
|
|
|
|
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 parse_xml(xml):
|
|
"""XML a user entered, read for the panel's checks: its entities not
|
|
expanded, nothing fetched and no DTD; libvirt gets the text itself."""
|
|
parser = etree.XMLParser(resolve_entities=False, no_network=True)
|
|
try:
|
|
tree = etree.fromstring(xml.encode(), parser) # bytes: the XML may declare its encoding
|
|
except etree.XMLSyntaxError as err:
|
|
raise OperationError(f"The XML is not valid: {err}") from None
|
|
if tree.getroottree().docinfo.internalDTD is not None:
|
|
raise OperationError("The XML may not declare a DTD")
|
|
return tree
|
|
|
|
|
|
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))
|
|
|
|
|
|
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"
|
|
|
|
|
|
def vol_driver_type(vol_format, pool_type=None):
|
|
"""The disk driver type for a volume of vol_format: raw for a volume
|
|
whose pool knows no image format of it (none, unknown, an ISO, a disk
|
|
pool's partition), as libvirt uses a disk without a driver type."""
|
|
if pool_type == "disk" or vol_format in (None, "", "none", "unknown", "iso"):
|
|
return "raw"
|
|
return vol_format
|
|
|
|
|
|
def is_loopback(address):
|
|
"""127.0.0.0/8, ::1 or localhost: reachable only on that host itself."""
|
|
try:
|
|
return ipaddress.ip_address(address).is_loopback
|
|
except ValueError:
|
|
return address == "localhost"
|