diff --git a/doc/notification_samples/common_payloads/ComputeTaskPayload.json b/doc/notification_samples/common_payloads/ComputeTaskPayload.json index 387ff4b01e77..cecddffb6fee 100644 --- a/doc/notification_samples/common_payloads/ComputeTaskPayload.json +++ b/doc/notification_samples/common_payloads/ComputeTaskPayload.json @@ -5,7 +5,21 @@ "nova_object.data": { "instance_uuid": "d5e6a7b7-80e5-4166-85a3-cd6115201082", "reason": {"$ref": "ExceptionPayload.json#"}, - "request_spec": {"$ref": "RequestSpecPayload.json#"}, + "request_spec": { + "$ref": "RequestSpecPayload.json#", + "nova_object.data": { + "flavor": { + "nova_object.data": { + "extra_specs": { + "hw:numa_cpus.0": "0", + "hw:numa_mem.0": "512", + "hw:numa_nodes": "1" + } + } + }, + "numa_topology": {"$ref": "InstanceNUMATopologyPayload.json#"} + } + }, "state": "error" } } diff --git a/doc/notification_samples/common_payloads/RequestSpecPayload.json b/doc/notification_samples/common_payloads/RequestSpecPayload.json index 98bcb9852a31..3301c18c589d 100644 --- a/doc/notification_samples/common_payloads/RequestSpecPayload.json +++ b/doc/notification_samples/common_payloads/RequestSpecPayload.json @@ -2,24 +2,23 @@ "nova_object.namespace": "nova", "nova_object.data": { "availability_zone": null, - "flavor": { - "$ref": "FlavorPayload.json#", - "nova_object.data": { - "extra_specs": { - "hw:numa_cpus.0": "0", - "hw:numa_mem.0": "512", - "hw:numa_nodes": "1" - } - } - }, + "flavor": {"$ref": "FlavorPayload.json#"}, + "ignore_hosts": null, "image": {"$ref": "ImageMetaPayload.json#"}, "instance_uuid": "d5e6a7b7-80e5-4166-85a3-cd6115201082", "num_instances": 1, - "numa_topology": {"$ref": "InstanceNUMATopologyPayload.json#"}, + "numa_topology": null, "pci_requests": {"$ref": "InstancePCIRequestsPayload.json#"}, "project_id": "6f70656e737461636b20342065766572", + "scheduler_hints": {}, + "security_groups": ["default"], + "force_hosts": null, + "force_nodes": null, + "instance_group": null, + "requested_destination": null, + "retry": null, "user_id": "fake" }, "nova_object.name": "RequestSpecPayload", - "nova_object.version": "1.0" + "nova_object.version": "1.1" } diff --git a/doc/notification_samples/scheduler-select_destinations-end.json b/doc/notification_samples/scheduler-select_destinations-end.json new file mode 100644 index 000000000000..76535710f1a7 --- /dev/null +++ b/doc/notification_samples/scheduler-select_destinations-end.json @@ -0,0 +1,6 @@ +{ + "priority": "INFO", + "payload": {"$ref": "common_payloads/RequestSpecPayload.json#"}, + "event_type": "scheduler.select_destinations.end", + "publisher_id": "nova-scheduler:fake-mini" +} diff --git a/doc/notification_samples/scheduler-select_destinations-start.json b/doc/notification_samples/scheduler-select_destinations-start.json new file mode 100644 index 000000000000..b5cacc141ba6 --- /dev/null +++ b/doc/notification_samples/scheduler-select_destinations-start.json @@ -0,0 +1,6 @@ +{ + "priority": "INFO", + "payload": {"$ref": "common_payloads/RequestSpecPayload.json#"}, + "event_type": "scheduler.select_destinations.start", + "publisher_id": "nova-scheduler:fake-mini" +} diff --git a/nova/compute/utils.py b/nova/compute/utils.py index 9c7ea30779bd..84524648f8a3 100644 --- a/nova/compute/utils.py +++ b/nova/compute/utils.py @@ -43,6 +43,8 @@ from nova.notifications.objects import instance as instance_notification from nova.notifications.objects import keypair as keypair_notification from nova.notifications.objects import libvirt as libvirt_notification from nova.notifications.objects import metrics as metrics_notification +from nova.notifications.objects import request_spec as reqspec_notification +from nova.notifications.objects import scheduler as scheduler_notification from nova.notifications.objects import server_group as sg_notification from nova.notifications.objects import volume as volume_notification from nova import objects @@ -471,6 +473,31 @@ def notify_about_instance_create(context, instance, host, phase=None, notification.emit(context) +@rpc.if_notifications_enabled +def notify_about_scheduler_action(context, request_spec, action, phase=None, + source=fields.NotificationSource.SCHEDULER): + """Send versioned notification about the action made by the scheduler + :param context: the RequestContext object + :param request_spec: the RequestSpec object + :param action: the name of the action + :param phase: the phase of the action + :param source: the source of the notification + """ + payload = reqspec_notification.RequestSpecPayload( + request_spec=request_spec) + notification = scheduler_notification.SelectDestinationsNotification( + context=context, + priority=fields.NotificationPriority.INFO, + publisher=notification_base.NotificationPublisher( + host=CONF.host, source=source), + event_type=notification_base.EventType( + object='scheduler', + action=action, + phase=phase), + payload=payload) + notification.emit(context) + + @rpc.if_notifications_enabled def notify_about_volume_attach_detach(context, instance, host, action, phase, volume_id=None, exception=None, tb=None): diff --git a/nova/notifications/objects/base.py b/nova/notifications/objects/base.py index 70ce3391c950..3753d50da646 100644 --- a/nova/notifications/objects/base.py +++ b/nova/notifications/objects/base.py @@ -69,7 +69,9 @@ class EventType(NotificationObject): # Version 1.17: USAGE is added to NotificationActionField enum # Version 1.18: ComputeTask related values have been added to # NotificationActionField enum - VERSION = '1.18' + # Version 1.19: SELECT_DESTINATIONS is added to the NotificationActionField + # enum + VERSION = '1.19' fields = { 'object': fields.StringField(nullable=False), diff --git a/nova/notifications/objects/request_spec.py b/nova/notifications/objects/request_spec.py index f372bafcaa69..587c693c1642 100644 --- a/nova/notifications/objects/request_spec.py +++ b/nova/notifications/objects/request_spec.py @@ -15,6 +15,7 @@ from nova.notifications.objects import base from nova.notifications.objects import flavor as flavor_payload from nova.notifications.objects import image as image_payload +from nova.notifications.objects import server_group as server_group_payload from nova.objects import base as nova_base from nova.objects import fields @@ -22,14 +23,19 @@ from nova.objects import fields @nova_base.NovaObjectRegistry.register_notification class RequestSpecPayload(base.NotificationPayloadBase): # Version 1.0: Initial version - VERSION = '1.0' + # Version 1.1: Add force_hosts, force_nodes, ignore_hosts, image_meta, + # instance_group, requested_destination, retry, + # scheduler_hints and security_groups fields + VERSION = '1.1' SCHEMA = { + 'ignore_hosts': ('request_spec', 'ignore_hosts'), 'instance_uuid': ('request_spec', 'instance_uuid'), 'project_id': ('request_spec', 'project_id'), 'user_id': ('request_spec', 'user_id'), 'availability_zone': ('request_spec', 'availability_zone'), - 'num_instances': ('request_spec', 'num_instances') + 'num_instances': ('request_spec', 'num_instances'), + 'scheduler_hints': ('request_spec', 'scheduler_hints'), } fields = { @@ -38,12 +44,23 @@ class RequestSpecPayload(base.NotificationPayloadBase): 'user_id': fields.StringField(nullable=True), 'availability_zone': fields.StringField(nullable=True), 'flavor': fields.ObjectField('FlavorPayload', nullable=True), + 'force_hosts': fields.StringField(nullable=True), + 'force_nodes': fields.StringField(nullable=True), + 'ignore_hosts': fields.ListOfStringsField(nullable=True), + 'image_meta': fields.ObjectField('ImageMetaPayload', nullable=True), + 'instance_group': fields.ObjectField('ServerGroupPayload', + nullable=True), 'image': fields.ObjectField('ImageMetaPayload', nullable=True), 'numa_topology': fields.ObjectField('InstanceNUMATopologyPayload', nullable=True), 'pci_requests': fields.ObjectField('InstancePCIRequestsPayload', nullable=True), - 'num_instances': fields.IntegerField(default=1) + 'num_instances': fields.IntegerField(default=1), + 'requested_destination': fields.ObjectField('DestinationPayload', + nullable=True), + 'retry': fields.ObjectField('SchedulerRetriesPayload', nullable=True), + 'scheduler_hints': fields.DictOfListOfStringsField(nullable=True), + 'security_groups': fields.ListOfStringsField(), } def __init__(self, request_spec): @@ -69,6 +86,32 @@ class RequestSpecPayload(base.NotificationPayloadBase): request_spec.pci_requests) else: self.pci_requests = None + if 'requested_destination' in request_spec \ + and request_spec.requested_destination: + self.requested_destination = DestinationPayload( + destination=request_spec.requested_destination) + else: + self.requested_destination = None + if 'retry' in request_spec and request_spec.retry: + self.retry = SchedulerRetriesPayload( + retry=request_spec.retry) + else: + self.retry = None + self.security_groups = [ + sec_group.identifier for sec_group in request_spec.security_groups] + if 'instance_group' in request_spec and request_spec.instance_group: + self.instance_group = server_group_payload.ServerGroupPayload( + group=request_spec.instance_group) + else: + self.instance_group = None + if 'force_hosts' in request_spec and request_spec.force_hosts: + self.force_hosts = request_spec.force_hosts[0] + else: + self.force_hosts = None + if 'force_nodes' in request_spec and request_spec.force_nodes: + self.force_nodes = request_spec.force_nodes[0] + else: + self.force_nodes = None self.populate_schema(request_spec=request_spec) @@ -224,3 +267,82 @@ class InstancePCIRequestPayload(base.NotificationPayloadBase): for pci_request in pci_request_list: payloads.append(cls(pci_request)) return payloads + + +@nova_base.NovaObjectRegistry.register_notification +class DestinationPayload(base.NotificationPayloadBase): + # Version 1.0: Initial version + VERSION = '1.0' + + SCHEMA = { + 'aggregates': ('destination', 'aggregates'), + } + + fields = { + 'host': fields.StringField(), + 'node': fields.StringField(nullable=True), + 'cell': fields.ObjectField('CellMappingPayload', nullable=True), + 'aggregates': fields.ListOfStringsField(nullable=True, + default=None), + } + + def __init__(self, destination): + super(DestinationPayload, self).__init__() + if (destination.obj_attr_is_set('host') and + destination.host is not None): + self.host = destination.host + if (destination.obj_attr_is_set('node') and + destination.node is not None): + self.node = destination.node + if (destination.obj_attr_is_set('cell') and + destination.cell is not None): + self.cell = CellMappingPayload(destination.cell) + self.populate_schema(destination=destination) + + +@nova_base.NovaObjectRegistry.register_notification +class SchedulerRetriesPayload(base.NotificationPayloadBase): + # Version 1.0: Initial version + VERSION = '1.0' + + SCHEMA = { + 'num_attempts': ('retry', 'num_attempts'), + } + + fields = { + 'num_attempts': fields.IntegerField(), + 'hosts': fields.ListOfStringsField(), + } + + def __init__(self, retry): + super(SchedulerRetriesPayload, self).__init__() + self.hosts = [] + for compute_node in retry.hosts: + self.hosts.append(compute_node.hypervisor_hostname) + self.populate_schema(retry=retry) + + +@nova_base.NovaObjectRegistry.register_notification +class CellMappingPayload(base.NotificationPayloadBase): + # Version 1.0: Initial version + VERSION = '1.0' + + SCHEMA = { + 'uuid': ('cell', 'uuid'), + 'name': ('cell', 'name'), + 'transport_url': ('cell', 'transport_url'), + 'database_connection': ('cell', 'database_connection'), + 'disabled': ('cell', 'disabled'), + } + + fields = { + 'uuid': fields.UUIDField(), + 'name': fields.StringField(nullable=True), + 'transport_url': fields.StringField(), + 'database_connection': fields.StringField(), + 'disabled': fields.BooleanField(default=False), + } + + def __init__(self, cell): + super(CellMappingPayload, self).__init__() + self.populate_schema(cell=cell) diff --git a/nova/notifications/objects/scheduler.py b/nova/notifications/objects/scheduler.py new file mode 100644 index 000000000000..da0bd510e02b --- /dev/null +++ b/nova/notifications/objects/scheduler.py @@ -0,0 +1,29 @@ +# Copyright 2017 Ericsson +# +# 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. + +from nova.notifications.objects import base +from nova.objects import base as nova_base +from nova.objects import fields + + +@base.notification_sample('scheduler-select_destinations-start.json') +@base.notification_sample('scheduler-select_destinations-end.json') +@nova_base.NovaObjectRegistry.register_notification +class SelectDestinationsNotification(base.NotificationBase): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'payload': fields.ObjectField('RequestSpecPayload') + } diff --git a/nova/objects/fields.py b/nova/objects/fields.py index f6af9b1f4e77..8b6c8e85d53e 100644 --- a/nova/objects/fields.py +++ b/nova/objects/fields.py @@ -819,6 +819,7 @@ class NotificationAction(BaseNovaEnum): RESIZE_CONFIRM = 'resize_confirm' RESIZE_PREP = 'resize_prep' RESIZE_REVERT = 'resize_revert' + SELECT_DESTINATIONS = 'select_destinations' SHELVE_OFFLOAD = 'shelve_offload' SOFT_DELETE = 'soft_delete' TRIGGER_CRASH_DUMP = 'trigger_crash_dump' @@ -848,7 +849,8 @@ class NotificationAction(BaseNovaEnum): SOFT_DELETE, TRIGGER_CRASH_DUMP, UNRESCUE, UNSHELVE, ADD_HOST, REMOVE_HOST, ADD_MEMBER, UPDATE_METADATA, LOCK, UNLOCK, REBUILD_SCHEDULED, UPDATE_PROP, LIVE_MIGRATION_FORCE_COMPLETE, - CONNECT, USAGE, BUILD_INSTANCES, MIGRATE_SERVER, REBUILD_SERVER) + CONNECT, USAGE, BUILD_INSTANCES, MIGRATE_SERVER, REBUILD_SERVER, + SELECT_DESTINATIONS) # TODO(rlrossit): These should be changed over to be a StateMachine enum from diff --git a/nova/scheduler/filter_scheduler.py b/nova/scheduler/filter_scheduler.py index de61b6a5e3fa..75b6151fbb15 100644 --- a/nova/scheduler/filter_scheduler.py +++ b/nova/scheduler/filter_scheduler.py @@ -24,10 +24,12 @@ import random from oslo_log import log as logging from six.moves import range +from nova.compute import utils as compute_utils import nova.conf from nova import exception from nova.i18n import _ from nova import objects +from nova.objects import fields as fields_obj from nova import rpc from nova.scheduler import client from nova.scheduler import driver @@ -85,6 +87,10 @@ class FilterScheduler(driver.Scheduler): self.notifier.info( context, 'scheduler.select_destinations.start', dict(request_spec=spec_obj.to_legacy_request_spec_dict())) + compute_utils.notify_about_scheduler_action( + context=context, request_spec=spec_obj, + action=fields_obj.NotificationAction.SELECT_DESTINATIONS, + phase=fields_obj.NotificationPhase.START) host_selections = self._schedule(context, spec_obj, instance_uuids, alloc_reqs_by_rp_uuid, provider_summaries, @@ -92,6 +98,10 @@ class FilterScheduler(driver.Scheduler): self.notifier.info( context, 'scheduler.select_destinations.end', dict(request_spec=spec_obj.to_legacy_request_spec_dict())) + compute_utils.notify_about_scheduler_action( + context=context, request_spec=spec_obj, + action=fields_obj.NotificationAction.SELECT_DESTINATIONS, + phase=fields_obj.NotificationPhase.END) return host_selections def _schedule(self, context, spec_obj, instance_uuids, diff --git a/nova/tests/functional/notification_sample_tests/notification_sample_base.py b/nova/tests/functional/notification_sample_tests/notification_sample_base.py index a1aa33bc14ed..568428447c65 100644 --- a/nova/tests/functional/notification_sample_tests/notification_sample_base.py +++ b/nova/tests/functional/notification_sample_tests/notification_sample_base.py @@ -156,6 +156,23 @@ class NotificationSampleTestBase(test.TestCase, self.assertJsonEqual(sample_obj, notification) + def _pop_and_verify_dest_select_notification(self, + server_id, replacements=None): + replacements = replacements or {} + replacements['instance_uuid'] = server_id + replacements['pci_requests.instance_uuid'] = server_id + replacements['flavor.extra_specs'] = self.ANY + replacements['numa_topology'] = self.ANY + scheduler_expected_notifications = [ + 'scheduler-select_destinations-start', + 'scheduler-select_destinations-end'] + self.assertLessEqual(2, len(fake_notifier.VERSIONED_NOTIFICATIONS)) + for notification in scheduler_expected_notifications: + self._verify_notification( + notification, + replacements=replacements, + actual=fake_notifier.VERSIONED_NOTIFICATIONS.pop(0)) + def _boot_a_server(self, expected_status='ACTIVE', extra_params=None, scheduler_hints=None, additional_extra_specs=None): @@ -227,6 +244,11 @@ class NotificationSampleTestBase(test.TestCase, expected_status) found_server['reservation_id'] = reservation_id + # Note(elod.illes): let's just pop and verify the dest_select + # notifications if we don't have a special case + if scheduler_hints is None and expected_status != 'ERROR': + self._pop_and_verify_dest_select_notification(found_server['id']) + if found_server['status'] == 'ACTIVE': self.api.put_server_tags(found_server['id'], ['tag1']) return found_server diff --git a/nova/tests/functional/notification_sample_tests/test_compute_task.py b/nova/tests/functional/notification_sample_tests/test_compute_task.py index 3a40bd92d954..4afb9c233323 100644 --- a/nova/tests/functional/notification_sample_tests/test_compute_task.py +++ b/nova/tests/functional/notification_sample_tests/test_compute_task.py @@ -38,19 +38,22 @@ class TestComputeTaskNotificationSample( 'hw:numa_cpus.0': '0', 'hw:numa_mem.0': 512}) self._wait_for_notification('compute_task.build_instances.error') - self.assertEqual(1, len(fake_notifier.VERSIONED_NOTIFICATIONS)) + # 0. scheduler.select_destinations.start + # 1. compute_task.rebuild_server.error + self.assertEqual(2, len(fake_notifier.VERSIONED_NOTIFICATIONS)) self._verify_notification( 'compute_task-build_instances-error', replacements={ 'instance_uuid': server['id'], 'request_spec.instance_uuid': server['id'], + 'request_spec.security_groups': [], 'request_spec.numa_topology.instance_uuid': server['id'], 'request_spec.pci_requests.instance_uuid': server['id'], 'reason.function_name': self.ANY, 'reason.module_name': self.ANY, 'reason.traceback': self.ANY }, - actual=fake_notifier.VERSIONED_NOTIFICATIONS[0]) + actual=fake_notifier.VERSIONED_NOTIFICATIONS[1]) def test_rebuild_fault(self): server = self._boot_a_server( @@ -72,20 +75,22 @@ class TestComputeTaskNotificationSample( self.admin_api.post_server_action(server['id'], post) self._wait_for_notification('compute_task.rebuild_server.error') # 0. instance.evacuate - # 1. compute_task.rebuild_server.error - self.assertEqual(2, len(fake_notifier.VERSIONED_NOTIFICATIONS)) + # 1. scheduler.select_destinations.start + # 2. compute_task.rebuild_server.error + self.assertEqual(3, len(fake_notifier.VERSIONED_NOTIFICATIONS)) self._verify_notification( 'compute_task-rebuild_server-error', replacements={ 'instance_uuid': server['id'], 'request_spec.instance_uuid': server['id'], + 'request_spec.security_groups': [], 'request_spec.numa_topology.instance_uuid': server['id'], 'request_spec.pci_requests.instance_uuid': server['id'], 'reason.function_name': self.ANY, 'reason.module_name': self.ANY, 'reason.traceback': self.ANY }, - actual=fake_notifier.VERSIONED_NOTIFICATIONS[1]) + actual=fake_notifier.VERSIONED_NOTIFICATIONS[2]) def test_migrate_fault(self): server = self._boot_a_server( @@ -104,16 +109,19 @@ class TestComputeTaskNotificationSample( self.admin_api.post_server_action, server['id'], {'migrate': None}) self._wait_for_notification('compute_task.migrate_server.error') - self.assertEqual(1, len(fake_notifier.VERSIONED_NOTIFICATIONS)) + # 0. scheduler.select_destinations.start + # 1. compute_task.migrate_server.error + self.assertEqual(2, len(fake_notifier.VERSIONED_NOTIFICATIONS)) self._verify_notification( 'compute_task-migrate_server-error', replacements={ 'instance_uuid': server['id'], 'request_spec.instance_uuid': server['id'], + 'request_spec.security_groups': [], 'request_spec.numa_topology.instance_uuid': server['id'], 'request_spec.pci_requests.instance_uuid': server['id'], 'reason.function_name': self.ANY, 'reason.module_name': self.ANY, 'reason.traceback': self.ANY }, - actual=fake_notifier.VERSIONED_NOTIFICATIONS[0]) + actual=fake_notifier.VERSIONED_NOTIFICATIONS[1]) diff --git a/nova/tests/functional/notification_sample_tests/test_instance.py b/nova/tests/functional/notification_sample_tests/test_instance.py index adffd3488ced..de2d8c98eaff 100644 --- a/nova/tests/functional/notification_sample_tests/test_instance.py +++ b/nova/tests/functional/notification_sample_tests/test_instance.py @@ -273,23 +273,25 @@ class TestInstanceNotificationSampleWithMultipleCompute( self._wait_for_notification( 'instance.live_migration_force_complete.end') - # 0. instance.live_migration_pre.start - # 1. instance.live_migration_pre.end - # 2. instance.live_migration_force_complete.start - # 3. instance.live_migration_force_complete.end - self.assertEqual(4, len(fake_notifier.VERSIONED_NOTIFICATIONS)) + # 0. scheduler.select_destinations.start + # 1. scheduler.select_destinations.end + # 2. instance.live_migration_pre.start + # 3. instance.live_migration_pre.end + # 4. instance.live_migration_force_complete.start + # 5. instance.live_migration_force_complete.end + self.assertEqual(6, len(fake_notifier.VERSIONED_NOTIFICATIONS)) self._verify_notification( 'instance-live_migration_force_complete-start', replacements={ 'reservation_id': server['reservation_id'], 'uuid': server['id']}, - actual=fake_notifier.VERSIONED_NOTIFICATIONS[2]) + actual=fake_notifier.VERSIONED_NOTIFICATIONS[4]) self._verify_notification( 'instance-live_migration_force_complete-end', replacements={ 'reservation_id': server['reservation_id'], 'uuid': server['id']}, - actual=fake_notifier.VERSIONED_NOTIFICATIONS[3]) + actual=fake_notifier.VERSIONED_NOTIFICATIONS[5]) class TestInstanceNotificationSampleWithMultipleComputeOldAttachFlow( @@ -435,9 +437,13 @@ class TestInstanceNotificationSample( 'tags': ['tag'], 'trusted_image_certificates': fake_trusted_certs}) - self.assertEqual(2, len(fake_notifier.VERSIONED_NOTIFICATIONS)) + # 0. scheduler.select_destinations.start + # 1. scheduler.select_destinations.end + # 2. instance-create-start + # 3. instance-create-error + self.assertEqual(4, len(fake_notifier.VERSIONED_NOTIFICATIONS)) - tb = fake_notifier.VERSIONED_NOTIFICATIONS[1]['payload'][ + tb = fake_notifier.VERSIONED_NOTIFICATIONS[3]['payload'][ 'nova_object.data']['fault']['nova_object.data']['traceback'] self.assertIn('raise exception.FlavorDiskTooSmall()', tb) @@ -446,14 +452,14 @@ class TestInstanceNotificationSample( replacements={ 'reservation_id': server['reservation_id'], 'uuid': server['id']}, - actual=fake_notifier.VERSIONED_NOTIFICATIONS[0]) + actual=fake_notifier.VERSIONED_NOTIFICATIONS[2]) self._verify_notification( 'instance-create-error', replacements={ 'reservation_id': server['reservation_id'], 'uuid': server['id'], 'fault.traceback': self.ANY}, - actual=fake_notifier.VERSIONED_NOTIFICATIONS[1]) + actual=fake_notifier.VERSIONED_NOTIFICATIONS[3]) fake_notifier.reset() @@ -825,19 +831,19 @@ class TestInstanceNotificationSample( post = {'unshelve': None} self.api.post_server_action(server['id'], post) self._wait_for_state_change(self.admin_api, server, 'ACTIVE') - self.assertEqual(7, len(fake_notifier.VERSIONED_NOTIFICATIONS)) + self.assertEqual(9, len(fake_notifier.VERSIONED_NOTIFICATIONS)) self._verify_notification( 'instance-unshelve-start', replacements={ 'reservation_id': server['reservation_id'], 'uuid': server['id']}, - actual=fake_notifier.VERSIONED_NOTIFICATIONS[5]) + actual=fake_notifier.VERSIONED_NOTIFICATIONS[7]) self._verify_notification( 'instance-unshelve-end', replacements={ 'reservation_id': server['reservation_id'], 'uuid': server['id']}, - actual=fake_notifier.VERSIONED_NOTIFICATIONS[6]) + actual=fake_notifier.VERSIONED_NOTIFICATIONS[8]) def _test_suspend_resume_server(self, server): post = {'suspend': {}} @@ -920,6 +926,29 @@ class TestInstanceNotificationSample( 'uuid': server['id']}, actual=fake_notifier.VERSIONED_NOTIFICATIONS[3]) + def _build_destination_payload(self): + cell1 = self.cell_mappings.get('cell1') + return { + 'nova_object.version': '1.0', + 'nova_object.namespace': 'nova', + 'nova_object.name': 'DestinationPayload', + 'nova_object.data': { + 'aggregates': None, + 'cell': { + 'nova_object.version': '1.0', + 'nova_object.namespace': 'nova', + 'nova_object.name': 'CellMappingPayload', + 'nova_object.data': { + 'database_connection': cell1.database_connection, + 'disabled': False, + 'name': u'cell1', + 'transport_url': u'fake://nowhere/', + 'uuid': cell1.uuid + } + } + } + } + def _test_resize_and_revert_server(self, server): self.flags(allow_resize_to_same_host=True) other_flavor_body = { @@ -948,9 +977,19 @@ class TestInstanceNotificationSample( self.api.post_server_action(server['id'], post) self._wait_for_state_change(self.api, server, 'VERIFY_RESIZE') + self._pop_and_verify_dest_select_notification(server['id'], + replacements={ + 'ignore_hosts': [], + 'flavor.memory_mb': other_flavor_body['flavor']['ram'], + 'flavor.name': other_flavor_body['flavor']['name'], + 'flavor.flavorid': other_flavor_id, + 'flavor.extra_specs': extra_specs['extra_specs'], + 'requested_destination': self._build_destination_payload()}) + self.assertEqual(7, len(fake_notifier.VERSIONED_NOTIFICATIONS)) # ignore instance.exists fake_notifier.VERSIONED_NOTIFICATIONS.pop(0) + # This list needs to be in order. expected_notifications = [ 'instance-resize_prep-start', @@ -1025,6 +1064,13 @@ class TestInstanceNotificationSample( mock_prep_resize.side_effect = _build_resources self.api.post_server_action(server['id'], post) self._wait_for_notification('instance.resize.error') + self._pop_and_verify_dest_select_notification(server['id'], + replacements={ + 'ignore_hosts': [], + 'flavor.name': other_flavor_body['flavor']['name'], + 'flavor.flavorid': other_flavor_id, + 'flavor.extra_specs': {}, + 'requested_destination': self._build_destination_payload()}) # 0: instance-exists # 1: instance-resize_prep-start # 2: instance-resize-error @@ -1089,7 +1135,8 @@ class TestInstanceNotificationSample( self.api.post_server_action(server['id'], post) self._wait_for_state_change(self.api, server, expected_status='ERROR') self._wait_for_notification('compute.exception') - # There should be the following notifications. + # There should be the following notifications after scheduler's + # select_destination notifications: # 0: instance-exists # 1: instance-resize_prep-start # 2: instance-resize-error @@ -1097,6 +1144,13 @@ class TestInstanceNotificationSample( # 4: compute.exception # (via the wrap_exception decorator on # the ComputeManager.prep_resize method.) + self._pop_and_verify_dest_select_notification(server['id'], + replacements={ + 'ignore_hosts': [], + 'flavor.name': other_flavor_body['flavor']['name'], + 'flavor.flavorid': other_flavor_id, + 'flavor.extra_specs': {}, + 'requested_destination': self._build_destination_payload()}) self.assertEqual(5, len(fake_notifier.VERSIONED_NOTIFICATIONS), 'Unexpected number of notifications: %s' % fake_notifier.VERSIONED_NOTIFICATIONS) @@ -1146,10 +1200,11 @@ class TestInstanceNotificationSample( fake_notifier.reset() + image_ref = 'a2459075-d96c-40d5-893e-577ff92e721c' post = { 'rebuild': { - 'imageRef': 'a2459075-d96c-40d5-893e-577ff92e721c', - 'metadata': {}, + 'imageRef': image_ref, + 'metadata': {} } } self.api.post_server_action(server['id'], post) @@ -1160,6 +1215,22 @@ class TestInstanceNotificationSample( self._wait_for_state_change(self.api, server, expected_status='ACTIVE') + self._pop_and_verify_dest_select_notification(server['id'], + replacements={ + 'image.container_format': 'ami', + 'image.disk_format': 'ami', + 'image.id': image_ref, + 'image.properties': { + 'nova_object.data': {}, + 'nova_object.name': 'ImageMetaPropsPayload', + 'nova_object.namespace': 'nova', + 'nova_object.version': u'1.0'}, + 'image.size': 58145823, + 'image.tags': [], + 'scheduler_hints': {'_nova_check_type': ['rebuild']}, + 'force_hosts': 'compute', + 'force_nodes': 'fake-mini'}) + # 0. instance.rebuild_scheduled # 1. instance.exists # 2. instance.rebuild.start @@ -1222,10 +1293,11 @@ class TestInstanceNotificationSample( fake_notifier.reset() + image_ref = 'a2459075-d96c-40d5-893e-577ff92e721c' rebuild_trusted_certs = ['rebuild-cert-id-1', 'rebuild-cert-id-2'] post = { 'rebuild': { - 'imageRef': 'a2459075-d96c-40d5-893e-577ff92e721c', + 'imageRef': image_ref, 'metadata': {}, 'trusted_image_certificates': rebuild_trusted_certs, } @@ -1238,6 +1310,22 @@ class TestInstanceNotificationSample( self._wait_for_state_change(self.api, server, expected_status='ACTIVE') + self._pop_and_verify_dest_select_notification(server['id'], + replacements={ + 'image.container_format': 'ami', + 'image.disk_format': 'ami', + 'image.id': image_ref, + 'image.properties': { + 'nova_object.data': {}, + 'nova_object.name': 'ImageMetaPropsPayload', + 'nova_object.namespace': 'nova', + 'nova_object.version': u'1.0'}, + 'image.size': 58145823, + 'image.tags': [], + 'scheduler_hints': {'_nova_check_type': ['rebuild']}, + 'force_hosts': 'compute', + 'force_nodes': 'fake-mini'}) + # 0. instance.rebuild_scheduled # 1. instance.exists # 2. instance.rebuild.start diff --git a/nova/tests/functional/notification_sample_tests/test_server_group.py b/nova/tests/functional/notification_sample_tests/test_server_group.py index 8c04193ebd2f..a56d9fe590df 100644 --- a/nova/tests/functional/notification_sample_tests/test_server_group.py +++ b/nova/tests/functional/notification_sample_tests/test_server_group.py @@ -61,11 +61,13 @@ class TestServerGroupNotificationSample( scheduler_hints={"group": group['id']}) self._wait_for_notification('instance.update') # 0: server_group.add_member - # 1: instance.create.start - # 2: instance.create.end - # 3: instance.update + # 1: scheduler-select_destinations-start + # 2: scheduler-select_destinations-end + # 3: instance.create.start + # 4: instance.create.end + # 5: instance.update # (Due to adding server tags in the '_boot_a_server' method.) - self.assertEqual(4, len(fake_notifier.VERSIONED_NOTIFICATIONS)) + self.assertEqual(6, len(fake_notifier.VERSIONED_NOTIFICATIONS)) self._verify_notification( 'server_group-add_member', replacements={'uuid': group['id'], diff --git a/nova/tests/unit/notifications/objects/test_notification.py b/nova/tests/unit/notifications/objects/test_notification.py index a3ffc4e172e2..221699802f1d 100644 --- a/nova/tests/unit/notifications/objects/test_notification.py +++ b/nova/tests/unit/notifications/objects/test_notification.py @@ -370,9 +370,11 @@ notification_object_data = { 'AuditPeriodPayload': '1.0-2b429dd307b8374636703b843fa3f9cb', 'BandwidthPayload': '1.0-ee2616a7690ab78406842a2b68e34130', 'BlockDevicePayload': '1.0-29751e1b6d41b1454e36768a1e764df8', + 'CellMappingPayload': '1.0-cf7faeb3cdd6b0c742ff74c80b88fb11', 'ComputeTaskNotification': '1.0-a73147b93b520ff0061865849d3dfa56', 'ComputeTaskPayload': '1.0-e3d34762c14d131c98337b72e8c600e1', - 'EventType': '1.18-44f33a06fd08fdba0b7dc266116c017b', + 'DestinationPayload': '1.0-4ccf26318dd18c4377dada2b1e74ec2e', + 'EventType': '1.19-000a76e83b06a9de11d365465a755a5e', 'ExceptionNotification': '1.0-a73147b93b520ff0061865849d3dfa56', 'ExceptionPayload': '1.1-6c43008bd81885a63bc7f7c629f0793b', 'FlavorNotification': '1.0-a73147b93b520ff0061865849d3dfa56', @@ -418,7 +420,9 @@ notification_object_data = { 'MetricsNotification': '1.0-a73147b93b520ff0061865849d3dfa56', 'MetricsPayload': '1.0-65c69b15b4de5a8c01971cb5bb9ab650', 'NotificationPublisher': '2.2-b6ad48126247e10b46b6b0240e52e614', - 'RequestSpecPayload': '1.0-ef9936c8da44e442e397b02dec3f6914', + 'RequestSpecPayload': '1.1-64d30723a2e381d0cd6a16a877002c64', + 'SchedulerRetriesPayload': '1.0-03a07d09575ef52cced5b1b24301d0b4', + 'SelectDestinationsNotification': '1.0-a73147b93b520ff0061865849d3dfa56', 'ServerGroupNotification': '1.0-a73147b93b520ff0061865849d3dfa56', 'ServerGroupPayload': '1.1-4ded2997ea1b07038f7af33ef5c45f7f', 'ServiceStatusNotification': '1.0-a73147b93b520ff0061865849d3dfa56', diff --git a/nova/tests/unit/scheduler/test_filter_scheduler.py b/nova/tests/unit/scheduler/test_filter_scheduler.py index 459f710bb0bf..da01fb1fde4d 100644 --- a/nova/tests/unit/scheduler/test_filter_scheduler.py +++ b/nova/tests/unit/scheduler/test_filter_scheduler.py @@ -83,7 +83,10 @@ class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase): root_gb=512, ephemeral_gb=0, swap=0, - vcpus=1), + vcpus=1, + disabled=False, + is_public=True, + name="small_flavor"), project_id=uuids.project_id, instance_group=None, instance_uuid=uuids.instance) # Reset the RequestSpec changes so they don't interfere with the @@ -149,7 +152,10 @@ class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase): root_gb=512, ephemeral_gb=0, swap=0, - vcpus=1), + vcpus=1, + disabled=False, + is_public=True, + name="small_flavor"), project_id=uuids.project_id, instance_group=group) @@ -198,7 +204,10 @@ class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase): root_gb=512, ephemeral_gb=0, swap=0, - vcpus=1), + vcpus=1, + disabled=False, + is_public=True, + name="small_flavor"), project_id=uuids.project_id, instance_group=None) @@ -268,7 +277,10 @@ class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase): root_gb=512, ephemeral_gb=0, swap=0, - vcpus=1), + vcpus=1, + disabled=False, + is_public=True, + name="small_flavor"), project_id=uuids.project_id, instance_group=None) @@ -321,7 +333,10 @@ class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase): root_gb=512, ephemeral_gb=0, swap=0, - vcpus=1), + vcpus=1, + disabled=False, + is_public=True, + name="small_flavor"), project_id=uuids.project_id, instance_group=None) @@ -500,7 +515,10 @@ class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase): root_gb=512, ephemeral_gb=0, swap=0, - vcpus=1), + vcpus=1, + disabled=False, + is_public=True, + name="small_flavor"), project_id=uuids.project_id, instance_group=ig, instance_uuid=uuids.instance0) # Reset the RequestSpec changes so they don't interfere with the @@ -765,9 +783,16 @@ class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase): root_gb=512, ephemeral_gb=0, swap=0, - vcpus=1), + vcpus=1, + disabled=False, + is_public=True, + name="small_flavor"), project_id=uuids.project_id, - num_instances=1) + num_instances=1, + image=None, + numa_topology=None, + pci_requests=None, + instance_uuid=uuids.instance_id) mock_schedule.return_value = [[fake_selection]] dests = self.driver.select_destinations(self.context, spec_obj, @@ -793,9 +818,16 @@ class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase): root_gb=512, ephemeral_gb=0, swap=0, - vcpus=1), + vcpus=1, + disabled=False, + is_public=True, + name="small_flavor"), project_id=uuids.project_id, - num_instances=2) + num_instances=2, + image=None, + numa_topology=None, + pci_requests=None, + instance_uuid=uuids.instance_id) mock_schedule.return_value = [[fake_selection]] dests = self.driver.select_destinations(self.context, spec_obj, @@ -824,8 +856,12 @@ class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase): root_gb=512, ephemeral_gb=0, swap=0, - vcpus=1), + vcpus=1, + disabled=False, + is_public=True, + name="small_flavor"), project_id=uuids.project_id, + instance_uuid=uuids.instance_id, instance_group=None) host_state = mock.Mock(spec=host_manager.HostState, host="fake_host", @@ -1020,16 +1056,32 @@ class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase): self._test_not_enough_alternates(num_hosts=3, max_attempts=5) self._test_not_enough_alternates(num_hosts=20, max_attempts=5) + @mock.patch('nova.compute.utils.notify_about_scheduler_action') @mock.patch.object(filter_scheduler.FilterScheduler, '_schedule') - def test_select_destinations_notifications(self, mock_schedule): + def test_select_destinations_notifications(self, mock_schedule, + mock_notify): mock_schedule.return_value = ([[mock.Mock()]], [[mock.Mock()]]) with mock.patch.object(self.driver.notifier, 'info') as mock_info: + flavor = objects.Flavor(memory_mb=512, + root_gb=512, + ephemeral_gb=0, + swap=0, + vcpus=1, + disabled=False, + is_public=True, + name="small_flavor") expected = {'num_instances': 1, - 'instance_properties': {'uuid': uuids.instance}, - 'instance_type': {}, + 'instance_properties': { + 'uuid': uuids.instance, + 'ephemeral_gb': 0, + 'memory_mb': 512, + 'vcpus': 1, + 'root_gb': 512}, + 'instance_type': flavor, 'image': {}} spec_obj = objects.RequestSpec(num_instances=1, + flavor=flavor, instance_uuid=uuids.instance) self.driver.select_destinations(self.context, spec_obj, @@ -1042,6 +1094,12 @@ class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase): dict(request_spec=expected))] self.assertEqual(expected, mock_info.call_args_list) + mock_notify.assert_has_calls([ + mock.call(context=self.context, request_spec=spec_obj, + action='select_destinations', phase='start'), + mock.call(context=self.context, request_spec=spec_obj, + action='select_destinations', phase='end')]) + def test_get_all_host_states_provider_summaries_is_none(self): """Tests that HostManager.get_host_states_by_uuids is called with compute_uuids being None when the incoming provider_summaries is None.