diff --git a/api-guide/source/accelerator-support.rst b/api-guide/source/accelerator-support.rst index cf9f185273d8..a33b3bdd664a 100644 --- a/api-guide/source/accelerator-support.rst +++ b/api-guide/source/accelerator-support.rst @@ -29,9 +29,8 @@ appropriate privileges) must do the following: openstack server create --flavor $myflavor --image $myimage $servername -As of 21.0.0 (Ussuri), nova supports only specific operations for instances -with accelerators. The lists of supported and unsupported operations are as -below: +Nova supports only specific operations for instances with accelerators. +The lists of supported and unsupported operations are as below: * Supported operations. @@ -42,17 +41,21 @@ below: * Take a snapshot. * Backup. * Rescue and unrescue. + * Rebuild. + * Evacuate. * Unsupported operations - * Rebuild. * Resize. - * Evacuate. * Suspend and resume. * Shelve and unshelve. * Cold migration. * Live migration. +.. versionchanged:: 22.0.0(Victoria) + + Added support for rebuild and evacuate operations. + Some operations, such as lock and unlock, work as they are effectively no-ops for accelerators. diff --git a/nova/accelerator/cyborg.py b/nova/accelerator/cyborg.py index bf74722a424d..b42ddd0e4659 100644 --- a/nova/accelerator/cyborg.py +++ b/nova/accelerator/cyborg.py @@ -302,3 +302,12 @@ class _CyborgClient(object): if err_msg: # No point raising an exception. LOG.error('Failed to delete ARQs %s', arq_uuid_str) + + def get_arq_uuids_for_instance(self, instance): + """Get ARQ UUIDs for the instance. + + :param instance: Instance Object + :return: ARQ UUIDs. + """ + return [arq['uuid'] + for arq in self.get_arqs_for_instance(instance.uuid)] diff --git a/nova/compute/api.py b/nova/compute/api.py index 056100d56b58..d3ddd1bf01f6 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -109,6 +109,8 @@ MIN_COMPUTE_SAME_HOST_COLD_MIGRATE = 48 # TODO(huaqiang): Remove in Wallaby MIN_VER_NOVA_COMPUTE_MIXED_POLICY = 52 +SUPPORT_ACCELERATOR_SERVICE_FOR_REBUILD = 53 + # FIXME(danms): Keep a global cache of the cells we find the # first time we look. This needs to be refreshed on a timer or # trigger. @@ -307,14 +309,27 @@ def _get_image_meta_obj(image_meta_dict): return image_meta -def block_accelerators(func): - @functools.wraps(func) - def wrapper(self, context, instance, *args, **kwargs): - dp_name = instance.flavor.extra_specs.get('accel:device_profile') - if dp_name: - raise exception.ForbiddenWithAccelerators() - return func(self, context, instance, *args, **kwargs) - return wrapper +def block_accelerators(until_service=None): + def inner(func): + @functools.wraps(func) + def wrapper(self, context, instance, *args, **kwargs): + # NOTE(brinzhang): Catch a request operating a mixed instance, + # make sure all nova-compute services have been upgraded and + # support the accelerators. + dp_name = instance.flavor.extra_specs.get('accel:device_profile') + service_support = False + if not dp_name: + service_support = True + elif until_service: + min_version = objects.service.get_minimum_version_all_cells( + nova_context.get_admin_context(), ['nova-compute']) + if min_version >= until_service: + service_support = True + if not service_support: + raise exception.ForbiddenWithAccelerators() + return func(self, context, instance, *args, **kwargs) + return wrapper + return inner @profiler.trace_cls("compute_api") @@ -3393,7 +3408,7 @@ class API(base.Base): fields_obj.Architecture.canonicalize(img_arch) @reject_vtpm_instances(instance_actions.REBUILD) - @block_accelerators + @block_accelerators(until_service=SUPPORT_ACCELERATOR_SERVICE_FOR_REBUILD) # TODO(stephenfin): We should expand kwargs out to named args @check_instance_lock @check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.STOPPED, @@ -3930,7 +3945,7 @@ class API(base.Base): # finally split resize and cold migration into separate code paths # TODO(stephenfin): The 'block_accelerators' decorator doesn't take into # account the accelerators requested in the new flavor - @block_accelerators + @block_accelerators() @check_instance_lock @check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.STOPPED]) @check_instance_host(check_is_up=True) @@ -4159,7 +4174,7 @@ class API(base.Base): return allow_same_host @reject_vtpm_instances(instance_actions.SHELVE) - @block_accelerators + @block_accelerators() @check_instance_lock @check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.STOPPED, vm_states.PAUSED, vm_states.SUSPENDED]) @@ -4324,7 +4339,7 @@ class API(base.Base): return self.compute_rpcapi.get_instance_diagnostics(context, instance=instance) - @block_accelerators + @block_accelerators() @reject_sev_instances(instance_actions.SUSPEND) @check_instance_lock @check_instance_state(vm_state=[vm_states.ACTIVE]) @@ -5028,7 +5043,7 @@ class API(base.Base): diff=diff) return _metadata - @block_accelerators + @block_accelerators() @reject_vtpm_instances(instance_actions.LIVE_MIGRATION) @reject_sev_instances(instance_actions.LIVE_MIGRATION) @check_instance_lock @@ -5160,7 +5175,7 @@ class API(base.Base): instance, migration.id) @reject_vtpm_instances(instance_actions.EVACUATE) - @block_accelerators + @block_accelerators(until_service=SUPPORT_ACCELERATOR_SERVICE_FOR_REBUILD) @check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.STOPPED, vm_states.ERROR]) def evacuate(self, context, instance, host, on_shared_storage, diff --git a/nova/compute/manager.py b/nova/compute/manager.py index b3317db582db..91c621afbf04 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -530,7 +530,7 @@ class ComputeVirtAPI(virtapi.VirtAPI): class ComputeManager(manager.Manager): """Manages the running instances from creation to destruction.""" - target = messaging.Target(version='5.11') + target = messaging.Target(version='5.12') def __init__(self, compute_driver=None, *args, **kwargs): """Load configuration options and connect to the hypervisor.""" @@ -3256,18 +3256,29 @@ class ComputeManager(manager.Manager): migration.status = status migration.save() - def _rebuild_default_impl(self, context, instance, image_meta, - injected_files, admin_password, allocations, - bdms, detach_block_devices, attach_block_devices, - network_info=None, - evacuate=False, block_device_info=None, - preserve_ephemeral=False): + def _rebuild_default_impl( + self, context, instance, image_meta, injected_files, + admin_password, allocations, bdms, detach_block_devices, + attach_block_devices, network_info=None, evacuate=False, + block_device_info=None, preserve_ephemeral=False, + accel_uuids=None): if preserve_ephemeral: # The default code path does not support preserving ephemeral # partitions. raise exception.PreserveEphemeralNotSupported() + accel_info = [] if evacuate: + if instance.flavor.extra_specs.get('accel:device_profile'): + try: + accel_info = self._get_bound_arq_resources( + context, instance, accel_uuids or []) + except (Exception, eventlet.timeout.Timeout) as exc: + LOG.exception(exc) + self._build_resources_cleanup(instance, network_info) + msg = _('Failure getting accelerator resources.') + raise exception.BuildAbortException( + instance_uuid=instance.uuid, reason=msg) detach_block_devices(context, bdms) else: self._power_off_instance(instance, clean_shutdown=True) @@ -3275,6 +3286,14 @@ class ComputeManager(manager.Manager): self.driver.destroy(context, instance, network_info=network_info, block_device_info=block_device_info) + try: + accel_info = self._get_accel_info(context, instance) + except Exception as exc: + LOG.exception(exc) + self._build_resources_cleanup(instance, network_info) + msg = _('Failure getting accelerator resources.') + raise exception.BuildAbortException( + instance_uuid=instance.uuid, reason=msg) instance.task_state = task_states.REBUILD_BLOCK_DEVICE_MAPPING instance.save(expected_task_state=[task_states.REBUILDING]) @@ -3289,7 +3308,8 @@ class ComputeManager(manager.Manager): self.driver.spawn(context, instance, image_meta, injected_files, admin_password, allocations, network_info=network_info, - block_device_info=new_block_device_info) + block_device_info=new_block_device_info, + accel_info=accel_info) def _notify_instance_rebuild_error(self, context, instance, error, bdms): self._notify_about_instance_usage(context, instance, @@ -3298,7 +3318,8 @@ class ComputeManager(manager.Manager): context, instance, self.host, phase=fields.NotificationPhase.ERROR, exception=error, bdms=bdms) - @messaging.expected_exceptions(exception.PreserveEphemeralNotSupported) + @messaging.expected_exceptions(exception.PreserveEphemeralNotSupported, + exception.BuildAbortException) @wrap_exception() @reverts_task_state @wrap_instance_event(prefix='compute') @@ -3307,7 +3328,7 @@ class ComputeManager(manager.Manager): injected_files, new_pass, orig_sys_metadata, bdms, recreate, on_shared_storage, preserve_ephemeral, migration, - scheduled_node, limits, request_spec): + scheduled_node, limits, request_spec, accel_uuids): """Destroy and re-make this instance. A 'rebuild' effectively purges all existing data from the system and @@ -3338,6 +3359,7 @@ class ComputeManager(manager.Manager): :param limits: Overcommit limits set by the scheduler. If a host was specified by the user, this will be None :param request_spec: a RequestSpec object used to schedule the instance + :param accel_uuids: a list of cyborg ARQ uuids. """ # recreate=True means the instance is being evacuated from a failed @@ -3402,7 +3424,7 @@ class ComputeManager(manager.Manager): image_meta, injected_files, new_pass, orig_sys_metadata, bdms, evacuate, on_shared_storage, preserve_ephemeral, migration, request_spec, allocs, rebuild_claim, - scheduled_node, limits) + scheduled_node, limits, accel_uuids) except (exception.ComputeResourcesUnavailable, exception.RescheduledException) as e: if isinstance(e, exception.ComputeResourcesUnavailable): @@ -3469,7 +3491,7 @@ class ComputeManager(manager.Manager): self, context, instance, orig_image_ref, image_meta, injected_files, new_pass, orig_sys_metadata, bdms, evacuate, on_shared_storage, preserve_ephemeral, migration, request_spec, - allocations, rebuild_claim, scheduled_node, limits): + allocations, rebuild_claim, scheduled_node, limits, accel_uuids): """Helper to avoid deep nesting in the top-level method.""" provider_mapping = None @@ -3490,7 +3512,7 @@ class ComputeManager(manager.Manager): context, instance, orig_image_ref, image_meta, injected_files, new_pass, orig_sys_metadata, bdms, evacuate, on_shared_storage, preserve_ephemeral, migration, request_spec, allocations, - provider_mapping) + provider_mapping, accel_uuids) @staticmethod def _get_image_name(image_meta): @@ -3499,12 +3521,12 @@ class ComputeManager(manager.Manager): else: return '' - def _do_rebuild_instance(self, context, instance, orig_image_ref, - image_meta, injected_files, new_pass, - orig_sys_metadata, bdms, evacuate, - on_shared_storage, preserve_ephemeral, - migration, request_spec, allocations, - request_group_resource_providers_mapping): + def _do_rebuild_instance( + self, context, instance, orig_image_ref, image_meta, + injected_files, new_pass, orig_sys_metadata, bdms, evacuate, + on_shared_storage, preserve_ephemeral, migration, request_spec, + allocations, request_group_resource_providers_mapping, + accel_uuids): orig_vm_state = instance.vm_state if evacuate: @@ -3645,7 +3667,8 @@ class ComputeManager(manager.Manager): block_device_info=block_device_info, network_info=network_info, preserve_ephemeral=preserve_ephemeral, - evacuate=evacuate) + evacuate=evacuate, + accel_uuids=accel_uuids) try: with instance.mutated_migration_context(): self.driver.rebuild(**kwargs) diff --git a/nova/compute/rpcapi.py b/nova/compute/rpcapi.py index 74d2f53f3c1f..c2dee603ebea 100644 --- a/nova/compute/rpcapi.py +++ b/nova/compute/rpcapi.py @@ -378,6 +378,8 @@ class ComputeAPI(object): * 5.10 - Add finish_revert_snapshot_based_resize_at_source() * 5.11 - Add accel_uuids (accelerator requests) parameter to build_and_run_instance() + * 5.12 - Add accel_uuids (accelerator requests) parameter to + rebuild_instance() ''' VERSION_ALIASES = { @@ -1056,20 +1058,29 @@ class ComputeAPI(object): block_device_info=block_device_info, reboot_type=reboot_type) - def rebuild_instance(self, ctxt, instance, new_pass, injected_files, + def rebuild_instance( + self, ctxt, instance, new_pass, injected_files, image_ref, orig_image_ref, orig_sys_metadata, bdms, recreate, on_shared_storage, host, node, - preserve_ephemeral, migration, limits, request_spec): + preserve_ephemeral, migration, limits, request_spec, accel_uuids): + # NOTE(edleafe): compute nodes can only use the dict form of limits. if isinstance(limits, objects.SchedulerLimits): limits = limits.to_dict() - msg_args = {'preserve_ephemeral': preserve_ephemeral, - 'migration': migration, - 'scheduled_node': node, - 'limits': limits, - 'request_spec': request_spec} - version = '5.0' + + msg_args = { + 'preserve_ephemeral': preserve_ephemeral, + 'migration': migration, + 'scheduled_node': node, + 'limits': limits, + 'request_spec': request_spec, + 'accel_uuids': accel_uuids + } + version = '5.12' client = self.router.client(ctxt) + if not client.can_send_version(version): + del msg_args['accel_uuids'] + version = '5.0' cctxt = client.prepare(server=_compute_host(host, instance), version=version) cctxt.cast(ctxt, 'rebuild_instance', diff --git a/nova/conductor/manager.py b/nova/conductor/manager.py index 1a3be3e84fac..aa79f8324e03 100644 --- a/nova/conductor/manager.py +++ b/nova/conductor/manager.py @@ -1147,14 +1147,21 @@ class ComputeTaskManager(base.Base): # is not forced to be the original host request_spec.reset_forced_destinations() - port_res_req = ( + external_resources = [] + external_resources += ( self.network_api.get_requested_resource_for_instance( context, instance.uuid)) - # NOTE(gibi): When cyborg or other module wants to handle - # similar non-nova resources then here we have to collect - # all the external resource requests in a single list and + extra_specs = request_spec.flavor.extra_specs + device_profile = extra_specs.get('accel:device_profile') + external_resources.extend( + cyborg.get_device_profile_request_groups( + context, device_profile) + if device_profile else []) + # NOTE(gibi): When other modules want to handle similar + # non-nova resources then here we have to collect all + # the external resource requests in a single list and # add them to the RequestSpec. - request_spec.requested_resources = port_res_req + request_spec.requested_resources = external_resources try: # if this is a rebuild of instance on the same host with @@ -1219,21 +1226,49 @@ class ComputeTaskManager(base.Base): instance.availability_zone = ( availability_zones.get_host_availability_zone( context, host)) + try: + accel_uuids = self._rebuild_cyborg_arq( + context, instance, host, request_spec, evacuate) + except exception.AcceleratorRequestBindingFailed as exc: + cyclient = cyborg.get_client(context) + cyclient.delete_arqs_by_uuid(exc.arqs) + LOG.exception('Failed to rebuild. Reason: %s', exc) + raise exc - self.compute_rpcapi.rebuild_instance(context, - instance=instance, - new_pass=new_pass, - injected_files=injected_files, - image_ref=image_ref, - orig_image_ref=orig_image_ref, - orig_sys_metadata=orig_sys_metadata, - bdms=bdms, - recreate=evacuate, - on_shared_storage=on_shared_storage, - preserve_ephemeral=preserve_ephemeral, - migration=migration, - host=host, node=node, limits=limits, - request_spec=request_spec) + self.compute_rpcapi.rebuild_instance( + context, + instance=instance, + new_pass=new_pass, + injected_files=injected_files, + image_ref=image_ref, + orig_image_ref=orig_image_ref, + orig_sys_metadata=orig_sys_metadata, + bdms=bdms, + recreate=evacuate, + on_shared_storage=on_shared_storage, + preserve_ephemeral=preserve_ephemeral, + migration=migration, + host=host, + node=node, + limits=limits, + request_spec=request_spec, + accel_uuids=accel_uuids) + + def _rebuild_cyborg_arq( + self, context, instance, host, request_spec, evacuate): + dp_name = instance.flavor.extra_specs.get('accel:device_profile') + if not dp_name: + return [] + + cyclient = cyborg.get_client(context) + if not evacuate: + return cyclient.get_arq_uuids_for_instance(instance) + + cyclient.delete_arqs_for_instance(instance.uuid) + resource_provider_mapping = request_spec.get_request_group_mapping() + return self._create_and_bind_arqs( + context, instance.uuid, instance.flavor.extra_specs, + host, resource_provider_mapping) def _validate_image_traits_for_rebuild(self, context, instance, image_ref): """Validates that the traits specified in the image can be satisfied diff --git a/nova/objects/service.py b/nova/objects/service.py index 82203e1246a4..33d1a5e2e8e0 100644 --- a/nova/objects/service.py +++ b/nova/objects/service.py @@ -31,7 +31,7 @@ LOG = logging.getLogger(__name__) # NOTE(danms): This is the global service version counter -SERVICE_VERSION = 52 +SERVICE_VERSION = 53 # NOTE(danms): This is our SERVICE_VERSION history. The idea is that any @@ -187,6 +187,9 @@ SERVICE_VERSION_HISTORY = ( {'compute_rpc': '5.11'}, # Version 52: Add support for the 'mixed' CPU allocation policy {'compute_rpc': '5.11'}, + # Version 53: Compute RPC v5.12: + # Add accel_uuids (accelerator requests) param to rebuild_instance + {'compute_rpc': '5.12'}, ) diff --git a/nova/tests/functional/integrated_helpers.py b/nova/tests/functional/integrated_helpers.py index fc3642165879..828734ca935a 100644 --- a/nova/tests/functional/integrated_helpers.py +++ b/nova/tests/functional/integrated_helpers.py @@ -477,6 +477,13 @@ class InstanceHelperMixin: self.api.post_server_action(server['id'], {'unshelve': {}}) return self._wait_for_state_change(server, expected_state) + def _evacuate_server(self, server, host, expected_state='ACTIVE'): + """Evacuate a server.""" + self.api.post_server_action(server['id'], {'evacuate': {}}) + self._wait_for_server_parameter( + self.server, {'OS-EXT-SRV-ATTR:host': host, + 'status': expected_state}) + class PlacementHelperMixin: """A helper mixin for interacting with placement.""" diff --git a/nova/tests/functional/test_servers.py b/nova/tests/functional/test_servers.py index 73e943a6ecd0..01e702b718cc 100644 --- a/nova/tests/functional/test_servers.py +++ b/nova/tests/functional/test_servers.py @@ -8086,6 +8086,20 @@ class AcceleratorServerOpsTest(AcceleratorServerBase): image_uuid='155d900f-4e14-4e4c-a73d-069cbf4541e6', networks='none', expected_state='ACTIVE') + def _test_evacuate(self, server, num_hosts): + server_hostname = server['OS-EXT-SRV-ATTR:host'] + for i in range(num_hosts): + if self.compute_services[i].host == server_hostname: + compute_to_stop = self.compute_services[i] + else: + compute_to_evacuate = self.compute_services[i] + # Stop and force down the compute service. + compute_id = self.admin_api.get_services( + host=server_hostname, binary='nova-compute')[0]['id'] + compute_to_stop.stop() + self.admin_api.put_service(compute_id, {'forced_down': 'true'}) + return compute_to_stop, compute_to_evacuate + def test_soft_reboot_ok(self): self._reboot_server(self.server) self._check_allocations_usage(self.server) @@ -8160,6 +8174,30 @@ class AcceleratorServerOpsTest(AcceleratorServerBase): self.api.post_server_action(self.server['id'], {'unrescue': {}}) self._check_allocations_usage(self.server) + def test_evacuate_ok(self): + server_hostname = self.server['OS-EXT-SRV-ATTR:host'] + arqs = self.cyborg.fake_get_arqs_for_instance(self.server['id']) + compute_to_stop, compute_to_evacuate = self._test_evacuate( + self.server, self.NUM_HOSTS) + self._evacuate_server(self.server, compute_to_evacuate.host) + compute_to_stop.start() + self.server = self.api.get_server(self.server['id']) + arqs_new = self.cyborg.fake_get_arqs_for_instance(self.server['id']) + evac_hostname = self.server['OS-EXT-SRV-ATTR:host'] + self.assertNotEqual(server_hostname, evac_hostname) + self.assertEqual(server_hostname, arqs[0]['hostname']) + self.assertEqual(evac_hostname, arqs_new[0]['hostname']) + + def test_rebuild_ok(self): + rebuild_image_ref = fake_image.AUTO_DISK_CONFIG_ENABLED_IMAGE_UUID + self.api.post_server_action(self.server['id'], + {'rebuild': { + 'imageRef': rebuild_image_ref, + 'OS-DCF:diskConfig': 'AUTO'}}) + fake_notifier.wait_for_versioned_notifications('instance.rebuild.end') + self._wait_for_state_change(self.server, 'ACTIVE') + self._check_allocations_usage(self.server) + def test_resize_fails(self): ex = self.assertRaises(client.OpenStackApiException, self.api.post_server_action, self.server['id'], @@ -8186,30 +8224,29 @@ class AcceleratorServerOpsTest(AcceleratorServerBase): self.assertEqual(403, ex.response.status_code) self._check_allocations_usage(self.server) - def test_evacuate_fails(self): - server_hostname = self.server['OS-EXT-SRV-ATTR:host'] - for i in range(self.NUM_HOSTS): - hostname = 'accel_host' + str(i) - if hostname != server_hostname: - other_hostname = hostname - if self.compute_services[i].host == server_hostname: - compute_to_stop = self.compute_services[i] - - # Stop and force down the compute service. - compute_id = self.admin_api.get_services( - host=server_hostname, binary='nova-compute')[0]['id'] - compute_to_stop.stop() - self.admin_api.put_service(compute_id, {'forced_down': 'true'}) + @mock.patch.object(objects.service, 'get_minimum_version_all_cells') + def test_evacuate_old_compute(self, old_compute_version): + """Tests when the source compute service is too old to call + evacuate so OpenStackApiException is raised. + """ + old_compute_version.return_value = 52 + _, compute_to_evacuate = self._test_evacuate( + self.server, self.NUM_HOSTS) ex = self.assertRaises(client.OpenStackApiException, self.api.post_server_action, self.server['id'], {'evacuate': { - 'host': other_hostname, + 'host': compute_to_evacuate.host, 'adminPass': 'MySecretPass'}}) self.assertEqual(403, ex.response.status_code) self._check_allocations_usage(self.server) - def test_rebuild_fails(self): + @mock.patch.object(objects.service, 'get_minimum_version_all_cells') + def test_rebuild_old_compute(self, old_compute_version): + """Tests when the source compute service is too old to call + rebuild so OpenStackApiException is raised. + """ + old_compute_version.return_value = 52 rebuild_image_ref = fake_image.AUTO_DISK_CONFIG_ENABLED_IMAGE_UUID ex = self.assertRaises(client.OpenStackApiException, self.api.post_server_action, self.server['id'], diff --git a/nova/tests/unit/accelerator/test_cyborg.py b/nova/tests/unit/accelerator/test_cyborg.py index f0736b2ccf40..67c92b0554ac 100644 --- a/nova/tests/unit/accelerator/test_cyborg.py +++ b/nova/tests/unit/accelerator/test_cyborg.py @@ -23,6 +23,7 @@ from oslo_serialization import jsonutils from nova.accelerator import cyborg from nova import context from nova import exception +from nova import objects from nova.objects import request_spec from nova import test from nova.tests.unit import fake_requests @@ -394,3 +395,25 @@ class CyborgTestCase(test.NoDBTestCase): self.client.ARQ_URL, params={'arqs': arq_uuid_str}) mock_log.assert_called_once_with('Failed to delete ARQs %s', arq_uuid_str) + + @mock.patch('keystoneauth1.adapter.Adapter.get') + def test_get_arq_uuids_for_instance(self, mock_cyborg_get): + # Happy path, without only_resolved=True + _, bound_arqs = self._get_bound_arqs() + instance_uuid = bound_arqs[0]['instance_uuid'] + flavor = objects.Flavor(extra_specs={'accel:device_profile': 'dp1'}) + instance = objects.Instance(flavor=flavor, + uuid=instance_uuid) + query = {"instance": instance_uuid} + content = jsonutils.dumps({'arqs': bound_arqs}) + resp = fake_requests.FakeResponse(200, content) + mock_cyborg_get.return_value = resp + + ret_arqs = self.client.get_arq_uuids_for_instance(instance) + + mock_cyborg_get.assert_called_once_with( + self.client.ARQ_URL, params=query) + bound_arqs = [bound_arq['uuid'] for bound_arq in bound_arqs] + bound_arqs.sort() + ret_arqs.sort() + self.assertEqual(bound_arqs, ret_arqs) diff --git a/nova/tests/unit/compute/test_api.py b/nova/tests/unit/compute/test_api.py index 9ddfd3231a6d..6ff7244dbe69 100644 --- a/nova/tests/unit/compute/test_api.py +++ b/nova/tests/unit/compute/test_api.py @@ -2122,7 +2122,7 @@ class _ComputeAPIUnitTestMixIn(object): fake_inst = self._create_instance_obj() fake_inst.flavor = cur_flavor new_flavor = objects.Flavor(id=2, name='bar', vcpus=1, memory_mb=2048, - root_gb=10, disabled=False) + root_gb=10, disabled=False, extra_specs={}) mock_get.return_value = new_flavor mock_check.side_effect = exception.OverQuota( overs=['ram'], quotas={'cores': 1, 'ram': 2048}, @@ -7569,8 +7569,9 @@ class ComputeAPIUnitTestCase(_ComputeAPIUnitTestMixIn, test.NoDBTestCase): mock_get_min_ver.assert_called_once_with( self.context, ['nova-compute']) - def _test_block_accelerators(self, instance, args_info): - @compute_api.block_accelerators + def _test_block_accelerators(self, instance, args_info, + until_service=None): + @compute_api.block_accelerators(until_service=until_service) def myfunc(self, context, instance, *args, **kwargs): args_info['args'] = (context, instance, *args) args_info['kwargs'] = dict(**kwargs) @@ -7604,6 +7605,37 @@ class ComputeAPIUnitTestCase(_ComputeAPIUnitTestMixIn, test.NoDBTestCase): # myfunc was not called self.assertEqual({}, args_info) + @mock.patch('nova.objects.service.get_minimum_version_all_cells', + return_value=54) + def test_block_accelerators_until_service(self, mock_get_min): + """Support operating server with acclerators until compute service + more than the version of 53. + """ + extra_specs = {'accel:device_profile': 'mydp'} + flavor = self._create_flavor(extra_specs=extra_specs) + instance = self._create_instance_obj(flavor=flavor) + args_info = {} + expected_args, kwargs = self._test_block_accelerators( + instance, args_info, until_service=53) + self.assertEqual(expected_args, args_info['args']) + self.assertEqual(kwargs, args_info['kwargs']) + + @mock.patch('nova.objects.service.get_minimum_version_all_cells', + return_value=52) + def test_block_accelerators_until_service_forbidden(self, mock_get_min): + """Ensure a 'ForbiddenWithAccelerators' exception raises if any + compute service less than the version of 53. + """ + extra_specs = {'accel:device_profile': 'mydp'} + flavor = self._create_flavor(extra_specs=extra_specs) + instance = self._create_instance_obj(flavor=flavor) + args_info = {} + self.assertRaisesRegex(exception.ForbiddenWithAccelerators, + 'Forbidden with instances that have accelerators.', + self._test_block_accelerators, instance, args_info, 53) + # myfunc was not called + self.assertEqual({}, args_info) + # TODO(huaqiang): Remove in Wallaby @mock.patch('nova.objects.service.get_minimum_version_all_cells') def test__check_compute_service_for_mixed_instance(self, mock_ver): diff --git a/nova/tests/unit/compute/test_compute.py b/nova/tests/unit/compute/test_compute.py index d604a67516b1..fb045f534356 100644 --- a/nova/tests/unit/compute/test_compute.py +++ b/nova/tests/unit/compute/test_compute.py @@ -2743,16 +2743,12 @@ class ComputeTestCase(BaseTestCase, block_device_mapping=[]) db.instance_update(self.context, instance['uuid'], {"task_state": task_states.REBUILDING}) - self.compute.rebuild_instance(self.context, instance, - image_ref, image_ref, - injected_files=[], - new_pass="new_password", - orig_sys_metadata=sys_metadata, - bdms=[], recreate=False, - on_shared_storage=False, - preserve_ephemeral=False, - migration=None, scheduled_node=None, - limits={}, request_spec=None) + self.compute.rebuild_instance( + self.context, instance, image_ref, image_ref, injected_files=[], + new_pass="new_password", orig_sys_metadata=sys_metadata, + bdms=[], recreate=False, on_shared_storage=False, + preserve_ephemeral=False, migration=None, scheduled_node=None, + limits={}, request_spec=None, accel_uuids=[]) self.compute.terminate_instance(self.context, instance, []) def test_rebuild_driver(self): @@ -2777,16 +2773,12 @@ class ComputeTestCase(BaseTestCase, block_device_mapping=[]) db.instance_update(self.context, instance['uuid'], {"task_state": task_states.REBUILDING}) - self.compute.rebuild_instance(self.context, instance, - image_ref, image_ref, - injected_files=[], - new_pass="new_password", - orig_sys_metadata=sys_metadata, - bdms=[], recreate=False, - on_shared_storage=False, - preserve_ephemeral=False, migration=None, - scheduled_node=None, limits={}, - request_spec=None) + self.compute.rebuild_instance( + self.context, instance, image_ref, image_ref, injected_files=[], + new_pass="new_password", orig_sys_metadata=sys_metadata, + bdms=[], recreate=False, on_shared_storage=False, + preserve_ephemeral=False, migration=None, scheduled_node=None, + limits={}, request_spec=None, accel_uuids=[]) self.assertTrue(called['rebuild']) self.compute.terminate_instance(self.context, instance, []) @@ -2833,16 +2825,12 @@ class ComputeTestCase(BaseTestCase, block_device_mapping=[]) db.instance_update(self.context, instance['uuid'], {"task_state": task_states.REBUILDING}) - self.compute.rebuild_instance(self.context, instance, - image_ref, image_ref, - injected_files=[], - new_pass="new_password", - orig_sys_metadata=sys_metadata, - bdms=bdms, recreate=False, - preserve_ephemeral=False, migration=None, - scheduled_node=None, limits={}, - on_shared_storage=False, - request_spec=None) + self.compute.rebuild_instance( + self.context, instance, image_ref, image_ref, injected_files=[], + new_pass="new_password", orig_sys_metadata=sys_metadata, + bdms=bdms, recreate=False, preserve_ephemeral=False, + migration=None, scheduled_node=None, limits={}, + on_shared_storage=False, request_spec=None, accel_uuids=[]) self.assertTrue(called['rebuild']) self.compute.terminate_instance(self.context, instance, []) @@ -2856,14 +2844,12 @@ class ComputeTestCase(BaseTestCase, block_device_mapping=[]) db.instance_update(self.context, instance['uuid'], {"task_state": task_states.REBUILDING}) - self.compute.rebuild_instance(self.context, instance, - '', '', injected_files=[], - new_pass="new_password", - orig_sys_metadata=sys_metadata, bdms=[], - recreate=False, on_shared_storage=False, - preserve_ephemeral=False, migration=None, - scheduled_node=None, limits=None, - request_spec=None) + self.compute.rebuild_instance( + self.context, instance, '', '', injected_files=[], + new_pass="new_password", orig_sys_metadata=sys_metadata, bdms=[], + recreate=False, on_shared_storage=False, preserve_ephemeral=False, + migration=None, scheduled_node=None, limits=None, + request_spec=None, accel_uuids=[]) self.compute.terminate_instance(self.context, instance, []) def test_rebuild_launched_at_time(self): @@ -2879,16 +2865,12 @@ class ComputeTestCase(BaseTestCase, time_fixture.advance_time_delta(cur_time - old_time) db.instance_update(self.context, instance['uuid'], {"task_state": task_states.REBUILDING}) - self.compute.rebuild_instance(self.context, instance, - image_ref, image_ref, - injected_files=[], - new_pass="new_password", - orig_sys_metadata={}, - bdms=[], recreate=False, - on_shared_storage=False, - preserve_ephemeral=False, migration=None, - scheduled_node=None, limits={}, - request_spec=None) + self.compute.rebuild_instance( + self.context, instance, image_ref, image_ref, injected_files=[], + new_pass="new_password", orig_sys_metadata={}, bdms=[], + recreate=False, on_shared_storage=False, preserve_ephemeral=False, + migration=None, scheduled_node=None, limits={}, request_spec=None, + accel_uuids=[]) instance.refresh() self.assertEqual(cur_time, instance['launched_at'].replace(tzinfo=None)) @@ -2916,16 +2898,12 @@ class ComputeTestCase(BaseTestCase, instance['uuid']) db.instance_update(self.context, instance['uuid'], {"task_state": task_states.REBUILDING}) - self.compute.rebuild_instance(self.context, instance, - image_ref, image_ref, - injected_files=injected_files, - new_pass="new_password", - orig_sys_metadata=sys_metadata, - bdms=[], recreate=False, - on_shared_storage=False, - preserve_ephemeral=False, - migration=None, scheduled_node=None, - limits={}, request_spec=None) + self.compute.rebuild_instance( + self.context, instance, image_ref, image_ref, + injected_files=injected_files, new_pass="new_password", + orig_sys_metadata=sys_metadata, bdms=[], recreate=False, + on_shared_storage=False, preserve_ephemeral=False, migration=None, + scheduled_node=None, limits={}, request_spec=None, accel_uuids=[]) self.compute.terminate_instance(self.context, instance, []) @mock.patch.object(objects.BlockDeviceMappingList, 'get_by_instance_uuid') @@ -4654,7 +4632,8 @@ class ComputeTestCase(BaseTestCase, 'scheduled_node': None, 'limits': {}, 'request_spec': None, - 'on_shared_storage': False}), + 'on_shared_storage': False, + 'accel_uuids': ()}), ("set_admin_password", task_states.UPDATING_PASSWORD, {'new_pass': None}), ("rescue_instance", task_states.RESCUING, @@ -5166,17 +5145,12 @@ class ComputeTestCase(BaseTestCase, inst_ref.task_state = task_states.REBUILDING inst_ref.save() - self.compute.rebuild_instance(self.context, - inst_ref, - image_ref, new_image_ref, - injected_files=[], - new_pass=password, - orig_sys_metadata=orig_sys_metadata, - bdms=[], recreate=False, - on_shared_storage=False, - preserve_ephemeral=False, migration=None, - scheduled_node=None, limits={}, - request_spec=None) + self.compute.rebuild_instance( + self.context, inst_ref, image_ref, new_image_ref, + injected_files=[], new_pass=password, + orig_sys_metadata=orig_sys_metadata, bdms=[], recreate=False, + on_shared_storage=False, preserve_ephemeral=False, migration=None, + scheduled_node=None, limits={}, request_spec=None, accel_uuids=[]) inst_ref.refresh() @@ -12818,7 +12792,7 @@ class EvacuateHostTestCase(BaseTestCase): image_ref, injected_files, 'newpass', {}, bdms, recreate=True, on_shared_storage=on_shared_storage, migration=migration, preserve_ephemeral=False, scheduled_node=node, limits=limits, - request_spec=None) + request_spec=None, accel_uuids=[]) if vm_states_is_stopped: mock_notify_rebuild.assert_has_calls([ mock.call(ctxt, self.inst, self.inst.host, phase='start', @@ -13007,7 +12981,7 @@ class EvacuateHostTestCase(BaseTestCase): test.MatchType(objects.ImageMeta), mock.ANY, 'newpass', mock.ANY, network_info=mock.ANY, - block_device_info=mock.ANY) + block_device_info=mock.ANY, accel_info=mock.ANY) @mock.patch.object(fake.FakeDriver, 'spawn') def test_rebuild_on_host_without_shared_storage(self, mock_spawn): @@ -13025,7 +12999,7 @@ class EvacuateHostTestCase(BaseTestCase): test.MatchType(objects.ImageMeta), mock.ANY, 'newpass', mock.ANY, network_info=mock.ANY, - block_device_info=mock.ANY) + block_device_info=mock.ANY, accel_info=mock.ANY) def test_rebuild_on_host_instance_exists(self): """Rebuild if instance exists raises an exception.""" @@ -13069,7 +13043,7 @@ class EvacuateHostTestCase(BaseTestCase): test.MatchObjPrims(self.inst.image_meta), mock.ANY, 'newpass', mock.ANY, network_info=mock.ANY, - block_device_info=mock.ANY) + block_device_info=mock.ANY, accel_info=mock.ANY) @mock.patch.object(fake.FakeDriver, 'spawn') def test_on_shared_storage_not_provided_host_with_shared_storage(self, @@ -13085,7 +13059,7 @@ class EvacuateHostTestCase(BaseTestCase): test.MatchType(objects.ImageMeta), mock.ANY, 'newpass', mock.ANY, network_info=mock.ANY, - block_device_info=mock.ANY) + block_device_info=mock.ANY, accel_info=mock.ANY) def test_rebuild_migration_passed_in(self): migration = mock.Mock(spec=objects.Migration) @@ -13128,7 +13102,7 @@ class EvacuateHostTestCase(BaseTestCase): 'nova.scheduler.client.report.SchedulerReportClient.' 'remove_provider_tree_from_instance_allocation') with patch_spawn, patch_on_disk, patch_claim, patch_remove_allocs: - self.assertRaises(exception.BuildAbortException, + self.assertRaises(messaging.ExpectedException, self._rebuild, migration=migration, send_node=True) self.assertEqual("failed", migration.status) @@ -13162,7 +13136,7 @@ class EvacuateHostTestCase(BaseTestCase): # NOTE(ndipanov): Make sure that we pass the topology from the context def fake_spawn(context, instance, image_meta, injected_files, admin_password, allocations, network_info=None, - block_device_info=None): + block_device_info=None, accel_info=None): self.assertIsNone(instance.numa_topology) self.inst.numa_topology = numa_topology diff --git a/nova/tests/unit/compute/test_compute_mgr.py b/nova/tests/unit/compute/test_compute_mgr.py index c8a483516f1d..d37618c80964 100644 --- a/nova/tests/unit/compute/test_compute_mgr.py +++ b/nova/tests/unit/compute/test_compute_mgr.py @@ -5138,10 +5138,9 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase, mock_rebuild.side_effect = exc - self.compute.rebuild_instance(self.context, instance, None, None, None, - None, None, None, recreate, - False, False, None, scheduled_node, {}, - None) + self.compute.rebuild_instance( + self.context, instance, None, None, None, None, None, None, + recreate, False, False, None, scheduled_node, {}, None, []) mock_set.assert_called_once_with(None, 'failed') mock_notify_about_instance_usage.assert_called_once_with( mock.ANY, instance, 'rebuild.error', fault=mock_rebuild.side_effect @@ -5205,7 +5204,7 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase, instance = fake_instance.fake_instance_obj(self.context) instance.vm_state = vm_states.ACTIVE ex = exception.ComputeResourcesUnavailable(reason='out of foo') - self.assertRaises(exception.BuildAbortException, + self.assertRaises(messaging.ExpectedException, self._test_rebuild_ex, instance, ex) # Make sure the instance vm_state did not change. self.assertEqual(vm_states.ACTIVE, instance.vm_state) @@ -5252,7 +5251,7 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase, None, recreate=True, on_shared_storage=None, preserve_ephemeral=False, migration=None, scheduled_node='fake-node', - limits={}, request_spec=request_spec) + limits={}, request_spec=request_spec, accel_uuids=[]) mock_validate_policy.assert_called_once_with( elevated_context, instance, {'group': [uuids.group]}) @@ -5287,11 +5286,11 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase, mock_validate_policy.side_effect = exc self.assertRaises( - exception.BuildAbortException, self.compute.rebuild_instance, + messaging.ExpectedException, self.compute.rebuild_instance, self.context, instance, None, None, None, None, None, None, recreate=True, on_shared_storage=None, preserve_ephemeral=False, migration=None, scheduled_node='fake-node', limits={}, - request_spec=request_spec) + request_spec=request_spec, accel_uuids=[]) mock_validate_policy.assert_called_once_with( elevated_context, instance, {'group': [uuids.group]}) @@ -5313,9 +5312,10 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase, mock.patch.object(objects.Instance, 'save'), mock.patch.object(self.compute, '_set_migration_status'), ) as (mock_get, mock_rebuild, mock_save, mock_set): - self.compute.rebuild_instance(self.context, instance, None, None, - None, None, None, None, False, - False, False, None, None, {}, None) + self.compute.rebuild_instance( + self.context, instance, None, None, + None, None, None, None, False, + False, False, None, None, {}, None, []) self.assertFalse(mock_get.called) self.assertEqual(node, instance.node) mock_set.assert_called_once_with(None, 'done') @@ -5335,9 +5335,9 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase, mock.patch.object(self.compute, '_set_migration_status'), ) as (mock_get, mock_rebuild, mock_save, mock_set): mock_get.return_value.hypervisor_hostname = 'new-node' - self.compute.rebuild_instance(self.context, instance, None, None, - None, None, None, None, True, - False, False, None, None, {}, None) + self.compute.rebuild_instance( + self.context, instance, None, None, None, None, None, + None, True, False, False, None, None, {}, None, []) mock_get.assert_called_once_with(mock.ANY, self.compute.host) self.assertEqual('new-node', instance.node) mock_set.assert_called_once_with(None, 'done') @@ -5419,7 +5419,7 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase, recreate, on_shared_storage, preserve_ephemeral, {}, {}, self.allocations, - mock.sentinel.mapping) + mock.sentinel.mapping, []) mock_notify_usage.assert_has_calls( [mock.call(self.context, instance, "rebuild.start", @@ -5450,7 +5450,7 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase, def _spawn(context, instance, image_meta, injected_files, admin_password, allocations, network_info=None, - block_device_info=None): + block_device_info=None, accel_info=None): self.assertEqual(block_device_info['block_device_mapping'], 'shared_block_storage') @@ -5462,12 +5462,15 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase, mock.patch.object(objects.Instance, 'save', return_value=None), mock.patch.object(self.compute, '_power_off_instance', - return_value=None) + return_value=None), + mock.patch.object(self.compute, '_get_accel_info', + return_value=[]) ) as( mock_destroy, mock_spawn, mock_save, - mock_power_off + mock_power_off, + mock_accel_info ): instance = fake_instance.fake_instance_obj(self.context) instance.migration_context = None @@ -5518,7 +5521,8 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase, preserve_ephemeral=False, migration=objects.Migration(), request_spec=objects.RequestSpec(), allocations=self.allocations, - request_group_resource_providers_mapping=mock.sentinel.mapping) + request_group_resource_providers_mapping=mock.sentinel.mapping, + accel_uuids=[]) self.assertIn('Trusted image certificates provided on host', six.text_type(ex)) diff --git a/nova/tests/unit/compute/test_rpcapi.py b/nova/tests/unit/compute/test_rpcapi.py index 42de77a5889f..4336991e2034 100644 --- a/nova/tests/unit/compute/test_rpcapi.py +++ b/nova/tests/unit/compute/test_rpcapi.py @@ -693,12 +693,55 @@ class ComputeRpcAPITestCase(test.NoDBTestCase): reboot_type='type') def test_rebuild_instance(self): - self._test_compute_api('rebuild_instance', 'cast', new_pass='None', - injected_files='None', image_ref='None', orig_image_ref='None', - bdms=[], instance=self.fake_instance_obj, host='new_host', - orig_sys_metadata=None, recreate=True, on_shared_storage=True, - preserve_ephemeral=True, migration=None, node=None, - limits=None, request_spec=None, version='5.0') + # With rpcapi 5.12, when a list of accel_uuids is passed as a param, + # that list must be passed to the client. That is tested in + # _test_compute_api with rpc_mock.assert, where expected_kwargs + # must have the accel_uuids. + self._test_compute_api( + 'rebuild_instance', 'cast', new_pass='None', + injected_files='None', image_ref='None', orig_image_ref='None', + bdms=[], instance=self.fake_instance_obj, host='new_host', + orig_sys_metadata=None, recreate=True, on_shared_storage=True, + preserve_ephemeral=True, migration=None, node=None, + limits=None, request_spec=None, accel_uuids=[], version='5.12') + + def test_rebuild_instance_old_rpcapi(self): + # With rpcapi < 5.12, accel_uuids must be dropped in the client call. + ctxt = context.RequestContext('fake_user', 'fake_project') + compute_api = compute_rpcapi.ComputeAPI() + compute_api.router.client = mock.Mock() + mock_client = mock.MagicMock() + compute_api.router.client.return_value = mock_client + # Force can_send_version to False, so that 5.0 version is used. + mock_client.can_send_version.return_value = False + mock_cctx = mock.MagicMock() + mock_client.prepare.return_value = mock_cctx + rebuild_args = { + 'new_pass': 'admin_password', + 'injected_files': 'files_to_inject', + 'image_ref': uuids.image_ref, + 'orig_image_ref': uuids.orig_image_ref, + 'orig_sys_metadata': 'orig_sys_meta', + 'bdms': {}, + 'recreate': False, + 'on_shared_storage': False, + 'preserve_ephemeral': False, + 'request_spec': None, + 'migration': None, + 'limits': None + } + compute_api.rebuild_instance( + ctxt, instance=self.fake_instance_obj, + accel_uuids=['938af7f9-f136-4e5a-bdbe-3b6feab54311'], + node=None, host=None, **rebuild_args) + + mock_client.can_send_version.assert_called_once_with('5.12') + mock_client.prepare.assert_called_with( + server=self.fake_instance_obj.host, version='5.0') + mock_cctx.cast.assert_called_with( # No accel_uuids + ctxt, 'rebuild_instance', + instance=self.fake_instance_obj, + scheduled_node=None, **rebuild_args) def test_reserve_block_device_name(self): self.flags(long_rpc_timeout=1234) diff --git a/nova/tests/unit/conductor/test_conductor.py b/nova/tests/unit/conductor/test_conductor.py index 946685fce810..12158cb343b9 100644 --- a/nova/tests/unit/conductor/test_conductor.py +++ b/nova/tests/unit/conductor/test_conductor.py @@ -390,6 +390,7 @@ class _BaseTaskTestCase(object): compute_rebuild_args['migration'] = migration compute_rebuild_args['node'] = node compute_rebuild_args['limits'] = limits + compute_rebuild_args['accel_uuids'] = [] return rebuild_args, compute_rebuild_args @@ -1780,6 +1781,7 @@ class _BaseTaskTestCase(object): rebuild_args, _ = self._prepare_rebuild_args( {'host': None, 'recreate': True}) fake_spec = objects.RequestSpec() + fake_spec.flavor = inst_obj.flavor rebuild_args['request_spec'] = fake_spec with test.nested( @@ -1847,6 +1849,7 @@ class _BaseTaskTestCase(object): fake_selection = objects.Selection(service_host=expected_host, nodename=expected_node, limits=None) fake_spec = objects.RequestSpec(ignore_hosts=[uuids.ignored_host]) + fake_spec.flavor = inst_obj.flavor rebuild_args, compute_args = self._prepare_rebuild_args( {'host': None, 'node': expected_node, 'limits': expected_limits, 'request_spec': fake_spec, 'recreate': True}) diff --git a/nova/virt/driver.py b/nova/virt/driver.py index 2e78333410c2..05d2dabf8066 100644 --- a/nova/virt/driver.py +++ b/nova/virt/driver.py @@ -286,7 +286,7 @@ class ComputeDriver(object): admin_password, allocations, bdms, detach_block_devices, attach_block_devices, network_info=None, evacuate=False, block_device_info=None, - preserve_ephemeral=False): + preserve_ephemeral=False, accel_uuids=None): """Destroy and re-make this instance. A 'rebuild' effectively purges all existing data from the system and @@ -323,6 +323,7 @@ class ComputeDriver(object): attached to the instance. :param preserve_ephemeral: True if the default ephemeral storage partition must be preserved on rebuild + :param accel_uuids: Accelerator UUIDs. """ raise NotImplementedError() diff --git a/nova/virt/ironic/driver.py b/nova/virt/ironic/driver.py index 5aee1ea3bcbc..aa00ef68395a 100644 --- a/nova/virt/ironic/driver.py +++ b/nova/virt/ironic/driver.py @@ -1670,7 +1670,7 @@ class IronicDriver(virt_driver.ComputeDriver): admin_password, allocations, bdms, detach_block_devices, attach_block_devices, network_info=None, evacuate=False, block_device_info=None, - preserve_ephemeral=False): + preserve_ephemeral=False, accel_uuids=None): """Rebuild/redeploy an instance. This version of rebuild() allows for supporting the option to @@ -1710,7 +1710,7 @@ class IronicDriver(virt_driver.ComputeDriver): information. Ignored by this driver. :param preserve_ephemeral: Boolean value; if True the ephemeral must be preserved on rebuild. - + :param accel_uuids: Accelerator UUIDs. Ignored by this driver. """ LOG.debug('Rebuild called for instance', instance=instance)