From a9d77265dbf29aecb66184a9147b4ae544ba96d6 Mon Sep 17 00:00:00 2001 From: Fabian Koller Date: Thu, 21 May 2026 10:48:22 +0200 Subject: [PATCH] compute: RequestSpec sanitization for cross-HV resize Sanitize VMware-specific image properties directly on the canonical RequestSpec before scheduling, matching how request_spec.flavor is already handled in the cold migrate flow. The sanitizer mutates request_spec.image.properties in place and returns a dict of original values. After task.execute() succeeds, the conductor persists this into MigrationContext.old_image_properties (via instance.save()) before calling request_spec.save(). This ordering ensures the rollback copy is always available if request_spec has been rewritten. Changes: - nova/compute/utils.py: add sanitize_image_props_for_kvm() - nova/conductor/tasks/migrate.py: call sanitizer in _execute() - nova/conductor/manager.py: persist old_image_properties to MigrationContext before request_spec.save() Change-Id: I4f7035358a9a7f46d942bfad07d748d1d333f8c5 --- nova/compute/utils.py | 59 ++++++++ nova/conductor/manager.py | 18 +++ nova/conductor/tasks/migrate.py | 19 +++ nova/objects/migration_context.py | 6 +- nova/tests/unit/compute/test_utils.py | 131 ++++++++++++++++++ .../unit/conductor/tasks/test_migrate.py | 80 +++++++++++ nova/tests/unit/conductor/test_conductor.py | 65 +++++++++ .../unit/objects/test_migration_context.py | 14 ++ nova/tests/unit/objects/test_objects.py | 2 +- 9 files changed, 392 insertions(+), 2 deletions(-) diff --git a/nova/compute/utils.py b/nova/compute/utils.py index 749e3a11f4c..8966cac401a 100644 --- a/nova/compute/utils.py +++ b/nova/compute/utils.py @@ -34,6 +34,7 @@ from nova.compute import vm_states import nova.conf from nova import exception +from nova.network import model as network_model from nova import notifications from nova.notifications.objects import aggregate as aggregate_notification from nova.notifications.objects import base as notification_base @@ -301,6 +302,64 @@ def heal_reqspec_is_bfv(ctxt, request_spec, instance): request_spec.save() +def sanitize_image_props_for_kvm(request_spec): + """Sanitize VMware-specific image properties for KVM scheduling. + + Mutates request_spec.image.properties in place to remove or replace + VMware-pinning values so the scheduler can select KVM/CH hosts. + + Returns a dict of original values that were overwritten or removed, + suitable for storage in MigrationContext.old_image_properties as a + rollback journal. + + Precondition: only valid for VMware-to-KVM cross-hypervisor resize. + + :param request_spec: nova.objects.RequestSpec to sanitize in place + :returns: dict of {field_name: original_value} for rollback, or + empty dict if nothing was changed + """ + if (not request_spec.obj_attr_is_set('image') or + request_spec.image is None): + return {} + + image = request_spec.image + if not image.obj_attr_is_set('properties'): + return {} + + props = image.properties + old = {} + + if props.obj_attr_is_set('img_hv_type'): + old['img_hv_type'] = props.img_hv_type + delattr(props, 'img_hv_type') + + if props.obj_attr_is_set('hw_disk_bus'): + old['hw_disk_bus'] = props.hw_disk_bus + props.hw_disk_bus = fields.DiskBus.VIRTIO + + if props.obj_attr_is_set('hw_cdrom_bus'): + old['hw_cdrom_bus'] = props.hw_cdrom_bus + props.hw_cdrom_bus = fields.DiskBus.VIRTIO + + if props.obj_attr_is_set('hw_scsi_model'): + old['hw_scsi_model'] = props.hw_scsi_model + delattr(props, 'hw_scsi_model') + + if props.obj_attr_is_set('hw_vif_model'): + old['hw_vif_model'] = props.hw_vif_model + props.hw_vif_model = network_model.VIF_MODEL_VIRTIO + + if props.obj_attr_is_set('hw_video_model'): + old['hw_video_model'] = props.hw_video_model + props.hw_video_model = fields.VideoModel.VIRTIO + + if props.obj_attr_is_set('img_hv_requested_version'): + old['img_hv_requested_version'] = props.img_hv_requested_version + delattr(props, 'img_hv_requested_version') + + return old + + def convert_mb_to_ceil_gb(mb_value): gb_int = 0 if mb_value: diff --git a/nova/conductor/manager.py b/nova/conductor/manager.py index b7a86c10967..47fa14416a4 100644 --- a/nova/conductor/manager.py +++ b/nova/conductor/manager.py @@ -449,6 +449,24 @@ def _cold_migrate(self, context, instance, flavor, filter_properties, # NOTE(sbauza): Make sure we persist the new flavor in case we had # a successful scheduler call if and only if nothing bad happened if request_spec.obj_what_changed(): + # Persist the rollback journal for image properties BEFORE + # saving the modified request_spec, so that revert can always + # find the originals even if a crash occurs between these two + # writes. + old_img_props = getattr(task, '_old_image_properties', None) + if old_img_props and isinstance(old_img_props, dict): + if ('migration_context' in instance and + instance.migration_context is not None): + mig_context = instance.migration_context + else: + mig_context = objects.MigrationContext( + context=context, + instance_uuid=instance.uuid, + migration_id=task._migration.id) + mig_context.old_image_properties = old_img_props + instance.migration_context = mig_context + instance.save() + # NOTE(jkulik): Make sure we do not store the "volume_sizes" # scheduler_hint as that can change at any time and saving it in # the DB thus is not useful. diff --git a/nova/conductor/tasks/migrate.py b/nova/conductor/tasks/migrate.py index 754f9e5ba7d..34ba3a66aae 100644 --- a/nova/conductor/tasks/migrate.py +++ b/nova/conductor/tasks/migrate.py @@ -133,6 +133,15 @@ def __init__(self, context, instance, flavor, self._held_allocations = None self._source_cn = None + def _is_vmware_to_kvm_resize(self): + """Return True if this resize is specifically VMware to KVM. + + Stub — always returns False until VMware-to-KVM detection is + implemented (see: Add Cross-Hypervisor Resize Detection and + Guardrails ticket). + """ + return False + def _preallocate_migration(self): # If this is a rescheduled migration, don't create a new record. migration_type = ("resize" if self.instance.flavor.id != self.flavor.id @@ -283,6 +292,16 @@ def _execute(self): self.request_spec.ensure_network_information(self.instance) compute_utils.heal_reqspec_is_bfv( self.context, self.request_spec, self.instance) + # For cross-hypervisor resize (VMware to KVM), sanitize the image + # properties directly on the canonical request_spec so the scheduler + # can select KVM/CH hosts. The returned dict of original values is + # stashed for the conductor to persist into MigrationContext before + # request_spec.save(). + self._old_image_properties = {} + if self._is_vmware_to_kvm_resize(): + self._old_image_properties = ( + compute_utils.sanitize_image_props_for_kvm( + self.request_spec)) # On an initial call to migrate, 'self.host_list' will be None, so we # have to call the scheduler to get a list of acceptable hosts to # migrate to. That list will consist of a selected host, along with diff --git a/nova/objects/migration_context.py b/nova/objects/migration_context.py index c00e471b0e3..67a21203e1a 100644 --- a/nova/objects/migration_context.py +++ b/nova/objects/migration_context.py @@ -40,7 +40,8 @@ class MigrationContext(base.NovaPersistentObject, base.NovaObject): # Version 1.0: Initial version # Version 1.1: Add old/new pci_devices and pci_requests # Version 1.2: Add old/new resources - VERSION = '1.2' + # Version 1.3: Add old_image_properties + VERSION = '1.3' fields = { 'instance_uuid': fields.UUIDField(), @@ -61,11 +62,14 @@ class MigrationContext(base.NovaPersistentObject, base.NovaObject): nullable=True), 'old_resources': fields.ObjectField('ResourceList', nullable=True), + 'old_image_properties': fields.DictOfStringsField(nullable=True), } @classmethod def obj_make_compatible(cls, primitive, target_version): target_version = versionutils.convert_version_to_tuple(target_version) + if target_version < (1, 3): + primitive.pop('old_image_properties', None) if target_version < (1, 2): primitive.pop('old_resources', None) primitive.pop('new_resources', None) diff --git a/nova/tests/unit/compute/test_utils.py b/nova/tests/unit/compute/test_utils.py index 32ebcebc3f8..edd031d4570 100644 --- a/nova/tests/unit/compute/test_utils.py +++ b/nova/tests/unit/compute/test_utils.py @@ -2074,3 +2074,134 @@ def test_delete_with_arq_uuid_and_dp(self, mock_del_inst, mock_del_uuid): compute_utils.delete_arqs_if_needed(self.context, instance, arq_uuids) mock_del_inst.assert_called_once_with(instance.uuid) mock_del_uuid.assert_called_once_with(arq_uuids) + + +class TestSanitizeImagePropsForKvm(test.NoDBTestCase): + """Tests for sanitize_image_props_for_kvm (early-commit approach).""" + + def _make_request_spec(self, **image_props): + props = objects.ImageMetaProps(**image_props) + image = objects.ImageMeta(properties=props) + return objects.RequestSpec(image=image) + + def test_img_hv_type_removed_and_journaled(self): + reqspec = self._make_request_spec(img_hv_type='vmware') + old = compute_utils.sanitize_image_props_for_kvm(reqspec) + self.assertFalse( + reqspec.image.properties.obj_attr_is_set('img_hv_type')) + self.assertEqual('vmware', old['img_hv_type']) + + def test_img_hv_type_any_value_removed(self): + reqspec = self._make_request_spec(img_hv_type='kvm') + old = compute_utils.sanitize_image_props_for_kvm(reqspec) + self.assertFalse( + reqspec.image.properties.obj_attr_is_set('img_hv_type')) + self.assertEqual('kvm', old['img_hv_type']) + + def test_hw_disk_bus_replaced_with_virtio(self): + reqspec = self._make_request_spec(hw_disk_bus='scsi') + old = compute_utils.sanitize_image_props_for_kvm(reqspec) + self.assertEqual('virtio', reqspec.image.properties.hw_disk_bus) + self.assertEqual('scsi', old['hw_disk_bus']) + + def test_hw_disk_bus_already_virtio_still_journaled(self): + reqspec = self._make_request_spec(hw_disk_bus='virtio') + old = compute_utils.sanitize_image_props_for_kvm(reqspec) + self.assertEqual('virtio', reqspec.image.properties.hw_disk_bus) + self.assertEqual('virtio', old['hw_disk_bus']) + + def test_hw_scsi_model_removed_and_journaled(self): + reqspec = self._make_request_spec(hw_scsi_model='virtio-scsi') + old = compute_utils.sanitize_image_props_for_kvm(reqspec) + self.assertFalse( + reqspec.image.properties.obj_attr_is_set('hw_scsi_model')) + self.assertEqual('virtio-scsi', old['hw_scsi_model']) + + def test_hw_scsi_model_absent_no_error(self): + reqspec = self._make_request_spec(hw_disk_bus='scsi') + old = compute_utils.sanitize_image_props_for_kvm(reqspec) + self.assertNotIn('hw_scsi_model', old) + + def test_hw_vif_model_replaced_with_virtio(self): + reqspec = self._make_request_spec(hw_vif_model='vmxnet3') + old = compute_utils.sanitize_image_props_for_kvm(reqspec) + self.assertEqual('virtio', reqspec.image.properties.hw_vif_model) + self.assertEqual('vmxnet3', old['hw_vif_model']) + + def test_hw_video_model_replaced_with_virtio(self): + reqspec = self._make_request_spec(hw_video_model='vmvga') + old = compute_utils.sanitize_image_props_for_kvm(reqspec) + self.assertEqual('virtio', + reqspec.image.properties.hw_video_model) + self.assertEqual('vmvga', old['hw_video_model']) + + def test_mutates_in_place(self): + reqspec = self._make_request_spec( + img_hv_type='vmware', hw_disk_bus='scsi') + props_before = reqspec.image.properties + compute_utils.sanitize_image_props_for_kvm(reqspec) + self.assertIs(props_before, reqspec.image.properties) + + def test_returns_complete_journal(self): + reqspec = self._make_request_spec( + img_hv_type='vmware', hw_disk_bus='scsi', + hw_cdrom_bus='ide', hw_scsi_model='virtio-scsi', + hw_vif_model='vmxnet3', hw_video_model='vmvga', + img_hv_requested_version='>=6.0') + old = compute_utils.sanitize_image_props_for_kvm(reqspec) + self.assertEqual({ + 'img_hv_type': 'vmware', + 'hw_disk_bus': 'scsi', + 'hw_cdrom_bus': 'ide', + 'hw_scsi_model': 'virtio-scsi', + 'hw_vif_model': 'vmxnet3', + 'hw_video_model': 'vmvga', + 'img_hv_requested_version': '>=6.0', + }, old) + + def test_no_image_returns_empty_dict(self): + reqspec = objects.RequestSpec() + old = compute_utils.sanitize_image_props_for_kvm(reqspec) + self.assertEqual({}, old) + + def test_image_none_returns_empty_dict(self): + reqspec = objects.RequestSpec(image=None) + old = compute_utils.sanitize_image_props_for_kvm(reqspec) + self.assertEqual({}, old) + + def test_hw_cdrom_bus_replaced_with_virtio(self): + reqspec = self._make_request_spec(hw_cdrom_bus='ide') + old = compute_utils.sanitize_image_props_for_kvm(reqspec) + self.assertEqual('virtio', reqspec.image.properties.hw_cdrom_bus) + self.assertEqual('ide', old['hw_cdrom_bus']) + + def test_hw_cdrom_bus_sata_replaced_with_virtio(self): + reqspec = self._make_request_spec(hw_cdrom_bus='sata') + old = compute_utils.sanitize_image_props_for_kvm(reqspec) + self.assertEqual('virtio', reqspec.image.properties.hw_cdrom_bus) + self.assertEqual('sata', old['hw_cdrom_bus']) + + def test_hw_cdrom_bus_already_virtio_still_journaled(self): + reqspec = self._make_request_spec(hw_cdrom_bus='virtio') + old = compute_utils.sanitize_image_props_for_kvm(reqspec) + self.assertEqual('virtio', reqspec.image.properties.hw_cdrom_bus) + self.assertEqual('virtio', old['hw_cdrom_bus']) + + def test_img_hv_requested_version_removed_and_journaled(self): + reqspec = self._make_request_spec( + img_hv_requested_version='>=6.0,<7.0') + old = compute_utils.sanitize_image_props_for_kvm(reqspec) + self.assertFalse( + reqspec.image.properties.obj_attr_is_set( + 'img_hv_requested_version')) + self.assertEqual('>=6.0,<7.0', old['img_hv_requested_version']) + + def test_img_hv_requested_version_absent_no_error(self): + reqspec = self._make_request_spec(img_hv_type='vmware') + old = compute_utils.sanitize_image_props_for_kvm(reqspec) + self.assertNotIn('img_hv_requested_version', old) + + def test_no_props_set_returns_empty_dict(self): + reqspec = self._make_request_spec() + old = compute_utils.sanitize_image_props_for_kvm(reqspec) + self.assertEqual({}, old) diff --git a/nova/tests/unit/conductor/tasks/test_migrate.py b/nova/tests/unit/conductor/tasks/test_migrate.py index 46cb033c5c6..fd375e9537c 100644 --- a/nova/tests/unit/conductor/tasks/test_migrate.py +++ b/nova/tests/unit/conductor/tasks/test_migrate.py @@ -62,6 +62,11 @@ def setUp(self): self.heal_reqspec_is_bfv_mock = _p.start() self.addCleanup(_p.stop) + _p = mock.patch('nova.compute.utils.sanitize_image_props_for_kvm') + self.sanitize_mock = _p.start() + self.sanitize_mock.return_value = {} + self.addCleanup(_p.stop) + _p = mock.patch('nova.objects.RequestSpec.ensure_network_information') self.ensure_network_information_mock = _p.start() self.addCleanup(_p.stop) @@ -393,6 +398,81 @@ def test_is_selected_host_in_source_cell_false(self): selection = objects.Selection(cell_uuid=uuids.cell2, service_host='x') self.assertFalse(task._is_selected_host_in_source_cell(selection)) + @mock.patch.object(objects.MigrationList, 'get_by_filters') + @mock.patch('nova.scheduler.client.report.SchedulerReportClient') + @mock.patch('nova.objects.ComputeNode.get_by_host_and_nodename') + @mock.patch('nova.objects.Migration.save') + @mock.patch('nova.objects.Migration.create') + @mock.patch('nova.objects.Service.get_minimum_version_multi') + @mock.patch('nova.availability_zones.get_host_availability_zone') + @mock.patch.object(scheduler_utils, 'setup_instance_group') + @mock.patch.object(query.SchedulerQueryClient, 'select_destinations') + @mock.patch.object(compute_rpcapi.ComputeAPI, 'prep_resize') + def test_execute_calls_sanitize_and_stashes_journal( + self, prep_resize_mock, sel_dest_mock, sig_mock, az_mock, + gmv_mock, cm_mock, sm_mock, cn_mock, rc_mock, gbf_mock): + """Verify sanitize is called for cross-HV and result stashed.""" + sel_dest_mock.return_value = self.host_lists + az_mock.return_value = 'myaz' + gbf_mock.return_value = objects.MigrationList() + mock_get_resources = \ + self.mock_network_api.get_requested_resource_for_instance + mock_get_resources.return_value = ([], objects.RequestLevelParams()) + gmv_mock.return_value = 23 + + fake_journal = {'img_hv_type': 'vmware', 'hw_disk_bus': 'scsi'} + self.sanitize_mock.return_value = fake_journal + + task = self._generate_task() + + def set_migration_uuid(*a, **k): + task._migration.uuid = uuids.migration + return mock.MagicMock() + + cn_mock.side_effect = set_migration_uuid + + with mock.patch.object(task, '_is_vmware_to_kvm_resize', + return_value=True): + task.execute() + + self.sanitize_mock.assert_called_once_with(self.request_spec) + self.assertEqual(fake_journal, task._old_image_properties) + + @mock.patch.object(objects.MigrationList, 'get_by_filters') + @mock.patch('nova.scheduler.client.report.SchedulerReportClient') + @mock.patch('nova.objects.ComputeNode.get_by_host_and_nodename') + @mock.patch('nova.objects.Migration.save') + @mock.patch('nova.objects.Migration.create') + @mock.patch('nova.objects.Service.get_minimum_version_multi') + @mock.patch('nova.availability_zones.get_host_availability_zone') + @mock.patch.object(scheduler_utils, 'setup_instance_group') + @mock.patch.object(query.SchedulerQueryClient, 'select_destinations') + @mock.patch.object(compute_rpcapi.ComputeAPI, 'prep_resize') + def test_execute_skips_sanitize_for_same_hv( + self, prep_resize_mock, sel_dest_mock, sig_mock, az_mock, + gmv_mock, cm_mock, sm_mock, cn_mock, rc_mock, gbf_mock): + """Verify sanitize is NOT called for same-HV resize.""" + sel_dest_mock.return_value = self.host_lists + az_mock.return_value = 'myaz' + gbf_mock.return_value = objects.MigrationList() + mock_get_resources = \ + self.mock_network_api.get_requested_resource_for_instance + mock_get_resources.return_value = ([], objects.RequestLevelParams()) + gmv_mock.return_value = 23 + + task = self._generate_task() + + def set_migration_uuid(*a, **k): + task._migration.uuid = uuids.migration + return mock.MagicMock() + + cn_mock.side_effect = set_migration_uuid + # _is_vmware_to_kvm_resize stub returns False by default + task.execute() + + self.sanitize_mock.assert_not_called() + self.assertEqual({}, task._old_image_properties) + class MigrationTaskAllocationUtils(test.NoDBTestCase): @mock.patch('nova.objects.ComputeNode.get_by_host_and_nodename') diff --git a/nova/tests/unit/conductor/test_conductor.py b/nova/tests/unit/conductor/test_conductor.py index dce7b9047ff..97dba40b088 100644 --- a/nova/tests/unit/conductor/test_conductor.py +++ b/nova/tests/unit/conductor/test_conductor.py @@ -4086,6 +4086,71 @@ def test_cold_migrate_updates_flavor_if_existing_reqspec(self, # ...and persisted spec_save_mock.assert_called_once_with() + @mock.patch.object(objects.Instance, 'get_bdms') + @mock.patch.object(objects.Instance, 'save') + @mock.patch.object(objects.RequestSpec, 'save') + @mock.patch.object(migrate.MigrationTask, 'execute') + @mock.patch.object(utils, 'get_image_from_system_metadata') + def test_cold_migrate_persists_old_image_properties( + self, image_mock, task_exec_mock, spec_save_mock, + inst_save_mock, bdms_mock): + """Verify old_image_properties is persisted to MigrationContext.""" + inst_obj = objects.Instance( + image_ref='fake-image_ref', + vm_state=vm_states.STOPPED, + instance_type_id=self.flavor.id, + system_metadata={}, + uuid=uuids.instance, + user_id=fakes.FAKE_USER_ID, + flavor=self.flavor, + availability_zone=None, + pci_requests=None, + numa_topology=None, + host='host1', + node='node1', + migration_context=None) + image = 'fake-image' + fake_spec = fake_request_spec.fake_spec_obj() + fake_journal = {'img_hv_type': 'vmware', 'hw_disk_bus': 'scsi'} + save_order = [] + + bdms_mock.return_value = [] + image_mock.return_value = image + inst_save_mock.side_effect = lambda *a, **k: save_order.append( + 'instance.save') + spec_save_mock.side_effect = lambda *a, **k: save_order.append( + 'request_spec.save') + + original_build = self.conductor._build_cold_migrate_task + + def patched_build(*args, **kwargs): + task = original_build(*args, **kwargs) + + def fake_task_execute(): + task._old_image_properties = fake_journal + task._migration = objects.Migration(id=1) + + task.execute = fake_task_execute + return task + + with mock.patch.object(self.conductor, + '_build_cold_migrate_task', + side_effect=patched_build): + self.conductor._cold_migrate( + self.context, inst_obj, self.flavor, {}, + True, fake_spec, None) + + # Verify instance.save() was called to persist the MigrationContext + inst_save_mock.assert_called() + # Verify MigrationContext has old_image_properties + self.assertIsNotNone(inst_obj.migration_context) + self.assertEqual( + fake_journal, + inst_obj.migration_context.old_image_properties) + # And reqspec was saved after + spec_save_mock.assert_called() + self.assertEqual(['instance.save', 'request_spec.save'], save_order) + @mock.patch.object(objects.Instance, 'get_bdms') @mock.patch('nova.objects.RequestSpec.from_primitives') @mock.patch.object(objects.RequestSpec, 'save') diff --git a/nova/tests/unit/objects/test_migration_context.py b/nova/tests/unit/objects/test_migration_context.py index 12becaee38c..a76559307ad 100644 --- a/nova/tests/unit/objects/test_migration_context.py +++ b/nova/tests/unit/objects/test_migration_context.py @@ -38,6 +38,10 @@ fake_migration_context_obj.old_pci_requests = None fake_migration_context_obj.new_resources = objects.ResourceList() fake_migration_context_obj.old_resources = None +fake_migration_context_obj.old_image_properties = { + 'img_hv_type': 'vmware', + 'hw_disk_bus': 'scsi', +} fake_db_context = { 'created_at': None, @@ -104,6 +108,8 @@ def _test_get_by_instance_uuid(self, db_data): mig_context.new_resources.__class__) self.assertIsInstance(expected_mig_context.old_resources, mig_context.old_resources.__class__) + self.assertEqual(expected_mig_context.old_image_properties, + mig_context.old_image_properties) else: self.assertIsNone(mig_context) @@ -135,6 +141,14 @@ def test_is_cross_cell_move(self, mock_get_by_id): self.assertTrue(mig_ctx.is_cross_cell_move()) mock_get_by_id.assert_called_once_with(ctxt, mig_ctx.migration_id) + def test_obj_make_compatible_pre_1_3(self): + primitive = fake_migration_context_obj.obj_to_primitive() + + fake_migration_context_obj.obj_make_compatible( + primitive['nova_object.data'], '1.2') + + self.assertNotIn('old_image_properties', primitive['nova_object.data']) + class TestMigrationContext(test_objects._LocalTest, _TestMigrationContext): diff --git a/nova/tests/unit/objects/test_objects.py b/nova/tests/unit/objects/test_objects.py index d0e612679b9..52293f8e2c2 100644 --- a/nova/tests/unit/objects/test_objects.py +++ b/nova/tests/unit/objects/test_objects.py @@ -1131,7 +1131,7 @@ def obj_name(cls): 'LibvirtVPMEMDevice': '1.0-17ffaf47585199eeb9a2b83d6bde069f', 'MemoryDiagnostics': '1.0-2c995ae0f2223bb0f8e523c5cc0b83da', 'Migration': '1.8-6ed577d80e71e9b9d88b8cf358af3781', - 'MigrationContext': '1.2-89f10a83999f852a489962ae37d8a026', + 'MigrationContext': '1.3-5a6f4e6ddeedf6ad1607da1fa7eed8e8', 'MigrationList': '1.5-36793f8d65bae421bd5564d09a4de7be', 'MonitorMetric': '1.1-53b1db7c4ae2c531db79761e7acc52ba', 'MonitorMetricList': '1.1-15ecf022a68ddbb8c2a6739cfc9f8f5e',