1#
2# Support for parsing libvirt's domcapabilities XML
3#
4# Copyright 2014 Red Hat, Inc.
5#
6# This work is licensed under the GNU GPLv2 or later.
7# See the COPYING file in the top-level directory.
8
9import re
10import xml.etree.ElementTree as ET
11
12import libvirt
13
14from .domain import DomainCpu
15from .logger import log
16from .xmlbuilder import XMLBuilder, XMLChildProperty, XMLProperty
17
18
19########################################
20# Genering <enum> and <value> handling #
21########################################
22
23class _Value(XMLBuilder):
24    XML_NAME = "value"
25    value = XMLProperty(".")
26
27
28class _HasValues(XMLBuilder):
29    values = XMLChildProperty(_Value)
30
31    def get_values(self):
32        return [v.value for v in self.values]
33
34
35class _Enum(_HasValues):
36    XML_NAME = "enum"
37    name = XMLProperty("./@name")
38
39
40class _CapsBlock(_HasValues):
41    supported = XMLProperty("./@supported", is_yesno=True)
42    enums = XMLChildProperty(_Enum)
43
44    def enum_names(self):
45        return [e.name for e in self.enums]
46
47    def get_enum(self, name):
48        for enum in self.enums:
49            if enum.name == name:
50                return enum
51        # Didn't find a match. Could be talking to older libvirt, or
52        # driver with incomplete info. Return a stub enum
53        return _Enum(self.conn)
54
55
56def _make_capsblock(xml_root_name):
57    """
58    Build a class object representing a list of <enum> in the XML. For
59    example, domcapabilities may have a block like:
60
61    <graphics supported='yes'>
62      <enum name='type'>
63        <value>sdl</value>
64        <value>vnc</value>
65        <value>spice</value>
66      </enum>
67    </graphics>
68
69    To build a class that tracks that whole <graphics> block, call this
70    like _make_capsblock("graphics")
71    """
72    class TmpClass(_CapsBlock):
73        pass
74    setattr(TmpClass, "XML_NAME", xml_root_name)
75    return TmpClass
76
77
78################################
79# SEV launch security handling #
80################################
81
82class _SEV(XMLBuilder):
83    XML_NAME = "sev"
84    supported = XMLProperty("./@supported", is_yesno=True)
85    cbitpos = XMLProperty("./cbitpos", is_int=True)
86    reducedPhysBits = XMLProperty("./reducedPhysBits", is_int=True)
87
88
89#############################
90# Misc toplevel XML classes #
91#############################
92
93class _OS(_CapsBlock):
94    XML_NAME = "os"
95    loader = XMLChildProperty(_make_capsblock("loader"), is_single=True)
96
97
98class _Devices(_CapsBlock):
99    XML_NAME = "devices"
100    hostdev = XMLChildProperty(_make_capsblock("hostdev"), is_single=True)
101    disk = XMLChildProperty(_make_capsblock("disk"), is_single=True)
102    video = XMLChildProperty(_make_capsblock("video"), is_single=True)
103
104
105class _Features(_CapsBlock):
106    XML_NAME = "features"
107    gic = XMLChildProperty(_make_capsblock("gic"), is_single=True)
108    sev = XMLChildProperty(_SEV, is_single=True)
109
110
111###############
112# CPU classes #
113###############
114
115class _CPUModel(XMLBuilder):
116    XML_NAME = "model"
117    model = XMLProperty(".")
118    usable = XMLProperty("./@usable")
119    fallback = XMLProperty("./@fallback")
120
121
122class _CPUFeature(XMLBuilder):
123    XML_NAME = "feature"
124    name = XMLProperty("./@name")
125    policy = XMLProperty("./@policy")
126
127
128class _CPUMode(XMLBuilder):
129    XML_NAME = "mode"
130    name = XMLProperty("./@name")
131    supported = XMLProperty("./@supported", is_yesno=True)
132    vendor = XMLProperty("./vendor")
133
134    models = XMLChildProperty(_CPUModel)
135    def get_model(self, name):
136        for model in self.models:
137            if model.model == name:
138                return model
139
140    features = XMLChildProperty(_CPUFeature)
141
142
143class _CPU(XMLBuilder):
144    XML_NAME = "cpu"
145    modes = XMLChildProperty(_CPUMode)
146
147    def get_mode(self, name):
148        for mode in self.modes:
149            if mode.name == name:
150                return mode
151
152
153#################################
154# DomainCapabilities main class #
155#################################
156
157class DomainCapabilities(XMLBuilder):
158    @staticmethod
159    def build_from_params(conn, emulator, arch, machine, hvtype):
160        xml = None
161        if conn.support.conn_domain_capabilities():
162            try:
163                xml = conn.getDomainCapabilities(emulator, arch,
164                    machine, hvtype)
165            except Exception:  # pragma: no cover
166                log.debug("Error fetching domcapabilities XML",
167                    exc_info=True)
168
169        if not xml:
170            # If not supported, just use a stub object
171            return DomainCapabilities(conn)
172        return DomainCapabilities(conn, parsexml=xml)
173
174    @staticmethod
175    def build_from_guest(guest):
176        return DomainCapabilities.build_from_params(guest.conn,
177            guest.emulator, guest.os.arch, guest.os.machine, guest.type)
178
179    # Mapping of UEFI binary names to their associated architectures. We
180    # only use this info to do things automagically for the user, it shouldn't
181    # validate anything the user explicitly enters.
182    _uefi_arch_patterns = {
183        "i686": [
184            r".*edk2-i386-.*\.fd",  # upstream qemu
185            r".*ovmf-ia32.*",  # fedora, gerd's firmware repo
186        ],
187        "x86_64": [
188            r".*edk2-x86_64-.*\.fd",  # upstream qemu
189            r".*OVMF_CODE\.fd",  # RHEL
190            r".*ovmf-x64/OVMF.*\.fd",  # gerd's firmware repo
191            r".*ovmf-x86_64-.*",  # SUSE
192            r".*ovmf.*", ".*OVMF.*",  # generic attempt at a catchall
193        ],
194        "aarch64": [
195            r".*AAVMF_CODE\.fd",  # RHEL
196            r".*aarch64/QEMU_EFI.*",  # gerd's firmware repo
197            r".*aarch64.*",  # generic attempt at a catchall
198            r".*edk2-aarch64-code\.fd",  # upstream qemu
199        ],
200        "armv7l": [
201            r".*AAVMF32_CODE\.fd",  # Debian qemu-efi-arm package
202            r".*arm/QEMU_EFI.*",  # fedora, gerd's firmware repo
203            r".*edk2-arm-code\.fd"  # upstream qemu
204        ],
205    }
206
207    def find_uefi_path_for_arch(self):
208        """
209        Search the loader paths for one that matches the passed arch
210        """
211        if not self.arch_can_uefi():
212            return  # pragma: no cover
213
214        patterns = self._uefi_arch_patterns.get(self.arch)
215        for pattern in patterns:
216            for path in [v.value for v in self.os.loader.values]:
217                if re.match(pattern, path):
218                    return path
219
220    def label_for_firmware_path(self, path):
221        """
222        Return a pretty label for passed path, based on if we know
223        about it or not
224        """
225        if not path:
226            if self.arch in ["i686", "x86_64"]:
227                return _("BIOS")
228            return _("None")
229
230        for arch, patterns in self._uefi_arch_patterns.items():
231            for pattern in patterns:
232                if re.match(pattern, path):
233                    return (_("UEFI %(arch)s: %(path)s") %
234                        {"arch": arch, "path": path})
235
236        return _("Custom: %(path)s" % {"path": path})
237
238    def arch_can_uefi(self):
239        """
240        Return True if we know how to setup UEFI for the passed arch
241        """
242        return self.arch in list(self._uefi_arch_patterns.keys())
243
244    def supports_uefi_xml(self):
245        """
246        Return True if libvirt advertises support for proper UEFI setup
247        """
248        return ("readonly" in self.os.loader.enum_names() and
249                "yes" in self.os.loader.get_enum("readonly").get_values())
250
251    def supports_safe_host_model(self):
252        """
253        Return True if domcaps reports support for cpu mode=host-model.
254        host-model in fact predates this support, however it wasn't
255        general purpose safe prior to domcaps advertisement.
256        """
257        for m in self.cpu.modes:
258            if (m.name == "host-model" and m.supported and
259                    m.models[0].fallback == "forbid"):
260                return True
261        return False
262
263    def get_cpu_models(self):
264        models = []
265
266        for m in self.cpu.modes:
267            if m.name == "custom" and m.supported:
268                for model in m.models:
269                    if model.usable != "no":
270                        models.append(model.model)
271
272        return models
273
274    def _convert_mode_to_cpu(self, xml):
275        root = ET.fromstring(xml)
276        root.tag = "cpu"
277        root.attrib = {}
278        arch = ET.SubElement(root, "arch")
279        arch.text = self.arch
280        return ET.tostring(root, encoding="unicode")
281
282    def _get_expanded_cpu(self, mode):
283        cpuXML = self._convert_mode_to_cpu(mode.get_xml())
284        log.debug("Generated CPU XML for security flag baseline:\n%s", cpuXML)
285
286        try:
287            expandedXML = self.conn.baselineHypervisorCPU(
288                    self.path, self.arch, self.machine, self.domain, [cpuXML],
289                    libvirt.VIR_CONNECT_BASELINE_CPU_EXPAND_FEATURES)
290        except (libvirt.libvirtError, AttributeError):
291            expandedXML = self.conn.baselineCPU([cpuXML],
292                    libvirt.VIR_CONNECT_BASELINE_CPU_EXPAND_FEATURES)
293
294        return DomainCpu(self.conn, expandedXML)
295
296    def _lookup_cpu_security_features(self):
297        ret = []
298        sec_features = [
299                'spec-ctrl',
300                'ssbd',
301                'ibpb',
302                'virt-ssbd',
303                'md-clear']
304
305        for m in self.cpu.modes:
306            if m.name != "host-model" or not m.supported:
307                continue  # pragma: no cover
308
309            try:
310                cpu = self._get_expanded_cpu(m)
311            except libvirt.libvirtError as e:  # pragma: no cover
312                log.warning(_("Failed to get expanded CPU XML: %s"), e)
313                break
314
315            for feature in cpu.features:
316                if feature.name in sec_features:
317                    ret.append(feature.name)
318
319        log.debug("Found host-model security features: %s", ret)
320        return ret
321
322    _features = None
323    def get_cpu_security_features(self):
324        if self._features is None:
325            self._features = self._lookup_cpu_security_features() or []
326        return self._features
327
328
329    def supports_sev_launch_security(self):
330        """
331        Returns False if either libvirt doesn't advertise support for SEV at
332        all (< libvirt-4.5.0) or if it explicitly advertises it as unsupported
333        on the platform
334        """
335        return bool(self.features.sev.supported)
336
337    def supports_video_bochs(self):
338        """
339        Returns False if either libvirt or qemu do not have support to bochs
340        video type.
341        """
342        models = self.devices.video.get_enum("modelType").get_values()
343        return bool("bochs" in models)
344
345    XML_NAME = "domainCapabilities"
346    os = XMLChildProperty(_OS, is_single=True)
347    cpu = XMLChildProperty(_CPU, is_single=True)
348    devices = XMLChildProperty(_Devices, is_single=True)
349    features = XMLChildProperty(_Features, is_single=True)
350
351    arch = XMLProperty("./arch")
352    domain = XMLProperty("./domain")
353    machine = XMLProperty("./machine")
354    path = XMLProperty("./path")
355