# # Copyright 2010, 2013 Red Hat, Inc. # # This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. from ..logger import log from ..xmlbuilder import XMLBuilder, XMLProperty, XMLChildProperty from .. import xmlutil ################################### # Misc child nodes for CPU domain # ################################### class _CPUTopology(XMLBuilder): """ Class for generating XML for child node . """ XML_NAME = "topology" _XML_PROP_ORDER = ["sockets", "dies", "cores", "threads"] sockets = XMLProperty("./@sockets", is_int=True) dies = XMLProperty("./@dies", is_int=True) cores = XMLProperty("./@cores", is_int=True) threads = XMLProperty("./@threads", is_int=True) # While `dies` is optional and defaults to 1 if omitted, # `sockets`, `cores`, and `threads` are mandatory. def set_defaults_from_vcpus(self, vcpus): # The hierarchy is sockets > dies > cores > threads. # # In real world silicon though it is rare to have # high socket/die counts, but common to have huge # core counts. # # Some OS will even refuse to use sockets over a # a certain count. # # Thus we prefer to expose cores to the guest rather # than sockets as the default for missing fields if not self.cores: self.cores = vcpus // self.total_vcpus() if not self.sockets: self.sockets = vcpus // self.total_vcpus() if not self.dies: self.dies = 1 if not self.threads: self.threads = vcpus // self.total_vcpus() if self.total_vcpus() != vcpus: raise ValueError( _( "Total CPUs implied by topology " "(sockets=%(sockets)d * dies=%(dies)d * cores=%(cores)d * threads=%(threads)d " "== %(total)d) does not match vCPU count %(vcpus)d" ) % { "sockets": self.sockets, "dies": self.dies, "cores": self.cores, "threads": self.threads, "total": self.total_vcpus(), "vcpus": vcpus, } ) return def total_vcpus(self): """ Determine the CPU count represented by topology """ return (self.sockets or 1) * (self.dies or 1) * (self.cores or 1) * (self.threads or 1) # Note: CPU cache is weird. The documentation implies that multiples instances # can be declared, one for each cache level one wishes to define. However, # libvirt doesn't accept more than one element, so it's implemented # with `is_single=True` for now (see actual CPU Domain below). class _CPUCache(XMLBuilder): """ Class for generating XML for child node . """ XML_NAME = "cache" _XML_PROP_ORDER = ["level", "mode"] level = XMLProperty("./@level", is_int=True) mode = XMLProperty("./@mode") class _CPUFeature(XMLBuilder): """ Class for generating XML for child nodes . """ XML_NAME = "feature" _XML_PROP_ORDER = ["policy", "name"] name = XMLProperty("./@name") policy = XMLProperty("./@policy") class _CPUMaxphysaddr(XMLBuilder): """ Class for generating XML for child node . """ XML_NAME = "maxphysaddr" _XML_PROP_ORDER = ["mode", "bits"] mode = XMLProperty("./@mode") bits = XMLProperty("./@bits", is_int=True) ############## # NUMA cells # ############## class _NUMACellSibling(XMLBuilder): """ Class for generating XML for child nodes , describing the distances to other NUMA cells. """ XML_NAME = "sibling" _XML_PROP_ORDER = ["id", "value"] id = XMLProperty("./@id", is_int=True) value = XMLProperty("./@value", is_int=True) class _NUMACellCache(XMLBuilder): """ Class for generating XML for child nodes , describing caches for NUMA cells. """ XML_NAME = "cache" _XML_PROP_ORDER = [ "level", "associativity", "policy", "size_value", "size_unit", "line_value", "line_unit", ] level = XMLProperty("./@level", is_int=True) associativity = XMLProperty("./@associativity") policy = XMLProperty("./@policy") size_value = XMLProperty("./size/@value", is_int=True) size_unit = XMLProperty("./size/@unit") line_value = XMLProperty("./line/@value", is_int=True) line_unit = XMLProperty("./line/@unit") class _NUMACell(XMLBuilder): """ Class for generating XML for child nodes XML, describing NUMA cells. """ XML_NAME = "cell" _XML_PROP_ORDER = ["id", "cpus", "memory", "unit", "memAccess", "discard", "siblings", "caches"] id = XMLProperty("./@id", is_int=True) cpus = XMLProperty("./@cpus") memory = XMLProperty("./@memory", is_int=True) unit = XMLProperty("./@unit") memAccess = XMLProperty("./@memAccess") discard = XMLProperty("./@discard", is_yesno=True) siblings = XMLChildProperty(_NUMACellSibling, relative_xpath="./distances") caches = XMLChildProperty(_NUMACellCache) ####################################### # Interconnections between NUMA cells # ####################################### class _NUMALatency(XMLBuilder): """ Class for generating XML for child nodes , describing latency between two NUMA memory nodes. """ XML_NAME = "latency" _XML_PROP_ORDER = ["initiator", "target", "cache", "type", "value", "unit"] # Note: While libvirt happily accepts XML with a unit= property, it is # currently ignored on nodes. initiator = XMLProperty("./@initiator", is_int=True) target = XMLProperty("./@target", is_int=True) cache = XMLProperty("./@cache", is_int=True) type = XMLProperty("./@type") value = XMLProperty("./@value", is_int=True) unit = XMLProperty("./@unit") class _NUMABandwidth(XMLBuilder): """ Class for generating XML for child nodes , describing bandwidth between two NUMA memory nodes. """ XML_NAME = "bandwidth" _XML_PROP_ORDER = ["initiator", "target", "cache", "type", "value", "unit"] # Note: The documentation only mentions nodes having a cache= # attribute, but and nodes are otherwise identical # and libvirt will happily accept XML with a cache= attribute on # nodes as well, so let's leave it here for now. initiator = XMLProperty("./@initiator", is_int=True) target = XMLProperty("./@target", is_int=True) cache = XMLProperty("./@cache", is_int=True) type = XMLProperty("./@type") value = XMLProperty("./@value", is_int=True) unit = XMLProperty("./@unit") ##################### # Actual CPU domain # ##################### class DomainCpu(XMLBuilder): """ Class for generating XML """ XML_NAME = "cpu" _XML_PROP_ORDER = [ "mode", "match", "check", "migratable", "model", "model_fallback", "model_vendor_id", "vendor", "topology", "cache", "features", "cells", "latencies", "bandwidths", "maxphysaddr", ] ################## # XML properties # ################## # Note: This is not a libvirt property. This is specific to the virt-* # tools and causes additional security features to be added to the VM. # See the security mitigation related functions below for more details. secure = True mode = XMLProperty("./@mode") match = XMLProperty("./@match") check = XMLProperty("./@check") migratable = XMLProperty("./@migratable", is_onoff=True) model = XMLProperty("./model") model_fallback = XMLProperty("./model/@fallback") model_vendor_id = XMLProperty("./model/@vendor_id") vendor = XMLProperty("./vendor") topology = XMLChildProperty(_CPUTopology, is_single=True) cache = XMLChildProperty(_CPUCache, is_single=True) features = XMLChildProperty(_CPUFeature) # NUMA related properties cells = XMLChildProperty(_NUMACell, relative_xpath="./numa") latencies = XMLChildProperty(_NUMALatency, relative_xpath="./numa/interconnects") bandwidths = XMLChildProperty(_NUMABandwidth, relative_xpath="./numa/interconnects") maxphysaddr = XMLChildProperty(_CPUMaxphysaddr, is_single=True) ############################# # Special CPU mode handling # ############################# special_mode_was_set = False # These values are exposed on the command line, so are stable API SPECIAL_MODE_HOST_MODEL_ONLY = "host-model-only" SPECIAL_MODE_HV_DEFAULT = "hv-default" SPECIAL_MODE_HOST_COPY = "host-copy" SPECIAL_MODE_HOST_MODEL = "host-model" SPECIAL_MODE_HOST_PASSTHROUGH = "host-passthrough" SPECIAL_MODE_MAXIMUM = "maximum" SPECIAL_MODE_CLEAR = "clear" SPECIAL_MODE_APP_DEFAULT = "default" SPECIAL_MODES = [ SPECIAL_MODE_HOST_MODEL_ONLY, SPECIAL_MODE_HV_DEFAULT, SPECIAL_MODE_HOST_COPY, SPECIAL_MODE_HOST_MODEL, SPECIAL_MODE_HOST_PASSTHROUGH, SPECIAL_MODE_MAXIMUM, SPECIAL_MODE_CLEAR, SPECIAL_MODE_APP_DEFAULT, ] def _should_use_maximum_cpu_mode(self, guest, domcaps): if ( domcaps.supports_maximum_cpu_mode() and guest.type == "qemu" and ( guest.os.is_x86() or guest.os.is_arm_machvirt() or guest.os.is_riscv_virt() or guest.os.is_loongarch64() ) ): return True return False def _get_app_default_mode(self, guest): # Depending on if libvirt+qemu is new enough, we prefer # host-passthrough, then host-model, and finally host-model-only # Emulated guests use maximum mode if available domcaps = guest.lookup_domcaps() if self._should_use_maximum_cpu_mode(guest, domcaps): return self.SPECIAL_MODE_MAXIMUM if domcaps.supports_safe_host_passthrough(): return self.SPECIAL_MODE_HOST_PASSTHROUGH log.debug("Safe host-passthrough is not available") if domcaps.supports_safe_host_model(): return self.SPECIAL_MODE_HOST_MODEL log.debug("Safe host-model is not available") return self.SPECIAL_MODE_HOST_MODEL_ONLY def set_special_mode(self, guest, val): if val == self.SPECIAL_MODE_APP_DEFAULT: val = self._get_app_default_mode(guest) log.debug("Using default cpu mode=%s", val) if ( val == self.SPECIAL_MODE_HOST_MODEL or val == self.SPECIAL_MODE_HOST_PASSTHROUGH or val == self.SPECIAL_MODE_MAXIMUM ): self.model = None self.vendor = None self.model_fallback = None self.migratable = None self.check = None for f in self.features: self.remove_child(f) self.mode = val elif val == self.SPECIAL_MODE_HV_DEFAULT or val == self.SPECIAL_MODE_CLEAR: self.clear() elif val == self.SPECIAL_MODE_HOST_MODEL_ONLY or val == self.SPECIAL_MODE_HOST_COPY: if val == self.SPECIAL_MODE_HOST_COPY: log.warning( "CPU mode=%s no longer supported, using mode=%s", val, self.SPECIAL_MODE_HOST_MODEL_ONLY, ) if self.conn.caps.host.cpu.model: self.clear() self.set_model(guest, self.conn.caps.host.cpu.model) else: raise xmlutil.DevError("unknown special cpu mode '%s'" % val) self.special_mode_was_set = True ######################## # Security mitigations # ######################## def _add_security_features(self, guest): domcaps = guest.lookup_domcaps() for feature in domcaps.get_cpu_security_features(): exists = False for f in self.features: if f.name == feature: exists = True break if not exists: self.add_feature(feature) def check_security_features(self, guest): """ Since 'secure' property is not exported into the domain XML we might need to refresh its state. """ domcaps = guest.lookup_domcaps() features = domcaps.get_cpu_security_features() if len(features) == 0: self.secure = False return guestFeatures = [f.name for f in self.features if f.policy == "require"] if self.model: if self.model.endswith("IBRS"): guestFeatures.append("spec-ctrl") if self.model.endswith("IBPB"): guestFeatures.append("ibpb") self.secure = set(features) <= set(guestFeatures) def _remove_security_features(self, guest): domcaps = guest.lookup_domcaps() for feature in domcaps.get_cpu_security_features(): for f in self.features: if f.name == feature and f.policy == "require": self.remove_child(f) break ########### # Helpers # ########### def set_model(self, guest, val): log.debug("setting cpu model %s", val) self.migratable = None if val: self.mode = "custom" if not self.match: self.match = "exact" if self.secure: self._add_security_features(guest) else: self._remove_security_features(guest) self.model = val def add_feature(self, name, policy="require"): feature = self.features.add_new() feature.name = name feature.policy = policy def vcpus_from_topology(self): """ Determine the CPU count represented by topology, or 1 if no topology is set """ return self.topology.total_vcpus() def has_topology(self): """ Return True if any topology info is set """ return bool(self.topology.get_xml()) def set_topology_defaults(self, vcpus, create=False): """ Fill in unset topology values, using the passed vcpus count. If @create is False, this will not set topology from scratch, just fill in missing topology values. If @create is True, this will create topology from scratch. """ if not self.has_topology() and not create: return self.topology.set_defaults_from_vcpus(vcpus) ################## # Default config # ################## def _validate_default_host_model_only(self, guest): # It's possible that the value HOST_MODEL_ONLY gets from # is not actually supported by qemu/kvm # combo which will be reported in if not self.model: return # pragma: no cover domcaps = guest.lookup_domcaps() domcaps_mode = domcaps.cpu.get_mode("custom") if not domcaps_mode: return # pragma: no cover cpu_model = domcaps_mode.get_model(self.model) if cpu_model and cpu_model.usable != "no": return log.debug( "Host capabilities CPU '%s' is not supported " "according to domain capabilities. Unsetting CPU model", self.model, ) self.model = None def _set_cpu_x86_kvm_default(self, guest): if guest.os.arch != self.conn.caps.host.cpu.arch: return mode = guest.x86_cpu_default self.set_special_mode(guest, mode) if mode == self.SPECIAL_MODE_HOST_MODEL_ONLY: self._validate_default_host_model_only(guest) def set_defaults(self, guest): if not self.conn.is_test() and not self.conn.is_qemu(): return if self.special_mode_was_set: return if self.model or self.mode: return if guest.os.is_arm_machvirt() and guest.type == "kvm": self.mode = self.SPECIAL_MODE_HOST_PASSTHROUGH elif guest.os.is_x86() and guest.type == "kvm": self._set_cpu_x86_kvm_default(guest) else: domcaps = guest.lookup_domcaps() # Prefer to emulate a feature-rich CPU instead of a basic one if self._should_use_maximum_cpu_mode(guest, domcaps): self.set_special_mode(guest, self.SPECIAL_MODE_MAXIMUM)