Transform instance.delete notifications

The instance.delete.start and instance.delete.end notifications
are transformed to the versioned framework using the generic
InstanceActionNotification and InstanceActionPayload class.

There is no instance.delete.error notification in legacy so
it is not added here but considered as a future improvement.

Change-Id: Iddbe50ce0ad3c14562df800bbc09ec5a7e840485
Implements: bp versioned-notification-transformation-newton
This commit is contained in:
Balazs Gibizer 2016-05-26 17:03:31 +02:00
parent 23153952a9
commit 998235da63
14 changed files with 752 additions and 19 deletions

View File

@ -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"
}

View File

@ -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"
}

View File

@ -85,6 +85,7 @@ from nova.network import model as network_model
from nova.network.security_group import openstack_driver from nova.network.security_group import openstack_driver
from nova import objects from nova import objects
from nova.objects import base as obj_base 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 instance as obj_instance
from nova.objects import migrate_data as migrate_data_obj from nova.objects import migrate_data as migrate_data_obj
from nova import rpc from nova import rpc
@ -727,7 +728,9 @@ class ComputeManager(manager.Manager):
self._update_resource_tracker(context, instance) self._update_resource_tracker(context, instance)
self._notify_about_instance_usage(context, instance, "delete.end", self._notify_about_instance_usage(context, instance, "delete.end",
system_metadata=system_meta) 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._clean_instance_console_tokens(context, instance)
self._delete_scheduler_instance_info(context, instance.uuid) self._delete_scheduler_instance_info(context, instance.uuid)
@ -2279,6 +2282,10 @@ class ComputeManager(manager.Manager):
instance=instance) instance=instance)
self._notify_about_instance_usage(context, instance, self._notify_about_instance_usage(context, instance,
"delete.start") "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) self._shutdown_instance(context, instance, bdms)
# NOTE(dims): instance.info_cache.delete() should be called after # NOTE(dims): instance.info_cache.delete() should be called after
# _shutdown_instance in the compute manager as shutdown calls # _shutdown_instance in the compute manager as shutdown calls

View File

@ -32,7 +32,10 @@ from nova import exception
from nova.i18n import _LW from nova.i18n import _LW
from nova.network import model as network_model from nova.network import model as network_model
from nova import notifications 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 import objects
from nova.objects import fields
from nova import rpc from nova import rpc
from nova import safe_utils from nova import safe_utils
from nova import 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) 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): def notify_about_server_group_update(context, event_suffix, sg_payload):
"""Send a notification about server group update. """Send a notification about server group update.

View File

@ -23,13 +23,22 @@ class NotificationObject(base.NovaObject):
# Version 1.0: Initial version # Version 1.0: Initial version
VERSION = '1.0' 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 @base.NovaObjectRegistry.register_notification
class EventType(NotificationObject): class EventType(NotificationObject):
# Version 1.0: Initial version # Version 1.0: Initial version
# Version 1.1: New valid actions values are added to the # Version 1.1: New valid actions values are added to the
# NotificationActionField enum # NotificationActionField enum
VERSION = '1.1' # Version 1.2: DELETE value is added to the NotificationActionField enum
VERSION = '1.2'
fields = { fields = {
'object': fields.StringField(nullable=False), 'object': fields.StringField(nullable=False),
@ -70,8 +79,8 @@ class NotificationPayloadBase(NotificationObject):
# Version 1.0: Initial version # Version 1.0: Initial version
VERSION = '1.0' VERSION = '1.0'
def __init__(self, *args, **kwargs): def __init__(self, **kwargs):
super(NotificationPayloadBase, self).__init__(*args, **kwargs) super(NotificationPayloadBase, self).__init__(**kwargs)
self.populated = not self.SCHEMA self.populated = not self.SCHEMA
def populate_schema(self, **kwargs): def populate_schema(self, **kwargs):
@ -86,6 +95,10 @@ class NotificationPayloadBase(NotificationObject):
setattr(self, key, getattr(source, field)) setattr(self, key, getattr(source, field))
self.populated = True 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 @base.NovaObjectRegistry.register_notification
class NotificationPublisher(NotificationObject): class NotificationPublisher(NotificationObject):

View File

@ -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')
}

View File

@ -609,14 +609,136 @@ class NotificationPhase(Enum):
class NotificationAction(Enum): class NotificationAction(Enum):
UPDATE = 'update' UPDATE = 'update'
EXCEPTION = 'exception' EXCEPTION = 'exception'
DELETE = 'delete'
ALL = (UPDATE, EXCEPTION) ALL = (UPDATE, EXCEPTION, DELETE)
def __init__(self): def __init__(self):
super(NotificationAction, self).__init__( super(NotificationAction, self).__init__(
valid_values=NotificationAction.ALL) 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): class IPV4AndV6Address(IPAddress):
@staticmethod @staticmethod
def coerce(obj, attr, value): def coerce(obj, attr, value):
@ -864,6 +986,18 @@ class NotificationActionField(BaseEnumField):
AUTO_TYPE = NotificationAction() AUTO_TYPE = NotificationAction()
class InstanceStateField(BaseEnumField):
AUTO_TYPE = InstanceState()
class InstanceTaskStateField(BaseEnumField):
AUTO_TYPE = InstanceTaskState()
class InstancePowerStateField(BaseEnumField):
AUTO_TYPE = InstancePowerState()
class IPV4AndV6AddressField(AutoTypedField): class IPV4AndV6AddressField(AutoTypedField):
AUTO_TYPE = IPV4AndV6Address() AUTO_TYPE = IPV4AndV6Address()

View File

@ -665,3 +665,90 @@ class AllServicesCurrent(fixtures.Fixture):
def _fake_minimum(self, *args, **kwargs): def _fake_minimum(self, *args, **kwargs):
return service_obj.SERVICE_VERSION 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)

View File

@ -239,22 +239,27 @@ class InstanceHelperMixin(object):
return server 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 = {} server = {}
image = api.get_images()[0] if image_uuid:
image_href = 'http://fake.server/%s' % image_uuid
if 'imageRef' in image:
image_href = image['imageRef']
else: else:
image_href = image['id'] image = api.get_images()[0]
image_href = 'http://fake.server/%s' % image_href
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 # We now have a valid imageId
server['imageRef'] = image_href server['imageRef'] = image_href
# Set a valid flavorId if not flavor_id:
flavor = api.get_flavors()[1] # Set a valid flavorId
server['flavorRef'] = ('http://fake.server/%s' % flavor['id']) flavor_id = api.get_flavors()[1]['id']
server['flavorRef'] = ('http://fake.server/%s' % flavor_id)
server['name'] = name server['name'] = name
return server return server

View File

@ -12,16 +12,27 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import mock
import os import os
import time
from oslo_config import cfg
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
from oslo_utils import fixture as utils_fixture
from nova import test from nova import test
from nova.tests import fixtures as nova_fixtures 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 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. """Base class for notification sample testing.
To add tests for a versioned notification you have to store a sample file 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): def setUp(self):
super(NotificationSampleTestBase, self).setUp() 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_fixture = self.useFixture(nova_fixtures.OSAPIFixture(
api_version='v2.1')) api_version='v2.1'))
@ -52,6 +67,18 @@ class NotificationSampleTestBase(test.TestCase):
fake_notifier.stub_notifier(self) fake_notifier.stub_notifier(self)
self.addCleanup(fake_notifier.reset) 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): def _get_notification_sample(self, sample):
sample_dir = os.path.dirname(os.path.abspath(__file__)) sample_dir = os.path.dirname(os.path.abspath(__file__))
sample_dir = os.path.normpath(os.path.join( sample_dir = os.path.normpath(os.path.join(
@ -106,3 +133,47 @@ class NotificationSampleTestBase(test.TestCase):
self._apply_replacements(replacements, sample_obj, notification) self._apply_replacements(replacements, sample_obj, notification)
self.assertJsonEqual(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

View File

@ -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])

View File

@ -119,7 +119,8 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase):
event_pwr_state=power_state.SHUTDOWN, event_pwr_state=power_state.SHUTDOWN,
current_pwr_state=power_state.RUNNING) 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 = mock.Mock()
call_tracker.clear_events_for_instance.return_value = None call_tracker.clear_events_for_instance.return_value = None
mgr_class = self.compute.__class__ mgr_class = self.compute.__class__
@ -128,6 +129,7 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase):
# spec out everything except for the method we really want # spec out everything except for the method we really want
# to test, then use call_tracker to verify call sequence # to test, then use call_tracker to verify call sequence
specd_compute._delete_instance = orig_delete specd_compute._delete_instance = orig_delete
specd_compute.host = 'compute'
mock_inst = mock.Mock() mock_inst = mock.Mock()
mock_inst.uuid = uuids.instance mock_inst.uuid = uuids.instance
@ -157,6 +159,11 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase):
'_notify_about_instance_usage', '_notify_about_instance_usage',
'_shutdown_instance', 'delete'], '_shutdown_instance', 'delete'],
methods_called) 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): def _make_compute_node(self, hyp_hostname, cn_id):
cn = mock.Mock(spec_set=['hypervisor_hostname', 'id', cn = mock.Mock(spec_set=['hypervisor_hostname', 'id',
@ -245,7 +252,8 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase):
else: else:
self.assertFalse(db_node.destroy.called) 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( instance = fake_instance.fake_instance_obj(
self.context, self.context,
uuid=uuids.instance, uuid=uuids.instance,
@ -267,6 +275,12 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase):
instance.info_cache = None instance.info_cache = None
self.compute._delete_instance(self.context, instance, [], quotas) 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): def test_check_device_tagging_no_tagging(self):
bdms = objects.BlockDeviceMappingList(objects=[ bdms = objects.BlockDeviceMappingList(objects=[
objects.BlockDeviceMapping(source_type='volume', objects.BlockDeviceMapping(source_type='volume',

View File

@ -486,6 +486,39 @@ class UsageInfoTestCase(test.TestCase):
uuids.fake_image_ref) uuids.fake_image_ref)
self.assertEqual(payload['image_ref_url'], image_ref_url) 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): def test_notify_usage_exists_instance_not_found(self):
# Ensure 'exists' notification generates appropriate usage data. # Ensure 'exists' notification generates appropriate usage data.
instance = create_instance(self.context) instance = create_instance(self.context)

View File

@ -255,9 +255,14 @@ class TestNotificationBase(test.NoDBTestCase):
notification_object_data = { notification_object_data = {
'EventType': '1.1-8291570eed00192197c7fa02ac677cd4', 'EventType': '1.2-b81c9f80d9344a24df0b1ecce376b515',
'ExceptionNotification': '1.0-a73147b93b520ff0061865849d3dfa56', 'ExceptionNotification': '1.0-a73147b93b520ff0061865849d3dfa56',
'ExceptionPayload': '1.0-4516ae282a55fe2fd5c754967ee6248b', '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', 'NotificationPublisher': '1.0-bbbc1402fb0e443a3eb227cc52b61545',
'ServiceStatusNotification': '1.0-a73147b93b520ff0061865849d3dfa56', 'ServiceStatusNotification': '1.0-a73147b93b520ff0061865849d3dfa56',
'ServiceStatusPayload': '1.0-a5e7b4fd6cc5581be45b31ff1f3a3f7f', 'ServiceStatusPayload': '1.0-a5e7b4fd6cc5581be45b31ff1f3a3f7f',