Enable versioned nova notifications

Listen for versioned notifications from Nova servers. The expected
versions for each type are coded into notification_handlers.

The versioned notifications are not entirely compatible with the
API or the old notifications; mostly notably in the network information.

Needs some functional tests as well

Change-Id: Iea3c2f19cca4b24f05b7edab4b307555fecdc1b3
Signed-off-by: Steve McLellan <steven.j.mclellan@gmail.com>
Story: #2003676
Task: #26198
This commit is contained in:
Steve McLellan 2017-04-04 16:50:17 -05:00 committed by Kevin_Zheng
parent a9a146a62c
commit 665a106e25
12 changed files with 452 additions and 63 deletions

View File

@ -151,6 +151,10 @@ function configure_searchlight {
iniset $SEARCHLIGHT_CONF resource_plugin:os_nova_flavor enabled True
iniset $SEARCHLIGHT_CONF resource_plugin:os_nova_flavor notifications_topics_exchanges versioned_notifications,nova
iniset $SEARCHLIGHT_CONF resource_plugin:os_nova_server enabled True
iniset $SEARCHLIGHT_CONF resource_plugin:os_nova_server notifications_topics_exchanges versioned_notifications,nova
iniset $SEARCHLIGHT_CONF resource_plugin:os_nova_server use_versioned_notifications True
# Plugin config - disable swift by default since it's not typically installed
iniset $SEARCHLIGHT_CONF resource_plugin:os_swift_account enabled False
iniset $SEARCHLIGHT_CONF resource_plugin:os_swift_container enabled False

View File

@ -59,6 +59,8 @@ Plugin: OS::Nova::Server
[resource_plugin:os_nova_server]
enabled = true
resource_group_name = searchlight
notifications_topics_exchanges = versioned_notifications,nova
use_versioned_notifications = true
Plugin: OS::Nova::Hypervisor
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -125,6 +127,7 @@ incremental updates. Enable notifications using the following::
[notifications]
notify_on_state_change = vm_and_task_state
# notification_format = versioned
.. note::
@ -132,6 +135,10 @@ incremental updates. Enable notifications using the following::
See :ref:`plugin_notifications` for more information on
notification topics.
The default setting for notification_format is 'both' which sends both
versioned and unversioned notifications. Searchlight uses
'use_versioned_notifications' to decide which to use.
local.conf (devstack)
---------------------

View File

@ -0,0 +1,17 @@
---
prelude: >
Adds support for versioned nova server notifications.
features:
- |
Support for versioned nova server notifications (and related notifications
where supported, like volume attach). This has to be enabled (it is not
yet the default).
This reduces the callbacks to the nova API significantly.
issues:
- |
Payload versioning is somewhat manual; a given release will support a
maxmimum major payload version and warn on later minor versions.
upgrade:
- |
Configuration needs changing to support versioned notifications; see docs.

View File

@ -125,3 +125,9 @@ class InvalidAPIVersionProvided(SearchlightException):
message = _("The provided API version is not supported, "
"the current available version range for %(service)s "
"is: from %(min_version)s to %(max_version)s.")
class VersionedNotificationMismatch(SearchlightException):
message = _("Provided notification version "
"%(provided_maj)s.%(provided_min)s did not match expected "
"%(expected_maj)s.%(expected_min)s for %(type)s")

View File

@ -39,6 +39,12 @@ FLAVOR_FIELDS_MAP = {
}
FLAVOR_BLACKLISTED_FIELDS = ['vcpu_weight', 'flavorid']
EXTENDED_FIELDS = {'OS-EXT-STS:task_state': 'task_state',
'OS-EXT-STS:vm_state': 'state',
'OS-EXT-AZ:availability_zone': 'availability_zone',
'OS-EXT-SRV-ATTR:hypervisor_hostname': 'host_name',
'OS-EXT-SRV-ATTR:host': 'host'}
def _get_flavor_access(flavor):
if flavor.is_public:
@ -80,9 +86,107 @@ def serialize_nova_server(server):
utils.normalize_date_fields(serialized)
serialized['status'] = serialized['status'].lower()
# Pop the fault stracktrace if any - it's big
fault = serialized.get('fault', None)
if fault and isinstance(fault, dict):
fault.pop('details', None)
return serialized
# TODO(sjmc7) - if https://review.openstack.org/#/c/485525/ lands, remove this
# If it doesn't, make it more accurate
def _get_server_status(vm_state, task_state):
# https://github.com/openstack/nova/blob/master/nova/api/openstack/common.py#L113
# Simplified version of that
if vm_state:
vm_state = vm_state.lower()
if task_state:
task_state = task_state.lower()
return {
'active': 'active',
'building': 'build',
'stopped': 'shutoff',
'resized': 'verify_resize',
'paused': 'paused',
'suspended': 'suspended',
'rescued': 'rescue',
'error': 'error',
'deleted': 'deleted',
'soft-delete': 'soft_deleted',
'shelved': 'shelved',
'shelved_offloaded': 'shelved_offloaded',
}.get(vm_state)
def serialize_server_versioned(payload):
# Based loosely on currently documented 1.1 InstanceActionPayload
# Some transforms - maybe these could be made the same in nova?
transform_keys = [('display_description', 'description'),
('display_name', 'name'), ('uuid', 'id')]
for src, dest in transform_keys:
payload[dest] = payload.pop(src)
copy_keys = [('tenant_id', 'project_id')]
for src, dest in copy_keys:
if src in payload:
payload[dest] = payload.get(src)
delete_keys = ['audit_period', 'node']
for key in delete_keys:
payload.pop(key, None)
# We should denormalize this because it'd be better for searching
flavor_id = payload.pop('flavor')['nova_object.data']['flavorid']
payload['flavor'] = {'id': flavor_id}
image_id = payload.pop('image_uuid')
payload['image'] = {'id': image_id}
# Translate the status, kind of. state and task_state will get
# popped off shortly
vm_state = payload.get('state', None)
task_state = payload.get('task_state', None)
payload['status'] = _get_server_status(vm_state, task_state)
# Map backwards to the OS-EXT- attributes
for ext_attr, simple_attr in EXTENDED_FIELDS.items():
attribute = payload.pop(simple_attr, None)
if attribute:
payload[ext_attr] = attribute
# Network information. This has to be transformed
# TODO(sjmc7) Try to better reconcile this with the API format
ip_addresses = [address['nova_object.data'] for address in
payload.pop("ip_addresses", [])]
def map_address(addr):
# TODO(sjmc7) Think this should be network name. Missing net type
net = {
"version": addr["version"],
"name": addr["device_name"],
"OS-EXT-IPS-MAC:mac_addr": addr["mac"],
}
if net["version"] == 4:
net["ipv4_addr"] = addr["address"]
else:
net["ipv6_addr"] = addr["address"]
return net
payload["networks"] = [map_address(address) for address in ip_addresses]
# Pop the fault stracktrace if any - it's big
fault = payload.get('fault', None)
if fault and isinstance(fault, dict):
fault.pop('details', None)
return payload
def serialize_nova_hypervisor(hypervisor, updated_at=None):
serialized = hypervisor.to_dict()
# The id for hypervisor is an integer, should be changed to

View File

@ -15,12 +15,14 @@
from copy import deepcopy
import novaclient.exceptions
from oslo_config import cfg
from oslo_log import log as logging
from elasticsearch import helpers
from searchlight.elasticsearch.plugins import base
from searchlight.elasticsearch.plugins.nova import serialize_nova_flavor
from searchlight.elasticsearch.plugins.nova import serialize_nova_server
from searchlight.elasticsearch.plugins.nova import serialize_server_versioned
from searchlight.elasticsearch.plugins import utils
from searchlight import pipeline
@ -67,6 +69,14 @@ class InstanceHandler(base.NotificationBase):
'spawning': 'unshelving'
}
# Supported major/minor notification versions. Major changes will likely
# require code changes.
notification_versions = {
'InstanceActionPayload': '1.2',
'InstanceUpdatePayload': '1.3',
'InstanceActionVolumeSwapPayload': '1.1',
}
@classmethod
def _get_notification_exchanges(cls):
return ['nova']
@ -78,38 +88,68 @@ class InstanceHandler(base.NotificationBase):
('old_task_state', payload.get('old_task_state')),
('new_task_state', payload.get('new_task_state')))
@classmethod
def get_plugin_opts(cls):
opts = super(InstanceHandler, cls).get_plugin_opts()
opts.extend([
cfg.BoolOpt(
'use_versioned_notifications',
help='Expect versioned notifications and ignore unversioned',
default=True)
])
return opts
def _use_versioned_notifications(self):
return self.plugin_options.use_versioned_notifications
def get_event_handlers(self):
return {
# compute.instance.update seems to be the event set as a
# result of a state change etc
'compute.instance.update': self.index_from_update,
if not self._use_versioned_notifications():
return {
# compute.instance.update seems to be the event set as a
# result of a state change etc
'compute.instance.update': self.index_from_update,
'compute.instance.create.start': self.index_from_api,
'compute.instance.create.end': self.index_from_api,
'compute.instance.create.start': self.index_from_api,
'compute.instance.create.end': self.index_from_api,
'compute.instance.power_on.end': self.index_from_api,
'compute.instance.power_off.end': self.index_from_api,
'compute.instance.resume.end': self.index_from_api,
'compute.instance.suspend.end': self.index_from_api,
'compute.instance.pause.end': self.index_from_api,
'compute.instance.unpause.end': self.index_from_api,
'compute.instance.power_on.end': self.index_from_api,
'compute.instance.power_off.end': self.index_from_api,
'compute.instance.resume.end': self.index_from_api,
'compute.instance.suspend.end': self.index_from_api,
'compute.instance.pause.end': self.index_from_api,
'compute.instance.unpause.end': self.index_from_api,
'compute.instance.shutdown.end': self.index_from_api,
'compute.instance.reboot.end': self.index_from_api,
'compute.instance.delete.end': self.delete,
'compute.instance.shutdown.end': self.index_from_api,
'compute.instance.reboot.end': self.index_from_api,
'compute.instance.delete.end': self.delete,
'compute.instance.shelve.end': self.index_from_api,
'compute.instance.shelve_offload.end': self.index_from_api,
'compute.instance.unshelve.end': self.index_from_api,
'compute.instance.shelve.end': self.index_from_api,
'compute.instance.shelve_offload.end': self.index_from_api,
'compute.instance.unshelve.end': self.index_from_api,
'compute.instance.volume.attach': self.index_from_api,
'compute.instance.volume.detach': self.index_from_api,
'compute.instance.volume.attach': self.index_from_api,
'compute.instance.volume.detach': self.index_from_api,
# Removing neutron port events for now; waiting on nova
# to implement interface notifications as with volumes
# https://launchpad.net/bugs/1567525
# https://blueprints.launchpad.net/nova/+spec/interface-notifications
}
# Removing neutron port events for now; waiting on nova
# to implement interface notifications as with volumes
# https://launchpad.net/bugs/1567525
# bps/nova/+spec/interface-notifications
}
# Otherwise listen for versioned notifications!
# Nova versioned notifications all include the entire payload
end_events = ['create', 'pause', 'power_off', 'power_on',
'reboot', 'rebuild', 'resize', 'restore', 'resume',
'shelve', 'shutdown', 'snapshot', 'suspend', 'unpause',
'unshelve', 'volume_attach', 'volume_detach']
notifications = {('instance.%s.end' % ev): self.index_from_versioned
for ev in end_events}
# instance.update has no start or end
notifications['instance.update'] = self.index_from_versioned
# This should become soft delete once that is supported
notifications['instance.delete.end'] = self.delete_from_versioned
return notifications
def index_from_update(self, event_type, payload, timestamp):
"""Determine whether or not to process a full update. The updates, and
@ -325,6 +365,49 @@ class InstanceHandler(base.NotificationBase):
'from index: %(exc)s' %
{'instance_id': instance_id, 'exc': exc})
def index_from_versioned(self, event_type, payload, timestamp):
notification_version = payload['nova_object.version']
notification_name = payload['nova_object.name']
expected_version = self.notification_versions.get(notification_name,
None)
if expected_version:
utils.check_notification_version(
expected_version, notification_version, notification_name)
else:
LOG.warning("No expected notification version for %s; "
"processing anyway", notification_name)
versioned_payload = payload['nova_object.data']
serialized = serialize_server_versioned(versioned_payload)
self.index_helper.save_document(
serialized,
version=self.get_version(serialized, timestamp))
return pipeline.IndexItem(self.index_helper.plugin,
event_type,
payload,
serialized)
def delete_from_versioned(self, event_type, payload, timestamp):
payload = payload['nova_object.data']
instance_id = payload['uuid']
version = self.get_version(payload, timestamp,
preferred_date_field='deleted_at')
try:
version = self.get_version(payload, timestamp,
preferred_date_field='deleted_at')
self.index_helper.delete_document(
{'_id': instance_id, '_version': version})
return pipeline.DeleteItem(self.index_helper.plugin,
event_type,
payload,
instance_id
)
except Exception as exc:
LOG.error(
'Error deleting instance %(instance_id)s '
'from index: %(exc)s' %
{'instance_id': instance_id, 'exc': exc})
class ServerGroupHandler(base.NotificationBase):
"""Handles nova server group notifications.

View File

@ -29,6 +29,7 @@ class ServerIndex(base.IndexBase):
NotificationHandlerCls = notification_handler.InstanceHandler
# Will be combined with 'admin_only_fields' from config
# https://developer.openstack.org/api-ref/compute/?expanded=show-server-details-detail
ADMIN_ONLY_FIELDS = ['OS-EXT-SRV-ATTR:*', 'host_status']
@classmethod
@ -90,6 +91,22 @@ class ServerIndex(base.IndexBase):
'type': 'string',
'index': 'not_analyzed'
},
'OS-EXT-SRV-ATTR:hypervisor_hostname': {
'type': 'string',
'index': 'not_analyzed'
},
'OS-EXT-STS:vm_state': {
'type': 'string',
'index': 'not_analyzed'
},
'fault': {
'type': 'object',
'properties': {
'code': {'type': 'integer'},
'created': {'type': 'date'},
'message': {'type': 'string'},
}
},
# Nova gives security group names, where neutron ports
# give ids in the same field. There's no solution that
# maintains compatibility with both
@ -163,7 +180,8 @@ class ServerIndex(base.IndexBase):
only be available to administrators.
"""
return {'tenant_id': True, 'project_id': True, 'host_status': True,
'created': False, 'updated': False}
'created': False, 'updated': False,
'OS-EXT-SRV-ATTR:hypervisor_hostname': True, 'fault': False}
@property
def resource_allowed_policy_target(self):

View File

@ -23,6 +23,7 @@ import six
from oslo_config import cfg
from oslo_utils import encodeutils
from searchlight.common import exception as sl_exc
from searchlight.common import utils
from searchlight.context import RequestContext
import searchlight.elasticsearch
@ -526,3 +527,28 @@ def normalize_es_document(es_doc, plugin):
admin_context = RequestContext()
plugin.filter_result({'_source': es_doc}, admin_context)
return es_doc
def check_notification_version(expected, actual, notification_type):
"""
If actual's major version is different from expected, a
VersionedNotificationMismatch error is raised.
If the minor versions are different, a DEBUG level log
message is output
"""
maj_ver, min_ver = map(int, actual.split('.'))
expected_maj, expected_min = map(int, expected.split('.'))
if maj_ver != expected_maj:
raise sl_exc.VersionedNotificationMismatch(
provided_maj=maj_ver, provided_min=min_ver,
expected_maj=expected_maj, expected_min=expected_min,
type=notification_type)
if min_ver != expected_min:
LOG.debug(
"Notification minor version mismatch. "
"Provided: %(provided_maj)s, %(provided_min)s. "
"Expected: %(expected_maj)s.%(expected_min)s." % {
"provided_maj": maj_ver, "provided_min": min_ver,
"expected_maj": expected_maj, "expected_min": expected_min}
)

View File

@ -270,6 +270,7 @@ service_policy_path = %(service_policy_path)s
workers = 0
bind_host = 127.0.0.1
bind_port = %(bind_port)s
"""
self.paste_conf_base = """[pipeline:searchlight]
pipeline = versionnegotiation unauthenticated-context rootapp

View File

@ -329,8 +329,8 @@ class TestSearchApi(functional.FunctionalTest):
expected = {
u'name': u'status',
u'options': [
{u'doc_count': 2, u'key': u'ACTIVE'},
{u'doc_count': 1, u'key': u'RESUMING'},
{u'doc_count': 2, u'key': u'active'},
{u'doc_count': 1, u'key': u'resuming'},
],
u'type': u'string'
}
@ -967,6 +967,7 @@ class TestSearchApi(functional.FunctionalTest):
u'id': 'abcdef',
u'tenant_id': TENANT1,
u'user_id': USER1,
u'status': 'ACTIVE',
u'image': {u'id': u'a'},
u'flavor': {u'id': u'1'},
u'created_at': u'2016-04-07T15:49:35Z',
@ -977,6 +978,7 @@ class TestSearchApi(functional.FunctionalTest):
u'id': '12341234',
u'tenant_id': TENANT2,
u'user_id': USER1,
u'status': 'ACTIVE',
u'image': {u'id': u'a'},
u'flavor': {u'id': u'1'},
u'created_at': u'2016-04-07T15:49:35Z',

View File

@ -126,9 +126,8 @@ class TestNovaPlugins(functional.FunctionalTest):
TENANT_ID)
self.assertEqual(200, response.status)
self.assertEqual(1, json_content['hits']['total'])
hits = json_content['hits']['hits']
host_id = u'41d7069823d74c9ea8debda9a3a02bb00b2f7d53a0accd1f79429407'
hits = json_content['hits']['hits']
expected_sources = [{
u'OS-DCF:diskConfig': u'MANUAL',
u'OS-EXT-AZ:availability_zone': u'nova',
@ -163,7 +162,7 @@ class TestNovaPlugins(functional.FunctionalTest):
u'owner': u'1dd2c5280b4e45fc9d7d08a81228c891',
u'project_id': u'1dd2c5280b4e45fc9d7d08a81228c891',
u'security_groups': [{u'name': u'default'}],
u'status': u'ACTIVE',
u'status': u'active',
u'tenant_id': u'1dd2c5280b4e45fc9d7d08a81228c891',
u'updated': u'2016-03-08T08:40:22Z',
u'user_id': u'7c97202cf58d43a9ab33016fc403f093'}]
@ -269,31 +268,6 @@ class TestNovaListeners(test_listener.TestSearchListenerBase):
)
self.listener_alias = self.servers_plugin.alias_name_listener
@mock.patch(nova_version_getter, return_value=fake_version_list)
@mock.patch(nova_server_getter)
def test_error_state_transition(self, mock_nova, mock_version_list):
inst_id = "4b86f534-16db-4de8-8ce0-f1ee68894835"
mock_nova.return_value = utils.DictObj(**{
'id': inst_id,
'name': 'test-error',
'tenant_id': EV_TENANT,
'addresses': {},
'image': {'id': '1'},
'flavor': {'id': 'a'},
'created': '2016-08-31T23:32:11Z',
'updated': '2016-08-31T23:32:11Z',
})
error_update = self.server_events[
'instance-update-error-final'
]
self._send_event_to_listener(error_update,
self.listener_alias)
result = self._verify_event_processing(error_update, owner=EV_TENANT)
self._verify_result(error_update, ['tenant_id'], result)
mock_nova.assert_called_with(inst_id)
def test_server_group_create_delete(self):
# Test #1: Create a server group.
create_event = self.server_group_events["servergroup.create"]
@ -393,3 +367,53 @@ class TestNovaListeners(test_listener.TestSearchListenerBase):
self.assertEqual(200, response.status)
self.assertEqual(0, json_content['hits']['total'])
class TestNovaUnversionedListener(test_listener.TestSearchListenerBase):
def setUp(self):
version_notifications = \
'searchlight.elasticsearch.plugins.nova.notification_handler'\
'.InstanceHandler._use_versioned_notifications'
mock_versioned = mock.patch(version_notifications,
return_value=False)
mock_versioned.start()
self.addCleanup(mock_versioned.stop)
super(TestNovaUnversionedListener, self).setUp()
self.servers_plugin = self.initialized_plugins['OS::Nova::Server']
self.server_events = self._load_fixture_data('events/servers.json')
sp = self.servers_plugin
notification_plugins = {sp.document_type: utils.StevedoreMock(sp)}
self.notification_endpoint = NotificationEndpoint(
notification_plugins,
PipelineManager(notification_plugins)
)
self.listener_alias = self.servers_plugin.alias_name_listener
@mock.patch(nova_version_getter, return_value=fake_version_list)
@mock.patch(nova_server_getter)
def test_error_state_transition(self, mock_nova, mock_version_list):
inst_id = "4b86f534-16db-4de8-8ce0-f1ee68894835"
mock_nova.return_value = utils.DictObj(**{
'id': inst_id,
'name': 'test-error',
'tenant_id': EV_TENANT,
'addresses': {},
'image': {'id': '1'},
'flavor': {'id': 'a'},
'status': 'ERROR',
'created': '2016-08-31T23:32:11Z',
'updated': '2016-08-31T23:32:11Z',
})
error_update = self.server_events[
'instance-update-error-final'
]
self._send_event_to_listener(error_update,
self.listener_alias)
result = self._verify_event_processing(error_update, owner=EV_TENANT)
self._verify_result(error_update, ['tenant_id'], result)
mock_nova.assert_called_with(inst_id)

View File

@ -143,6 +143,7 @@ def _instance_fixture(instance_id, name, tenant_id, **kwargs):
}]
},
u'hostId': u'd86d2c042a1f233227f70c5e9d2c5829de98d222d0922f469054ac17',
u'host_name': u'devstack',
u'id': instance_id,
u'image': {
u'id': u'46b77e67-ce40-44ca-823d-e6f83489f21e',
@ -167,7 +168,7 @@ def _instance_fixture(instance_id, name, tenant_id, **kwargs):
u'os-extended-volumes:volumes_attached': [],
u'progress': 0,
u'security_groups': [{u'name': u'default'}],
u'status': u'ACTIVE',
u'status': u'active',
u'tenant_id': tenant_id,
u'updated': updated_now,
u'user_id': USER1}
@ -181,6 +182,15 @@ def _instance_fixture(instance_id, name, tenant_id, **kwargs):
class TestServerLoaderPlugin(test_utils.BaseTestCase):
def setUp(self):
super(TestServerLoaderPlugin, self).setUp()
# Use unversioned notifications
version_notifications = \
'searchlight.elasticsearch.plugins.nova.notification_handler'\
'.InstanceHandler._use_versioned_notifications'
mock_versioned = mock.patch(version_notifications,
return_value=False)
mock_versioned.start()
self.addCleanup(mock_versioned.stop)
self.plugin = servers_plugin.ServerIndex()
self._create_fixtures()
@ -233,6 +243,7 @@ class TestServerLoaderPlugin(test_utils.BaseTestCase):
u'config_drive': u'True',
u'flavor': {u'id': u'1'},
u'hostId': u'host1',
u'host_name': u'devstack',
u'id': u'6c41b4d1-f0fa-42d6-9d8d-e3b99695aa69',
u'image': {u'id': u'a'},
u'key_name': u'key',
@ -241,7 +252,7 @@ class TestServerLoaderPlugin(test_utils.BaseTestCase):
u'os-extended-volumes:volumes_attached': [],
u'owner': u'4d64ac83-87af-4d2a-b884-cc42c3e8f2c0',
u'security_groups': [u'default'],
u'status': u'ACTIVE',
u'status': u'active',
u'tenant_id': u'4d64ac83-87af-4d2a-b884-cc42c3e8f2c0',
u'project_id': u'4d64ac83-87af-4d2a-b884-cc42c3e8f2c0',
u'updated': updated_now,
@ -301,6 +312,7 @@ class TestServerLoaderPlugin(test_utils.BaseTestCase):
u'config_drive': u'True',
u'flavor': {u'id': u'1'},
u'hostId': u'host1',
u'host_name': u'devstack',
u'id': u'a380287d-1f61-4887-959c-8c5ab8f75f8f',
u'key_name': u'key',
u'metadata': {},
@ -309,7 +321,7 @@ class TestServerLoaderPlugin(test_utils.BaseTestCase):
u'owner': u'4d64ac83-87af-4d2a-b884-cc42c3e8f2c0',
u'project_id': u'4d64ac83-87af-4d2a-b884-cc42c3e8f2c0',
u'security_groups': [u'default'],
u'status': u'ACTIVE',
u'status': u'active',
u'tenant_id': u'4d64ac83-87af-4d2a-b884-cc42c3e8f2c0',
u'updated': updated_now,
u'user_id': u'27f4d76b-be62-4e4e-aa33bb11cc55',
@ -349,10 +361,11 @@ class TestServerLoaderPlugin(test_utils.BaseTestCase):
network_facets = ('name', 'version', 'ipv6_addr', 'ipv4_addr',
'OS-EXT-IPS-MAC:mac_addr', 'OS-EXT-IPS:type')
expected_facet_names = [
'OS-EXT-AZ:availability_zone', 'created_at', 'description',
'OS-EXT-AZ:availability_zone',
'created_at', 'description',
'flavor.id', 'id', 'image.id', 'locked', 'name',
'owner', 'security_groups', 'status', 'tags', 'updated_at',
'user_id']
'user_id', 'OS-EXT-STS:vm_state']
expected_facet_names.extend(['networks.' + f for f in network_facets])
self.assertEqual(set(expected_facet_names), set(facet_names))
@ -408,10 +421,12 @@ class TestServerLoaderPlugin(test_utils.BaseTestCase):
network_facets = ('name', 'version', 'ipv6_addr', 'ipv4_addr',
'OS-EXT-IPS-MAC:mac_addr', 'OS-EXT-IPS:type')
expected_facet_names = [
'OS-EXT-SRV-ATTR:hypervisor_hostname',
'OS-EXT-AZ:availability_zone', 'created_at', 'description',
'flavor.id', 'host_status', 'id', 'image.id', 'locked',
'name', 'owner', 'project_id', 'security_groups', 'status',
'tags', 'tenant_id', 'updated_at', 'user_id']
'tags', 'tenant_id', 'updated_at', 'user_id',
'OS-EXT-STS:vm_state']
expected_facet_names.extend(['networks.' + f for f in network_facets])
self.assertEqual(set(expected_facet_names), set(facet_names))
@ -1315,6 +1330,88 @@ class TestServerLoaderPlugin(test_utils.BaseTestCase):
return_value=self.instance1) as nova_getter:
type_handler('compute.instance.update', update_event,
'2016-07-17 19:52:13.523135')
'2016-03-17 19:52:13.523135')
nova_getter.assert_called_with(instance_id)
self.assertEqual(1, mock_save.call_count)
class TestVersionedServerNotifications(test_utils.BaseTestCase):
def setUp(self):
super(TestVersionedServerNotifications, self).setUp()
self.plugin = servers_plugin.ServerIndex()
def test_versioned_create(self):
create_event = {
"nova_object.name": "InstanceActionPayload",
"nova_object.namespace": "nova",
"nova_object.version": "1.1",
"nova_object.data": {
"image_uuid": "155d900f-4e14-4e4c-a73d-069cbf4541e6",
"tenant_id": "6f70656e737461636b20342065766572",
"created_at": "2017-03-17T19:52:13Z",
"display_name": "some-server",
"display_description": "some-server",
"state": "active",
"flavor": {
"nova_object.name": "FlavorPayload",
"nova_object.data": {
"flavorid": "a22d5517-147c-4147-a0d1-e698df5cd4e3",
"name": "test_flavor",
"root_gb": 1,
"vcpus": 1,
"ephemeral_gb": 0,
"memory_mb": 512
}
},
"uuid": "178b0921-8f85-4257-88b6-2e743b5a975c",
"power_state": "running",
"ip_addresses": [{
"nova_object.name": "IpPayload",
"nova_object.data": {
"mac": "fa:16:3e:4c:2c:30",
"address": "192.168.1.3",
"port_uuid": "ce531f90-199f-48c0-816c-13e38010b442",
"version": 4,
"label": "private-network",
"device_name": "tapce531f90-19"
}
}],
'OS-EXT-STS:vm_state': 'active',
}
}
handler = self.plugin.get_notification_handler()
event_handlers = handler.get_event_handlers()
expected = {
'image': {'id': "155d900f-4e14-4e4c-a73d-069cbf4541e6"},
'tenant_id': "6f70656e737461636b20342065766572",
'project_id': "6f70656e737461636b20342065766572",
"created_at": "2017-03-17T19:52:13Z",
"name": "some-server",
"description": "some-server",
"flavor": {"id": "a22d5517-147c-4147-a0d1-e698df5cd4e3"},
"id": "178b0921-8f85-4257-88b6-2e743b5a975c",
"power_state": "running",
"status": "active",
'OS-EXT-STS:vm_state': 'active',
"networks": [{
"version": 4,
"ipv4_addr": "192.168.1.3",
"OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:4c:2c:30",
"name": "tapce531f90-19"
}],
}
expected_version = 489780333780333818
with mock.patch.object(self.plugin.index_helper,
'save_documents') as mock_save:
handler = event_handlers.get('instance.create.end')
self.assertIsNotNone(handler)
handler(payload=create_event,
event_type='instance.create.end',
timestamp='2017-03-17 19:52:13.818362')
self.assertEqual(1, mock_save.call_count)
mock_save.assert_called_with([expected], [expected_version])