Fixes failed notification when deleting instance

In the notifier plugin for nova, the class Instance that translates instance_ref
returned by conduct.API to nova_client like Instance is not very robust, which
results some attributes are none or missing, and then causes pollsters raise
AttributeError or NoneTypeError

In this change, add the missing or none attributes to the Instance class.

Change-Id: I3e5aa6382021633a6bf7240bf58d3c3bed074785
Fixs: bug #1206731
This commit is contained in:
Guangyu Suo 2013-08-08 06:39:40 +08:00
parent b59a03109c
commit f73f3e17af
2 changed files with 96 additions and 54 deletions

View File

@ -31,6 +31,7 @@ for name in ['openstack', 'openstack.common', 'openstack.common.log']:
sys.modules['ceilometer.' + name] = sys.modules['nova.' + name]
from nova import conductor
from nova import utils
from stevedore import extension
@ -46,7 +47,7 @@ from ceilometer.openstack.common.gettextutils import _
LOG = logging.getLogger('nova.ceilometer.notifier')
_gatherer = None
instance_info_source = conductor.API()
conductor_api = conductor.API()
class DeletedInstanceStatsGatherer(object):
@ -100,9 +101,16 @@ class Instance(object):
dictionary. This class makes an object from the dictonary so we
can pass it to the pollsters.
"""
def __init__(self, info):
def __init__(self, context, info):
for k, v in info.iteritems():
if k == 'name':
setattr(self, 'OS-EXT-SRV-ATTR:instance_name', v)
elif k == 'metadata':
setattr(self, k, utils.metadata_to_dict(v))
else:
setattr(self, k, v)
self.flavor_name = conductor_api.instance_type_get(
context, self.instance_type_id).get('name', 'UNKNOWN')
LOG.debug(_('INFO %r'), info)
@property
@ -113,7 +121,7 @@ class Instance(object):
def flavor(self):
return {
'id': self.instance_type_id,
'name': self.instance_type.get('name', 'UNKNOWN'),
'name': self.flavor_name,
}
@property
@ -124,6 +132,10 @@ class Instance(object):
def image(self):
return {'id': self.image_ref}
@property
def name(self):
return self.display_name
def notify(context, message):
if message['event_type'] != 'compute.instance.delete.start':
@ -136,7 +148,7 @@ def notify(context, message):
LOG.debug(_('polling final stats for %r'), instance_id)
# Ask for the instance details
instance_ref = instance_info_source.instance_get_by_uuid(
instance_ref = conductor_api.instance_get_by_uuid(
context,
instance_id,
)
@ -148,7 +160,7 @@ def notify(context, message):
# 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)
instance = Instance(context, instance_ref)
counters = gatherer(instance)
payload['samples'] = [{'name': c.name,
'type': c.type,

View File

@ -59,6 +59,7 @@ import ceilometer.openstack.common.fixture.moxstubout
from ceilometer.compute import nova_notifier
from ceilometer import sample
from ceilometer.compute.pollsters import util
from ceilometer.tests import base
LOG = logging.getLogger(__name__)
@ -69,8 +70,8 @@ class TestNovaNotifier(base.TestCase):
class Pollster(object):
instances = []
test_data = sample.Sample(
name='test',
test_data_1 = sample.Sample(
name='test1',
type=sample.TYPE_CUMULATIVE,
unit='units-go-here',
volume=1,
@ -84,10 +85,17 @@ class TestNovaNotifier(base.TestCase):
def get_samples(self, manager, cache, instance):
self.instances.append((manager, instance))
return [self.test_data]
test_data_2 = util.make_counter_from_instance(
instance,
name='test2',
type=sample.TYPE_CUMULATIVE,
unit='units-go-here',
volume=1,
)
return [self.test_data_1, test_data_2]
def get_counter_names(self):
return ['test']
return ['test1', 'test2']
@mock.patch('ceilometer.pipeline.setup_pipeline', mock.MagicMock())
def setUp(self):
@ -104,8 +112,7 @@ class TestNovaNotifier(base.TestCase):
self.context = context.get_admin_context()
fake_network.set_stub_network_methods(self.stubs)
instance_data = {"display_name": "instance-1",
'OS-EXT-SRV-ATTR:instance_name': 'instance-1',
self.instance_data = {"display_name": "instance-1",
"id": 1,
"image_ref": "FAKE",
"user_id": "FAKE",
@ -138,7 +145,7 @@ class TestNovaNotifier(base.TestCase):
"system_metadata": {}}
self.instance = nova_instance.Instance()
for key, value in instance_data.iteritems():
for key, value in self.instance_data.iteritems():
setattr(self.instance, key, value)
self.stubs.Set(db, 'instance_info_cache_delete', self.do_nothing)
@ -166,6 +173,8 @@ class TestNovaNotifier(base.TestCase):
])
self.ext_mgr = ext_mgr
self.gatherer = nova_notifier.DeletedInstanceStatsGatherer(ext_mgr)
# Initialize the global _gatherer in nova_notifier to use the
# gatherer in this test instead of the gatherer in nova_notifier.
nova_notifier.initialize_gatherer(self.gatherer)
# Terminate the instance to trigger the notification.
@ -180,9 +189,12 @@ class TestNovaNotifier(base.TestCase):
# 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,
mock.patch.object(nova_notifier.conductor_api,
'instance_get_by_uuid',
self.fake_instance_ref_get),
mock.patch.object(nova_notifier.conductor_api,
'instance_type_get',
self.fake_instance_type_get),
mock.patch('nova.openstack.common.notifier.rpc_notifier.notify',
self.notify)
):
@ -194,10 +206,14 @@ class TestNovaNotifier(base.TestCase):
super(TestNovaNotifier, self).tearDown()
nova_notifier._gatherer = None
# The instance returned by conductor API is a dictionary actually,
# and it will be transformed to an nova_notifier.Instance object
# that looks like what the novaclient gives them.
def fake_instance_ref_get(self, context, id_):
if self.instance.uuid == id_:
return self.instance
return {}
return self.instance_data
def fake_instance_type_get(self, context, id_):
return {'id': '1', 'name': 'm1.tiny'}
@staticmethod
def do_nothing(*args, **kwargs):
@ -229,13 +245,27 @@ class TestNovaNotifier(base.TestCase):
continue
payload = message['payload']
samples = payload['samples']
self.assertEqual(len(samples), 1)
s = payload['samples'][0]
self.assertEqual(s, {'name': 'test',
# Because the playload's samples doesn't include instance
# metadata, we can't check the metadata field directly.
# But if we make a mistake in the instance attributes, such
# as missing instance.name or instance.flavor['name'], it
# will raise AttributeError, which results the number of
# the samples doesn't equal to 2.
self.assertEqual(len(samples), 2)
s1 = payload['samples'][0]
self.assertEqual(s1, {'name': 'test1',
'type': sample.TYPE_CUMULATIVE,
'unit': 'units-go-here',
'volume': 1,
})
s2 = payload['samples'][1]
self.assertEqual(s2, {'name': 'test2',
'type': sample.TYPE_CUMULATIVE,
'unit': 'units-go-here',
'volume': 1,
})
break
else:
assert False, 'Did not find expected event'