summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--MANIFEST.in3
-rw-r--r--ceilometer/collector/service.py7
-rw-r--r--ceilometer/compute/__init__.py28
-rw-r--r--ceilometer/compute/instance.py9
-rw-r--r--ceilometer/compute/manager.py27
-rw-r--r--ceilometer/compute/notifications.py26
-rw-r--r--ceilometer/compute/nova_notifier/__init__.py28
-rw-r--r--ceilometer/compute/nova_notifier/folsom.py (renamed from ceilometer/compute/nova_notifier.py)11
-rw-r--r--ceilometer/compute/nova_notifier/grizzly.py175
-rw-r--r--ceilometer/compute/virt/inspector.py30
-rw-r--r--ceilometer/plugin.py4
-rw-r--r--nova_tests/test_folsom.py (renamed from tests/compute/test_nova_notifier.py)17
-rw-r--r--nova_tests/test_grizzly.py229
-rwxr-xr-xsetup.py1
-rw-r--r--tests/compute/test_notifications.py75
-rw-r--r--tests/compute/test_pollsters.py4
-rw-r--r--tox.ini1
17 files changed, 627 insertions, 48 deletions
diff --git a/MANIFEST.in b/MANIFEST.in
index 29782f5b..16188e03 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 c6237cd3..d6ceb897 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 e69de29b..ef4e3558 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 <doug.hellmann@dreamhost.com>
+#
+# 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 728ccc85..dff5ec6e 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 f741ff40..cf41dcf7 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 904d3702..19c85bee 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 00000000..8b659382
--- /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 <doug.hellmann@dreamhost.com>
+#
+# 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
index eebdf6a1..eaced71b 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 00000000..e8f016f5
--- /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 <julien@danjou.info>
+# Doug Hellmann <doug.hellmann@dreamhost.com>
+#
+# 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 44cbc4ca..d2222978 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 <eglynn@redhat.com>
+# Doug Hellmann <doug.hellmann@dreamhost.com>
#
# 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 c1cd5fe1..141a3167 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
index d0337121..67a84016 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 00000000..e742a055
--- /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 <julien@danjou.info>
+# Doug Hellmann <doug.hellmann@dreamhost.com>
+#
+# 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 e2c156d9..9fe04fb1 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 b281a6cc..0e541b0a 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 65b64a3c..34518546 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 754ff0be..b64505cc 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