Add regression test to repoduce bug 2139351
This tests repoduces the current bug where the iothread pinning is not updated for numa instnace on live migration and enhance the libvirt fixture to make this possible we also provide a sanity check for non numa instnace to show the vcpu cpuset is correctly. Related-Bug: #2139351 Assisted-By: claude-code opus 4.5 Change-Id: Ib2c0d1f826ad4f31e3e9b3f61f2c9b2111bf7edd Signed-off-by: Sean Mooney <work@seanmooney.info>
This commit is contained in:
Vendored
+12
-3
@@ -1221,6 +1221,10 @@ class Domain(object):
|
||||
if emulator_pin is not None:
|
||||
definition['emulator_pin'] = emulator_pin.get('cpuset')
|
||||
|
||||
iothread_pin = tree.find('./cputune/iothreadpin')
|
||||
if iothread_pin is not None:
|
||||
definition['iothread_pin'] = iothread_pin.get('cpuset')
|
||||
|
||||
memnodes = {}
|
||||
|
||||
for node in tree.findall('./numatune/memnode'):
|
||||
@@ -1671,12 +1675,17 @@ class Domain(object):
|
||||
cputune = ''
|
||||
for vcpu, cpuset in self._def['cpu_pins'].items():
|
||||
cputune += '<vcpupin vcpu="%d" cpuset="%s"/>' % (int(vcpu), cpuset)
|
||||
emulatorpin = None
|
||||
emulatorpin = ''
|
||||
if 'emulator_pin' in self._def:
|
||||
emulatorpin = ('<emulatorpin cpuset="%s"/>' %
|
||||
self._def['emulator_pin'])
|
||||
if cputune or emulatorpin:
|
||||
cputune = '<cputune>%s%s</cputune>' % (emulatorpin, cputune)
|
||||
iothreadpin = ''
|
||||
if 'iothread_pin' in self._def:
|
||||
iothreadpin = ('<iothreadpin iothread="1" cpuset="%s"/>' %
|
||||
self._def['iothread_pin'])
|
||||
if cputune or emulatorpin or iothreadpin:
|
||||
cputune = '<cputune>%s%s%s</cputune>' % (
|
||||
emulatorpin, iothreadpin, cputune)
|
||||
|
||||
numatune = ''
|
||||
for cellid, nodeset in self._def['memnodes'].items():
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
# 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 2139351.
|
||||
|
||||
https://bugs.launchpad.net/nova/+bug/2139351
|
||||
|
||||
Commit 76d64b9cb4241b73e62b3775f13d8eddcc0cb778 added iothread support,
|
||||
creating one iothread per VM and pinning iothreads to the same cpuset as
|
||||
emulator threads. During live migration, _update_numa_xml() in
|
||||
nova/virt/libvirt/migration.py updates vcpupin and emulatorpin elements
|
||||
but NOT iothreadpin. When cpu_shared_set differs between source and
|
||||
destination hosts, the iothread remains incorrectly pinned to the source
|
||||
host's CPU set.
|
||||
"""
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from nova.tests.fixtures import libvirt as fakelibvirt
|
||||
from nova.tests.functional import integrated_helpers
|
||||
from nova.tests.functional.libvirt import base
|
||||
|
||||
|
||||
class TestLiveMigrationIOThreadPinning(
|
||||
base.LibvirtMigrationMixin,
|
||||
base.ServersTestBase,
|
||||
integrated_helpers.InstanceHelperMixin
|
||||
):
|
||||
"""Regression test for iothreadpin during live migration.
|
||||
|
||||
This tests that iothreadpin is correctly updated when live migrating
|
||||
between hosts with different cpu_shared_set configurations.
|
||||
"""
|
||||
|
||||
microversion = 'latest'
|
||||
ADMIN_API = True
|
||||
ADDITIONAL_FILTERS = ['NUMATopologyFilter']
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.src_hostname = self.start_compute(
|
||||
hostname='src',
|
||||
host_info=fakelibvirt.HostInfo(
|
||||
cpu_nodes=1, cpu_sockets=1, cpu_cores=4, cpu_threads=1))
|
||||
self.dest_hostname = self.start_compute(
|
||||
hostname='dest',
|
||||
host_info=fakelibvirt.HostInfo(
|
||||
cpu_nodes=1, cpu_sockets=1, cpu_cores=4, cpu_threads=1))
|
||||
self.src = self.computes['src']
|
||||
self.dest = self.computes['dest']
|
||||
|
||||
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_live_migrate_iothread_pinning_numa(self):
|
||||
"""Test iothread pinning updated for NUMA VMs with dedicated CPUs.
|
||||
|
||||
BUG: emulatorpin updates correctly but iothreadpin does not.
|
||||
"""
|
||||
# Configure both hosts: shared=0,1 dedicated=2,3
|
||||
self.flags(
|
||||
cpu_shared_set='0,1', cpu_dedicated_set='2,3', group='compute')
|
||||
self.restart_compute_service('src')
|
||||
self.restart_compute_service('dest')
|
||||
|
||||
# Create VM with dedicated CPUs and shared emulator threads
|
||||
extra_spec = {
|
||||
'hw:cpu_policy': 'dedicated',
|
||||
'hw:emulator_threads_policy': 'share'
|
||||
}
|
||||
flavor = self._create_flavor(vcpu=1, extra_spec=extra_spec)
|
||||
self.server = self._create_server(
|
||||
flavor_id=flavor, host='src', networks='none')
|
||||
|
||||
# Get source XML and verify pinning matches cpu_shared_set
|
||||
conn = self.src.driver._host.get_connection()
|
||||
dom = conn.lookupByUUIDString(self.server['id'])
|
||||
src_xml = dom.XMLDesc(0)
|
||||
|
||||
src_emulatorpin = self._get_xml_element(
|
||||
src_xml, './cputune/emulatorpin')
|
||||
src_iothreadpin = self._get_xml_element(
|
||||
src_xml, './cputune/iothreadpin')
|
||||
self.assertIsNotNone(src_emulatorpin)
|
||||
self.assertIsNotNone(src_iothreadpin)
|
||||
self.assertEqual('0-1', src_emulatorpin.get('cpuset'))
|
||||
self.assertEqual('0-1', src_iothreadpin.get('cpuset'))
|
||||
|
||||
# Configure dest: shared=2,3 dedicated=0,1 (swapped)
|
||||
self.flags(
|
||||
cpu_shared_set='2,3', cpu_dedicated_set='0,1', group='compute')
|
||||
self.restart_compute_service('dest')
|
||||
|
||||
# Live migrate
|
||||
self._live_migrate(self.server, 'completed')
|
||||
self.assertEqual('dest', self.get_host(self.server['id']))
|
||||
|
||||
# Get dest XML
|
||||
conn = self.dest.driver._host.get_connection()
|
||||
dom = conn.lookupByUUIDString(self.server['id'])
|
||||
dest_xml = dom.XMLDesc(0)
|
||||
|
||||
# Verify emulatorpin updated (this works)
|
||||
dest_emulatorpin = self._get_xml_element(
|
||||
dest_xml, './cputune/emulatorpin')
|
||||
self.assertIsNotNone(dest_emulatorpin)
|
||||
self.assertEqual('2-3', dest_emulatorpin.get('cpuset'))
|
||||
|
||||
# Verify iothreadpin updated
|
||||
dest_iothreadpin = self._get_xml_element(
|
||||
dest_xml, './cputune/iothreadpin')
|
||||
self.assertIsNotNone(dest_iothreadpin)
|
||||
# FIXME: this is bug 2139351
|
||||
self.assertEqual('0-1', dest_iothreadpin.get('cpuset'))
|
||||
self.assertNotEqual(
|
||||
dest_emulatorpin.get('cpuset'), dest_iothreadpin.get('cpuset'))
|
||||
# self.assertEqual(
|
||||
# '2-3', dest_iothreadpin.get('cpuset'),
|
||||
# f"iothreadpin was not updated during live migration. "
|
||||
# f"Expected '2-3' but got '{dest_iothreadpin.get('cpuset')}'")
|
||||
|
||||
# # Both should match
|
||||
# self.assertEqual(
|
||||
# dest_emulatorpin.get('cpuset'), dest_iothreadpin.get('cpuset'))
|
||||
|
||||
def test_live_migrate_unpinned_vcpu_cpuset_updated(self):
|
||||
"""Test vcpu cpuset updated for unpinned VMs with cpu_shared_set.
|
||||
|
||||
This is a sanity check that the existing migration code correctly
|
||||
updates the vcpu cpuset for unpinned VMs. Unpinned VMs don't have
|
||||
cputune/emulatorpin or cputune/iothreadpin elements.
|
||||
"""
|
||||
# Configure both hosts with cpu_shared_set
|
||||
self.flags(cpu_shared_set='0,1', group='compute')
|
||||
self.restart_compute_service('src')
|
||||
self.restart_compute_service('dest')
|
||||
|
||||
# Create unpinned VM (default flavor, no dedicated CPU policy)
|
||||
self.server = self._create_server(host='src', networks='none')
|
||||
|
||||
# Verify source XML has vcpu cpuset matching cpu_shared_set
|
||||
conn = self.src.driver._host.get_connection()
|
||||
dom = conn.lookupByUUIDString(self.server['id'])
|
||||
src_xml = dom.XMLDesc(0)
|
||||
self.assertIn('<vcpu cpuset="0-1">1</vcpu>', src_xml)
|
||||
|
||||
# Verify no cputune elements for unpinned VMs
|
||||
src_emulatorpin = self._get_xml_element(
|
||||
src_xml, './cputune/emulatorpin')
|
||||
src_iothreadpin = self._get_xml_element(
|
||||
src_xml, './cputune/iothreadpin')
|
||||
self.assertIsNone(src_emulatorpin)
|
||||
self.assertIsNone(src_iothreadpin)
|
||||
|
||||
# Configure dest with different cpu_shared_set
|
||||
self.flags(cpu_shared_set='2,3', group='compute')
|
||||
self.restart_compute_service('dest')
|
||||
|
||||
# Live migrate
|
||||
self._live_migrate(self.server, 'completed')
|
||||
self.assertEqual('dest', self.get_host(self.server['id']))
|
||||
|
||||
# Verify dest XML has vcpu cpuset updated
|
||||
conn = self.dest.driver._host.get_connection()
|
||||
dom = conn.lookupByUUIDString(self.server['id'])
|
||||
dest_xml = dom.XMLDesc(0)
|
||||
self.assertNotIn('<vcpu cpuset="0-1">1</vcpu>', dest_xml)
|
||||
self.assertIn('<vcpu cpuset="2-3">1</vcpu>', dest_xml)
|
||||
Reference in New Issue
Block a user