diff --git a/doc/notification_samples/instance-delete-end.json b/doc/notification_samples/instance-delete-end.json new file mode 100644 index 000000000000..3c0cd2518b87 --- /dev/null +++ b/doc/notification_samples/instance-delete-end.json @@ -0,0 +1,49 @@ +{ + "event_type":"instance.delete.end", + "payload":{ + "nova_object.data":{ + "architecture":"x86_64", + "availability_zone":null, + "created_at":"2012-10-29T13:42:11Z", + "deleted_at":"2012-10-29T13:42:11Z", + "display_name":"some-server", + "fault":null, + "host":"compute", + "host_name":"some-server", + "ip_addresses":[], + "kernel_id":"", + "launched_at":"2012-10-29T13:42:11Z", + "image_uuid": "155d900f-4e14-4e4c-a73d-069cbf4541e6", + "metadata":{}, + "node":"fake-mini", + "os_type":null, + "progress":0, + "ramdisk_id":"", + "reservation_id":"r-npxv0e40", + "state":"deleted", + "task_state":null, + "power_state":"pending", + "tenant_id":"6f70656e737461636b20342065766572", + "terminated_at":"2012-10-29T13:42:11Z", + "flavor": { + "nova_object.name": "FlavorPayload", + "nova_object.data": { + "flavorid": "a22d5517-147c-4147-a0d1-e698df5cd4e3", + "root_gb": 1, + "vcpus": 1, + "ephemeral_gb": 0, + "memory_mb": 512 + }, + "nova_object.version": "1.0", + "nova_object.namespace": "nova" + }, + "user_id":"fake", + "uuid":"178b0921-8f85-4257-88b6-2e743b5a975c" + }, + "nova_object.name":"InstanceActionPayload", + "nova_object.namespace":"nova", + "nova_object.version":"1.0" + }, + "priority":"INFO", + "publisher_id":"nova-compute:compute" +} \ No newline at end of file diff --git a/doc/notification_samples/instance-delete-start.json b/doc/notification_samples/instance-delete-start.json new file mode 100644 index 000000000000..51008a131182 --- /dev/null +++ b/doc/notification_samples/instance-delete-start.json @@ -0,0 +1,62 @@ +{ + "event_type":"instance.delete.start", + "payload":{ + "nova_object.data":{ + "architecture":"x86_64", + "availability_zone":null, + "created_at":"2012-10-29T13:42:11Z", + "deleted_at":null, + "display_name":"some-server", + "fault":null, + "host":"compute", + "host_name":"some-server", + "ip_addresses": [{ + "nova_object.name": "IpPayload", + "nova_object.namespace": "nova", + "nova_object.version": "1.0", + "nova_object.data": { + "mac": "fa:16:3e:4c:2c:30", + "address": "192.168.1.3", + "port_uuid": "ce531f90-199f-48c0-816c-13e38010b442", + "meta": {}, + "version": 4, + "label": "private-network", + "device_name": "tapce531f90-19" + } + }], + "kernel_id":"", + "launched_at":"2012-10-29T13:42:11Z", + "image_uuid": "155d900f-4e14-4e4c-a73d-069cbf4541e6", + "metadata":{}, + "node":"fake-mini", + "os_type":null, + "progress":0, + "ramdisk_id":"", + "reservation_id":"r-npxv0e40", + "state":"active", + "task_state":"deleting", + "power_state":"running", + "tenant_id":"6f70656e737461636b20342065766572", + "terminated_at":null, + "flavor": { + "nova_object.name": "FlavorPayload", + "nova_object.data": { + "flavorid": "a22d5517-147c-4147-a0d1-e698df5cd4e3", + "root_gb": 1, + "vcpus": 1, + "ephemeral_gb": 0, + "memory_mb": 512 + }, + "nova_object.version": "1.0", + "nova_object.namespace": "nova" + }, + "user_id":"fake", + "uuid":"178b0921-8f85-4257-88b6-2e743b5a975c" + }, + "nova_object.name":"InstanceActionPayload", + "nova_object.namespace":"nova", + "nova_object.version":"1.0" + }, + "priority":"INFO", + "publisher_id":"nova-compute:compute" +} \ No newline at end of file diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 219560c61d96..dc59f51c0276 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -85,6 +85,7 @@ from nova.network import model as network_model from nova.network.security_group import openstack_driver from nova import objects from nova.objects import base as obj_base +from nova.objects import fields from nova.objects import instance as obj_instance from nova.objects import migrate_data as migrate_data_obj from nova import rpc @@ -727,7 +728,9 @@ class ComputeManager(manager.Manager): self._update_resource_tracker(context, instance) self._notify_about_instance_usage(context, instance, "delete.end", system_metadata=system_meta) - + compute_utils.notify_about_instance_action(context, instance, + self.host, action=fields.NotificationAction.DELETE, + phase=fields.NotificationPhase.END) self._clean_instance_console_tokens(context, instance) self._delete_scheduler_instance_info(context, instance.uuid) @@ -2279,6 +2282,10 @@ class ComputeManager(manager.Manager): instance=instance) self._notify_about_instance_usage(context, instance, "delete.start") + compute_utils.notify_about_instance_action(context, instance, + self.host, action=fields.NotificationAction.DELETE, + phase=fields.NotificationPhase.START) + self._shutdown_instance(context, instance, bdms) # NOTE(dims): instance.info_cache.delete() should be called after # _shutdown_instance in the compute manager as shutdown calls diff --git a/nova/compute/utils.py b/nova/compute/utils.py index 4c5dadc084cf..babc8d31c883 100644 --- a/nova/compute/utils.py +++ b/nova/compute/utils.py @@ -32,7 +32,10 @@ from nova import exception from nova.i18n import _LW from nova.network import model as network_model from nova import notifications +from nova.notifications.objects import base as notification_base +from nova.notifications.objects import instance as instance_notification from nova import objects +from nova.objects import fields from nova import rpc from nova import safe_utils from nova import utils @@ -318,6 +321,49 @@ def notify_about_instance_usage(notifier, context, instance, event_suffix, method(context, 'compute.instance.%s' % event_suffix, usage_info) +def notify_about_instance_action(context, instance, host, action, phase=None, + binary='nova-compute'): + """Send versioned notification about the action made on the instance + :param instance: the instance which the action performed on + :param host: the host emitting the notification + :param action: the name of the action + :param phase: the phase of the action + :param binary: the binary emitting the notification + """ + network_info = get_nw_info_for_instance(instance) + ips = [] + if network_info is not None: + for vif in network_info: + for ip in vif.fixed_ips(): + ips.append(instance_notification.IpPayload( + label=vif["network"]["label"], + mac=vif["address"], + meta=vif["meta"], + port_uuid=vif["id"], + version=ip["version"], + address=ip["address"], + device_name=vif["devname"])) + flavor = instance_notification.FlavorPayload(instance=instance) + # TODO(gibi): handle fault during the transformation of the first error + # notifications + payload = instance_notification.InstanceActionPayload( + instance=instance, + fault=None, + ip_addresses=ips, + flavor=flavor) + notification = instance_notification.InstanceActionNotification( + context=context, + priority=fields.NotificationPriority.INFO, + publisher=notification_base.NotificationPublisher( + context=context, host=host, binary=binary), + event_type=notification_base.EventType( + object='instance', + action=action, + phase=phase), + payload=payload) + notification.emit(context) + + def notify_about_server_group_update(context, event_suffix, sg_payload): """Send a notification about server group update. diff --git a/nova/notifications/objects/base.py b/nova/notifications/objects/base.py index cd6e40b9a8ea..354766fc3655 100644 --- a/nova/notifications/objects/base.py +++ b/nova/notifications/objects/base.py @@ -23,13 +23,22 @@ class NotificationObject(base.NovaObject): # Version 1.0: Initial version VERSION = '1.0' + def __init__(self, **kwargs): + super(NotificationObject, self).__init__(**kwargs) + # The notification objects are created on the fly when nova emits the + # notification. This causes that every object shows every field as + # changed. We don't want to send this meaningless information so we + # reset the object after creation. + self.obj_reset_changes(recursive=False) + @base.NovaObjectRegistry.register_notification class EventType(NotificationObject): # Version 1.0: Initial version # Version 1.1: New valid actions values are added to the # NotificationActionField enum - VERSION = '1.1' + # Version 1.2: DELETE value is added to the NotificationActionField enum + VERSION = '1.2' fields = { 'object': fields.StringField(nullable=False), @@ -70,8 +79,8 @@ class NotificationPayloadBase(NotificationObject): # Version 1.0: Initial version VERSION = '1.0' - def __init__(self, *args, **kwargs): - super(NotificationPayloadBase, self).__init__(*args, **kwargs) + def __init__(self, **kwargs): + super(NotificationPayloadBase, self).__init__(**kwargs) self.populated = not self.SCHEMA def populate_schema(self, **kwargs): @@ -86,6 +95,10 @@ class NotificationPayloadBase(NotificationObject): setattr(self, key, getattr(source, field)) self.populated = True + # the schema population will create changed fields but we don't need + # this information in the notification + self.obj_reset_changes(recursive=False) + @base.NovaObjectRegistry.register_notification class NotificationPublisher(NotificationObject): diff --git a/nova/notifications/objects/instance.py b/nova/notifications/objects/instance.py new file mode 100644 index 000000000000..488caf99aaf4 --- /dev/null +++ b/nova/notifications/objects/instance.py @@ -0,0 +1,158 @@ +# 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 + + +@nova_base.NovaObjectRegistry.register_notification +class InstancePayload(base.NotificationPayloadBase): + SCHEMA = { + 'uuid': ('instance', 'uuid'), + 'user_id': ('instance', 'user_id'), + 'tenant_id': ('instance', 'project_id'), + 'reservation_id': ('instance', 'reservation_id'), + 'display_name': ('instance', 'display_name'), + 'host_name': ('instance', 'hostname'), + 'host': ('instance', 'host'), + 'node': ('instance', 'node'), + 'os_type': ('instance', 'os_type'), + 'architecture': ('instance', 'architecture'), + 'availability_zone': ('instance', 'availability_zone'), + + 'image_uuid': ('instance', 'image_ref'), + + 'kernel_id': ('instance', 'kernel_id'), + 'ramdisk_id': ('instance', 'ramdisk_id'), + + 'created_at': ('instance', 'created_at'), + 'launched_at': ('instance', 'launched_at'), + 'terminated_at': ('instance', 'terminated_at'), + 'deleted_at': ('instance', 'deleted_at'), + + 'state': ('instance', 'vm_state'), + 'power_state': ('instance', 'power_state'), + 'task_state': ('instance', 'task_state'), + 'progress': ('instance', 'progress'), + + 'metadata': ('instance', 'metadata'), + } + # Version 1.0: Initial version + VERSION = '1.0' + fields = { + 'uuid': fields.UUIDField(), + 'user_id': fields.StringField(nullable=True), + 'tenant_id': fields.StringField(nullable=True), + 'reservation_id': fields.StringField(nullable=True), + 'display_name': fields.StringField(nullable=True), + 'host_name': fields.StringField(nullable=True), + 'host': fields.StringField(nullable=True), + 'node': fields.StringField(nullable=True), + 'os_type': fields.StringField(nullable=True), + 'architecture': fields.StringField(nullable=True), + 'availability_zone': fields.StringField(nullable=True), + + 'flavor': fields.ObjectField('FlavorPayload'), + 'image_uuid': fields.StringField(nullable=True), + + 'kernel_id': fields.StringField(nullable=True), + 'ramdisk_id': fields.StringField(nullable=True), + + 'created_at': fields.DateTimeField(nullable=True), + 'launched_at': fields.DateTimeField(nullable=True), + 'terminated_at': fields.DateTimeField(nullable=True), + 'deleted_at': fields.DateTimeField(nullable=True), + + 'state': fields.InstanceStateField(nullable=True), + 'power_state': fields.InstancePowerStateField(nullable=True), + 'task_state': fields.InstanceTaskStateField(nullable=True), + 'progress': fields.IntegerField(nullable=True), + + 'ip_addresses': fields.ListOfObjectsField('IpPayload'), + + 'metadata': fields.DictOfStringsField(), + } + + def __init__(self, instance, **kwargs): + super(InstancePayload, self).__init__(**kwargs) + self.populate_schema(instance=instance) + + +@nova_base.NovaObjectRegistry.register_notification +class InstanceActionPayload(InstancePayload): + # No SCHEMA as all the additional fields are calculated + + VERSION = '1.0' + fields = { + 'fault': fields.ObjectField('ExceptionPayload', nullable=True), + } + + def __init__(self, instance, fault, ip_addresses, flavor): + super(InstanceActionPayload, self).__init__( + instance=instance, + fault=fault, + ip_addresses=ip_addresses, + flavor=flavor) + + +@nova_base.NovaObjectRegistry.register_notification +class IpPayload(base.NotificationPayloadBase): + # Version 1.0: Initial version + VERSION = '1.0' + fields = { + 'label': fields.StringField(), + 'mac': fields.MACAddressField(), + 'meta': fields.DictOfStringsField(), + 'port_uuid': fields.UUIDField(nullable=True), + 'version': fields.IntegerField(), + 'address': fields.IPV4AndV6AddressField(), + 'device_name': fields.StringField(nullable=True) + } + + +@nova_base.NovaObjectRegistry.register_notification +class FlavorPayload(base.NotificationPayloadBase): + # Version 1.0: Initial version + VERSION = '1.0' + + SCHEMA = { + 'flavorid': ('flavor', 'flavorid'), + 'memory_mb': ('instance', 'memory_mb'), + 'vcpus': ('instance', 'vcpus'), + 'root_gb': ('instance', 'root_gb'), + 'ephemeral_gb': ('instance', 'ephemeral_gb'), + } + + fields = { + 'flavorid': fields.StringField(nullable=True), + 'memory_mb': fields.IntegerField(nullable=True), + 'vcpus': fields.IntegerField(nullable=True), + 'root_gb': fields.IntegerField(nullable=True), + 'ephemeral_gb': fields.IntegerField(nullable=True), + } + + def __init__(self, instance, **kwargs): + super(FlavorPayload, self).__init__(**kwargs) + self.populate_schema(instance=instance, flavor=instance.flavor) + + +@base.notification_sample('instance-delete-start.json') +@base.notification_sample('instance-delete-end.json') +@nova_base.NovaObjectRegistry.register_notification +class InstanceActionNotification(base.NotificationBase): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'payload': fields.ObjectField('InstanceActionPayload') + } diff --git a/nova/objects/fields.py b/nova/objects/fields.py index 7a4a01035f4e..4b5a45a58734 100644 --- a/nova/objects/fields.py +++ b/nova/objects/fields.py @@ -609,14 +609,136 @@ class NotificationPhase(Enum): class NotificationAction(Enum): UPDATE = 'update' EXCEPTION = 'exception' + DELETE = 'delete' - ALL = (UPDATE, EXCEPTION) + ALL = (UPDATE, EXCEPTION, DELETE) def __init__(self): super(NotificationAction, self).__init__( valid_values=NotificationAction.ALL) +class InstanceState(Enum): + # TODO(gibi): this is currently a copy of nova.compute.vm_states, remove + # the duplication + ACTIVE = 'active' + BUILDING = 'building' + PAUSED = 'paused' + SUSPENDED = 'suspended' + STOPPED = 'stopped' + RESCUED = 'rescued' + RESIZED = 'resized' + SOFT_DELETED = 'soft-delete' + DELETED = 'deleted' + ERROR = 'error' + SHELVED = 'shelved' + SHELVED_OFFLOADED = 'shelved_offloaded' + + ALL = (ACTIVE, BUILDING, PAUSED, SUSPENDED, STOPPED, RESCUED, RESIZED, + SOFT_DELETED, DELETED, ERROR, SHELVED, SHELVED_OFFLOADED) + + def __init__(self): + super(InstanceState, self).__init__( + valid_values=InstanceState.ALL) + + +class InstanceTaskState(Enum): + # TODO(gibi): this is currently a copy of nova.compute.task_states, remove + # the duplication + SCHEDULING = 'scheduling' + BLOCK_DEVICE_MAPPING = 'block_device_mapping' + NETWORKING = 'networking' + SPAWNING = 'spawning' + IMAGE_SNAPSHOT = 'image_snapshot' + IMAGE_SNAPSHOT_PENDING = 'image_snapshot_pending' + IMAGE_PENDING_UPLOAD = 'image_pending_upload' + IMAGE_UPLOADING = 'image_uploading' + IMAGE_BACKUP = 'image_backup' + UPDATING_PASSWORD = 'updating_password' + RESIZE_PREP = 'resize_prep' + RESIZE_MIGRATING = 'resize_migrating' + RESIZE_MIGRATED = 'resize_migrated' + RESIZE_FINISH = 'resize_finish' + RESIZE_REVERTING = 'resize_reverting' + RESIZE_CONFIRMING = 'resize_confirming' + REBOOTING = 'rebooting' + REBOOT_PENDING = 'reboot_pending' + REBOOT_STARTED = 'reboot_started' + REBOOTING_HARD = 'rebooting_hard' + REBOOT_PENDING_HARD = 'reboot_pending_hard' + REBOOT_STARTED_HARD = 'reboot_started_hard' + PAUSING = 'pausing' + UNPAUSING = 'unpausing' + SUSPENDING = 'suspending' + RESUMING = 'resuming' + POWERING_OFF = 'powering-off' + POWERING_ON = 'powering-on' + RESCUING = 'rescuing' + UNRESCUING = 'unrescuing' + REBUILDING = 'rebuilding' + REBUILD_BLOCK_DEVICE_MAPPING = "rebuild_block_device_mapping" + REBUILD_SPAWNING = 'rebuild_spawning' + MIGRATING = "migrating" + DELETING = 'deleting' + SOFT_DELETING = 'soft-deleting' + RESTORING = 'restoring' + SHELVING = 'shelving' + SHELVING_IMAGE_PENDING_UPLOAD = 'shelving_image_pending_upload' + SHELVING_IMAGE_UPLOADING = 'shelving_image_uploading' + SHELVING_OFFLOADING = 'shelving_offloading' + UNSHELVING = 'unshelving' + + ALL = (SCHEDULING, BLOCK_DEVICE_MAPPING, NETWORKING, SPAWNING, + IMAGE_SNAPSHOT, IMAGE_SNAPSHOT_PENDING, IMAGE_PENDING_UPLOAD, + IMAGE_UPLOADING, IMAGE_BACKUP, UPDATING_PASSWORD, RESIZE_PREP, + RESIZE_MIGRATING, RESIZE_MIGRATED, RESIZE_FINISH, RESIZE_REVERTING, + RESIZE_CONFIRMING, REBOOTING, REBOOT_PENDING, REBOOT_STARTED, + REBOOTING_HARD, REBOOT_PENDING_HARD, REBOOT_STARTED_HARD, PAUSING, + UNPAUSING, SUSPENDING, RESUMING, POWERING_OFF, POWERING_ON, + RESCUING, UNRESCUING, REBUILDING, REBUILD_BLOCK_DEVICE_MAPPING, + REBUILD_SPAWNING, MIGRATING, DELETING, SOFT_DELETING, RESTORING, + SHELVING, SHELVING_IMAGE_PENDING_UPLOAD, SHELVING_IMAGE_UPLOADING, + SHELVING_OFFLOADING, UNSHELVING) + + def __init__(self): + super(InstanceTaskState, self).__init__( + valid_values=InstanceTaskState.ALL) + + +class InstancePowerState(Enum): + # TODO(gibi): this is currently a copy of nova.compute.power_state, remove + # the duplication + NOSTATE = 'pending' + RUNNING = 'running' + PAUSED = 'paused' + SHUTDOWN = 'shutdown' + CRASHED = 'crashed' + SUSPENDED = 'suspended' + + VALUE_MAP = { + 0x00: NOSTATE, + 0x01: RUNNING, + 0x03: PAUSED, + 0x04: SHUTDOWN, + 0x06: CRASHED, + 0x07: SUSPENDED + } + + ALL = (NOSTATE, RUNNING, PAUSED, SHUTDOWN, CRASHED, SUSPENDED) + + def __init__(self): + super(InstancePowerState, self).__init__( + valid_values=InstancePowerState.ALL) + + def coerce(self, obj, attr, value): + try: + value = int(value) + value = InstancePowerState.VALUE_MAP[value] + except (ValueError, KeyError): + pass + return super(InstancePowerState, self).coerce(obj, attr, value) + + class IPV4AndV6Address(IPAddress): @staticmethod def coerce(obj, attr, value): @@ -864,6 +986,18 @@ class NotificationActionField(BaseEnumField): AUTO_TYPE = NotificationAction() +class InstanceStateField(BaseEnumField): + AUTO_TYPE = InstanceState() + + +class InstanceTaskStateField(BaseEnumField): + AUTO_TYPE = InstanceTaskState() + + +class InstancePowerStateField(BaseEnumField): + AUTO_TYPE = InstancePowerState() + + class IPV4AndV6AddressField(AutoTypedField): AUTO_TYPE = IPV4AndV6Address() diff --git a/nova/tests/fixtures.py b/nova/tests/fixtures.py index 1cff0e596a87..ccdc99fc1848 100644 --- a/nova/tests/fixtures.py +++ b/nova/tests/fixtures.py @@ -665,3 +665,90 @@ class AllServicesCurrent(fixtures.Fixture): def _fake_minimum(self, *args, **kwargs): return service_obj.SERVICE_VERSION + + +class NeutronFixture(fixtures.Fixture): + """A fixture to boot instances with neutron ports""" + + # the default project_id in OsaAPIFixtures + tenant_id = '6f70656e737461636b20342065766572' + network_1 = { + 'status': 'ACTIVE', + 'subnets': [], + 'name': 'private-network', + 'admin_state_up': True, + 'tenant_id': tenant_id, + 'id': '3cb9bc59-5699-4588-a4b1-b87f96708bc6', + } + subnet_1 = { + 'name': 'private-subnet', + 'enable_dhcp': True, + 'network_id': network_1['id'], + 'tenant_id': tenant_id, + 'dns_nameservers': [], + 'allocation_pools': [ + { + 'start': '192.168.1.1', + 'end': '192.168.1.254' + } + ], + 'host_routes': [], + 'ip_version': 4, + 'gateway_ip': '192.168.1.1', + 'cidr': '192.168.1.1/24', + 'id': 'f8a6e8f8-c2ec-497c-9f23-da9616de54ef' + } + network_1['subnets'] = [subnet_1['id']] + + port_1 = { + 'id': 'ce531f90-199f-48c0-816c-13e38010b442', + 'network_id': network_1['id'], + 'admin_state_up': True, + 'status': 'ACTIVE', + 'mac_address': 'fa:16:3e:4c:2c:30', + 'fixed_ips': [ + { + 'ip_address': '192.168.1.3', + 'subnet_id': subnet_1['id'] + } + ], + 'tenant_id': tenant_id + } + + def __init__(self, test): + super(NeutronFixture, self).__init__() + self.test = test + + def setUp(self): + super(NeutronFixture, self).setUp() + + self.test.stub_out( + 'nova.network.neutronv2.api.API.' + 'validate_networks', + lambda *args, **kwargs: 1) + self.test.stub_out( + 'nova.network.neutronv2.api.API.' + 'create_pci_requests_for_sriov_ports', + lambda *args, **kwargs: None) + self.test.stub_out( + 'nova.network.security_group.neutron_driver.SecurityGroupAPI.' + 'get_instances_security_groups_bindings', + lambda *args, **kwargs: {}) + + mock_neutron_client = mock.Mock() + mock_neutron_client.list_extensions.return_value = {'extensions': []} + mock_neutron_client.show_port.return_value = { + 'port': NeutronFixture.port_1} + mock_neutron_client.list_networks.return_value = { + 'networks': [NeutronFixture.network_1]} + mock_neutron_client.list_ports.return_value = { + 'ports': [NeutronFixture.port_1]} + mock_neutron_client.list_subnets.return_value = { + 'subnets': [NeutronFixture.subnet_1]} + mock_neutron_client.list_floatingips.return_value = {'floatingips': []} + mock_neutron_client.update_port.return_value = { + 'port': NeutronFixture.port_1} + + self.test.stub_out( + 'nova.network.neutronv2.api.get_client', + lambda *args, **kwargs: mock_neutron_client) diff --git a/nova/tests/functional/integrated_helpers.py b/nova/tests/functional/integrated_helpers.py index d7e235a0256e..38f21b63a365 100644 --- a/nova/tests/functional/integrated_helpers.py +++ b/nova/tests/functional/integrated_helpers.py @@ -239,22 +239,27 @@ class InstanceHelperMixin(object): return server - def _build_minimal_create_server_request(self, api, name): + def _build_minimal_create_server_request(self, api, name, image_uuid=None, + flavor_id=None): server = {} - image = api.get_images()[0] - - if 'imageRef' in image: - image_href = image['imageRef'] + if image_uuid: + image_href = 'http://fake.server/%s' % image_uuid else: - image_href = image['id'] - image_href = 'http://fake.server/%s' % image_href + image = api.get_images()[0] + + if 'imageRef' in image: + image_href = image['imageRef'] + else: + image_href = image['id'] + image_href = 'http://fake.server/%s' % image_href # We now have a valid imageId server['imageRef'] = image_href - # Set a valid flavorId - flavor = api.get_flavors()[1] - server['flavorRef'] = ('http://fake.server/%s' % flavor['id']) + if not flavor_id: + # Set a valid flavorId + flavor_id = api.get_flavors()[1]['id'] + server['flavorRef'] = ('http://fake.server/%s' % flavor_id) server['name'] = name return server 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 c72e6881e429..8106404dddbf 100644 --- a/nova/tests/functional/notification_sample_tests/notification_sample_base.py +++ b/nova/tests/functional/notification_sample_tests/notification_sample_base.py @@ -12,16 +12,27 @@ # License for the specific language governing permissions and limitations # under the License. +import mock import os +import time +from oslo_config import cfg from oslo_serialization import jsonutils +from oslo_utils import fixture as utils_fixture from nova import test from nova.tests import fixtures as nova_fixtures +from nova.tests.functional.api import client as api_client +from nova.tests.functional import integrated_helpers +from nova.tests.unit.api.openstack.compute import test_services from nova.tests.unit import fake_notifier +import nova.tests.unit.image.fake + +CONF = cfg.CONF -class NotificationSampleTestBase(test.TestCase): +class NotificationSampleTestBase(test.TestCase, + integrated_helpers.InstanceHelperMixin): """Base class for notification sample testing. To add tests for a versioned notification you have to store a sample file @@ -43,6 +54,10 @@ class NotificationSampleTestBase(test.TestCase): def setUp(self): super(NotificationSampleTestBase, self).setUp() + # Needs to mock this to avoid REQUIRES_LOCKING to be set to True + patcher = mock.patch('oslo_concurrency.lockutils.lock') + self.addCleanup(patcher.stop) + patcher.start() api_fixture = self.useFixture(nova_fixtures.OSAPIFixture( api_version='v2.1')) @@ -52,6 +67,18 @@ class NotificationSampleTestBase(test.TestCase): fake_notifier.stub_notifier(self) self.addCleanup(fake_notifier.reset) + self.useFixture(utils_fixture.TimeFixture(test_services.fake_utcnow())) + + self.flags(scheduler_driver='nova.scheduler.chance.ChanceScheduler') + # the image fake backend needed for image discovery + nova.tests.unit.image.fake.stub_out_image_service(self) + self.addCleanup(nova.tests.unit.image.fake.FakeImageService_reset) + + self.start_service('conductor', manager=CONF.conductor.manager) + self.start_service('scheduler') + self.start_service('network') + self.start_service('compute') + def _get_notification_sample(self, sample): sample_dir = os.path.dirname(os.path.abspath(__file__)) sample_dir = os.path.normpath(os.path.join( @@ -106,3 +133,47 @@ class NotificationSampleTestBase(test.TestCase): self._apply_replacements(replacements, sample_obj, notification) self.assertJsonEqual(sample_obj, notification) + + def _boot_a_server(self, expected_status='ACTIVE', extra_params=None): + + # We have to depend on a specific image and flavor to fix the content + # of the notification that will be emitted + flavor_body = {'flavor': {'name': 'test_flavor', + 'ram': 512, + 'vcpus': 1, + 'disk': 1, + 'id': 'a22d5517-147c-4147-a0d1-e698df5cd4e3' + }} + + flavor_id = self.api.post_flavor(flavor_body)['id'] + + server = self._build_minimal_create_server_request( + self.api, 'some-server', + image_uuid='155d900f-4e14-4e4c-a73d-069cbf4541e6', + flavor_id=flavor_id) + + if extra_params: + server.update(extra_params) + + post = {'server': server} + created_server = self.api.post_server(post) + self.assertTrue(created_server['id']) + + # Wait for it to finish being created + found_server = self._wait_for_state_change(self.api, created_server, + expected_status) + + return found_server + + def _wait_until_deleted(self, server): + try: + for i in range(40): + server = self.api.get_server(server['id']) + if server['status'] == 'ERROR': + self.fail('Server went to error state instead of' + 'disappearing.') + time.sleep(0.5) + + self.fail('Server failed to delete.') + except api_client.OpenStackApiNotFoundException: + return diff --git a/nova/tests/functional/notification_sample_tests/test_instance.py b/nova/tests/functional/notification_sample_tests/test_instance.py new file mode 100644 index 000000000000..4d19bdba5a0a --- /dev/null +++ b/nova/tests/functional/notification_sample_tests/test_instance.py @@ -0,0 +1,49 @@ +# 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.tests import fixtures +from nova.tests.functional.notification_sample_tests \ + import notification_sample_base +from nova.tests.unit import fake_notifier + + +class TestInstanceNotificationSample( + notification_sample_base.NotificationSampleTestBase): + + def setUp(self): + self.flags(use_neutron=True) + super(TestInstanceNotificationSample, self).setUp() + self.neutron = fixtures.NeutronFixture(self) + self.useFixture(self.neutron) + + def test_create_delete_server(self): + server = self._boot_a_server( + extra_params={'networks': [{'port': self.neutron.port_1['id']}]}) + + # TODO(gibi) verify instance.create notifications here when transformed + self.api.delete_server(server['id']) + self._wait_until_deleted(server) + + self.assertEqual(2, len(fake_notifier.VERSIONED_NOTIFICATIONS)) + self._verify_notification( + 'instance-delete-start', + replacements={ + 'reservation_id': + notification_sample_base.NotificationSampleTestBase.ANY, + 'uuid': server['id']}, + actual=fake_notifier.VERSIONED_NOTIFICATIONS[0]) + self._verify_notification( + 'instance-delete-end', + replacements={ + 'reservation_id': + notification_sample_base.NotificationSampleTestBase.ANY, + 'uuid': server['id']}, + actual=fake_notifier.VERSIONED_NOTIFICATIONS[1]) diff --git a/nova/tests/unit/compute/test_compute_mgr.py b/nova/tests/unit/compute/test_compute_mgr.py index 1a8162dbf008..062161142235 100644 --- a/nova/tests/unit/compute/test_compute_mgr.py +++ b/nova/tests/unit/compute/test_compute_mgr.py @@ -119,7 +119,8 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase): event_pwr_state=power_state.SHUTDOWN, current_pwr_state=power_state.RUNNING) - def test_delete_instance_info_cache_delete_ordering(self): + @mock.patch('nova.compute.utils.notify_about_instance_action') + def test_delete_instance_info_cache_delete_ordering(self, mock_notify): call_tracker = mock.Mock() call_tracker.clear_events_for_instance.return_value = None mgr_class = self.compute.__class__ @@ -128,6 +129,7 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase): # spec out everything except for the method we really want # to test, then use call_tracker to verify call sequence specd_compute._delete_instance = orig_delete + specd_compute.host = 'compute' mock_inst = mock.Mock() mock_inst.uuid = uuids.instance @@ -157,6 +159,11 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase): '_notify_about_instance_usage', '_shutdown_instance', 'delete'], methods_called) + mock_notify.assert_called_once_with(self.context, + mock_inst, + specd_compute.host, + action='delete', + phase='start') def _make_compute_node(self, hyp_hostname, cn_id): cn = mock.Mock(spec_set=['hypervisor_hostname', 'id', @@ -245,7 +252,8 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase): else: self.assertFalse(db_node.destroy.called) - def test_delete_instance_without_info_cache(self): + @mock.patch('nova.compute.utils.notify_about_instance_action') + def test_delete_instance_without_info_cache(self, mock_notify): instance = fake_instance.fake_instance_obj( self.context, uuid=uuids.instance, @@ -267,6 +275,12 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase): instance.info_cache = None self.compute._delete_instance(self.context, instance, [], quotas) + mock_notify.assert_has_calls([ + mock.call(self.context, instance, 'fake-mini', + action='delete', phase='start'), + mock.call(self.context, instance, 'fake-mini', + action='delete', phase='end')]) + def test_check_device_tagging_no_tagging(self): bdms = objects.BlockDeviceMappingList(objects=[ objects.BlockDeviceMapping(source_type='volume', diff --git a/nova/tests/unit/compute/test_compute_utils.py b/nova/tests/unit/compute/test_compute_utils.py index 668d512062d0..55fb7df2ba92 100644 --- a/nova/tests/unit/compute/test_compute_utils.py +++ b/nova/tests/unit/compute/test_compute_utils.py @@ -486,6 +486,39 @@ class UsageInfoTestCase(test.TestCase): uuids.fake_image_ref) self.assertEqual(payload['image_ref_url'], image_ref_url) + def test_notify_about_instance_action(self): + instance = create_instance(self.context) + + compute_utils.notify_about_instance_action( + self.context, + instance, + host='fake-compute', + action='delete', + phase='start') + + self.assertEqual(len(fake_notifier.VERSIONED_NOTIFICATIONS), 1) + notification = fake_notifier.VERSIONED_NOTIFICATIONS[0] + + self.assertEqual(notification['priority'], 'INFO') + self.assertEqual(notification['event_type'], 'instance.delete.start') + self.assertEqual(notification['publisher_id'], + 'nova-compute:fake-compute') + + payload = notification['payload']['nova_object.data'] + self.assertEqual(payload['tenant_id'], self.project_id) + self.assertEqual(payload['user_id'], self.user_id) + self.assertEqual(payload['uuid'], instance['uuid']) + + flavorid = flavors.get_flavor_by_name('m1.tiny')['flavorid'] + flavor = payload['flavor']['nova_object.data'] + self.assertEqual(str(flavor['flavorid']), flavorid) + + for attr in ('display_name', 'created_at', 'launched_at', + 'state', 'task_state'): + self.assertIn(attr, payload, "Key %s not in payload" % attr) + + self.assertEqual(payload['image_uuid'], uuids.fake_image_ref) + def test_notify_usage_exists_instance_not_found(self): # Ensure 'exists' notification generates appropriate usage data. instance = create_instance(self.context) diff --git a/nova/tests/unit/notifications/objects/test_notification.py b/nova/tests/unit/notifications/objects/test_notification.py index 2494c48c7e0d..3ed0e99823e3 100644 --- a/nova/tests/unit/notifications/objects/test_notification.py +++ b/nova/tests/unit/notifications/objects/test_notification.py @@ -255,9 +255,14 @@ class TestNotificationBase(test.NoDBTestCase): notification_object_data = { - 'EventType': '1.1-8291570eed00192197c7fa02ac677cd4', + 'EventType': '1.2-b81c9f80d9344a24df0b1ecce376b515', 'ExceptionNotification': '1.0-a73147b93b520ff0061865849d3dfa56', 'ExceptionPayload': '1.0-4516ae282a55fe2fd5c754967ee6248b', + 'FlavorPayload': '1.0-8ad962ab0bafc7270f474c7dda0b7c20', + 'InstanceActionNotification': '1.0-a73147b93b520ff0061865849d3dfa56', + 'InstanceActionPayload': '1.0-aa6a322cf1a3a19d090259fee65d1094', + 'InstancePayload': '1.0-878bbc5a7a20bdeac7c6570f438a53aa', + 'IpPayload': '1.0-26b40117c41ed95a61ae104f0fcb5fdc', 'NotificationPublisher': '1.0-bbbc1402fb0e443a3eb227cc52b61545', 'ServiceStatusNotification': '1.0-a73147b93b520ff0061865849d3dfa56', 'ServiceStatusPayload': '1.0-a5e7b4fd6cc5581be45b31ff1f3a3f7f',