Skip to content

Commit e06fec8

Browse files
authored
Merge pull request #153 from stackhpc/upstream/2023.1-2025-10-06
Synchronise 2023.1 with upstream
2 parents f3c317d + 6f8decf commit e06fec8

19 files changed

+730
-56
lines changed

nova/cmd/manage.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3306,9 +3306,10 @@ def _validate_image_properties(self, image_properties):
33063306
# Return the dict so we can update the instance system_metadata
33073307
return image_properties
33083308

3309-
def _update_image_properties(self, instance, image_properties):
3309+
def _update_image_properties(self, ctxt, instance, image_properties):
33103310
"""Update instance image properties
33113311
3312+
:param ctxt: nova.context.RequestContext
33123313
:param instance: The instance to update
33133314
:param image_properties: List of image properties and values to update
33143315
"""
@@ -3332,8 +3333,13 @@ def _update_image_properties(self, instance, image_properties):
33323333
for image_property, value in image_properties.items():
33333334
instance.system_metadata[f'image_{image_property}'] = value
33343335

3336+
request_spec = objects.RequestSpec.get_by_instance_uuid(
3337+
ctxt, instance.uuid)
3338+
request_spec.image = instance.image_meta
3339+
33353340
# Save and return 0
33363341
instance.save()
3342+
request_spec.save()
33373343
return 0
33383344

33393345
@action_description(_(
@@ -3368,7 +3374,7 @@ def set(self, instance_uuid=None, image_properties=None):
33683374
instance = objects.Instance.get_by_uuid(
33693375
cctxt, instance_uuid, expected_attrs=['system_metadata'])
33703376
return self._update_image_properties(
3371-
instance, image_properties)
3377+
ctxt, instance, image_properties)
33723378
except ValueError as e:
33733379
print(str(e))
33743380
return 6

nova/compute/manager.py

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4990,6 +4990,50 @@ def _delete_volume_attachments(self, ctxt, bdms):
49904990
'Error: %s', bdm.attachment_id, str(e),
49914991
instance_uuid=bdm.instance_uuid)
49924992

4993+
def _update_bdm_for_swap_to_finish_resize(
4994+
self, context, instance, confirm=True):
4995+
"""This updates bdm.swap with new swap info"""
4996+
4997+
bdms = instance.get_bdms()
4998+
if not (instance.old_flavor and instance.new_flavor):
4999+
return bdms
5000+
5001+
if instance.old_flavor.swap == instance.new_flavor.swap:
5002+
return bdms
5003+
5004+
old_swap = instance.old_flavor.swap
5005+
new_swap = instance.new_flavor.swap
5006+
if not confirm:
5007+
# revert flavor on _finish_revert_resize
5008+
old_swap = instance.new_flavor.swap
5009+
new_swap = instance.old_flavor.swap
5010+
5011+
# add swap
5012+
if old_swap == 0 and new_swap:
5013+
# (auniyal)old_swap = 0 means we did not have swap bdm
5014+
# for this instance.
5015+
# and as there is a new_swap, its a swap addition
5016+
new_swap_bdm = block_device.create_blank_bdm(new_swap, 'swap')
5017+
bdm_obj = objects.BlockDeviceMapping(
5018+
context, instance_uuid=instance.uuid, **new_swap_bdm)
5019+
bdm_obj.update_or_create()
5020+
return instance.get_bdms()
5021+
5022+
# update swap
5023+
for bdm in bdms:
5024+
if bdm.guest_format == 'swap' and bdm.device_type == 'disk':
5025+
if new_swap > 0:
5026+
LOG.info('Adding swap BDM.', instance=instance)
5027+
bdm.volume_size = new_swap
5028+
bdm.save()
5029+
break
5030+
elif new_swap == 0:
5031+
LOG.info('Deleting swap BDM.', instance=instance)
5032+
bdm.destroy()
5033+
bdms.objects.remove(bdm)
5034+
break
5035+
return bdms
5036+
49935037
@wrap_exception()
49945038
@reverts_task_state
49955039
@wrap_instance_event(prefix='compute')
@@ -5327,8 +5371,9 @@ def _finish_revert_resize(
53275371
):
53285372
"""Inner version of finish_revert_resize."""
53295373
with self._error_out_instance_on_exception(context, instance):
5330-
bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(
5331-
context, instance.uuid)
5374+
bdms = self._update_bdm_for_swap_to_finish_resize(
5375+
context, instance, confirm=False)
5376+
53325377
self._notify_about_instance_usage(
53335378
context, instance, "resize.revert.start")
53345379
compute_utils.notify_about_instance_action(context, instance,
@@ -6265,8 +6310,7 @@ def _finish_resize_helper(self, context, disk_info, image, instance,
62656310
The caller must revert the instance's allocations if the migration
62666311
process failed.
62676312
"""
6268-
bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(
6269-
context, instance.uuid)
6313+
bdms = self._update_bdm_for_swap_to_finish_resize(context, instance)
62706314

62716315
with self._error_out_instance_on_exception(context, instance):
62726316
image_meta = objects.ImageMeta.from_dict(image)

nova/objects/instance_numa.py

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import itertools
1616

17+
from oslo_log import log as logging
1718
from oslo_serialization import jsonutils
1819
from oslo_utils import versionutils
1920

@@ -24,6 +25,8 @@
2425
from nova.objects import fields as obj_fields
2526
from nova.virt import hardware
2627

28+
LOG = logging.getLogger(__name__)
29+
2730

2831
# TODO(berrange): Remove NovaObjectDictCompat
2932
@base.NovaObjectRegistry.register
@@ -61,6 +64,11 @@ def obj_make_compatible(self, primitive, target_version):
6164
obj_fields.CPUAllocationPolicy.DEDICATED):
6265
primitive['cpuset'] = primitive['pcpuset']
6366
primitive.pop('pcpuset', None)
67+
LOG.warning(
68+
f'Downgrading InstanceNUMACell to version {target_version} '
69+
f'may cause the loss of pinned CPUs if mixing different '
70+
f'verisons of nova on different hosts. This should not '
71+
f'happen on any supported version after Victoria.')
6472

6573
if target_version < (1, 4):
6674
primitive.pop('cpuset_reserved', None)
@@ -188,15 +196,79 @@ def _migrate_legacy_dedicated_instance_cpuset(cls, obj):
188196
# come from instance_extra or request_spec too.
189197
update_db = False
190198
for cell in obj.cells:
191-
if len(cell.cpuset) == 0:
199+
version = versionutils.convert_version_to_tuple(cell.VERSION)
200+
201+
if version < (1, 4):
202+
LOG.warning(
203+
"InstanceNUMACell %s with version %s for instance %s has "
204+
"too old version in the DB, don't know how to update, "
205+
"ignoring.", cell, cell.VERSION, obj.instance_uuid)
192206
continue
193207

194-
if cell.cpu_policy != obj_fields.CPUAllocationPolicy.DEDICATED:
208+
if (version >= (1, 5) and
209+
cell.cpu_policy == obj_fields.CPUAllocationPolicy.DEDICATED and
210+
(cell.cpuset or not cell.pcpuset)
211+
):
212+
LOG.warning(
213+
"InstanceNUMACell %s with version %s is inconsistent as "
214+
"the version is 1.5 or greater, cpu_policy is dedicated, "
215+
"but cpuset is not empty or pcpuset is empty.",
216+
cell, cell.VERSION)
195217
continue
196218

197-
cell.pcpuset = cell.cpuset
198-
cell.cpuset = set()
199-
update_db = True
219+
# NOTE(gibi): The data migration between 1.4. and 1.5 populates the
220+
# pcpuset field that is new in version 1.5. However below we update
221+
# the object version to 1.6 directly. This is intentional. The
222+
# version 1.6 introduced a new possible value 'mixed' for the
223+
# cpu_policy field. As that is a forward compatible change we don't
224+
# have a specific data migration for it. But we also don't have an
225+
# automated way to update old object versions from 1.5 to 1.6. So
226+
# we do it here just to avoid inconsistency between data and
227+
# version in the DB.
228+
if version < (1, 6):
229+
if cell.cpu_policy == obj_fields.CPUAllocationPolicy.DEDICATED:
230+
if "pcpuset" not in cell or not cell.pcpuset:
231+
# this cell was never migrated to 1.6, migrate it.
232+
cell.pcpuset = cell.cpuset
233+
cell.cpuset = set()
234+
cell.VERSION = '1.6'
235+
update_db = True
236+
else:
237+
# This data was already migrated to 1.6 format but the
238+
# version string wasn't updated to 1.6. This happened
239+
# before the fix
240+
# https://bugs.launchpad.net/nova/+bug/2097360
241+
# Only update the version string.
242+
cell.VERSION = '1.6'
243+
update_db = True
244+
elif cell.cpu_policy in (
245+
None, obj_fields.CPUAllocationPolicy.SHARED):
246+
# no data migration needed just add the new field and
247+
# stamp the new version in the DB
248+
cell.pcpuset = set()
249+
cell.VERSION = '1.6'
250+
update_db = True
251+
else: # obj_fields.CPUAllocationPolicy.MIXED
252+
# This means the cell data already got updated to the 1.6
253+
# content as MIXED only supported with 1.6 but the version
254+
# was not updated to 1.6.
255+
# We should not do the data migration as that would trample
256+
# the pcpuset field. Just stamp the 1.6 version in the DB
257+
# and hope for the best.
258+
LOG.warning(
259+
"InstanceNUMACell %s with version %s for instance %s "
260+
"has older than 1.6 version in the DB but using the "
261+
"1.6 feature CPUAllocationPolicy.MIXED. So nova "
262+
"assumes that the data is in 1.6 format and only the "
263+
"version string is old. Correcting the version string "
264+
"in the DB.", cell, cell.VERSION, obj.instance_uuid)
265+
cell.VERSION = '1.6'
266+
update_db = True
267+
268+
# When the next ovo version 1.7 is added it needs to be handed
269+
# here to do any migration if needed and to ensure the version in
270+
# the DB is stamped to 1.7
271+
200272
return update_db
201273

202274
# TODO(huaqiang): Remove after Yoga once we are sure these objects have

nova/test.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -482,7 +482,7 @@ def _start_compute(self, host, cell_name=None):
482482
self.computes[host] = compute
483483
return compute
484484

485-
def _run_periodics(self):
485+
def _run_periodics(self, raise_on_error=False):
486486
"""Run the update_available_resource task on every compute manager
487487
488488
This runs periodics on the computes in an undefined order; some child
@@ -497,6 +497,15 @@ class redefine this function to force a specific order.
497497
with context.target_cell(
498498
ctx, self.host_mappings[host].cell_mapping) as cctxt:
499499
compute.manager.update_available_resource(cctxt)
500+
501+
if raise_on_error:
502+
if 'Traceback (most recent call last' in self.stdlog.logger.output:
503+
# Get the last line of the traceback, for example:
504+
# TypeError: virNodeDeviceLookupByName() argument 2 must be
505+
# str or None, not Proxy
506+
last_tb_line = self.stdlog.logger.output.splitlines()[-1]
507+
raise TestingException(last_tb_line)
508+
500509
LOG.info('Finished with periodics')
501510

502511
def restart_compute_service(self, compute, keep_hypervisor_state=True):

nova/tests/fixtures/libvirt.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2067,6 +2067,17 @@ def device_lookup_by_name(self, dev_name):
20672067
return self.pci_info.get_device_by_name(dev_name)
20682068

20692069
def nodeDeviceLookupByName(self, name):
2070+
# See bug https://bugs.launchpad.net/nova/+bug/2098892
2071+
# We don't test this by importing the libvirt module because the
2072+
# libvirt module is forbidden to be imported into our test
2073+
# environment. It is excluded from test-requirements.txt and we
2074+
# also use the ImportModulePoisonFixture in nova/test.py to prevent
2075+
# use of modules such as libvirt.
2076+
if not isinstance(name, str) and name is not None:
2077+
raise TypeError(
2078+
'virNodeDeviceLookupByName() argument 2 must be str or '
2079+
f'None, not {type(name)}')
2080+
20702081
if name.startswith('mdev'):
20712082
return self.mdev_info.get_device_by_name(name)
20722083

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
2+
# not use this file except in compliance with the License. You may obtain
3+
# a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
# License for the specific language governing permissions and limitations
11+
# under the License.
12+
13+
from nova.tests.fixtures import libvirt as fakelibvirt
14+
from nova.tests.functional.libvirt import test_vgpu
15+
16+
17+
class VGPUTestsListDevices(test_vgpu.VGPUTestBase):
18+
"""Regression test for bug 2098892.
19+
20+
Test that nodeDeviceLookupByName() is called with valid types to prevent:
21+
22+
File "/usr/lib64/python3.9/site-packages/libvirt.py", line 5201, in
23+
nodeDeviceLookupByName ret =
24+
libvirtmod.virNodeDeviceLookupByName(self._o, name)
25+
TypeError: virNodeDeviceLookupByName() argument 2 must be str or None,
26+
not Proxy
27+
28+
in the future. This test relies on the LibvirtFixture checking for the
29+
correct types in its nodeDeviceLookupByName() method and raising TypeError
30+
if they are invalid.
31+
32+
We don't test this by importing the libvirt module because the libvirt
33+
module is forbidden to be imported into our test environment. It is
34+
excluded from test-requirements.txt and we also use the
35+
ImportModulePoisonFixture in nova/test.py to prevent use of modules such as
36+
libvirt.
37+
"""
38+
39+
def setUp(self):
40+
super().setUp()
41+
42+
# Start compute supporting only nvidia-11
43+
self.flags(
44+
enabled_mdev_types=fakelibvirt.NVIDIA_11_VGPU_TYPE,
45+
group='devices')
46+
47+
self.start_compute_with_vgpu('host1')
48+
49+
def test_update_available_resource(self):
50+
# We only want to verify no errors were logged by
51+
# update_available_resource (logging under the 'except Exception:').
52+
self._run_periodics(raise_on_error=True)

nova/tests/functional/test_servers.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3207,6 +3207,73 @@ def test_finish_resize_fails_allocation_cleanup(self):
32073207
self._test_resize_to_same_host_instance_fails(
32083208
'_finish_resize', 'compute_finish_resize')
32093209

3210+
def _verify_swap_resize_in_bdm(self, server_id, swap_size):
3211+
"""Verify swap dev in BDM"""
3212+
ctxt = context.get_admin_context()
3213+
bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(
3214+
ctxt, server_id)
3215+
if swap_size != 0:
3216+
self.assertIn('swap', [bdm.guest_format for bdm in bdms])
3217+
swaps = [
3218+
bdm.volume_size for bdm in bdms if bdm.guest_format == 'swap']
3219+
self.assertEqual(len(swaps), 1)
3220+
self.assertIn(swap_size, swaps)
3221+
else:
3222+
self.assertNotIn('swap', [bdm.guest_format for bdm in bdms])
3223+
3224+
def _test_swap_resize(self, swap1, swap2, confirm=True):
3225+
fl_1 = self._create_flavor(swap=swap1)
3226+
fl_2 = self._create_flavor(swap=swap2)
3227+
server = self._create_server(flavor_id=fl_1, networks=[])
3228+
# before resize
3229+
self.assertEqual(server['flavor']['swap'], swap1)
3230+
server = self._resize_server(server, fl_2)
3231+
self.assertEqual(server['flavor']['swap'], swap2)
3232+
self._verify_swap_resize_in_bdm(server['id'], swap2)
3233+
3234+
if confirm:
3235+
server = self._confirm_resize(server)
3236+
# after resize
3237+
self.assertEqual(server['flavor']['swap'], swap2)
3238+
# verify block device mapping
3239+
self._verify_swap_resize_in_bdm(server['id'], swap2)
3240+
else:
3241+
server = self._revert_resize(server)
3242+
# after revert
3243+
self.assertEqual(server['flavor']['swap'], swap1)
3244+
# verify block device mapping
3245+
self._verify_swap_resize_in_bdm(server['id'], swap1)
3246+
3247+
def test_swap_expand_0_to_0_confirm(self):
3248+
self._test_swap_resize(0, 0)
3249+
3250+
def test_swap_expand_0_to_1024_confirm(self):
3251+
self._test_swap_resize(0, 1024)
3252+
3253+
def test_swap_expand_0_to_1024_revert(self):
3254+
self._test_swap_resize(0, 1024, confirm=False)
3255+
3256+
def test_swap_expand_1024_to_2048_confirm(self):
3257+
self._test_swap_resize(1024, 2048)
3258+
3259+
def test_swap_expand_1024_to_2048_revert(self):
3260+
self._test_swap_resize(1024, 2048, confirm=False)
3261+
3262+
def test_swap_expand_2048_to_2048_confirm(self):
3263+
self._test_swap_resize(2048, 2048)
3264+
3265+
def test_swap_shrink_1024_to_0_confirm(self):
3266+
self._test_swap_resize(1024, 0)
3267+
3268+
def test_swap_shrink_1024_to_0_revert(self):
3269+
self._test_swap_resize(1024, 0, confirm=False)
3270+
3271+
def test_swap_shrink_2048_to_1024_confirm(self):
3272+
self._test_swap_resize(2048, 1024)
3273+
3274+
def test_swap_shrink_2048_to_1024_revert(self):
3275+
self._test_swap_resize(2048, 1024, confirm=False)
3276+
32103277
def _server_created_with_host(self):
32113278
hostname = self.compute1.host
32123279
server_req = self._build_server(

0 commit comments

Comments
 (0)