Merge "Add regression test to repoduce bug 2140537"
This commit is contained in:
Vendored
+31
@@ -1228,6 +1228,10 @@ class Domain(object):
|
|||||||
if emulator_pin is not None:
|
if emulator_pin is not None:
|
||||||
definition['emulator_pin'] = emulator_pin.get('cpuset')
|
definition['emulator_pin'] = emulator_pin.get('cpuset')
|
||||||
|
|
||||||
|
iothreads = tree.find('./iothreads')
|
||||||
|
if iothreads is not None:
|
||||||
|
definition['iothreads'] = iothreads.text
|
||||||
|
|
||||||
iothread_pin = tree.find('./cputune/iothreadpin')
|
iothread_pin = tree.find('./cputune/iothreadpin')
|
||||||
if iothread_pin is not None:
|
if iothread_pin is not None:
|
||||||
definition['iothread_pin'] = iothread_pin.get('cpuset')
|
definition['iothread_pin'] = iothread_pin.get('cpuset')
|
||||||
@@ -1711,6 +1715,10 @@ class Domain(object):
|
|||||||
cputune = '<cputune>%s%s%s</cputune>' % (
|
cputune = '<cputune>%s%s%s</cputune>' % (
|
||||||
emulatorpin, iothreadpin, cputune)
|
emulatorpin, iothreadpin, cputune)
|
||||||
|
|
||||||
|
iothreads = ''
|
||||||
|
if 'iothreads' in self._def:
|
||||||
|
iothreads = '<iothreads>%s</iothreads>' % self._def['iothreads']
|
||||||
|
|
||||||
numatune = ''
|
numatune = ''
|
||||||
for cellid, nodeset in self._def['memnodes'].items():
|
for cellid, nodeset in self._def['memnodes'].items():
|
||||||
numatune += '<memnode cellid="%d" nodeset="%s"/>' % (int(cellid),
|
numatune += '<memnode cellid="%d" nodeset="%s"/>' % (int(cellid),
|
||||||
@@ -1736,6 +1744,7 @@ class Domain(object):
|
|||||||
<memory>%(memory)s</memory>
|
<memory>%(memory)s</memory>
|
||||||
<currentMemory>%(memory)s</currentMemory>
|
<currentMemory>%(memory)s</currentMemory>
|
||||||
<vcpu%(vcpuset)s>%(vcpu)s</vcpu>
|
<vcpu%(vcpuset)s>%(vcpu)s</vcpu>
|
||||||
|
%(iothreads)s
|
||||||
<os>
|
<os>
|
||||||
<type arch='%(arch)s' machine='pc-0.12'>hvm</type>
|
<type arch='%(arch)s' machine='pc-0.12'>hvm</type>
|
||||||
%(loader)s
|
%(loader)s
|
||||||
@@ -1787,6 +1796,7 @@ class Domain(object):
|
|||||||
'memory': self._def['memory'],
|
'memory': self._def['memory'],
|
||||||
'vcpuset': vcpuset,
|
'vcpuset': vcpuset,
|
||||||
'vcpu': self._def['vcpu']['number'],
|
'vcpu': self._def['vcpu']['number'],
|
||||||
|
'iothreads': iothreads,
|
||||||
'arch': self._def['os']['arch'],
|
'arch': self._def['os']['arch'],
|
||||||
'loader': loader,
|
'loader': loader,
|
||||||
'disks': disks,
|
'disks': disks,
|
||||||
@@ -2085,6 +2095,27 @@ class Connection(object):
|
|||||||
self._emit_lifecycle(dom, VIR_DOMAIN_EVENT_DEFINED, 0)
|
self._emit_lifecycle(dom, VIR_DOMAIN_EVENT_DEFINED, 0)
|
||||||
return dom
|
return dom
|
||||||
|
|
||||||
|
# TODO(lajoskatona): Move this validation to defineXML once fix for
|
||||||
|
# bug/2140537 is merged.
|
||||||
|
# This method is only used temporarily from
|
||||||
|
# nova/tests/functional/regressions/test_bug_2140537.py
|
||||||
|
def _defineXMLIOThreads(self, xml):
|
||||||
|
xml_doc = etree.fromstring(xml.encode('utf-8'))
|
||||||
|
iothreadpin = xml_doc.find('./cputune/iothreadpin')
|
||||||
|
|
||||||
|
if iothreadpin is not None and iothreadpin.get('iothread') is None:
|
||||||
|
raise make_libvirtError(
|
||||||
|
libvirtError,
|
||||||
|
"XML error: Missing required attribute 'iothread' "
|
||||||
|
"in element 'iothreadpin'",
|
||||||
|
error_code=VIR_ERR_XML_ERROR,
|
||||||
|
error_domain=VIR_FROM_DOMAIN)
|
||||||
|
|
||||||
|
dom = Domain(connection=self, running=False, transient=False, xml=xml)
|
||||||
|
self._vms[dom.name()] = dom
|
||||||
|
self._emit_lifecycle(dom, VIR_DOMAIN_EVENT_DEFINED, 0)
|
||||||
|
return dom
|
||||||
|
|
||||||
def createXML(self, xml, flags):
|
def createXML(self, xml, flags):
|
||||||
dom = Domain(connection=self, running=True, transient=True, xml=xml)
|
dom = Domain(connection=self, running=True, transient=True, xml=xml)
|
||||||
self._vms[dom.name()] = dom
|
self._vms[dom.name()] = dom
|
||||||
|
|||||||
@@ -0,0 +1,300 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""Regression test for bug 2140537.
|
||||||
|
|
||||||
|
https://bugs.launchpad.net/nova/+bug/2140537
|
||||||
|
|
||||||
|
When creating an instance with hw:cpu_policy=dedicated, pinned CPUs,
|
||||||
|
Nova was incorrectly setting <iothreads>1</iothreads> in the libvirt XML
|
||||||
|
without a corresponding <iothreadpin> element. This caused libvirt to
|
||||||
|
reject the XML with: "XML error: Missing required attribute 'iothread'
|
||||||
|
in element 'iothreadpin'".
|
||||||
|
"""
|
||||||
|
|
||||||
|
from lxml import etree
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from nova.tests.fixtures import libvirt as fakelibvirt
|
||||||
|
from nova.tests.functional import integrated_helpers
|
||||||
|
from nova.tests.functional.libvirt import base
|
||||||
|
|
||||||
|
|
||||||
|
class TestIOThreadPinningPinnedCPU(
|
||||||
|
base.LibvirtMigrationMixin,
|
||||||
|
base.ServersTestBase,
|
||||||
|
integrated_helpers.InstanceHelperMixin
|
||||||
|
):
|
||||||
|
"""Regression test for bug 2140537.
|
||||||
|
|
||||||
|
Instances with dedicated CPU policy (pinned CPUs) should NOT have
|
||||||
|
IOThreads configured and have IOThreadPin in their domain xml,
|
||||||
|
as they are incompatible with CPU pinning.
|
||||||
|
"""
|
||||||
|
|
||||||
|
microversion = 'latest'
|
||||||
|
ADMIN_API = True
|
||||||
|
ADDITIONAL_FILTERS = ['NUMATopologyFilter']
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
# TODO(lajoskatona): remove this patch when the fix for
|
||||||
|
# bug/2140537 is merged, and the libvirt fixture has the
|
||||||
|
# necessary validation for XML fields for IOThreads.
|
||||||
|
patcher = mock.patch.object(
|
||||||
|
fakelibvirt.Connection, 'defineXML',
|
||||||
|
fakelibvirt.Connection._defineXMLIOThreads)
|
||||||
|
patcher.start()
|
||||||
|
self.addCleanup(patcher.stop)
|
||||||
|
|
||||||
|
super().setUp()
|
||||||
|
self.hostname = self.start_compute(
|
||||||
|
hostname='host1',
|
||||||
|
host_info=fakelibvirt.HostInfo(
|
||||||
|
cpu_nodes=2, cpu_sockets=1, cpu_cores=4, cpu_threads=1))
|
||||||
|
self.host = self.computes['host1']
|
||||||
|
|
||||||
|
def get_host(self, server_id):
|
||||||
|
server = self.api.get_server(server_id)
|
||||||
|
return server['OS-EXT-SRV-ATTR:host']
|
||||||
|
|
||||||
|
def _get_xml_element(self, xml, xpath):
|
||||||
|
"""Get element from XML using xpath."""
|
||||||
|
xml_doc = etree.fromstring(xml.encode('utf-8'))
|
||||||
|
element = xml_doc.find(xpath)
|
||||||
|
return element
|
||||||
|
|
||||||
|
def test_iothread_pinning_pinned_cpu(self):
|
||||||
|
"""Test that pinned CPU instances fail with iothreadpin bug.
|
||||||
|
|
||||||
|
This reproduces bug 2140537: When creating an instance with
|
||||||
|
hw:cpu_policy=dedicated, Nova incorrectly creates an <iothreadpin>
|
||||||
|
element without the required 'iothread' attribute, causing libvirt
|
||||||
|
to reject the XML.
|
||||||
|
"""
|
||||||
|
# Configure host with dedicated CPUs
|
||||||
|
self.flags(
|
||||||
|
cpu_shared_set='0,1', cpu_dedicated_set='2,3', group='compute')
|
||||||
|
self.restart_compute_service('host1')
|
||||||
|
|
||||||
|
# Create VM with dedicated CPUs
|
||||||
|
extra_spec = {'hw:cpu_policy': 'dedicated'}
|
||||||
|
flavor = self._create_flavor(vcpu=1, extra_spec=extra_spec)
|
||||||
|
|
||||||
|
# BUG: This fails with libvirt error about missing 'iothread' attribute
|
||||||
|
# The server creation will fail and go to ERROR state
|
||||||
|
server = self._create_server(
|
||||||
|
flavor_id=flavor, host='host1', networks='none',
|
||||||
|
expected_state='ERROR')
|
||||||
|
|
||||||
|
# Verify the server is in ERROR state due to the libvirt XML error
|
||||||
|
self.assertEqual('ERROR', server['status'])
|
||||||
|
|
||||||
|
# Check the fault message contains the libvirt error
|
||||||
|
self.assertIn('Exceeded maximum number of retries',
|
||||||
|
server['fault']['message'])
|
||||||
|
# Check the logs for the exception
|
||||||
|
self.assertIn("Missing required attribute 'iothread'",
|
||||||
|
self.stdlog.logger.output)
|
||||||
|
self.assertIn("element 'iothreadpin'", self.stdlog.logger.output)
|
||||||
|
|
||||||
|
def test_iothread_pinning_explicit_numa(self):
|
||||||
|
"""Test iothread pinning with explicit multi-node NUMA topology."""
|
||||||
|
self.flags(
|
||||||
|
cpu_shared_set='0-1', cpu_dedicated_set='2-7', group='compute')
|
||||||
|
self.restart_compute_service('host1')
|
||||||
|
|
||||||
|
# Create flavor with explicit 2-node NUMA topology
|
||||||
|
extra_spec = {
|
||||||
|
'hw:cpu_policy': 'dedicated',
|
||||||
|
'hw:numa_nodes': '2',
|
||||||
|
}
|
||||||
|
flavor = self._create_flavor(vcpu=4, extra_spec=extra_spec)
|
||||||
|
|
||||||
|
# Server should go ACTIVE
|
||||||
|
# server = self._create_server(
|
||||||
|
# flavor_id=flavor, host='host1', networks='none',
|
||||||
|
# expected_state='ACTIVE')
|
||||||
|
server = self._create_server(
|
||||||
|
flavor_id=flavor, host='host1', networks='none',
|
||||||
|
expected_state='ERROR')
|
||||||
|
|
||||||
|
# conn = self.host.driver._host.get_connection()
|
||||||
|
# dom = conn.lookupByUUIDString(server['id'])
|
||||||
|
# srv_xml = dom.XMLDesc(0)
|
||||||
|
|
||||||
|
# # Should have iothreads element
|
||||||
|
# srv_iothread = self._get_xml_element(srv_xml, './iothreads')
|
||||||
|
# self.assertIsNotNone(srv_iothread)
|
||||||
|
# self.assertEqual('1', srv_iothread.text)
|
||||||
|
|
||||||
|
# # Should have emulatorpin and iothreadpin
|
||||||
|
# srv_emulatorpin = self._get_xml_element(
|
||||||
|
# srv_xml, './cputune/emulatorpin')
|
||||||
|
# srv_iothreadpin = self._get_xml_element(
|
||||||
|
# srv_xml, './cputune/iothreadpin')
|
||||||
|
# self.assertIsNotNone(srv_emulatorpin)
|
||||||
|
# self.assertIsNotNone(srv_iothreadpin)
|
||||||
|
|
||||||
|
# # iothreadpin should have iothread attribute set to 1
|
||||||
|
# self.assertEqual('1', srv_iothreadpin.get('iothread'))
|
||||||
|
|
||||||
|
# # Both should be pinned to the union of NUMA nodes
|
||||||
|
# self.assertEqual(srv_emulatorpin.get('cpuset'),
|
||||||
|
# srv_iothreadpin.get('cpuset'))
|
||||||
|
|
||||||
|
self.assertEqual('ERROR', server['status'])
|
||||||
|
|
||||||
|
# Check the fault message contains the libvirt error
|
||||||
|
self.assertIn('Exceeded maximum number of retries',
|
||||||
|
server['fault']['message'])
|
||||||
|
# Check the logs for the exception
|
||||||
|
self.assertIn("Missing required attribute 'iothread'",
|
||||||
|
self.stdlog.logger.output)
|
||||||
|
self.assertIn("element 'iothreadpin'", self.stdlog.logger.output)
|
||||||
|
|
||||||
|
def test_iothread_pinning_isolated_emulator(self):
|
||||||
|
"""Test iothread pinning with isolated emulator threads policy."""
|
||||||
|
self.flags(
|
||||||
|
cpu_shared_set='0-1', cpu_dedicated_set='2-7', group='compute')
|
||||||
|
self.restart_compute_service('host1')
|
||||||
|
|
||||||
|
# Create flavor with isolated emulator threads
|
||||||
|
extra_spec = {
|
||||||
|
'hw:cpu_policy': 'dedicated',
|
||||||
|
'hw:emulator_threads_policy': 'isolate',
|
||||||
|
}
|
||||||
|
flavor = self._create_flavor(vcpu=2, extra_spec=extra_spec)
|
||||||
|
|
||||||
|
server = self._create_server(
|
||||||
|
flavor_id=flavor, host='host1', networks='none',
|
||||||
|
expected_state='ERROR')
|
||||||
|
# Server should go ACTIVE
|
||||||
|
# server = self._create_server(
|
||||||
|
# flavor_id=flavor, host='host1', networks='none',
|
||||||
|
# expected_state='ACTIVE')
|
||||||
|
|
||||||
|
# conn = self.host.driver._host.get_connection()
|
||||||
|
# dom = conn.lookupByUUIDString(server['id'])
|
||||||
|
# srv_xml = dom.XMLDesc(0)
|
||||||
|
|
||||||
|
# # Should have iothreads element
|
||||||
|
# srv_iothread = self._get_xml_element(srv_xml, './iothreads')
|
||||||
|
# self.assertIsNotNone(srv_iothread)
|
||||||
|
# self.assertEqual('1', srv_iothread.text)
|
||||||
|
|
||||||
|
# # Should have emulatorpin and iothreadpin
|
||||||
|
# srv_emulatorpin = self._get_xml_element(
|
||||||
|
# srv_xml, './cputune/emulatorpin')
|
||||||
|
# srv_iothreadpin = self._get_xml_element(
|
||||||
|
# srv_xml, './cputune/iothreadpin')
|
||||||
|
# self.assertIsNotNone(srv_emulatorpin)
|
||||||
|
# self.assertIsNotNone(srv_iothreadpin)
|
||||||
|
|
||||||
|
# # iothreadpin should have iothread attribute set to 1
|
||||||
|
# self.assertEqual('1', srv_iothreadpin.get('iothread'))
|
||||||
|
|
||||||
|
# # Both should be pinned to the same reserved/isolated CPU
|
||||||
|
# self.assertEqual(srv_emulatorpin.get('cpuset'),
|
||||||
|
# srv_iothreadpin.get('cpuset'))
|
||||||
|
|
||||||
|
# # Should be pinned to a single CPU (the reserved one)
|
||||||
|
# # With vcpu=2 and isolate policy, one extra CPU is reserved
|
||||||
|
# cpuset = srv_iothreadpin.get('cpuset')
|
||||||
|
# # The cpuset should be a single CPU from cpu_dedicated_set
|
||||||
|
# self.assertIsNotNone(cpuset)
|
||||||
|
self.assertEqual('ERROR', server['status'])
|
||||||
|
|
||||||
|
# Check the fault message contains the libvirt error
|
||||||
|
self.assertIn('Exceeded maximum number of retries',
|
||||||
|
server['fault']['message'])
|
||||||
|
# Check the logs for the exception
|
||||||
|
self.assertIn("Missing required attribute 'iothread'",
|
||||||
|
self.stdlog.logger.output)
|
||||||
|
self.assertIn("element 'iothreadpin'", self.stdlog.logger.output)
|
||||||
|
|
||||||
|
def test_iothread_pinning_shared_emulator(self):
|
||||||
|
"""Test iothread pinning with shared emulator threads policy."""
|
||||||
|
self.flags(
|
||||||
|
cpu_shared_set='0-1', cpu_dedicated_set='2-7', group='compute')
|
||||||
|
self.restart_compute_service('host1')
|
||||||
|
|
||||||
|
# Create flavor with shared emulator threads
|
||||||
|
extra_spec = {
|
||||||
|
'hw:cpu_policy': 'dedicated',
|
||||||
|
'hw:emulator_threads_policy': 'share',
|
||||||
|
}
|
||||||
|
flavor = self._create_flavor(vcpu=2, extra_spec=extra_spec)
|
||||||
|
|
||||||
|
server = self._create_server(
|
||||||
|
flavor_id=flavor, host='host1', networks='none',
|
||||||
|
expected_state='ERROR')
|
||||||
|
# Server should go ACTIVE
|
||||||
|
# server = self._create_server(
|
||||||
|
# flavor_id=flavor, host='host1', networks='none',
|
||||||
|
# expected_state='ACTIVE')
|
||||||
|
|
||||||
|
# conn = self.host.driver._host.get_connection()
|
||||||
|
# dom = conn.lookupByUUIDString(server['id'])
|
||||||
|
# srv_xml = dom.XMLDesc(0)
|
||||||
|
|
||||||
|
# # Should have iothreads element
|
||||||
|
# srv_iothread = self._get_xml_element(srv_xml, './iothreads')
|
||||||
|
# self.assertIsNotNone(srv_iothread)
|
||||||
|
# self.assertEqual('1', srv_iothread.text)
|
||||||
|
|
||||||
|
# # Should have emulatorpin and iothreadpin
|
||||||
|
# srv_emulatorpin = self._get_xml_element(
|
||||||
|
# srv_xml, './cputune/emulatorpin')
|
||||||
|
# srv_iothreadpin = self._get_xml_element(
|
||||||
|
# srv_xml, './cputune/iothreadpin')
|
||||||
|
# self.assertIsNotNone(srv_emulatorpin)
|
||||||
|
# self.assertIsNotNone(srv_iothreadpin)
|
||||||
|
|
||||||
|
# # iothreadpin should have iothread attribute set to 1
|
||||||
|
# self.assertEqual('1', srv_iothreadpin.get('iothread'))
|
||||||
|
|
||||||
|
# # Both should be pinned to cpu_shared_set (0-1)
|
||||||
|
# self.assertEqual(srv_emulatorpin.get('cpuset'),
|
||||||
|
# srv_iothreadpin.get('cpuset'))
|
||||||
|
# self.assertEqual('0-1', srv_iothreadpin.get('cpuset'))
|
||||||
|
|
||||||
|
self.assertEqual('ERROR', server['status'])
|
||||||
|
|
||||||
|
# Check the fault message contains the libvirt error
|
||||||
|
self.assertIn('Exceeded maximum number of retries',
|
||||||
|
server['fault']['message'])
|
||||||
|
# Check the logs for the exception
|
||||||
|
self.assertIn("Missing required attribute 'iothread'",
|
||||||
|
self.stdlog.logger.output)
|
||||||
|
self.assertIn("element 'iothreadpin'", self.stdlog.logger.output)
|
||||||
|
|
||||||
|
def test_iothread_no_pinning(self):
|
||||||
|
# No CPU pinning (shared CPUs only)
|
||||||
|
# Expects: NO cputune element at all (no emulatorpin, no iothreadpin)
|
||||||
|
flavor = self._create_flavor(vcpu=1,)
|
||||||
|
server = self._create_server(
|
||||||
|
flavor_id=flavor, host='host1', networks='none',
|
||||||
|
expected_state='ACTIVE')
|
||||||
|
conn = self.host.driver._host.get_connection()
|
||||||
|
dom = conn.lookupByUUIDString(server['id'])
|
||||||
|
srv_xml = dom.XMLDesc(0)
|
||||||
|
|
||||||
|
srv_iothread = self._get_xml_element(srv_xml, './iothreads')
|
||||||
|
srv_iothreadpin = self._get_xml_element(
|
||||||
|
srv_xml, './cputune/iothreadpin')
|
||||||
|
|
||||||
|
# iothreads should be set to 1 for all instances
|
||||||
|
self.assertIsNotNone(srv_iothread)
|
||||||
|
self.assertEqual('1', srv_iothread.text)
|
||||||
|
|
||||||
|
# And no pinning for shared CPUs, no iothreadpin
|
||||||
|
self.assertIsNone(srv_iothreadpin)
|
||||||
Reference in New Issue
Block a user