Track libvirt host/domain capabilities for multiple machine types

Currently we're only calling libvirt's getDomainCapabilities API once
per architecture, with the assumption that covering a single machine
type (the default) for that architecture is enough.  However, the
default for x86_64 is 'pc', but we need domain capabilities for 'q35'
in order to allow guests with SEV or Secure Boot enabled.  So for
x86_64, we need domain capabilities for at least two machine types:
'pc' and 'q35'.

We can obtain a sensibly small list of machine types with which to
call getDomainCapabilities by looking for the 'canonical' attribute in
machine types returned from getCapabilities (N.B. not
getDomainCapabilities). For example, getCapabilities returns these:

    <machine canonical='pc-i440fx-2.11' maxCpus='255'>pc</machine>
    <machine canonical='pc-q35-2.11' maxCpus='288'>q35</machine>

So change Host.get_domain_capabilities() to call the API not just with
the default machine type, but also once per canonical machine type.

In order to obtain the canonical machine types, enhance config.py so
that it can extract them from the capabilities XML, modernize the
fixtures for i686 and x86_64 capabilities so that they include recent
versions of the 'pc' and 'q35' families of machine types, and add
corresponding tests.  A new class LibvirtConfigCapsGuestDomain is
introduced to model the elements of the capabilities XML such as
<domain type='kvm'>, and their associated machine types.  This
supersedes the previous domtype attribute of LibvirtConfigCapsGuest
objects.  Canonical machine types are tracked separately from the
others in order to allow Host.get_domain_capabilities to invoke
libvirt's getDomainCapabilities only on those types.

As before, we register both the shortened canonical (alias) machine
type (e.g. 'q35') plus its full expanded counterpart (e.g. 'pc-q35-2.11')
if that is different.

As Host.get_domain_capabilities() is already long and complex, and
needs more functionality adding to support these changes, split out
much of the code into smaller methods:

    - _get_machine_types()
    - _add_to_domain_capabilities()

The new tests require the allow_mixed_nodes option of assertXmlEqual
to be enabled to compare XML fragments with elements in different
orders, since the children of <arch> generated by
LibvirtConfigCapsGuest.format_dom have the non-canonical machine types
first, followed by the canonical ones.

The tests also require removing the 'bamboo' canonical machine type
for the ppc architecture, since the getDomainCapabilities fixture for
this architecture is static and only returns results with a fixed
'g3beige' machine type which would cause a mismatch when called with
the 'bamboo' machine type.  The same applies with the 'q35' canonical
machine type for the i686 architecture, since the static fixture for
i686 only returns results with a fixed 'pc-i440fx-2.11' machine type.

However, the x86_64 fixtures are sufficient to test these code paths,
so this is a simpler alternative to adding complexity to the i686 and
ppc fixtures.

blueprint: amd-sev-libvirt-support
blueprint: allow-secure-boot-for-qemu-kvm-guests
Change-Id: I9da9ce682dc8c8b72fb31dd4e732b556b2ed7f90
This commit is contained in:
Adam Spiers 2019-07-27 18:30:02 +01:00
parent 415ed543dd
commit a53c867913
8 changed files with 565 additions and 206 deletions

View File

@ -48,40 +48,120 @@ CAPABILITIES_HOST_TEMPLATE = '''
</secmodel>
</host>'''
# NOTE(aspiers): HostTestCase has tests which assert that for any
# given (arch, domain) listed in the guest capabilities here, all
# canonical machine types (e.g. 'pc' or 'q35') must be a substring of
# the expanded machine type returned in the <machine> element of the
# corresponding fake getDomainCapabilities response for that (arch,
# domain, canonical_machine_type) combination. Those responses are
# defined by the DOMCAPABILITIES_* variables below. While
# DOMCAPABILITIES_X86_64_TEMPLATE can return multiple values for the
# <machine> element, DOMCAPABILITIES_I686 is fixed to fake a response
# of the 'pc-i440fx-2.11' machine type, therefore
# CAPABILITIES_GUEST['i686'] should return 'pc' as the only canonical
# machine type.
#
# CAPABILITIES_GUEST does not include canonical machine types for
# other non-x86 architectures, so these test assertions on apply to
# x86.
CAPABILITIES_GUEST = {
'i686': '''
<guest>
<os_type>hvm</os_type>
<arch name='i686'>
<wordsize>32</wordsize>
<emulator>/usr/bin/qemu</emulator>
<machine>pc-0.14</machine>
<machine canonical='pc-0.14'>pc</machine>
<machine>pc-0.13</machine>
<machine>pc-0.12</machine>
<machine>pc-0.11</machine>
<machine>pc-0.10</machine>
<machine>isapc</machine>
<domain type='qemu'>
</domain>
<emulator>/usr/bin/qemu-system-i386</emulator>
<machine maxCpus='255'>pc-i440fx-2.11</machine>
<machine canonical='pc-i440fx-2.11' maxCpus='255'>pc</machine>
<machine maxCpus='1'>isapc</machine>
<machine maxCpus='255'>pc-1.1</machine>
<machine maxCpus='255'>pc-1.2</machine>
<machine maxCpus='255'>pc-1.3</machine>
<machine maxCpus='255'>pc-i440fx-2.8</machine>
<machine maxCpus='255'>pc-1.0</machine>
<machine maxCpus='255'>pc-i440fx-2.9</machine>
<machine maxCpus='255'>pc-i440fx-2.6</machine>
<machine maxCpus='255'>pc-i440fx-2.7</machine>
<machine maxCpus='128'>xenfv</machine>
<machine maxCpus='255'>pc-i440fx-2.3</machine>
<machine maxCpus='255'>pc-i440fx-2.4</machine>
<machine maxCpus='255'>pc-i440fx-2.5</machine>
<machine maxCpus='255'>pc-i440fx-2.1</machine>
<machine maxCpus='255'>pc-i440fx-2.2</machine>
<machine maxCpus='255'>pc-i440fx-2.0</machine>
<machine maxCpus='288'>pc-q35-2.11</machine>
<machine maxCpus='288'>q35</machine>
<machine maxCpus='1'>xenpv</machine>
<machine maxCpus='288'>pc-q35-2.10</machine>
<machine maxCpus='255'>pc-i440fx-1.7</machine>
<machine maxCpus='288'>pc-q35-2.9</machine>
<machine maxCpus='255'>pc-0.15</machine>
<machine maxCpus='255'>pc-i440fx-1.5</machine>
<machine maxCpus='255'>pc-q35-2.7</machine>
<machine maxCpus='255'>pc-i440fx-1.6</machine>
<machine maxCpus='288'>pc-q35-2.8</machine>
<machine maxCpus='255'>pc-0.13</machine>
<machine maxCpus='255'>pc-0.14</machine>
<machine maxCpus='255'>pc-q35-2.4</machine>
<machine maxCpus='255'>pc-q35-2.5</machine>
<machine maxCpus='255'>pc-q35-2.6</machine>
<machine maxCpus='255'>pc-i440fx-1.4</machine>
<machine maxCpus='255'>pc-i440fx-2.10</machine>
<machine maxCpus='255'>pc-0.11</machine>
<machine maxCpus='255'>pc-0.12</machine>
<machine maxCpus='255'>pc-0.10</machine>
<domain type='qemu'/>
<domain type='kvm'>
<emulator>/usr/bin/kvm</emulator>
<machine>pc-0.14</machine>
<machine canonical='pc-0.14'>pc</machine>
<machine>pc-0.13</machine>
<machine>pc-0.12</machine>
<machine>pc-0.11</machine>
<machine>pc-0.10</machine>
<machine>isapc</machine>
<emulator>/usr/bin/qemu-kvm</emulator>
<machine maxCpus='255'>pc-i440fx-2.11</machine>
<machine canonical='pc-i440fx-2.11' maxCpus='255'>pc</machine>
<machine maxCpus='1'>isapc</machine>
<machine maxCpus='255'>pc-1.1</machine>
<machine maxCpus='255'>pc-1.2</machine>
<machine maxCpus='255'>pc-1.3</machine>
<machine maxCpus='255'>pc-i440fx-2.8</machine>
<machine maxCpus='255'>pc-1.0</machine>
<machine maxCpus='255'>pc-i440fx-2.9</machine>
<machine maxCpus='255'>pc-i440fx-2.6</machine>
<machine maxCpus='255'>pc-i440fx-2.7</machine>
<machine maxCpus='128'>xenfv</machine>
<machine maxCpus='255'>pc-i440fx-2.3</machine>
<machine maxCpus='255'>pc-i440fx-2.4</machine>
<machine maxCpus='255'>pc-i440fx-2.5</machine>
<machine maxCpus='255'>pc-i440fx-2.1</machine>
<machine maxCpus='255'>pc-i440fx-2.2</machine>
<machine maxCpus='255'>pc-i440fx-2.0</machine>
<machine maxCpus='288'>pc-q35-2.11</machine>
<machine maxCpus='288'>q35</machine>
<machine maxCpus='1'>xenpv</machine>
<machine maxCpus='288'>pc-q35-2.10</machine>
<machine maxCpus='255'>pc-i440fx-1.7</machine>
<machine maxCpus='288'>pc-q35-2.9</machine>
<machine maxCpus='255'>pc-0.15</machine>
<machine maxCpus='255'>pc-i440fx-1.5</machine>
<machine maxCpus='255'>pc-q35-2.7</machine>
<machine maxCpus='255'>pc-i440fx-1.6</machine>
<machine maxCpus='288'>pc-q35-2.8</machine>
<machine maxCpus='255'>pc-0.13</machine>
<machine maxCpus='255'>pc-0.14</machine>
<machine maxCpus='255'>pc-q35-2.4</machine>
<machine maxCpus='255'>pc-q35-2.5</machine>
<machine maxCpus='255'>pc-q35-2.6</machine>
<machine maxCpus='255'>pc-i440fx-1.4</machine>
<machine maxCpus='255'>pc-i440fx-2.10</machine>
<machine maxCpus='255'>pc-0.11</machine>
<machine maxCpus='255'>pc-0.12</machine>
<machine maxCpus='255'>pc-0.10</machine>
</domain>
</arch>
<features>
<cpuselection/>
<deviceboot/>
<pae/>
<nonpae/>
<disksnapshot default='on' toggle='no'/>
<acpi default='on' toggle='yes'/>
<apic default='on' toggle='no'/>
<pae/>
<nonpae/>
</features>
</guest>''',
@ -91,29 +171,93 @@ CAPABILITIES_GUEST = {
<arch name='x86_64'>
<wordsize>64</wordsize>
<emulator>/usr/bin/qemu-system-x86_64</emulator>
<machine>pc-0.14</machine>
<machine canonical='pc-0.14'>pc</machine>
<machine>pc-0.13</machine>
<machine>pc-0.12</machine>
<machine>pc-0.11</machine>
<machine>pc-0.10</machine>
<machine>isapc</machine>
<domain type='qemu'>
</domain>
<machine maxCpus='255'>pc-i440fx-2.11</machine>
<machine canonical='pc-i440fx-2.11' maxCpus='255'>pc</machine>
<machine maxCpus='1'>isapc</machine>
<machine maxCpus='255'>pc-1.1</machine>
<machine maxCpus='255'>pc-1.2</machine>
<machine maxCpus='255'>pc-1.3</machine>
<machine maxCpus='255'>pc-i440fx-2.8</machine>
<machine maxCpus='255'>pc-1.0</machine>
<machine maxCpus='255'>pc-i440fx-2.9</machine>
<machine maxCpus='255'>pc-i440fx-2.6</machine>
<machine maxCpus='255'>pc-i440fx-2.7</machine>
<machine maxCpus='128'>xenfv</machine>
<machine maxCpus='255'>pc-i440fx-2.3</machine>
<machine maxCpus='255'>pc-i440fx-2.4</machine>
<machine maxCpus='255'>pc-i440fx-2.5</machine>
<machine maxCpus='255'>pc-i440fx-2.1</machine>
<machine maxCpus='255'>pc-i440fx-2.2</machine>
<machine maxCpus='255'>pc-i440fx-2.0</machine>
<machine maxCpus='288'>pc-q35-2.11</machine>
<machine canonical='pc-q35-2.11' maxCpus='288'>q35</machine>
<machine maxCpus='1'>xenpv</machine>
<machine maxCpus='288'>pc-q35-2.10</machine>
<machine maxCpus='255'>pc-i440fx-1.7</machine>
<machine maxCpus='288'>pc-q35-2.9</machine>
<machine maxCpus='255'>pc-0.15</machine>
<machine maxCpus='255'>pc-i440fx-1.5</machine>
<machine maxCpus='255'>pc-q35-2.7</machine>
<machine maxCpus='255'>pc-i440fx-1.6</machine>
<machine maxCpus='288'>pc-q35-2.8</machine>
<machine maxCpus='255'>pc-0.13</machine>
<machine maxCpus='255'>pc-0.14</machine>
<machine maxCpus='255'>pc-q35-2.4</machine>
<machine maxCpus='255'>pc-q35-2.5</machine>
<machine maxCpus='255'>pc-q35-2.6</machine>
<machine maxCpus='255'>pc-i440fx-1.4</machine>
<machine maxCpus='255'>pc-i440fx-2.10</machine>
<machine maxCpus='255'>pc-0.11</machine>
<machine maxCpus='255'>pc-0.12</machine>
<machine maxCpus='255'>pc-0.10</machine>
<domain type='qemu'/>
<domain type='kvm'>
<emulator>/usr/bin/kvm</emulator>
<machine>pc-0.14</machine>
<machine canonical='pc-0.14'>pc</machine>
<machine>pc-0.13</machine>
<machine>pc-0.12</machine>
<machine>pc-0.11</machine>
<machine>pc-0.10</machine>
<machine>isapc</machine>
<emulator>/usr/bin/qemu-kvm</emulator>
<machine maxCpus='255'>pc-i440fx-2.11</machine>
<machine canonical='pc-i440fx-2.11' maxCpus='255'>pc</machine>
<machine maxCpus='1'>isapc</machine>
<machine maxCpus='255'>pc-1.1</machine>
<machine maxCpus='255'>pc-1.2</machine>
<machine maxCpus='255'>pc-1.3</machine>
<machine maxCpus='255'>pc-i440fx-2.8</machine>
<machine maxCpus='255'>pc-1.0</machine>
<machine maxCpus='255'>pc-i440fx-2.9</machine>
<machine maxCpus='255'>pc-i440fx-2.6</machine>
<machine maxCpus='255'>pc-i440fx-2.7</machine>
<machine maxCpus='128'>xenfv</machine>
<machine maxCpus='255'>pc-i440fx-2.3</machine>
<machine maxCpus='255'>pc-i440fx-2.4</machine>
<machine maxCpus='255'>pc-i440fx-2.5</machine>
<machine maxCpus='255'>pc-i440fx-2.1</machine>
<machine maxCpus='255'>pc-i440fx-2.2</machine>
<machine maxCpus='255'>pc-i440fx-2.0</machine>
<machine maxCpus='288'>pc-q35-2.11</machine>
<machine canonical='pc-q35-2.11' maxCpus='288'>q35</machine>
<machine maxCpus='1'>xenpv</machine>
<machine maxCpus='288'>pc-q35-2.10</machine>
<machine maxCpus='255'>pc-i440fx-1.7</machine>
<machine maxCpus='288'>pc-q35-2.9</machine>
<machine maxCpus='255'>pc-0.15</machine>
<machine maxCpus='255'>pc-i440fx-1.5</machine>
<machine maxCpus='255'>pc-q35-2.7</machine>
<machine maxCpus='255'>pc-i440fx-1.6</machine>
<machine maxCpus='288'>pc-q35-2.8</machine>
<machine maxCpus='255'>pc-0.13</machine>
<machine maxCpus='255'>pc-0.14</machine>
<machine maxCpus='255'>pc-q35-2.4</machine>
<machine maxCpus='255'>pc-q35-2.5</machine>
<machine maxCpus='255'>pc-q35-2.6</machine>
<machine maxCpus='255'>pc-i440fx-1.4</machine>
<machine maxCpus='255'>pc-i440fx-2.10</machine>
<machine maxCpus='255'>pc-0.11</machine>
<machine maxCpus='255'>pc-0.12</machine>
<machine maxCpus='255'>pc-0.10</machine>
</domain>
</arch>
<features>
<cpuselection/>
<deviceboot/>
<disksnapshot default='on' toggle='no'/>
<acpi default='on' toggle='yes'/>
<apic default='on' toggle='no'/>
</features>
@ -232,7 +376,6 @@ CAPABILITIES_GUEST = {
<machine>g3beige</machine>
<machine>virtex-ml507</machine>
<machine>mpc8544ds</machine>
<machine canonical='bamboo-0.13'>bamboo</machine>
<machine>bamboo-0.13</machine>
<machine>bamboo-0.12</machine>
<machine>ref405ep</machine>
@ -838,6 +981,11 @@ STATIC_DOMCAPABILITIES = {
Architecture.I686: DOMCAPABILITIES_I686
}
# NOTE(aspiers): see the above note for CAPABILITIES_GUEST which
# explains why the <machine> element here needs to be parametrised.
#
# The <features> element needs to be parametrised for emulating
# environments with and without the SEV feature.
DOMCAPABILITIES_X86_64_TEMPLATE = """
<domainCapabilities>
<path>/usr/bin/qemu-kvm</path>

View File

@ -1366,9 +1366,10 @@ class Connection(object):
return fake_libvirt_data.STATIC_DOMCAPABILITIES[arch]
if arch == 'x86_64':
aliases = {'pc': 'pc-i440fx-2.11', 'q35': 'pc-q35-2.11'}
return fake_libvirt_data.DOMCAPABILITIES_X86_64_TEMPLATE % \
{'features': self._domain_capability_features,
'mtype': machine_type}
'mtype': aliases.get(machine_type, machine_type)}
raise Exception("fakelibvirt doesn't support getDomainCapabilities "
"for %s architecture" % arch)

View File

@ -111,22 +111,6 @@ class LibvirtConfigCapsTest(LibvirtConfigBaseTest):
</cells>
</topology>
</host>
<guest>
<os_type>hvm</os_type>
<arch name='x86_64'>
<emulator>/usr/bin/qemu-system-x86_64</emulator>
<domain type="qemu" />
<domain type="kvm">
<emulator>/usr/bin/qemu-kvm</emulator>
</domain>
</arch>
</guest>
<guest>
<os_type>hvm</os_type>
<arch name='i686'>
<emulator>/usr/bin/qemu-system-i386</emulator>
</arch>
</guest>
</capabilities>"""
obj = config.LibvirtConfigCaps()
@ -134,17 +118,6 @@ class LibvirtConfigCapsTest(LibvirtConfigBaseTest):
self.assertIsInstance(obj.host, config.LibvirtConfigCapsHost)
self.assertEqual(obj.host.uuid, "c7a5fdbd-edaf-9455-926a-d65c16db1809")
self.assertEqual(2, len(obj.guests))
for guest in obj.guests:
self.assertIsInstance(guest, config.LibvirtConfigCapsGuest)
self.assertEqual('hvm', guest.ostype)
self.assertEqual('x86_64', obj.guests[0].arch)
self.assertEqual('i686', obj.guests[1].arch)
self.assertEqual('/usr/bin/qemu-system-x86_64', obj.guests[0].emulator)
self.assertNotIn('qemu', obj.guests[0].domemulator)
self.assertEqual('/usr/bin/qemu-kvm', obj.guests[0].domemulator['kvm'])
self.assertEqual('/usr/bin/qemu-system-i386', obj.guests[1].emulator)
xmlout = obj.to_xml()
@ -172,6 +145,95 @@ class LibvirtConfigCapsTest(LibvirtConfigBaseTest):
self.assertEqual(128, obj.memory)
self.assertEqual(0, len(obj.cpus))
def test_config_guest(self):
xmlin = """
<capabilities>
<guest>
<os_type>hvm</os_type>
<arch name='x86_64'>
<emulator>/usr/bin/qemu-system-x86_64</emulator>
<machine maxCpus='255'>pc-i440fx-2.11</machine>
<machine canonical='pc-i440fx-2.11' maxCpus='255'>pc</machine>
<machine maxCpus='1'>isapc</machine>
<machine maxCpus='255'>pc-1.1</machine>
<machine maxCpus='255'>pc-i440fx-2.0</machine>
<machine maxCpus='288'>pc-q35-2.11</machine>
<machine canonical='pc-q35-2.11' maxCpus='288'>q35</machine>
<machine maxCpus='1'>xenpv</machine>
<machine maxCpus='288'>pc-q35-2.10</machine>
<domain type="qemu" />
<domain type="kvm">
<emulator>/usr/bin/qemu-kvm</emulator>
<machine maxCpus='255'>pc-i440fx-2.11</machine>
<machine canonical='pc-i440fx-2.11' maxCpus='255'>pc</machine>
<machine maxCpus='1'>isapc</machine>
<machine maxCpus='255'>pc-1.1</machine>
<machine maxCpus='255'>pc-i440fx-2.0</machine>
<machine maxCpus='288'>pc-q35-2.11</machine>
<machine canonical='pc-q35-2.11' maxCpus='288'>q35</machine>
<machine maxCpus='1'>xenpv</machine>
<machine maxCpus='288'>pc-q35-2.10</machine>
</domain>
</arch>
</guest>
<guest>
<os_type>hvm</os_type>
<arch name='i686'>
<emulator>/usr/bin/qemu-system-i386</emulator>
<machine maxCpus='255'>pc-i440fx-2.11</machine>
<machine canonical='pc-i440fx-2.11' maxCpus='255'>pc</machine>
<machine maxCpus='1'>isapc</machine>
<machine maxCpus='255'>pc-1.1</machine>
<machine maxCpus='255'>pc-i440fx-2.0</machine>
<machine maxCpus='288'>pc-q35-2.11</machine>
<machine canonical='pc-q35-2.11' maxCpus='288'>q35</machine>
<machine maxCpus='1'>xenpv</machine>
<machine maxCpus='288'>pc-q35-2.10</machine>
<domain type="qemu" />
<domain type="kvm">
<emulator>/usr/bin/qemu-kvm</emulator>
<machine maxCpus='255'>pc-i440fx-2.11</machine>
<machine canonical='pc-i440fx-2.11' maxCpus='255'>pc</machine>
<machine maxCpus='1'>isapc</machine>
<machine maxCpus='255'>pc-1.1</machine>
<machine maxCpus='255'>pc-i440fx-2.0</machine>
<machine maxCpus='288'>pc-q35-2.11</machine>
<machine canonical='pc-q35-2.11' maxCpus='288'>q35</machine>
<machine maxCpus='1'>xenpv</machine>
<machine maxCpus='288'>pc-q35-2.10</machine>
</domain>
</arch>
</guest>
</capabilities>"""
obj = config.LibvirtConfigCaps()
obj.parse_str(xmlin)
self.assertEqual(2, len(obj.guests))
for guest in obj.guests:
self.assertIsInstance(guest, config.LibvirtConfigCapsGuest)
self.assertEqual('hvm', guest.ostype)
self.assertEqual('x86_64', obj.guests[0].arch)
self.assertEqual('i686', obj.guests[1].arch)
guest = obj.guests[0]
self.assertIn('qemu', guest.domains)
self.assertIn('kvm', guest.domains)
self.assertEqual('qemu', guest.default_domain.domtype)
self.assertEqual('/usr/bin/qemu-system-x86_64',
guest.default_domain.emulator)
self.assertEqual(guest.default_domain, guest.domains['qemu'])
for domtype, domain in guest.domains.items():
self.assertEqual(7, len(domain.machines))
self.assertIn('pc-i440fx-2.0', domain.machines)
self.assertIn('xenpv', domain.machines)
self.assertEqual(2, len(domain.aliases))
self.assertIn('pc', domain.aliases)
self.assertIn('q35', domain.aliases)
xmlout = obj.to_xml()
self.assertXmlEqual(xmlin, xmlout, allow_mixed_nodes=True)
class LibvirtConfigGuestTimerTest(LibvirtConfigBaseTest):
def test_config_platform(self):

View File

@ -15449,13 +15449,13 @@ class LibvirtConnTestCase(test.NoDBTestCase,
guest = vconfig.LibvirtConfigCapsGuest()
guest.ostype = fields.VMMode.HVM
guest.arch = fields.Architecture.X86_64
guest.domtype = ["kvm"]
guest.domains['kvm'] = vconfig.LibvirtConfigCapsGuestDomain()
caps.guests.append(guest)
guest = vconfig.LibvirtConfigCapsGuest()
guest.ostype = fields.VMMode.HVM
guest.arch = fields.Architecture.I686
guest.domtype = ["kvm"]
guest.domains['kvm'] = vconfig.LibvirtConfigCapsGuestDomain()
caps.guests.append(guest)
return caps
@ -16483,13 +16483,14 @@ class LibvirtConnTestCase(test.NoDBTestCase,
guest = vconfig.LibvirtConfigCapsGuest()
guest.ostype = 'hvm'
guest.arch = fields.Architecture.X86_64
guest.domtype = ['kvm', 'qemu']
guest.domains['kvm'] = vconfig.LibvirtConfigCapsGuestDomain()
guest.domains['qemu'] = vconfig.LibvirtConfigCapsGuestDomain()
caps.guests.append(guest)
guest = vconfig.LibvirtConfigCapsGuest()
guest.ostype = 'hvm'
guest.arch = fields.Architecture.I686
guest.domtype = ['kvm']
guest.domains['kvm'] = vconfig.LibvirtConfigCapsGuestDomain()
caps.guests.append(guest)
# Include one that is not known to nova to make sure it
@ -16497,7 +16498,7 @@ class LibvirtConnTestCase(test.NoDBTestCase,
guest = vconfig.LibvirtConfigCapsGuest()
guest.ostype = 'hvm'
guest.arch = 'itanic'
guest.domtype = ['kvm']
guest.domains['kvm'] = vconfig.LibvirtConfigCapsGuestDomain()
caps.guests.append(guest)
return caps

View File

@ -28,6 +28,7 @@ from nova import exception
from nova import objects
from nova.objects import fields as obj_fields
from nova import test
from nova.tests.unit.virt.libvirt import fake_libvirt_data
from nova.tests.unit.virt.libvirt import fakelibvirt
from nova.virt import event
from nova.virt.libvirt import config as vconfig
@ -638,10 +639,36 @@ class HostTestCase(test.NoDBTestCase):
self.assertIsNone(caps.host.cpu.model)
self.assertEqual(0, len(caps.host.cpu.features))
def test__get_machine_types(self):
expected = [
# NOTE(aspiers): in the real world, i686 would probably
# have q35 too, but our fixtures are manipulated to
# exclude it to allow more thorough testing the our
# canonical machine types logic is correct.
('i686', 'qemu', ['pc']),
('i686', 'kvm', ['pc']),
('x86_64', 'qemu', ['pc', 'q35']),
('x86_64', 'kvm', ['pc', 'q35']),
('armv7l', 'qemu', ['virt']),
# NOTE(aspiers): we're currently missing default machine
# types for the other architectures for which we have fake
# capabilities.
]
for arch, domain, expected_mach_types in expected:
guest_xml = fake_libvirt_data.CAPABILITIES_GUEST[arch]
guest = vconfig.LibvirtConfigCapsGuest()
guest.parse_str(guest_xml)
domain = guest.domains[domain]
self.assertEqual(set(expected_mach_types),
self.host._get_machine_types(arch, domain),
"for arch %s domain %s" %
(arch, domain.domtype))
def _test_get_domain_capabilities(self):
caps = self.host.get_domain_capabilities()
for arch, mtypes in caps.items():
for mtype, dom_cap in mtypes.items():
self.assertIsInstance(dom_cap, vconfig.LibvirtConfigDomainCaps)
# NOTE(sean-k-mooney): this should always be true since we are
# mapping from an arch and machine_type to a domain cap object
# for that pair. We use 'in' to allow libvirt to expand the
@ -649,10 +676,18 @@ class HostTestCase(test.NoDBTestCase):
# form e.g. pc-i440fx-2.11
self.assertIn(mtype, dom_cap.machine_type)
self.assertIn(dom_cap.machine_type_alias, mtype)
# We assume we are testing with x86_64 in other parts of the code
# so we just assert it's in the test data and return it.
self.assertIn('x86_64', caps)
self.assertIn('pc', caps['x86_64'])
expected = [
('i686', ['pc', 'pc-i440fx-2.11']),
('x86_64', ['pc', 'pc-i440fx-2.11', 'q35', 'pc-q35-2.11']),
]
for arch, expected_mtypes in expected:
self.assertIn(arch, caps)
for mach_type in expected_mtypes:
self.assertIn(mach_type, caps[arch], "for arch %s" % arch)
return caps['x86_64']['pc']
def test_get_domain_capabilities(self):
@ -742,7 +777,7 @@ class HostTestCase(test.NoDBTestCase):
caps = self.host.get_domain_capabilities()
for arch, mtype in six.iteritems(archs):
for arch, mtype in archs.items():
self.assertIn(arch, caps)
self.assertNotIn('pc', caps[arch])
self.assertIn(mtype, caps[arch])

View File

@ -25,6 +25,7 @@ helpers for populating up config object instances.
import time
from collections import OrderedDict
from lxml import etree
from oslo_utils import strutils
from oslo_utils import units
@ -391,43 +392,54 @@ class LibvirtConfigCapsGuest(LibvirtConfigObject):
self.arch = None
self.ostype = None
self.domtype = list()
# Track <emulator> values, which we need in order to be able
# to call virConnectGetDomainCapabilities() - typically
# something like '/usr/bin/qemu-system-i386'.
#
# Firstly we track the default for any <domain> child without
# its own <emulator> sub-child:
self.emulator = None
#
# Also per-<domain> overrides for the default in self.emulator.
# The dict maps domain types such as 'kvm' to the emulator
# path for that domain type. Note that these overrides come
# from <emulator> elements under each <domain>; there is no
# <domemulator> element.
self.domemulator = dict()
# Map domain types such as 'qemu' and 'kvm' to
# LibvirtConfigCapsGuestDomain instances.
self.domains = OrderedDict()
self.default_domain = None
def parse_dom(self, xmldoc):
super(LibvirtConfigCapsGuest, self).parse_dom(xmldoc)
for c in xmldoc:
if c.tag == "os_type":
self.ostype = c.text
elif c.tag == "arch":
self.arch = c.get("name")
for ac in c:
if ac.tag == "domain":
self.parse_domain(ac)
elif ac.tag == "emulator":
self.emulator = ac.text
for child in xmldoc:
if child.tag == "os_type":
self.ostype = child.text
elif child.tag == "arch":
self.parse_arch(child)
def parse_domain(self, domxml):
domtype = domxml.get("type")
self.domtype.append(domtype)
for dc in domxml:
if dc.tag == "emulator":
self.domemulator[domtype] = dc.text
def parse_arch(self, xmldoc):
self.arch = xmldoc.get("name")
# NOTE(aspiers): The data relating to each <domain> element
# under <arch> (such as <emulator> and many <machine>
# elements) is structured in a slightly odd way. There is one
# "default" domain such as
#
# <domain type='qemu'/>
#
# which has no child elements, and all its data is provided in
# sibling elements. Then others such as
#
# <domain type='kvm'>
#
# will have their <emulator> and <machine> elements as
# children. So we need to handle the two cases separately.
self.default_domain = LibvirtConfigCapsGuestDomain()
for child in xmldoc:
if child.tag == "domain":
if list(child):
# This domain has children, so create a new instance,
# parse it, and register it in the dict of domains.
domain = LibvirtConfigCapsGuestDomain()
domain.parse_dom(child)
self.domains[domain.domtype] = domain
else:
# This is the childless <domain/> element for the
# default domain
self.default_domain.parse_domain(child)
self.domains[self.default_domain.domtype] = \
self.default_domain
else:
# Sibling element of the default domain
self.default_domain.parse_child(child)
def format_dom(self):
caps = super(LibvirtConfigCapsGuest, self).format_dom()
@ -435,20 +447,82 @@ class LibvirtConfigCapsGuest(LibvirtConfigObject):
if self.ostype is not None:
caps.append(self._text_node("os_type", self.ostype))
if self.arch:
arch = etree.Element("arch", name=self.arch)
if self.emulator is not None:
arch.append(self._text_node("emulator", self.emulator))
for dt in self.domtype:
dte = etree.Element("domain")
dte.set("type", dt)
if dt in self.domemulator:
dte.append(self._text_node("emulator",
self.domemulator[dt]))
arch.append(dte)
arch = self.format_arch()
caps.append(arch)
return caps
def format_arch(self):
arch = etree.Element("arch", name=self.arch)
for c in self.default_domain.format_dom():
arch.append(c)
arch.append(self._new_node("domain", type=self.default_domain.domtype))
for domtype, domain in self.domains.items():
if domtype == self.default_domain.domtype:
# We've already added this domain at the top level
continue
arch.append(domain.format_dom())
return arch
class LibvirtConfigCapsGuestDomain(LibvirtConfigObject):
def __init__(self, **kwargs):
super(LibvirtConfigCapsGuestDomain, self).__init__(
root_name="domain", **kwargs)
self.domtype = None
# Track <emulator> values, which we need in order to be able
# to call virConnectGetDomainCapabilities() - typically
# something like '/usr/bin/qemu-system-i386'.
self.emulator = None
self.machines = {}
self.aliases = {}
def parse_dom(self, xmldoc):
super(LibvirtConfigCapsGuestDomain, self).parse_dom(xmldoc)
self.parse_domain(xmldoc)
for c in xmldoc:
self.parse_child(c)
def parse_child(self, xmldoc):
if xmldoc.tag == "emulator":
self.emulator = xmldoc.text
elif xmldoc.tag == "machine":
self.parse_machine(xmldoc)
def parse_domain(self, xmldoc):
self.domtype = xmldoc.get("type")
if self.domtype is None:
raise exception.InvalidInput(
"Didn't find domain type in %s", xmldoc)
def parse_machine(self, xmldoc):
if 'canonical' in xmldoc.attrib:
self.aliases[xmldoc.text] = xmldoc.attrib
else:
self.machines[xmldoc.text] = xmldoc.attrib
def format_dom(self):
domain = super(LibvirtConfigCapsGuestDomain, self).format_dom()
if self.domtype is not None:
domain.set("type", self.domtype)
if self.emulator is not None:
domain.append(self._text_node("emulator", self.emulator))
for mach_type, machine in self.machines.items():
domain.append(self._text_node("machine", mach_type, **machine))
for alias, machine in self.aliases.items():
domain.append(self._text_node("machine", alias, **machine))
return domain
class LibvirtConfigGuestTimer(LibvirtConfigObject):

View File

@ -6057,11 +6057,11 @@ class LibvirtDriver(driver.ComputeDriver):
caps = self._host.get_capabilities()
instance_caps = list()
for g in caps.guests:
for dt in g.domtype:
for domain_type in g.domains:
try:
instance_cap = (
fields.Architecture.canonicalize(g.arch),
fields.HVType.canonicalize(dt),
fields.HVType.canonicalize(domain_type),
fields.VMMode.canonicalize(g.ostype))
instance_caps.append(instance_cap)
except exception.InvalidArchitectureName:

View File

@ -700,12 +700,29 @@ class Host(object):
getDomainCapabilities() once for every single (architecture,
machine_type) combination. In particular, SEV support isn't
reported per-machine type, and since there are usually many
machine types, we follow the advice of the experts that for
now it's sufficient to call it once per host architecture:
machine types, we heed the advice of the experts that it's
typically sufficient to call it once per host architecture:
https://bugzilla.redhat.com/show_bug.cgi?id=1683471#c7
However, future domain capabilities might report SEV in a more
However, that's not quite sufficient in the context of nova,
because SEV guests typically require a q35 machine type, as do
KVM/QEMU guests that want Secure Boot, whereas the current
default machine type for x86_64 is 'pc'. So we need results
from the getDomainCapabilities API for at least those two.
Fortunately we can take advantage of the results from the
getCapabilities API which marks selected machine types as
canonical, e.g.:
<machine canonical='pc-i440fx-2.11' maxCpus='255'>pc</machine>
<machine canonical='pc-q35-2.11' maxCpus='288'>q35</machine>
So for now, we call getDomainCapabilities for these canonical
machine types of each architecture, plus for the
architecture's default machine type, if that is not one of the
canonical types.
Future domain capabilities might report SEV in a more
fine-grained manner, and we also expect to use this method to
detect other features, such as for gracefully handling machine
types and potentially for detecting OVMF binaries. Therefore
@ -741,90 +758,22 @@ class Host(object):
for guest in caps.guests:
arch = guest.arch
machine_type = \
libvirt_utils.get_default_machine_type(arch)
domain = guest.domains.get(virt_type, guest.default_domain)
machine_types = self._get_machine_types(arch, domain)
emulator_bin = guest.emulator
if virt_type in guest.domemulator:
emulator_bin = guest.domemulator[virt_type]
# It is expected that each <guest> will have a different
# architecture, but it doesn't hurt to add a safety net to
# avoid needlessly calling libvirt's API more times than
# we need.
if machine_type and machine_type in domain_caps[arch]:
continue
# NOTE(aspiers): machine_type could be None here if nova
# doesn't have a default machine type for this
# architecture. In that case we pass a machine_type of
# None to the libvirt API and rely on it choosing a
# sensible default which will be returned in the <machine>
# element. It could also be an alias like 'pc' rather
# than a full machine type.
#
# NOTE(kchamart): Prior to libvirt v4.7.0 libvirt picked
# its default machine type for x86, 'pc', as reported by
# QEMU's default. From libvirt v4.7.0 onwards, libvirt
# _explicitly_ declared the "preferred" default for x86 as
# 'pc' (and appropriate values for other architectures),
# and only uses QEMU's reported default (whatever that may
# be) if 'pc' does not exist. This was done "to isolate
# applications from hypervisor changes that may cause
# incompatibilities" -- i.e. if, or when, QEMU changes its
# default machine type to something else. Refer to this
# libvirt commit:
#
# https://libvirt.org/git/?p=libvirt.git;a=commit;h=26cfb1a3
try:
cap_obj = self._get_domain_capabilities(
emulator_bin=emulator_bin, arch=arch,
machine_type=machine_type, virt_type=virt_type)
except libvirt.libvirtError as ex:
# NOTE(sean-k-mooney): This can happen for several
# reasons, but one common example is if you have
# multiple QEMU emulators installed and you set
# virt-type=kvm. In this case any non-native emulator,
# e.g. AArch64 on an x86 host, will (correctly) raise
# an exception as KVM cannot be used to accelerate CPU
# instructions for non-native architectures.
error_code = ex.get_error_code()
LOG.debug(
"Error from libvirt when retrieving domain capabilities "
"for arch %(arch)s / virt_type %(virt_type)s / "
"machine_type %(mach_type)s: "
"[Error Code %(error_code)s]: %(exception)s",
{'arch': arch, 'virt_type': virt_type,
'mach_type': machine_type, 'error_code': error_code,
'exception': ex})
# Remove archs added by default dict lookup when checking
# if the machine type has already been recoded.
if arch in domain_caps:
domain_caps.pop(arch)
continue
# Register the domain caps using the expanded form of
# machine type returned by libvirt in the <machine>
# element (e.g. pc-i440fx-2.11)
if cap_obj.machine_type:
domain_caps[arch][cap_obj.machine_type] = cap_obj
else:
# NOTE(aspiers): In theory this should never happen,
# but better safe than sorry.
LOG.warning(
"libvirt getDomainCapabilities("
"emulator_bin=%(emulator_bin)s, arch=%(arch)s, "
"machine_type=%(machine_type)s, virt_type=%(virt_type)s) "
"returned null <machine> type",
{'emulator_bin': emulator_bin, 'arch': arch,
'machine_type': machine_type, 'virt_type': virt_type}
)
# And if we passed an alias, register the domain caps
# under that too.
if machine_type and machine_type != cap_obj.machine_type:
domain_caps[arch][machine_type] = cap_obj
cap_obj.machine_type_alias = machine_type
for machine_type in machine_types:
# It is expected that if there are multiple <guest>
# elements, each will have a different architecture;
# for example, on x86 hosts one <guest> will contain
# <arch name='i686'> and one will contain <arch
# name='x86_64'>. But it doesn't hurt to add a safety
# net to avoid needlessly calling libvirt's API more
# times than we need.
if machine_type and machine_type in domain_caps[arch]:
continue
self._add_to_domain_capabilities(domain.emulator, arch,
domain_caps, machine_type,
virt_type)
# NOTE(aspiers): Use a temporary variable to update the
# instance variable atomically, otherwise if some API
@ -834,9 +783,98 @@ class Host(object):
return self._domain_caps
def _get_domain_capabilities(self, emulator_bin=None, arch=None,
machine_type=None, virt_type=None, flags=0):
def _get_machine_types(self, arch, domain):
"""Get the machine types for this architecture for which we need to
call getDomainCapabilities, i.e. the canonical machine types,
and the default machine type (if it's not one of the canonical
machine types).
See the docstring for get_domain_capabilities() for an explanation
of why we choose this set of machine types.
"""
# NOTE(aspiers): machine_type could be None here if nova
# doesn't have a default machine type for this architecture.
# See _add_to_domain_capabilities() below for how this is handled.
mtypes = set([libvirt_utils.get_default_machine_type(arch)])
mtypes.update(domain.aliases.keys())
LOG.debug("Getting domain capabilities for %(arch)s via "
"machine types: %(mtypes)s",
{'arch': arch, 'mtypes': mtypes})
return mtypes
def _add_to_domain_capabilities(self, emulator_bin, arch, domain_caps,
machine_type, virt_type):
# NOTE(aspiers): machine_type could be None here if nova
# doesn't have a default machine type for this architecture.
# In that case we pass a machine_type of None to the libvirt
# API and rely on it choosing a sensible default which will be
# returned in the <machine> element. It could also be an
# alias like 'pc' rather than a full machine type.
#
# NOTE(kchamart): Prior to libvirt v4.7.0 libvirt picked its
# default machine type for x86, 'pc', as reported by QEMU's
# default. From libvirt v4.7.0 onwards, libvirt _explicitly_
# declared the "preferred" default for x86 as 'pc' (and
# appropriate values for other architectures), and only uses
# QEMU's reported default (whatever that may be) if 'pc' does
# not exist. This was done "to isolate applications from
# hypervisor changes that may cause incompatibilities" --
# i.e. if, or when, QEMU changes its default machine type to
# something else. Refer to this libvirt commit:
#
# https://libvirt.org/git/?p=libvirt.git;a=commit;h=26cfb1a3
try:
cap_obj = self._get_domain_capabilities(
emulator_bin=emulator_bin, arch=arch,
machine_type=machine_type, virt_type=virt_type)
except libvirt.libvirtError as ex:
# NOTE(sean-k-mooney): This can happen for several
# reasons, but one common example is if you have
# multiple QEMU emulators installed and you set
# virt-type=kvm. In this case any non-native emulator,
# e.g. AArch64 on an x86 host, will (correctly) raise
# an exception as KVM cannot be used to accelerate CPU
# instructions for non-native architectures.
error_code = ex.get_error_code()
LOG.debug(
"Error from libvirt when retrieving domain capabilities "
"for arch %(arch)s / virt_type %(virt_type)s / "
"machine_type %(mach_type)s: "
"[Error Code %(error_code)s]: %(exception)s",
{'arch': arch, 'virt_type': virt_type,
'mach_type': machine_type, 'error_code': error_code,
'exception': ex})
# Remove archs added by default dict lookup when checking
# if the machine type has already been recoded.
if arch in domain_caps:
domain_caps.pop(arch)
return
# Register the domain caps using the expanded form of
# machine type returned by libvirt in the <machine>
# element (e.g. pc-i440fx-2.11)
if cap_obj.machine_type:
domain_caps[arch][cap_obj.machine_type] = cap_obj
else:
# NOTE(aspiers): In theory this should never happen,
# but better safe than sorry.
LOG.warning(
"libvirt getDomainCapabilities("
"emulator_bin=%(emulator_bin)s, arch=%(arch)s, "
"machine_type=%(machine_type)s, virt_type=%(virt_type)s) "
"returned null <machine> type",
{'emulator_bin': emulator_bin, 'arch': arch,
'machine_type': machine_type, 'virt_type': virt_type}
)
# And if we passed an alias, register the domain caps
# under that too.
if machine_type and machine_type != cap_obj.machine_type:
domain_caps[arch][machine_type] = cap_obj
cap_obj.machine_type_alias = machine_type
def _get_domain_capabilities(self, emulator_bin=None, arch=None,
machine_type=None, virt_type=None, flags=0):
xmlstr = self.get_connection().getDomainCapabilities(
emulator_bin,
arch,