summaryrefslogtreecommitdiff
path: root/virtinst/domain/cpu.py
blob: b7ca764c0e01c3952703320337e32f1445d1afa6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
#
# 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 <cpu> child node <topology>.
    """
    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):
        if not self.sockets:
            if not self.cores:
                self.sockets = vcpus // self.threads
            else:
                self.sockets = vcpus // self.cores

        if not self.cores:
            if not self.threads:
                self.cores = vcpus // self.sockets
            else:
                self.cores = vcpus // (self.sockets * self.threads)

        if not self.threads:
            self.threads = vcpus // (self.sockets * self.cores)

        return


# 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 <cache> element, so it's implemented
# with `is_single=True` for now (see actual CPU Domain below).
class _CPUCache(XMLBuilder):
    """
    Class for generating XML for <cpu> child node <cache>.
    """
    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 <cpu> child nodes <feature>.
    """
    XML_NAME = "feature"
    _XML_PROP_ORDER = ["policy", "name"]

    name = XMLProperty("./@name")
    policy = XMLProperty("./@policy")


##############
# NUMA cells #
##############

class _NUMACellSibling(XMLBuilder):
    """
    Class for generating XML for <cpu><numa><cell><distances> child nodes
    <sibling>, 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 <cpu><numa><cell> child nodes <cache>,
    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 <cpu><numa> child nodes <cell> 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 <cpu><numa><cell><interconnects> child nodes
    <latency>, 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 <latency> 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 <cpu><numa><cell><interconnects> child nodes
    <bandwidth>, describing bandwidth between two NUMA memory nodes.
    """
    XML_NAME = "bandwidth"
    _XML_PROP_ORDER = ["initiator", "target", "cache", "type", "value", "unit"]

    # Note: The documentation only mentions <latency> nodes having a cache=
    # attribute, but <bandwidth> and <latency> nodes are otherwise identical
    # and libvirt will happily accept XML with a cache= attribute on
    # <bandwidth> 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 <cpu> XML
    """
    XML_NAME = "cpu"
    _XML_PROP_ORDER = ["mode", "match", "check", "migratable",
            "model", "model_fallback", "model_vendor_id", "vendor",
            "topology", "cache", "features",
            "cells", "latencies", "bandwidths"]


    ##################
    # 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")


    #############################
    # 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_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_CLEAR,
                     SPECIAL_MODE_APP_DEFAULT]
    def set_special_mode(self, guest, val):
        if val == self.SPECIAL_MODE_APP_DEFAULT:
            # If libvirt is new enough to support reliable mode=host-model
            # then use it, otherwise use previous default HOST_MODEL_ONLY
            domcaps = guest.lookup_domcaps()
            val = self.SPECIAL_MODE_HOST_MODEL_ONLY
            if domcaps.supports_safe_host_model():
                val = self.SPECIAL_MODE_HOST_MODEL

        if (val == self.SPECIAL_MODE_HOST_MODEL or
            val == self.SPECIAL_MODE_HOST_PASSTHROUGH):
            self.model = None
            self.vendor = None
            self.model_fallback = None
            for f in self.features:
                self.remove_child(f)
            self.mode = val
        elif val == self.SPECIAL_MODE_HOST_COPY:
            self.copy_host_cpu(guest)
        elif (val == self.SPECIAL_MODE_HV_DEFAULT or
              val == self.SPECIAL_MODE_CLEAR):
            self.clear()
        elif 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

    def copy_host_cpu(self, guest):
        """
        Try to manually mimic host-model, copying all the info
        preferably out of domcapabilities, but capabilities as fallback.
        """
        domcaps = guest.lookup_domcaps()
        if domcaps.supports_safe_host_model():
            log.debug("Using domcaps for host-copy")
            cpu = domcaps.cpu.get_mode("host-model")
            model = cpu.models[0].model
            fallback = cpu.models[0].fallback
        else:
            cpu = self.conn.caps.host.cpu
            model = cpu.model
            fallback = None
            if not model:  # pragma: no cover
                raise ValueError(_("No host CPU reported in capabilities"))

        self.mode = "custom"
        self.match = "exact"
        self.set_model(guest, model)
        if fallback:
            self.model_fallback = fallback
        self.vendor = cpu.vendor

        for feature in self.features:
            self.remove_child(feature)
        for feature in cpu.features:
            policy = getattr(feature, "policy", "require")
            self.add_feature(feature.name, policy)


    ########################
    # 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)
        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.sockets or 1) *
                (self.topology.cores or 1) *
                (self.topology.threads or 1))

    def has_topology(self):
        """
        Return True if any topology info is set
        """
        return bool(self.topology.get_xml())

    def set_topology_defaults(self, vcpus):
        """
        Fill in unset topology values, using the passed vcpus count.
        Will not set topology from scratch, this just fills in missing
        topology values.
        """
        if not self.has_topology():
            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
        # <capabilities> is not actually supported by qemu/kvm
        # combo which will be reported in <domainCapabilities>
        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.get_xml().strip() or
            self.special_mode_was_set):
            # User already configured CPU
            return

        if guest.os.is_arm_machvirt() and guest.type == "kvm":
            self.mode = self.SPECIAL_MODE_HOST_PASSTHROUGH

        elif guest.os.is_arm64() and guest.os.is_arm_machvirt():
            # -M virt defaults to a 32bit CPU, even if using aarch64
            self.set_model(guest, "cortex-a57")

        elif guest.os.is_x86() and guest.type == "kvm":
            self._set_cpu_x86_kvm_default(guest)