virt-manager/virtinst/osdict.py

639 lines
22 KiB
Python

#
# List of OS Specific data
#
# Copyright 2006-2008, 2013-2014 Red Hat, Inc.
#
# This work is licensed under the GNU GPLv2 or later.
# See the COPYING file in the top-level directory.
import datetime
import os
import re
from gi.repository import Libosinfo
from . import xmlutil
from .logger import log
def _media_create_from_location(location):
if not hasattr(Libosinfo.Media, "create_from_location_with_flags"):
return Libosinfo.Media.create_from_location( # pragma: no cover
location, None)
# We prefer this API, because by default it will not
# reject non-bootable media, like debian s390x
# pylint: disable=no-member
return Libosinfo.Media.create_from_location_with_flags(location, None, 0)
class _OsinfoIter:
"""
Helper to turn osinfo style get_length/get_nth lists into python
iterables
"""
def __init__(self, listobj):
self.current = 0
self.listobj = listobj
self.high = -1
if self.listobj:
self.high = self.listobj.get_length() - 1
def __iter__(self):
return self
def __next__(self):
if self.current > self.high:
raise StopIteration
ret = self.listobj.get_nth(self.current)
self.current += 1
return ret
class _OSDB(object):
"""
Entry point for the public API
"""
def __init__(self):
self.__os_loader = None
self.__os_generic = None
#################
# Internal APIs #
#################
@property
def _os_generic(self):
if not self.__os_generic:
# Add our custom generic variant
o = Libosinfo.Os()
o.set_param("short-id", "generic")
o.set_param("name",
_("Generic or unknown OS. Usage is not recommended."))
self.__os_generic = _OsVariant(o)
return self.__os_generic
@property
def _os_loader(self):
if not self.__os_loader:
loader = Libosinfo.Loader()
loader.process_default_path()
self.__os_loader = loader
return self.__os_loader
@property
def _os_db(self):
return self._os_loader.get_db()
###############
# Public APIs #
###############
def lookup_os_by_full_id(self, full_id, raise_error=False):
osobj = self._os_db.get_os(full_id)
if osobj is None:
if raise_error:
raise ValueError(_("Unknown libosinfo ID '%s'") % full_id)
return None
return _OsVariant(osobj)
def lookup_os(self, key, raise_error=False):
if key == self._os_generic.name:
return self._os_generic
flt = Libosinfo.Filter()
flt.add_constraint(Libosinfo.PRODUCT_PROP_SHORT_ID,
key)
oslist = self._os_db.get_os_list().new_filtered(flt).get_elements()
if len(oslist) == 0:
if raise_error:
raise ValueError(_("Unknown OS name '%s'. "
"See `--osinfo list` for valid values.") % key)
return None
return _OsVariant(oslist[0])
def guess_os_by_iso(self, location):
try:
media = _media_create_from_location(location)
except Exception as e:
log.debug("Error creating libosinfo media object: %s", str(e))
return None
if not self._os_db.identify_media(media):
return None
return media.get_os().get_short_id(), _OsMedia(media)
def guess_os_by_tree(self, location):
if location.startswith("/"):
location = "file://" + location
if xmlutil.in_testsuite() and not location.startswith("file:"):
# We have mock network tests, but we don't want to pass the
# fake URL to libosinfo because it slows down the testcase
return None
try:
tree = Libosinfo.Tree.create_from_location(location, None)
except Exception as e:
log.debug("Error creating libosinfo tree object for "
"location=%s : %s", location, str(e))
return None
if hasattr(self._os_db, "identify_tree"):
# osinfo_db_identify_tree is part of libosinfo 1.6.0
if not self._os_db.identify_tree(tree):
return None # pragma: no cover
return tree.get_os().get_short_id(), _OsTree(tree)
else: # pragma: no cover
osobj, treeobj = self._os_db.guess_os_from_tree(tree)
if not osobj:
return None # pragma: no cover
return osobj.get_short_id(), _OsTree(treeobj)
def list_os(self, sortkey="name"):
"""
List all OSes in the DB, sorting by the passes _OsVariant attribute
"""
oslist = [_OsVariant(osent) for osent in
self._os_db.get_os_list().get_elements()]
oslist.append(self._os_generic)
# human/natural sort, but with reverse sorted numbers
def to_int(text):
return (int(text) * -1) if text.isdigit() else text.lower()
def alphanum_key(obj):
val = getattr(obj, sortkey)
return [to_int(c) for c in re.split('([0-9]+)', val)]
return list(sorted(oslist, key=alphanum_key))
OSDB = _OSDB()
#####################
# OsResources class #
#####################
class _OsResources:
def __init__(self, minimum, recommended):
self._minimum = self._convert_to_dict(minimum)
self._recommended = self._convert_to_dict(recommended)
def _convert_to_dict(self, resources):
"""
Convert an OsResources object to a dictionary for easier
lookups. Layout is: {arch: {strkey: value}}
"""
ret = {}
for r in _OsinfoIter(resources):
vals = {}
vals["ram"] = r.get_ram()
vals["n-cpus"] = r.get_n_cpus()
vals["storage"] = r.get_storage()
ret[r.get_architecture()] = vals
return ret
def _get_key(self, resources, key, arch):
for checkarch in [arch, "all"]:
val = resources.get(checkarch, {}).get(key, -1)
if val != -1:
return val
def _get_minimum_key(self, key, arch):
val = self._get_key(self._minimum, key, arch)
if val and val > 0:
return val
def _get_recommended_key(self, key, arch):
val = self._get_key(self._recommended, key, arch)
if val and val > 0:
return val
# If we are looking for a recommended value, but the OS
# DB only has minimum resources tracked, double the minimum
# value as an approximation at a 'recommended' value
val = self._get_minimum_key(key, arch)
if val:
log.debug("No recommended value found for key='%s', "
"using minimum=%s * 2", key, val)
return val * 2
return None
def get_minimum_ram(self, arch):
return self._get_minimum_key("ram", arch)
def get_recommended_ram(self, arch):
return self._get_recommended_key("ram", arch)
def get_recommended_ncpus(self, arch):
return self._get_recommended_key("n-cpus", arch)
def get_recommended_storage(self, arch):
return self._get_recommended_key("storage", arch)
#####################
# OsVariant classes #
#####################
class _OsVariant(object):
def __init__(self, o):
self._os = o
self._short_ids = [self._os.get_short_id()]
if hasattr(self._os, "get_short_id_list"):
self._short_ids = self._os.get_short_id_list()
self.name = self._short_ids[0]
self.all_names = list(sorted(set(self._short_ids)))
self._family = self._os.get_family()
self.full_id = self._os.get_id()
self.label = self._os.get_name()
self.codename = self._os.get_codename() or ""
self.distro = self._os.get_distro() or ""
self.version = self._os.get_version()
self.eol = self._get_eol()
def __repr__(self):
return "<%s name=%s>" % (self.__class__.__name__, self.name)
########################
# Internal helper APIs #
########################
def _is_related_to(self, related_os_list, osobj=None,
check_derives=True, check_upgrades=True, check_clones=True):
osobj = osobj or self._os
if osobj.get_short_id() in related_os_list:
return True
check_list = []
def _extend(newl):
for obj in newl:
if obj not in check_list:
check_list.append(obj)
if check_derives:
_extend(osobj.get_related(
Libosinfo.ProductRelationship.DERIVES_FROM).get_elements())
if check_clones:
_extend(osobj.get_related(
Libosinfo.ProductRelationship.CLONES).get_elements())
if check_upgrades:
_extend(osobj.get_related(
Libosinfo.ProductRelationship.UPGRADES).get_elements())
for checkobj in check_list:
if (checkobj.get_short_id() in related_os_list or
self._is_related_to(related_os_list, osobj=checkobj,
check_upgrades=check_upgrades,
check_derives=check_derives,
check_clones=check_clones)):
return True
return False
def _get_all_devices(self):
return list(_OsinfoIter(self._os.get_all_devices()))
def _device_filter(self, devids=None, cls=None, extra_devs=None):
ret = []
devids = devids or []
for dev in self._get_all_devices():
if devids and dev.get_id() not in devids:
continue
if cls and not re.match(cls, dev.get_class()):
continue
ret.append(dev.get_name())
extra_devs = extra_devs or []
for dev in extra_devs:
if dev.get_id() not in devids:
continue
ret.append(dev.get_name())
return ret
###############
# Cached APIs #
###############
def _get_eol(self):
eol = self._os.get_eol_date()
rel = self._os.get_release_date()
# We can use os.get_release_status() & osinfo.ReleaseStatus.ROLLING
# if we require libosinfo >= 1.4.0.
release_status = self._os.get_param_value(
Libosinfo.OS_PROP_RELEASE_STATUS) or None
def _glib_to_datetime(glibdate):
date = "%s-%s" % (glibdate.get_year(), glibdate.get_day_of_year())
return datetime.datetime.strptime(date, "%Y-%j")
now = datetime.datetime.today()
if eol is not None:
return now > _glib_to_datetime(eol)
# Rolling distributions are never EOL.
if release_status == "rolling":
return False
# If no EOL is present, assume EOL if release was > 10 years ago
if rel is not None:
rel5 = _glib_to_datetime(rel) + datetime.timedelta(days=365 * 10)
return now > rel5
return False
###############
# Public APIs #
###############
def get_handle(self):
return self._os
def is_generic(self):
return self.name == "generic"
def is_linux_generic(self):
return re.match(r"linux\d\d\d\d", self.name)
def is_windows(self):
return self._family in ['win9x', 'winnt', 'win16']
def get_clock(self):
if self.is_windows() or self._family in ['solaris']:
return "localtime"
return "utc"
def supported_netmodels(self):
return self._device_filter(cls="net")
def supports_virtiodisk(self, extra_devs=None):
# virtio-block and virtio1.0-block
devids = ["http://pcisig.com/pci/1af4/1001",
"http://pcisig.com/pci/1af4/1042"]
return bool(self._device_filter(devids=devids, extra_devs=extra_devs))
def supports_virtioscsi(self, extra_devs=None):
# virtio-scsi and virtio1.0-scsi
devids = ["http://pcisig.com/pci/1af4/1004",
"http://pcisig.com/pci/1af4/1048"]
return bool(self._device_filter(devids=devids, extra_devs=extra_devs))
def supports_virtionet(self, extra_devs=None):
# virtio-net and virtio1.0-net
devids = ["http://pcisig.com/pci/1af4/1000",
"http://pcisig.com/pci/1af4/1041"]
return bool(self._device_filter(devids=devids, extra_devs=extra_devs))
def supports_virtiorng(self, extra_devs=None):
# virtio-rng and virtio1.0-rng
devids = ["http://pcisig.com/pci/1af4/1005",
"http://pcisig.com/pci/1af4/1044"]
return bool(self._device_filter(devids=devids, extra_devs=extra_devs))
def supports_virtiogpu(self, extra_devs=None):
# virtio1.0-gpu and virtio1.0
devids = ["http://pcisig.com/pci/1af4/1050"]
return bool(self._device_filter(devids=devids, extra_devs=extra_devs))
def supports_virtioballoon(self, extra_devs=None):
# virtio-balloon and virtio1.0-balloon
devids = ["http://pcisig.com/pci/1af4/1002",
"http://pcisig.com/pci/1af4/1045"]
return bool(self._device_filter(devids=devids, extra_devs=extra_devs))
def supports_virtioserial(self, extra_devs=None):
devids = ["http://pcisig.com/pci/1af4/1003",
"http://pcisig.com/pci/1af4/1043"]
if self._device_filter(devids=devids, extra_devs=extra_devs):
return True
# osinfo data was wrong for RHEL/centos here until Oct 2018
# Remove this hack after 6 months or so
return self._is_related_to("rhel6.0")
def supports_virtioinput(self, extra_devs=None):
# virtio1.0-input
devids = ["http://pcisig.com/pci/1af4/1052"]
return bool(self._device_filter(devids=devids, extra_devs=extra_devs))
def supports_usb3(self, extra_devs=None):
# qemu-xhci
devids = ["http://pcisig.com/pci/1b36/0004"]
return bool(self._device_filter(devids=devids, extra_devs=extra_devs))
def supports_virtio1(self, extra_devs=None):
# Use virtio1.0-net device as a proxy for virtio1.0 as a whole
devids = ["http://pcisig.com/pci/1af4/1041"]
return bool(self._device_filter(devids=devids, extra_devs=extra_devs))
def supports_chipset_q35(self, extra_devs=None):
# For our purposes, check for the union of q35 + virtio1.0 support
if (self.supports_virtionet(extra_devs=extra_devs) and
not self.supports_virtio1(extra_devs=extra_devs)):
return False
devids = ["http://qemu.org/chipset/x86/q35"]
return bool(self._device_filter(devids=devids, extra_devs=extra_devs))
def _get_firmware_list(self):
if hasattr(self._os, "get_complete_firmware_list"): # pragma: no cover
return self._os.get_complete_firmware_list().get_elements()
return [] # pragma: no cover
def _supports_firmware_type(self, name, arch, default):
firmwares = self._get_firmware_list()
for firmware in firmwares: # pragma: no cover
if firmware.get_architecture() != arch:
continue
if firmware.get_firmware_type() == name:
return firmware.is_supported()
return default
def requires_firmware_efi(self, arch):
ret = False
try:
supports_efi = self._supports_firmware_type("efi", arch, False)
supports_bios = self._supports_firmware_type("bios", arch, True)
ret = supports_efi and not supports_bios
except Exception: # pragma: no cover
log.debug("Error checking osinfo firmware support", exc_info=True)
if self.name == "win11": # pragma: no cover
# 2022-03 the libosinfo APIs for firmware haven't landed, and
# there's no osinfo-db entry for win11. But we know win11 requires
# UEFI. Hardcode it for now, so the next virt-install release has
# a better chance of doing the right thing for win11 when
# it pops up in a osinfo-db release.
ret = True
return ret
def get_recommended_resources(self):
minimum = self._os.get_minimum_resources()
recommended = self._os.get_recommended_resources()
return _OsResources(minimum, recommended)
def get_network_install_required_ram(self, guest):
if hasattr(self._os, "get_network_install_resources"):
resources = self._os.get_network_install_resources()
for r in _OsinfoIter(resources):
arch = r.get_architecture()
if arch == guest.os.arch or arch == "all":
return r.get_ram()
def get_kernel_url_arg(self):
"""
Kernel argument name the distro's installer uses to reference
a network source, possibly bypassing some installer prompts
"""
# Let's ask the OS for its kernel argument for the source
if hasattr(self._os, "get_kernel_url_argument"):
osarg = self._os.get_kernel_url_argument()
if osarg is not None:
return osarg
# SUSE distros
if self.distro in ["caasp", "sle", "sled", "sles", "opensuse"]:
return "install"
if self.distro not in ["centos", "rhel", "fedora"]:
return None
# Default for RH distros, in case libosinfo data isn't complete
return "inst.repo" # pragma: no cover
def _get_generic_location(self, treelist, arch, profile):
if not hasattr(Libosinfo.Tree, "get_os_variants"): # pragma: no cover
for tree in treelist:
if tree.get_architecture() == arch:
return tree.get_url()
return None
fallback_tree = None
if profile == "jeos":
profile = "Server"
elif profile == "desktop":
profile = "Workstation"
elif not profile:
profile = "Everything"
for tree in treelist:
if tree.get_architecture() != arch:
continue
variant_list = tree.get_os_variants()
fallback_tree = tree
for variant in _OsinfoIter(variant_list):
if profile in variant.get_name():
return tree.get_url()
if fallback_tree:
return fallback_tree.get_url()
return None
def get_location(self, arch, profile=None):
treelist = list(_OsinfoIter(self._os.get_tree_list()))
if not treelist:
raise RuntimeError(
_("OS '%s' does not have a URL location") % self.name)
# Some distros have more than one URL for a specific architecture,
# which is the case for Fedora and different variants (Server,
# Workstation). Later on, we'll have to differentiate that and return
# the right one. However, for now, let's just rely on returning the
# most generic tree possible.
location = self._get_generic_location(treelist, arch, profile)
if location:
return location
raise RuntimeError(
_("OS '%(osname)s' does not have a URL location "
"for the architecture '%(archname)s'") %
{"osname": self.name, "archname": arch})
def get_install_script_list(self):
return list(_OsinfoIter(self._os.get_install_script_list()))
def _get_installable_drivers(self, arch):
installable_drivers = []
device_drivers = list(_OsinfoIter(self._os.get_device_drivers()))
for device_driver in device_drivers:
if arch != "all" and device_driver.get_architecture() != arch:
continue
installable_drivers.append(device_driver)
return installable_drivers
def _get_pre_installable_drivers(self, arch):
installable_drivers = self._get_installable_drivers(arch)
pre_inst_drivers = []
for driver in installable_drivers:
if driver.get_pre_installable():
pre_inst_drivers.append(driver)
return pre_inst_drivers
def _get_drivers_location(self, drivers):
locations = []
for driver in drivers:
filenames = driver.get_files()
for filename in filenames:
location = os.path.join(driver.get_location(), filename)
locations.append(location)
return locations
def get_pre_installable_drivers_location(self, arch):
pre_inst_drivers = self._get_pre_installable_drivers(arch)
return self._get_drivers_location(pre_inst_drivers)
def get_pre_installable_devices(self, arch):
drivers = self._get_pre_installable_drivers(arch)
devices = []
for driver in drivers:
devices += list(_OsinfoIter(driver.get_devices()))
return devices
def supports_unattended_drivers(self, arch):
if self._get_pre_installable_drivers(arch):
return True
return False
class _OsMedia(object):
def __init__(self, osinfo_media):
self._media = osinfo_media
def get_kernel_path(self):
return self._media.get_kernel_path()
def get_initrd_path(self):
return self._media.get_initrd_path()
def supports_installer_script(self):
return self._media.supports_installer_script()
def is_netinst(self):
variants = list(_OsinfoIter(self._media.get_os_variants()))
for variant in variants:
if "netinst" in variant.get_id():
return True
return False # pragma: no cover
def get_install_script_list(self):
return list(_OsinfoIter(self._media.get_install_script_list()))
def get_osinfo_media(self):
return self._media
class _OsTree(object):
def __init__(self, osinfo_tree):
self._tree = osinfo_tree
def get_osinfo_tree(self):
return self._tree