From 7d8bd50d08f40475bbe49e5066dde84c06c9c6fd Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Wed, 20 Feb 2013 18:47:13 -0500 Subject: [PATCH] Decouple the nova notifier from ceilometer code The move to oslo.config introduced a conflict in the nova notifier because both nova and ceilometer have copies of the openstack.common.rpc library and define an option for the AMQP exchange name for the project. This changeset decouples the notifier plugin in ceilometer from most of the ceilometer code, to remove that conflict. The nova notifier is rewritten to emit a new notification message with event type `compute.instance.delete.samples` instead of making the old RPC calls directly to the ceilometer collector, and a notification listener plugin is provided to convert those messages to sample data. The notifier implementations are split between the one that worked in folsom and the one that works in grizzly to maintain backwards compatibility. get_hypervisor_inspector() is moved to a location where it can be imported both by the compute agent manager and the notifier plugin. The definition of `disabled_compute_pollsters` option is also moved for the same reason. The tox configuration is changed to run the nova notifier tests separately from the other tests, since nose cannot import nova and ceilometer code in the same process. bug 1130952 Change-Id: I39ba4564c9c14f09dbdd768d7a83f6940e3942ad Signed-off-by: Doug Hellmann --- MANIFEST.in | 3 +- ceilometer/collector/service.py | 7 + ceilometer/compute/__init__.py | 28 +++ ceilometer/compute/instance.py | 9 +- ceilometer/compute/manager.py | 27 +-- ceilometer/compute/notifications.py | 26 ++ ceilometer/compute/nova_notifier/__init__.py | 28 +++ .../folsom.py} | 11 +- ceilometer/compute/nova_notifier/grizzly.py | 175 +++++++++++++ ceilometer/compute/virt/inspector.py | 30 +++ ceilometer/plugin.py | 4 - .../test_folsom.py | 17 +- nova_tests/test_grizzly.py | 229 ++++++++++++++++++ setup.py | 1 + tests/compute/test_notifications.py | 75 +++++- tests/compute/test_pollsters.py | 4 +- tox.ini | 1 + 17 files changed, 627 insertions(+), 48 deletions(-) create mode 100644 ceilometer/compute/nova_notifier/__init__.py rename ceilometer/compute/{nova_notifier.py => nova_notifier/folsom.py} (93%) create mode 100644 ceilometer/compute/nova_notifier/grizzly.py rename tests/compute/test_nova_notifier.py => nova_tests/test_folsom.py (97%) create mode 100644 nova_tests/test_grizzly.py diff --git a/MANIFEST.in b/MANIFEST.in index 29782f5b4..16188e036 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,7 +3,8 @@ include ChangeLog include ceilometer/storage/sqlalchemy/migrate_repo/migrate.cfg exclude .gitignore exclude .gitreview - +recursive-include tests *.py +recursive-include nova_tests *.py global-exclude *.pyc recursive-include public * recursive-include ceilometer/locale * diff --git a/ceilometer/collector/service.py b/ceilometer/collector/service.py index c6237cd31..d6ceb897a 100644 --- a/ceilometer/collector/service.py +++ b/ceilometer/collector/service.py @@ -29,6 +29,10 @@ from ceilometer.openstack.common import log from ceilometer.openstack.common import timeutils from ceilometer.openstack.common.rpc import dispatcher as rpc_dispatcher +# Import rpc_notifier to register `notification_topics` flag so that +# plugins can use it +# FIXME(dhellmann): Use option importing feature of oslo.config instead. +import ceilometer.openstack.common.notifier.rpc_notifier OPTS = [ cfg.ListOpt('disabled_notification_listeners', @@ -56,6 +60,7 @@ class CollectorService(service.PeriodicService): def initialize_service_hook(self, service): '''Consumers must be declared before consume_thread start.''' + LOG.debug('initialize_service_hooks') publisher_manager = dispatch.NameDispatchExtensionManager( namespace=pipeline.PUBLISHER_NAMESPACE, check_func=lambda x: True, @@ -63,6 +68,8 @@ class CollectorService(service.PeriodicService): ) self.pipeline_manager = pipeline.setup_pipeline(publisher_manager) + LOG.debug('loading notification handlers from %s', + self.COLLECTOR_NAMESPACE) self.notification_manager = \ extension_manager.ActivatedExtensionManager( namespace=self.COLLECTOR_NAMESPACE, diff --git a/ceilometer/compute/__init__.py b/ceilometer/compute/__init__.py index e69de29bb..ef4e35586 100644 --- a/ceilometer/compute/__init__.py +++ b/ceilometer/compute/__init__.py @@ -0,0 +1,28 @@ +# -*- encoding: utf-8 -*- +# +# Copyright © 2013 New Dream Network, LLC +# +# Author: Doug Hellmann +# +# 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 oslo.config import cfg + +OPTS = [ + cfg.ListOpt('disabled_compute_pollsters', + default=[], + help='list of compute agent pollsters to disable', + ), +] + +cfg.CONF.register_opts(OPTS) diff --git a/ceilometer/compute/instance.py b/ceilometer/compute/instance.py index 728ccc854..dff5ec6e0 100644 --- a/ceilometer/compute/instance.py +++ b/ceilometer/compute/instance.py @@ -46,10 +46,15 @@ def get_metadata_from_object(instance): 'host': instance.hostId, # Image properties 'image_ref': (instance.image['id'] if instance.image else None), - 'image_ref_url': (instance.image['links'][0]['href'] - if instance.image else None), } + # Images that come through the conductor API in the nova notifier + # plugin will not have links. + if instance.image and instance.image.get('links'): + metadata['image_ref_url'] = instance.image['links'][0]['href'] + else: + metadata['image_ref_url'] = None + for name in INSTANCE_PROPERTIES: metadata[name] = getattr(instance, name, u'') return metadata diff --git a/ceilometer/compute/manager.py b/ceilometer/compute/manager.py index f741ff400..cf41dcf7d 100644 --- a/ceilometer/compute/manager.py +++ b/ceilometer/compute/manager.py @@ -17,7 +17,6 @@ # under the License. from oslo.config import cfg -from stevedore import driver from ceilometer import agent from ceilometer import extension_manager @@ -25,18 +24,6 @@ from ceilometer import nova_client from ceilometer.compute.virt import inspector as virt_inspector from ceilometer.openstack.common import log -OPTS = [ - cfg.ListOpt('disabled_compute_pollsters', - default=[], - help='list of compute agent pollsters to disable', - ), - cfg.StrOpt('hypervisor_inspector', - default='libvirt', - help='Inspector to use for inspecting the hypervisor layer'), -] - -cfg.CONF.register_opts(OPTS) - LOG = log.getLogger(__name__) @@ -64,18 +51,6 @@ class PollingTask(agent.PollingTask): self.manager.nv.instance_get_all_by_host(cfg.CONF.host)) -def get_hypervisor_inspector(): - try: - namespace = 'ceilometer.compute.virt' - mgr = driver.DriverManager(namespace, - cfg.CONF.hypervisor_inspector, - invoke_on_load=True) - return mgr.driver - except ImportError as e: - LOG.error("Unable to load the hypervisor inspector: %s" % (e)) - return virt_inspector.Inspector() - - class AgentManager(agent.AgentManager): def __init__(self): @@ -85,7 +60,7 @@ class AgentManager(agent.AgentManager): disabled_names=cfg.CONF.disabled_compute_pollsters, ), ) - self._inspector = get_hypervisor_inspector() + self._inspector = virt_inspector.get_hypervisor_inspector() self.nv = nova_client.Client() def create_polling_task(self): diff --git a/ceilometer/compute/notifications.py b/ceilometer/compute/notifications.py index 904d37025..19c85bee1 100644 --- a/ceilometer/compute/notifications.py +++ b/ceilometer/compute/notifications.py @@ -172,3 +172,29 @@ class InstanceFlavor(_Base): ) ) return counters + + +class InstanceDelete(_Base): + """Handle the messages sent by the nova notifier plugin + when an instance is being deleted. + """ + + @staticmethod + def get_event_types(): + return ['compute.instance.delete.samples'] + + def process_notification(self, message): + return [ + counter.Counter(name=sample['name'], + type=sample['type'], + unit=sample['unit'], + volume=sample['volume'], + user_id=message['payload']['user_id'], + project_id=message['payload']['tenant_id'], + resource_id=message['payload']['instance_id'], + timestamp=message['timestamp'], + resource_metadata=self.notification_to_metadata( + message), + ) + for sample in message['payload'].get('samples', []) + ] diff --git a/ceilometer/compute/nova_notifier/__init__.py b/ceilometer/compute/nova_notifier/__init__.py new file mode 100644 index 000000000..8b6593827 --- /dev/null +++ b/ceilometer/compute/nova_notifier/__init__.py @@ -0,0 +1,28 @@ +# -*- encoding: utf-8 -*- +# +# Copyright © 2013 New Dream Network, LLC (DreamHost) +# +# Author: Doug Hellmann +# +# 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. + +# NOTE(dhellmann): The implementations of the notifier for folsom and +# grizzly are completely different. Rather than have lots of checks +# throughout the code, the two implementations are placed in separate +# modules and the right version is imported here. +try: + import nova.conductor +except ImportError: + from .folsom import * +else: + from .grizzly import * diff --git a/ceilometer/compute/nova_notifier.py b/ceilometer/compute/nova_notifier/folsom.py similarity index 93% rename from ceilometer/compute/nova_notifier.py rename to ceilometer/compute/nova_notifier/folsom.py index eebdf6a14..eaced71b9 100644 --- a/ceilometer/compute/nova_notifier.py +++ b/ceilometer/compute/nova_notifier/folsom.py @@ -16,17 +16,18 @@ # License for the specific language governing permissions and limitations # under the License. +__all__ = [ + 'notify', + 'initialize_manager', +] + from oslo.config import cfg from ceilometer.openstack.common import log as logging from ceilometer.compute.manager import AgentManager -try: - from nova.conductor import api - instance_info_source = api.API() -except ImportError: - from nova import db as instance_info_source +from nova import db as instance_info_source # This module runs inside the nova compute # agent, which only configures the "nova" logger. diff --git a/ceilometer/compute/nova_notifier/grizzly.py b/ceilometer/compute/nova_notifier/grizzly.py new file mode 100644 index 000000000..e8f016f5b --- /dev/null +++ b/ceilometer/compute/nova_notifier/grizzly.py @@ -0,0 +1,175 @@ +# -*- encoding: utf-8 -*- +# +# Copyright © 2012 New Dream Network, LLC (DreamHost) +# +# Author: Julien Danjou +# Doug Hellmann +# +# 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. + +__all__ = [ + 'notify', + 'DeletedInstanceStatsGatherer', + 'initialize_gatherer', + 'instance_info_source', + '_gatherer', # for tests to mock +] + +import sys + +from nova import notifications +from nova.openstack.common.notifier import api as notifier_api +from nova.openstack.common import log as logging + +# HACK(dhellmann): Insert the nova version of openstack.common into +# sys.modules as though it was the copy from ceilometer, so that when +# we use modules from ceilometer below they do not re-define options. +import ceilometer # use the real ceilometer base package +for name in ['openstack', 'openstack.common', 'openstack.common.log']: + sys.modules['ceilometer.' + name] = sys.modules['nova.' + name] + +from nova.conductor import api + +from oslo.config import cfg + +from ceilometer import extension_manager +from ceilometer.compute.virt import inspector + +# This module runs inside the nova compute +# agent, which only configures the "nova" logger. +# We use a fake logger name in that namespace +# so that messages from this module appear +# in the log file. +LOG = logging.getLogger('nova.ceilometer.notifier') + +_gatherer = None +instance_info_source = api.API() + + +class DeletedInstanceStatsGatherer(object): + + def __init__(self, extensions): + self.mgr = extensions + self.inspector = inspector.get_hypervisor_inspector() + + def _get_counters_from_plugin(self, ext, instance, *args, **kwds): + """Used with the extenaion manager map() method.""" + return ext.obj.get_counters(self, instance) + + def __call__(self, instance): + counters = self.mgr.map(self._get_counters_from_plugin, + instance=instance, + ) + # counters is a list of lists, so flatten it before returning + # the results + results = [] + for clist in counters: + results.extend(clist) + return results + + +def initialize_gatherer(gatherer=None): + """Set the callable used to gather stats for the instance. + + gatherer should be a callable accepting one argument (the instance + ref), or None to have a default gatherer used + """ + global _gatherer + if gatherer is not None: + LOG.debug('using provided stats gatherer %r', gatherer) + _gatherer = gatherer + if _gatherer is None: + LOG.debug('making a new stats gatherer') + mgr = extension_manager.ActivatedExtensionManager( + namespace='ceilometer.poll.compute', + disabled_names=cfg.CONF.disabled_compute_pollsters, + ) + _gatherer = DeletedInstanceStatsGatherer(mgr) + return _gatherer + + +class Instance(object): + """Model class for instances + + The pollsters all expect an instance that looks like what the + novaclient gives them, but the conductor API gives us a + dictionary. This class makes an object from the dictonary so we + can pass it to the pollsters. + """ + def __init__(self, info): + for k, v in info.iteritems(): + setattr(self, k, v) + LOG.debug('INFO %r', info) + + @property + def tenant_id(self): + return self.project_id + + @property + def flavor(self): + return { + 'id': self.instance_type_id, + 'name': self.instance_type.get('name', 'UNKNOWN'), + } + + @property + def hostId(self): + return self.host + + @property + def image(self): + return {'id': self.image_ref} + + +def notify(context, message): + if message['event_type'] != 'compute.instance.delete.start': + LOG.debug('ignoring %s', message['event_type']) + return + LOG.info('processing %s', message['event_type']) + gatherer = initialize_gatherer() + + instance_id = message['payload']['instance_id'] + LOG.debug('polling final stats for %r', instance_id) + + # Ask for the instance details + instance_ref = instance_info_source.instance_get_by_uuid( + context, + instance_id, + ) + + # Get the default notification payload + payload = notifications.info_from_instance( + context, instance_ref, None, None) + + # Extend the payload with samples from our plugins. We only need + # to send some of the data from the counter objects, since a lot + # of the fields are the same. + instance = Instance(instance_ref) + counters = gatherer(instance) + payload['samples'] = [{'name': c.name, + 'type': c.type, + 'unit': c.unit, + 'volume': c.volume} + for c in counters] + + publisher_id = notifier_api.publisher_id('compute', None) + + # We could simply modify the incoming message payload, but we + # can't be sure that this notifier will be called before the RPC + # notifier. Modifying the content may also break the message + # signature. So, we start a new message publishing. We will be + # called again recursively as a result, but we ignore the event we + # generate so it doesn't matter. + notifier_api.notify(context, publisher_id, + 'compute.instance.delete.samples', + notifier_api.INFO, payload) diff --git a/ceilometer/compute/virt/inspector.py b/ceilometer/compute/virt/inspector.py index 44cbc4caf..d2222978b 100644 --- a/ceilometer/compute/virt/inspector.py +++ b/ceilometer/compute/virt/inspector.py @@ -3,6 +3,7 @@ # Copyright © 2012 Red Hat, Inc # # Author: Eoghan Glynn +# Doug Hellmann # # 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 @@ -19,6 +20,23 @@ import collections +from oslo.config import cfg +from stevedore import driver + +from ceilometer.openstack.common import log + + +OPTS = [ + cfg.StrOpt('hypervisor_inspector', + default='libvirt', + help='Inspector to use for inspecting the hypervisor layer'), +] + +cfg.CONF.register_opts(OPTS) + + +LOG = log.getLogger(__name__) + # Named tuple representing instances. # # name: the name of the instance @@ -128,3 +146,15 @@ class Inspector(object): read and written, and the error count """ raise NotImplementedError() + + +def get_hypervisor_inspector(): + try: + namespace = 'ceilometer.compute.virt' + mgr = driver.DriverManager(namespace, + cfg.CONF.hypervisor_inspector, + invoke_on_load=True) + return mgr.driver + except ImportError as e: + LOG.error("Unable to load the hypervisor inspector: %s" % (e)) + return Inspector() diff --git a/ceilometer/plugin.py b/ceilometer/plugin.py index c1cd5fe13..141a31672 100644 --- a/ceilometer/plugin.py +++ b/ceilometer/plugin.py @@ -21,10 +21,6 @@ import abc from collections import namedtuple -# Import rpc_notifier to register notification_topics flag so that -# plugins can use it -import ceilometer.openstack.common.notifier.rpc_notifier - ExchangeTopics = namedtuple('ExchangeTopics', ['exchange', 'topics']) diff --git a/tests/compute/test_nova_notifier.py b/nova_tests/test_folsom.py similarity index 97% rename from tests/compute/test_nova_notifier.py rename to nova_tests/test_folsom.py index d03371212..67a840163 100644 --- a/tests/compute/test_nova_notifier.py +++ b/nova_tests/test_folsom.py @@ -18,6 +18,13 @@ """Tests for ceilometer.compute.nova_notifier """ +try: + import nova.conductor + import nose.plugins.skip + raise nose.SkipTest('do not run folsom tests under grizzly') +except ImportError: + pass + # FIXME(dhellmann): Temporarily disable these tests so we can get a # fix to go through Jenkins. import nose.plugins.skip @@ -31,13 +38,9 @@ from stevedore import extension from stevedore.tests import manager as test_manager from ceilometer.compute import manager -try: - from nova import config - nova_CONF = config.cfg.CONF -except ImportError: - # XXX Folsom compat - from nova import flags - nova_CONF = flags.FLAGS +# XXX Folsom compat +from nova import flags +nova_CONF = flags.FLAGS from nova import db from nova import context from nova import service # For nova_CONF.compute_manager diff --git a/nova_tests/test_grizzly.py b/nova_tests/test_grizzly.py new file mode 100644 index 000000000..e742a055a --- /dev/null +++ b/nova_tests/test_grizzly.py @@ -0,0 +1,229 @@ +# -*- encoding: utf-8 -*- +# +# Copyright © 2012 New Dream Network, LLC (DreamHost) +# +# Author: Julien Danjou +# Doug Hellmann +# +# 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. +"""Tests for ceilometer.compute.nova_notifier +""" + +try: + import nova.conductor +except ImportError: + import nose.plugins.skip + raise nose.SkipTest('do not run grizzly tests under folsom') + +import contextlib +import datetime + +import mock + +from oslo.config import cfg + +from stevedore import extension +from stevedore.tests import manager as test_manager + +## NOTE(dhellmann): These imports are not in the generally approved +## alphabetical order, but they are in the order that actually +## works. Please don't change them. + +from nova import config +from nova import db +from nova import context +from nova.tests import fake_network +from nova.compute import vm_states +from nova.openstack.common.notifier import api as notifier_api +from nova.openstack.common import importutils +from nova.openstack.common import log as logging + +# For nova_CONF.compute_manager, used in the nova_notifier module. +from nova import service + +# HACK(dhellmann): Import this before any other ceilometer code +# because the notifier module messes with the import path to force +# nova's version of oslo to be used instead of ceilometer's. +from ceilometer.compute import nova_notifier + +from ceilometer import counter +from ceilometer.tests import base + +LOG = logging.getLogger(__name__) +nova_CONF = config.cfg.CONF + + +class TestNovaNotifier(base.TestCase): + + class Pollster(object): + instances = [] + test_data = counter.Counter( + name='test', + type=counter.TYPE_CUMULATIVE, + unit='units-go-here', + volume=1, + user_id='test', + project_id='test', + resource_id='test_run_tasks', + timestamp=datetime.datetime.utcnow().isoformat(), + resource_metadata={'name': 'Pollster', + }, + ) + + def get_counters(self, manager, instance): + self.instances.append((manager, instance)) + return [self.test_data] + + def get_counter_names(self): + return ['test'] + + @mock.patch('ceilometer.pipeline.setup_pipeline', mock.MagicMock()) + def setUp(self): + super(TestNovaNotifier, self).setUp() + nova_CONF.compute_driver = 'nova.virt.fake.FakeDriver' + nova_CONF.notification_driver = [nova_notifier.__name__] + nova_CONF.rpc_backend = 'nova.openstack.common.rpc.impl_fake' + nova_CONF.vnc_enabled = False + nova_CONF.spice.enabled = False + self.compute = importutils.import_object(nova_CONF.compute_manager) + self.context = context.get_admin_context() + fake_network.set_stub_network_methods(self.stubs) + + self.instance = {"name": "instance-1", + 'OS-EXT-SRV-ATTR:instance_name': 'instance-1', + "id": 1, + "image_ref": "FAKE", + "user_id": "FAKE", + "project_id": "FAKE", + "display_name": "FAKE NAME", + "hostname": "abcdef", + "reservation_id": "FAKE RID", + "instance_type_id": 1, + "architecture": "x86", + "memory_mb": "1024", + "root_gb": "20", + "ephemeral_gb": "0", + "vcpus": 1, + "host": "fakehost", + "availability_zone": + "1e3ce043029547f1a61c1996d1a531a4", + "created_at": '2012-05-08 20:23:41', + "os_type": "linux", + "kernel_id": "kernelid", + "ramdisk_id": "ramdiskid", + "vm_state": vm_states.ACTIVE, + "access_ip_v4": "someip", + "access_ip_v6": "someip", + "metadata": {}, + "uuid": "144e08f4-00cb-11e2-888e-5453ed1bbb5f", + "system_metadata": {}} + self.stubs.Set(db, 'instance_info_cache_delete', self.do_nothing) + self.stubs.Set(db, 'instance_destroy', self.do_nothing) + self.stubs.Set(db, 'instance_system_metadata_get', + self.fake_db_instance_system_metadata_get) + self.stubs.Set(db, 'block_device_mapping_get_all_by_instance', + lambda context, instance: {}) + self.stubs.Set(db, 'instance_update_and_get_original', + lambda context, uuid, kwargs: (self.instance, + self.instance)) + + # Set up to capture the notification messages generated by the + # plugin and to invoke our notifier plugin. + self.notifications = [] + notifier_api._reset_drivers() + notifier_api.add_driver(self) + notifier_api.add_driver(nova_notifier) + + ext_mgr = test_manager.TestExtensionManager([ + extension.Extension('test', + None, + None, + self.Pollster(), + ), + ]) + self.ext_mgr = ext_mgr + self.gatherer = nova_notifier.DeletedInstanceStatsGatherer(ext_mgr) + nova_notifier.initialize_gatherer(self.gatherer) + + # Terminate the instance to trigger the notification. + with contextlib.nested( + # Under Grizzly, Nova has moved to no-db access on the + # compute node. The compute manager uses RPC to talk to + # the conductor. We need to disable communication between + # the nova manager and the remote system since we can't + # expect the message bus to be available, or the remote + # controller to be there if the message bus is online. + mock.patch.object(self.compute, 'conductor_api'), + # The code that looks up the instance uses a global + # reference to the API, so we also have to patch that to + # return our fake data. + mock.patch.object(nova_notifier.instance_info_source, + 'instance_get_by_uuid', + self.fake_instance_ref_get), + ): + self.compute.terminate_instance(self.context, + instance=self.instance) + + def tearDown(self): + notifier_api._reset_drivers() + self.Pollster.instances = [] + super(TestNovaNotifier, self).tearDown() + nova_notifier._gatherer = None + + def fake_instance_ref_get(self, context, id_): + if self.instance['uuid'] == id_: + return self.instance + return {} + + @staticmethod + def do_nothing(*args, **kwargs): + pass + + @staticmethod + def fake_db_instance_system_metadata_get(context, uuid): + return dict(meta_a=123, meta_b="foobar") + + def notify(self, context, message): + self.notifications.append(message) + + def test_pollster_called(self): + # The notifier plugin sends another notification for the same + # instance, so we expect to have 2 entries in the list. + self.assertEqual(len(self.Pollster.instances), 2) + + def test_correct_instance(self): + for i, (gatherer, inst) in enumerate(self.Pollster.instances): + self.assertEqual((i, inst.uuid), (i, self.instance['uuid'])) + + def test_correct_gatherer(self): + for i, (gatherer, inst) in enumerate(self.Pollster.instances): + self.assertEqual((i, gatherer), (i, self.gatherer)) + + def test_samples(self): + # Ensure that the outgoing notification looks like what we expect + for message in self.notifications: + event = message['event_type'] + if event != 'compute.instance.delete.samples': + continue + payload = message['payload'] + samples = payload['samples'] + self.assertEqual(len(samples), 1) + s = payload['samples'][0] + self.assertEqual(s, {'name': 'test', + 'type': counter.TYPE_CUMULATIVE, + 'unit': 'units-go-here', + 'volume': 1, + }) + break + else: + assert False, 'Did not find expected event' diff --git a/setup.py b/setup.py index e2c156d96..9fe04fb14 100755 --- a/setup.py +++ b/setup.py @@ -91,6 +91,7 @@ setuptools.setup( [ceilometer.collector] instance = ceilometer.compute.notifications:Instance instance_flavor = ceilometer.compute.notifications:InstanceFlavor + instance_delete = ceilometer.compute.notifications:InstanceDelete memory = ceilometer.compute.notifications:Memory vcpus = ceilometer.compute.notifications:VCpus disk_root_size = ceilometer.compute.notifications:RootDiskSize diff --git a/tests/compute/test_notifications.py b/tests/compute/test_notifications.py index b281a6cc4..0e541b0a3 100644 --- a/tests/compute/test_notifications.py +++ b/tests/compute/test_notifications.py @@ -285,13 +285,79 @@ INSTANCE_RESIZE_REVERT_END = { u'priority': u'INFO' } +INSTANCE_DELETE_SAMPLES = { + u'_context_roles': [u'admin'], + u'_context_request_id': u'req-9da1d714-dabe-42fd-8baa-583e57cd4f1a', + u'_context_quota_class': None, + u'event_type': u'compute.instance.delete.samples', + u'_context_user_name': u'admin', + u'_context_project_name': u'admin', + u'timestamp': u'2013-01-04 15:20:32.009532', + u'_context_is_admin': True, + u'message_id': u'c48deeba-d0c3-4154-b3db-47480b52267a', + u'_context_auth_token': None, + u'_context_instance_lock_checked': False, + u'_context_project_id': u'cea4b25edb484e5392727181b7721d29', + u'_context_timestamp': u'2013-01-04T15:19:51.018218', + u'_context_read_deleted': u'no', + u'_context_user_id': u'01b83a5e23f24a6fb6cd073c0aee6eed', + u'_context_remote_address': u'10.147.132.184', + u'publisher_id': u'compute.ip-10-147-132-184.ec2.internal', + u'payload': {u'state_description': u'resize_reverting', + u'availability_zone': None, + u'ephemeral_gb': 0, + u'instance_type_id': 2, + u'deleted_at': u'', + u'reservation_id': u'r-u3fvim06', + u'memory_mb': 512, + u'user_id': u'01b83a5e23f24a6fb6cd073c0aee6eed', + u'hostname': u's1', + u'state': u'resized', + u'launched_at': u'2013-01-04T15:10:14.000000', + u'metadata': [], + u'ramdisk_id': u'5f23128e-5525-46d8-bc66-9c30cd87141a', + u'access_ip_v6': None, + u'disk_gb': 0, + u'access_ip_v4': None, + u'kernel_id': u'571478e0-d5e7-4c2e-95a5-2bc79443c28a', + u'host': u'ip-10-147-132-184.ec2.internal', + u'display_name': u's1', + u'image_ref_url': u'http://10.147.132.184:9292/images/' + 'a130b9d9-e00e-436e-9782-836ccef06e8a', + u'root_gb': 0, + u'tenant_id': u'cea4b25edb484e5392727181b7721d29', + u'created_at': u'2013-01-04T11:21:48.000000', + u'instance_id': u'648e8963-6886-4c3c-98f9-4511c292f86b', + u'instance_type': u'm1.tiny', + u'vcpus': 1, + u'image_meta': {u'kernel_id': + u'571478e0-d5e7-4c2e-95a5-2bc79443c28a', + u'ramdisk_id': + u'5f23128e-5525-46d8-bc66-9c30cd87141a', + u'base_image_ref': + u'a130b9d9-e00e-436e-9782-836ccef06e8a'}, + u'architecture': None, + u'os_type': None, + u'samples': [{u'name': u'sample-name1', + u'type': u'sample-type1', + u'unit': u'sample-units1', + u'volume': 1}, + {u'name': u'sample-name2', + u'type': u'sample-type2', + u'unit': u'sample-units2', + u'volume': 2}, + ], + }, + u'priority': u'INFO' +} + class TestNotifications(unittest.TestCase): + def test_process_notification(self): info = notifications.Instance().process_notification( INSTANCE_CREATE_END )[0] - for name, actual, expected in [ ('counter_name', info.name, 'instance'), ('counter_type', info.type, counter.TYPE_GAUGE), @@ -435,3 +501,10 @@ class TestNotifications(unittest.TestCase): c = counters[0] self.assertEqual(c.volume, INSTANCE_RESIZE_REVERT_END['payload']['vcpus']) + + def test_instance_delete_samples(self): + ic = notifications.InstanceDelete() + counters = ic.process_notification(INSTANCE_DELETE_SAMPLES) + self.assertEqual(len(counters), 2) + names = [c.name for c in counters] + self.assertEqual(names, ['sample-name1', 'sample-name2']) diff --git a/tests/compute/test_pollsters.py b/tests/compute/test_pollsters.py index 65b64a3cc..34518546d 100644 --- a/tests/compute/test_pollsters.py +++ b/tests/compute/test_pollsters.py @@ -33,9 +33,9 @@ class TestPollsterBase(test_base.TestCase): def setUp(self): super(TestPollsterBase, self).setUp() - self.mox.StubOutWithMock(manager, 'get_hypervisor_inspector') + self.mox.StubOutWithMock(virt_inspector, 'get_hypervisor_inspector') self.inspector = self.mox.CreateMock(virt_inspector.Inspector) - manager.get_hypervisor_inspector().AndReturn(self.inspector) + virt_inspector.get_hypervisor_inspector().AndReturn(self.inspector) self.instance = mock.MagicMock() self.instance.name = 'instance-00000001' setattr(self.instance, 'OS-EXT-SRV-ATTR:instance_name', diff --git a/tox.ini b/tox.ini index 754ff0be6..b64505cc3 100644 --- a/tox.ini +++ b/tox.ini @@ -11,6 +11,7 @@ setenv = VIRTUAL_ENV={envdir} NOSE_OPENSTACK_YELLOW=0.025 NOSE_OPENSTACK_SHOW_ELAPSED=1 commands = + nosetests --no-path-adjustment --where=./nova_tests nosetests --no-path-adjustment --where=./tests sitepackages = False