Merge "Factorize field definition of declarative code"

This commit is contained in:
Jenkins 2015-11-04 22:38:56 +00:00 committed by Gerrit Code Review
commit 2be016ec68
9 changed files with 333 additions and 317 deletions

118
ceilometer/declarative.py Normal file
View File

@ -0,0 +1,118 @@
#
# 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 jsonpath_rw_ext import parser
import six
from ceilometer.i18n import _
class DefinitionException(Exception):
def __init__(self, message, definition_cfg):
super(DefinitionException, self).__init__(message)
self.definition_cfg = definition_cfg
class Definition(object):
JSONPATH_RW_PARSER = parser.ExtentedJsonPathParser()
def __init__(self, name, cfg, plugin_manager):
self.cfg = cfg
self.name = name
self.plugin = None
if isinstance(cfg, dict):
if 'fields' not in cfg:
raise DefinitionException(
_("The field 'fields' is required for %s") % name,
self.cfg)
if 'plugin' in cfg:
plugin_cfg = cfg['plugin']
if isinstance(plugin_cfg, six.string_types):
plugin_name = plugin_cfg
plugin_params = {}
else:
try:
plugin_name = plugin_cfg['name']
except KeyError:
raise DefinitionException(
_('Plugin specified, but no plugin name supplied '
'for %s') % name, self.cfg)
plugin_params = plugin_cfg.get('parameters')
if plugin_params is None:
plugin_params = {}
try:
plugin_ext = plugin_manager[plugin_name]
except KeyError:
raise DefinitionException(
_('No plugin named %(plugin)s available for '
'%(name)s') % dict(
plugin=plugin_name,
name=name), self.cfg)
plugin_class = plugin_ext.plugin
self.plugin = plugin_class(**plugin_params)
fields = cfg['fields']
else:
# Simple definition "foobar: jsonpath"
fields = cfg
if isinstance(fields, list):
# NOTE(mdragon): if not a string, we assume a list.
if len(fields) == 1:
fields = fields[0]
else:
fields = '|'.join('(%s)' % path for path in fields)
if isinstance(fields, six.integer_types):
self.getter = fields
else:
try:
self.getter = self.JSONPATH_RW_PARSER.parse(fields).find
except Exception as e:
raise DefinitionException(
_("Parse error in JSONPath specification "
"'%(jsonpath)s' for %(name)s: %(err)s")
% dict(jsonpath=fields, name=name, err=e), self.cfg)
def _get_path(self, match):
if match.context is not None:
for path_element in self._get_path(match.context):
yield path_element
yield str(match.path)
def parse(self, obj, return_all_values=False):
if callable(self.getter):
values = self.getter(obj)
else:
return self.getter
values = [match for match in values
if return_all_values or match.value is not None]
if self.plugin is not None:
if return_all_values and not self.plugin.support_return_all_values:
raise DefinitionException("Plugin %s don't allows to "
"return multiple values" %
self.cfg["plugin"]["name"])
values_map = [('.'.join(self._get_path(match)), match.value) for
match in values]
values = [v for v in self.plugin.trait_values(values_map)
if v is not None]
else:
values = [match.value for match in values if match is not None]
if return_all_values:
return values
else:
return values[0] if values else None

View File

@ -13,18 +13,18 @@
# License for the specific language governing permissions and limitations
# under the License.
import fnmatch
import functools
import itertools
import operator
import os
import threading
from jsonpath_rw_ext import parser
from oslo_config import cfg
from oslo_log import log
import six
from stevedore import extension
import yaml
from ceilometer import declarative
from ceilometer import dispatcher
from ceilometer.dispatcher import gnocchi_client
from ceilometer.i18n import _, _LE
@ -81,42 +81,23 @@ class ResourcesDefinition(object):
MANDATORY_FIELDS = {'resource_type': six.string_types,
'metrics': list}
JSONPATH_RW_PARSER = parser.ExtentedJsonPathParser()
def __init__(self, definition_cfg, default_archive_policy):
def __init__(self, definition_cfg, default_archive_policy, plugin_manager):
self._default_archive_policy = default_archive_policy
self.cfg = definition_cfg
for field, field_type in self.MANDATORY_FIELDS.items():
if field not in self.cfg:
raise ResourcesDefinitionException(
raise declarative.DefinitionException(
_LE("Required field %s not specified") % field, self.cfg)
if not isinstance(self.cfg[field], field_type):
raise ResourcesDefinitionException(
raise declarative.DefinitionException(
_LE("Required field %(field)s should be a %(type)s") %
{'field': field, 'type': field_type}, self.cfg)
self._field_getter = {}
for name, fval in self.cfg.get('attributes', {}).items():
if isinstance(fval, six.integer_types):
self._field_getter[name] = fval
else:
try:
parts = self.JSONPATH_RW_PARSER.parse(fval)
except Exception as e:
raise ResourcesDefinitionException(
_LE("Parse error in JSONPath specification "
"'%(jsonpath)s': %(err)s")
% dict(jsonpath=fval, err=e), self.cfg)
self._field_getter[name] = functools.partial(
self._parse_jsonpath_field, parts)
@staticmethod
def _parse_jsonpath_field(parts, sample):
values = [match.value for match in parts.find(sample)
if match.value is not None]
if values:
return values[0]
self._attributes = {}
for name, attr_cfg in self.cfg.get('attributes', {}).items():
self._attributes[name] = declarative.Definition(name, attr_cfg,
plugin_manager)
def match(self, metric_name):
for t in self.cfg['metrics']:
@ -126,13 +107,10 @@ class ResourcesDefinition(object):
def attributes(self, sample):
attrs = {}
for attr, getter in self._field_getter.items():
if callable(getter):
value = getter(sample)
else:
value = getter
for name, definition in self._attributes.items():
value = definition.parse(sample)
if value is not None:
attrs[attr] = value
attrs[name] = value
return attrs
def metrics(self):
@ -172,6 +150,8 @@ class GnocchiDispatcher(dispatcher.Base):
@classmethod
def _load_resources_definitions(cls, conf):
plugin_manager = extension.ExtensionManager(
namespace='ceilometer.event.trait_plugin')
res_def_file = cls._get_config_file(
conf, conf.dispatcher_gnocchi.resources_definition_file)
data = {}
@ -182,7 +162,8 @@ class GnocchiDispatcher(dispatcher.Base):
except ValueError:
data = {}
return [ResourcesDefinition(r, conf.dispatcher_gnocchi.archive_policy)
return [ResourcesDefinition(r, conf.dispatcher_gnocchi.archive_policy,
plugin_manager)
for r in data.get('resources', [])]
@property

View File

@ -16,13 +16,14 @@
import fnmatch
import os
from jsonpath_rw_ext import parser
from debtcollector import moves
from oslo_config import cfg
from oslo_log import log
from oslo_utils import timeutils
import six
import yaml
from ceilometer import declarative
from ceilometer.event.storage import models
from ceilometer.i18n import _, _LI
@ -46,97 +47,26 @@ cfg.CONF.register_opts(OPTS, group='event')
LOG = log.getLogger(__name__)
class EventDefinitionException(Exception):
def __init__(self, message, definition_cfg):
super(EventDefinitionException, self).__init__(message)
self.definition_cfg = definition_cfg
def __str__(self):
return '%s %s: %s' % (self.__class__.__name__,
self.definition_cfg, self.message)
EventDefinitionException = moves.moved_class(declarative.DefinitionException,
'EventDefinitionException',
__name__,
version=6.0,
removal_version="?")
class TraitDefinition(object):
JSONPATH_RW_PARSER = parser.ExtentedJsonPathParser()
class TraitDefinition(declarative.Definition):
def __init__(self, name, trait_cfg, plugin_manager):
self.cfg = trait_cfg
self.name = name
type_name = trait_cfg.get('type', 'text')
if 'plugin' in trait_cfg:
plugin_cfg = trait_cfg['plugin']
if isinstance(plugin_cfg, six.string_types):
plugin_name = plugin_cfg
plugin_params = {}
else:
try:
plugin_name = plugin_cfg['name']
except KeyError:
raise EventDefinitionException(
_('Plugin specified, but no plugin name supplied for '
'trait %s') % name, self.cfg)
plugin_params = plugin_cfg.get('parameters')
if plugin_params is None:
plugin_params = {}
try:
plugin_ext = plugin_manager[plugin_name]
except KeyError:
raise EventDefinitionException(
_('No plugin named %(plugin)s available for '
'trait %(trait)s') % dict(plugin=plugin_name,
trait=name), self.cfg)
plugin_class = plugin_ext.plugin
self.plugin = plugin_class(**plugin_params)
else:
self.plugin = None
if 'fields' not in trait_cfg:
raise EventDefinitionException(
_("Required field in trait definition not specified: "
"'%s'") % 'fields',
self.cfg)
fields = trait_cfg['fields']
if not isinstance(fields, six.string_types):
# NOTE(mdragon): if not a string, we assume a list.
if len(fields) == 1:
fields = fields[0]
else:
fields = '|'.join('(%s)' % path for path in fields)
try:
self.fields = self.JSONPATH_RW_PARSER.parse(fields)
except Exception as e:
raise EventDefinitionException(
_("Parse error in JSONPath specification "
"'%(jsonpath)s' for %(trait)s: %(err)s")
% dict(jsonpath=fields, trait=name, err=e), self.cfg)
super(TraitDefinition, self).__init__(name, trait_cfg, plugin_manager)
type_name = (trait_cfg.get('type', 'text')
if isinstance(trait_cfg, dict) else 'text')
self.trait_type = models.Trait.get_type_by_name(type_name)
if self.trait_type is None:
raise EventDefinitionException(
raise declarative.DefinitionException(
_("Invalid trait type '%(type)s' for trait %(trait)s")
% dict(type=type_name, trait=name), self.cfg)
def _get_path(self, match):
if match.context is not None:
for path_element in self._get_path(match.context):
yield path_element
yield str(match.path)
def to_trait(self, notification_body):
values = [match for match in self.fields.find(notification_body)
if match.value is not None]
if self.plugin is not None:
value_map = [('.'.join(self._get_path(match)), match.value) for
match in values]
value = self.plugin.trait_value(value_map)
else:
value = values[0].value if values else None
value = self.parse(notification_body)
if value is None:
return None
@ -175,7 +105,7 @@ class EventDefinition(object):
event_type = definition_cfg['event_type']
traits = definition_cfg['traits']
except KeyError as err:
raise EventDefinitionException(
raise declarative.DefinitionException(
_("Required field %s not specified") % err.args[0], self.cfg)
if isinstance(event_type, six.string_types):

View File

@ -15,6 +15,7 @@
import abc
from debtcollector import moves
from oslo_log import log
import six
@ -30,6 +31,12 @@ class TraitPluginBase(object):
It converts notification fields to Trait values.
"""
support_return_all_values = False
"""If True, an exception will be raised if the user expect
the plugin to return one trait per match_list, but
the plugin doesn't allow/support that.
"""
def __init__(self, **kw):
"""Setup the trait plugin.
@ -43,9 +50,12 @@ class TraitPluginBase(object):
"""
super(TraitPluginBase, self).__init__()
@abc.abstractmethod
@moves.moved_method('trait_values', version=6.0, removal_version="?")
def trait_value(self, match_list):
"""Convert a set of fields to a Trait value.
pass
def trait_values(self, match_list):
"""Convert a set of fields to one or multiple Trait values.
This method is called each time a trait is attempted to be extracted
from a notification. It will be called *even if* no matching fields
@ -93,13 +103,18 @@ class TraitPluginBase(object):
def trait_value(self, match_list):
if not match_list:
return None
return match_list[0][1]
return [ match[1] for match in match_list]
"""
# For backwards compatibility for the renamed method.
return [self.trait_value(match_list)]
class SplitterTraitPlugin(TraitPluginBase):
"""Plugin that splits a piece off of a string value."""
support_return_all_values = True
def __init__(self, separator=".", segment=0, max_split=None, **kw):
"""Setup how do split the field.
@ -120,10 +135,12 @@ class SplitterTraitPlugin(TraitPluginBase):
self.max_split = max_split
super(SplitterTraitPlugin, self).__init__(**kw)
def trait_value(self, match_list):
if not match_list:
return None
value = six.text_type(match_list[0][1])
def trait_values(self, match_list):
return [self._trait_value(match)
for match in match_list]
def _trait_value(self, match):
value = six.text_type(match[1])
if self.max_split is not None:
values = value.split(self.separator, self.max_split)
else:
@ -158,7 +175,7 @@ class BitfieldTraitPlugin(TraitPluginBase):
self.flags = flags
super(BitfieldTraitPlugin, self).__init__(**kw)
def trait_value(self, match_list):
def trait_values(self, match_list):
matches = dict(match_list)
bitfield = self.initial_bitfield
for flagdef in self.flags:
@ -170,4 +187,4 @@ class BitfieldTraitPlugin(TraitPluginBase):
bitfield |= bit
else:
bitfield |= bit
return bitfield
return [bitfield]

View File

@ -12,19 +12,20 @@
# under the License.
import fnmatch
import functools
import itertools
import os
import pkg_resources
import six
import yaml
from jsonpath_rw_ext import parser
from debtcollector import moves
from oslo_config import cfg
from oslo_log import log
import oslo_messaging
from stevedore import extension
from ceilometer.agent import plugin_base
from ceilometer import declarative
from ceilometer.i18n import _LE, _LI
from ceilometer import sample
@ -42,95 +43,129 @@ cfg.CONF.import_opt('disable_non_metric_meters', 'ceilometer.notification',
LOG = log.getLogger(__name__)
class MeterDefinitionException(Exception):
def __init__(self, message, definition_cfg):
super(MeterDefinitionException, self).__init__(message)
self.message = message
self.definition_cfg = definition_cfg
def __str__(self):
return '%s %s: %s' % (self.__class__.__name__,
self.definition_cfg, self.message)
MeterDefinitionException = moves.moved_class(declarative.DefinitionException,
'MeterDefinitionException',
__name__,
version=6.0,
removal_version="?")
class MeterDefinition(object):
JSONPATH_RW_PARSER = parser.ExtentedJsonPathParser()
SAMPLE_ATTRIBUTES = ["name", "type", "volume", "unit", "timestamp",
"user_id", "project_id", "resource_id"]
REQUIRED_FIELDS = ['name', 'type', 'event_type', 'unit', 'volume',
'resource_id']
def __init__(self, definition_cfg):
def __init__(self, definition_cfg, plugin_manager):
self.cfg = definition_cfg
missing = [field for field in self.REQUIRED_FIELDS
if not self.cfg.get(field)]
if missing:
raise MeterDefinitionException(
raise declarative.DefinitionException(
_LE("Required fields %s not specified") % missing, self.cfg)
self._event_type = self.cfg.get('event_type')
if isinstance(self._event_type, six.string_types):
self._event_type = [self._event_type]
if ('type' not in self.cfg.get('lookup', []) and
self.cfg['type'] not in sample.TYPES):
raise MeterDefinitionException(
raise declarative.DefinitionException(
_LE("Invalid type %s specified") % self.cfg['type'], self.cfg)
self._field_getter = {}
for name, field in self.cfg.items():
if name in ["event_type", "lookup"] or not field:
continue
elif isinstance(field, six.integer_types):
self._field_getter[name] = field
elif isinstance(field, dict) and name == 'metadata':
meta = {}
for key, val in field.items():
parts = self.parse_jsonpath(val)
meta[key] = functools.partial(self._parse_jsonpath_field,
parts)
self._field_getter['metadata'] = meta
else:
parts = self.parse_jsonpath(field)
self._field_getter[name] = functools.partial(
self._parse_jsonpath_field, parts)
self._fallback_user_id = declarative.Definition(
'user_id', "_context_user_id|_context_user", plugin_manager)
self._fallback_project_id = declarative.Definition(
'project_id', "_context_tenant_id|_context_tenant", plugin_manager)
self._attributes = {}
self._metadata_attributes = {}
def parse_jsonpath(self, field):
try:
parts = self.JSONPATH_RW_PARSER.parse(field)
except Exception as e:
raise MeterDefinitionException(_LE(
"Parse error in JSONPath specification "
"'%(jsonpath)s': %(err)s")
% dict(jsonpath=field, err=e), self.cfg)
return parts
for name in self.SAMPLE_ATTRIBUTES:
attr_cfg = self.cfg.get(name)
if attr_cfg:
self._attributes[name] = declarative.Definition(
name, attr_cfg, plugin_manager)
metadata = self.cfg.get('metadata', {})
for name in metadata:
self._metadata_attributes[name] = declarative.Definition(
name, metadata[name], plugin_manager)
# List of fields we expected when multiple meter are in the payload
self.lookup = self.cfg.get('lookup')
if isinstance(self.lookup, six.string_types):
self.lookup = [self.lookup]
def match_type(self, meter_name):
for t in self._event_type:
if fnmatch.fnmatch(meter_name, t):
return True
def parse_fields(self, field, message, all_values=False):
getter = self._field_getter.get(field)
if not getter:
return
elif isinstance(getter, dict):
dict_val = {}
for key, val in getter.items():
dict_val[key] = val(message, all_values)
return dict_val
elif callable(getter):
return getter(message, all_values)
else:
return getter
def to_samples(self, message, all_values=False):
# Sample defaults
sample = {
'name': self.cfg["name"], 'type': self.cfg["type"],
'unit': self.cfg["unit"], 'volume': None, 'timestamp': None,
'user_id': self._fallback_user_id.parse(message),
'project_id': self._fallback_project_id.parse(message),
'resource_id': None, 'message': message, 'metadata': {},
}
for name, parser in self._metadata_attributes.items():
value = parser.parse(message)
if value:
sample['metadata'][name] = value
@staticmethod
def _parse_jsonpath_field(parts, message, all_values):
values = [match.value for match in parts.find(message)
if match.value is not None]
if values:
if not all_values:
return values[0]
return values
# NOTE(sileht): We expect multiple samples in the payload
# so put each attribute into a list
if self.lookup:
for name in sample:
sample[name] = [sample[name]]
for name in self.SAMPLE_ATTRIBUTES:
parser = self._attributes.get(name)
if parser is not None:
value = parser.parse(message, bool(self.lookup))
# NOTE(sileht): If we expect multiple samples
# some attributes and overriden even we doesn't get any
# result. Also note in this case value is always a list
if ((not self.lookup and value is not None) or
(self.lookup and ((name in self.lookup + ["name"])
or value))):
sample[name] = value
if self.lookup:
nb_samples = len(sample['name'])
# skip if no meters in payload
if nb_samples <= 0:
raise StopIteration
attributes = self.SAMPLE_ATTRIBUTES + ["message", "metadata"]
samples_values = []
for name in attributes:
values = sample.get(name)
nb_values = len(values)
if nb_values == nb_samples:
samples_values.append(values)
elif nb_values == 1 and name not in self.lookup:
samples_values.append(itertools.cycle(values))
else:
nb = (0 if nb_values == 1 and values[0] is None
else nb_values)
LOG.warning('Only %(nb)d fetched meters contain '
'"%(name)s" field instead of %(total)d.' %
dict(name=name, nb=nb,
total=nb_samples))
raise StopIteration
# NOTE(sileht): Transform the sample with multiple values per
# attribute into multiple samples with one value per attribute.
for values in zip(*samples_values):
yield dict((attributes[idx], value)
for idx, value in enumerate(values))
else:
yield sample
def get_config_file():
@ -182,23 +217,23 @@ def setup_meters_config():
def load_definitions(config_def):
if not config_def:
return []
plugin_manager = extension.ExtensionManager(
namespace='ceilometer.event.trait_plugin')
meter_defs = []
for event_def in reversed(config_def['metric']):
try:
if (event_def['volume'] != 1 or
not cfg.CONF.notification.disable_non_metric_meters):
meter_defs.append(MeterDefinition(event_def))
except MeterDefinitionException as me:
meter_defs.append(MeterDefinition(event_def, plugin_manager))
except declarative.DefinitionException as me:
errmsg = (_LE("Error loading meter definition : %(err)s")
% dict(err=me.message))
% dict(err=six.text_type(me)))
LOG.error(errmsg)
return meter_defs
class InvalidPayload(Exception):
pass
class ProcessMeterNotifications(plugin_base.NotificationBase):
event_types = []
@ -237,82 +272,8 @@ class ProcessMeterNotifications(plugin_base.NotificationBase):
for topic in conf.notification_topics)
return targets
@staticmethod
def _normalise_as_list(value, d, body, length):
values = d.parse_fields(value, body, True)
if not values:
if value in d.cfg.get('lookup'):
LOG.warning('Could not find %s values', value)
raise InvalidPayload
values = [d.cfg[value]]
elif value in d.cfg.get('lookup') and length != len(values):
LOG.warning('Not all fetched meters contain "%s" field', value)
raise InvalidPayload
return values if isinstance(values, list) else [values]
def process_notification(self, notification_body):
for d in self.definitions:
if d.match_type(notification_body['event_type']):
userid = self.get_user_id(d, notification_body)
projectid = self.get_project_id(d, notification_body)
resourceid = d.parse_fields('resource_id', notification_body)
ts = d.parse_fields('timestamp', notification_body)
metadata = d.parse_fields('metadata', notification_body)
if d.cfg.get('lookup'):
meters = d.parse_fields('name', notification_body, True)
if not meters: # skip if no meters in payload
break
try:
resources = self._normalise_as_list(
'resource_id', d, notification_body, len(meters))
volumes = self._normalise_as_list(
'volume', d, notification_body, len(meters))
units = self._normalise_as_list(
'unit', d, notification_body, len(meters))
types = self._normalise_as_list(
'type', d, notification_body, len(meters))
users = (self._normalise_as_list(
'user_id', d, notification_body, len(meters))
if 'user_id' in d.cfg['lookup'] else [userid])
projs = (self._normalise_as_list(
'project_id', d, notification_body, len(meters))
if 'project_id' in d.cfg['lookup']
else [projectid])
times = (self._normalise_as_list(
'timestamp', d, notification_body, len(meters))
if 'timestamp' in d.cfg['lookup'] else [ts])
except InvalidPayload:
break
for m, v, unit, t, r, p, user, ts in zip(
meters, volumes, itertools.cycle(units),
itertools.cycle(types), itertools.cycle(resources),
itertools.cycle(projs), itertools.cycle(users),
itertools.cycle(times)):
yield sample.Sample.from_notification(
name=m, type=t, unit=unit, volume=v,
resource_id=r, user_id=user, project_id=p,
message=notification_body, timestamp=ts,
metadata=metadata)
else:
yield sample.Sample.from_notification(
name=d.cfg['name'],
type=d.cfg['type'],
unit=d.cfg['unit'],
volume=d.parse_fields('volume', notification_body),
resource_id=resourceid,
user_id=userid,
project_id=projectid,
message=notification_body,
timestamp=ts, metadata=metadata)
@staticmethod
def get_user_id(d, notification_body):
return (d.parse_fields('user_id', notification_body) or
notification_body.get('_context_user_id') or
notification_body.get('_context_user', None))
@staticmethod
def get_project_id(d, notification_body):
return (d.parse_fields('project_id', notification_body) or
notification_body.get('_context_tenant_id') or
notification_body.get('_context_tenant', None))
for s in d.to_samples(notification_body):
yield sample.Sample.from_notification(**s)

View File

@ -28,6 +28,7 @@ import six
import six.moves.urllib.parse as urlparse
import testscenarios
from ceilometer import declarative
from ceilometer.dispatcher import gnocchi
from ceilometer import service as ceilometer_service
from ceilometer.tests import base
@ -133,7 +134,7 @@ class DispatcherTest(base.BaseTestCase):
self.conf.config(filter_service_activity=False,
resources_definition_file=temp,
group='dispatcher_gnocchi')
self.assertRaises(gnocchi.ResourcesDefinitionException,
self.assertRaises(declarative.DefinitionException,
gnocchi.GnocchiDispatcher, self.conf.conf)
@mock.patch('ceilometer.dispatcher.gnocchi.GnocchiDispatcher'

View File

@ -20,6 +20,7 @@ import mock
from oslo_config import fixture as fixture_config
import six
from ceilometer import declarative
from ceilometer.event import converter
from ceilometer.event.storage import models
from ceilometer.tests import base
@ -112,13 +113,13 @@ class TestTraitDefinition(ConverterBase):
self.ext1 = mock.MagicMock(name='mock_test_plugin')
self.test_plugin_class = self.ext1.plugin
self.test_plugin = self.test_plugin_class()
self.test_plugin.trait_value.return_value = 'foobar'
self.test_plugin.trait_values.return_value = ['foobar']
self.ext1.reset_mock()
self.ext2 = mock.MagicMock(name='mock_nothing_plugin')
self.nothing_plugin_class = self.ext2.plugin
self.nothing_plugin = self.nothing_plugin_class()
self.nothing_plugin.trait_value.return_value = None
self.nothing_plugin.trait_values.return_value = [None]
self.ext2.reset_mock()
self.fake_plugin_mgr = dict(test=self.ext1, nothing=self.ext2)
@ -136,7 +137,7 @@ class TestTraitDefinition(ConverterBase):
self.assertEqual(models.Trait.TEXT_TYPE, t.dtype)
self.assertEqual('foobar', t.value)
self.test_plugin_class.assert_called_once_with()
self.test_plugin.trait_value.assert_called_once_with([
self.test_plugin.trait_values.assert_called_once_with([
('payload.instance_id', 'id-for-instance-0001'),
('payload.instance_uuid', 'uuid-for-instance-0001')])
@ -153,7 +154,7 @@ class TestTraitDefinition(ConverterBase):
self.assertEqual(models.Trait.TEXT_TYPE, t.dtype)
self.assertEqual('foobar', t.value)
self.test_plugin_class.assert_called_once_with()
self.test_plugin.trait_value.assert_called_once_with([])
self.test_plugin.trait_values.assert_called_once_with([])
def test_to_trait_with_plugin_null(self):
cfg = dict(type='text',
@ -165,7 +166,7 @@ class TestTraitDefinition(ConverterBase):
t = tdef.to_trait(self.n1)
self.assertIs(None, t)
self.nothing_plugin_class.assert_called_once_with()
self.nothing_plugin.trait_value.assert_called_once_with([
self.nothing_plugin.trait_values.assert_called_once_with([
('payload.instance_id', 'id-for-instance-0001'),
('payload.instance_uuid', 'uuid-for-instance-0001')])
@ -182,7 +183,7 @@ class TestTraitDefinition(ConverterBase):
self.assertEqual(models.Trait.TEXT_TYPE, t.dtype)
self.assertEqual('foobar', t.value)
self.test_plugin_class.assert_called_once_with(a=1, b='foo')
self.test_plugin.trait_value.assert_called_once_with([
self.test_plugin.trait_values.assert_called_once_with([
('payload.instance_id', 'id-for-instance-0001'),
('payload.instance_uuid', 'uuid-for-instance-0001')])
@ -287,7 +288,7 @@ class TestTraitDefinition(ConverterBase):
self.assertIs(None, t)
def test_missing_fields_config(self):
self.assertRaises(converter.EventDefinitionException,
self.assertRaises(declarative.DefinitionException,
converter.TraitDefinition,
'bogus_trait',
dict(),
@ -296,19 +297,20 @@ class TestTraitDefinition(ConverterBase):
def test_string_fields_config(self):
cfg = dict(fields='payload.test')
t = converter.TraitDefinition('test_trait', cfg, self.fake_plugin_mgr)
self.assertPathsEqual(t.fields, jsonpath_rw_ext.parse('payload.test'))
self.assertPathsEqual(t.getter.__self__,
jsonpath_rw_ext.parse('payload.test'))
def test_list_fields_config(self):
cfg = dict(fields=['payload.test', 'payload.other'])
t = converter.TraitDefinition('test_trait', cfg, self.fake_plugin_mgr)
self.assertPathsEqual(
t.fields,
t.getter.__self__,
jsonpath_rw_ext.parse('(payload.test)|(payload.other)'))
def test_invalid_path_config(self):
# test invalid jsonpath...
cfg = dict(fields='payload.bogus(')
self.assertRaises(converter.EventDefinitionException,
self.assertRaises(declarative.DefinitionException,
converter.TraitDefinition,
'bogus_trait',
cfg,
@ -317,7 +319,7 @@ class TestTraitDefinition(ConverterBase):
def test_invalid_plugin_config(self):
# test invalid jsonpath...
cfg = dict(fields='payload.test', plugin=dict(bogus="true"))
self.assertRaises(converter.EventDefinitionException,
self.assertRaises(declarative.DefinitionException,
converter.TraitDefinition,
'test_trait',
cfg,
@ -326,7 +328,7 @@ class TestTraitDefinition(ConverterBase):
def test_unknown_plugin(self):
# test invalid jsonpath...
cfg = dict(fields='payload.test', plugin=dict(name='bogus'))
self.assertRaises(converter.EventDefinitionException,
self.assertRaises(declarative.DefinitionException,
converter.TraitDefinition,
'test_trait',
cfg,
@ -352,7 +354,7 @@ class TestTraitDefinition(ConverterBase):
def test_invalid_type_config(self):
# test invalid jsonpath...
cfg = dict(type='bogus', fields='payload.test')
self.assertRaises(converter.EventDefinitionException,
self.assertRaises(declarative.DefinitionException,
converter.TraitDefinition,
'bogus_trait',
cfg,
@ -438,14 +440,14 @@ class TestEventDefinition(ConverterBase):
def test_bogus_cfg_no_traits(self):
bogus = dict(event_type='test.foo')
self.assertRaises(converter.EventDefinitionException,
self.assertRaises(declarative.DefinitionException,
converter.EventDefinition,
bogus,
self.fake_plugin_mgr)
def test_bogus_cfg_no_type(self):
bogus = dict(traits=self.traits_cfg)
self.assertRaises(converter.EventDefinitionException,
self.assertRaises(declarative.DefinitionException,
converter.EventDefinition,
bogus,
self.fake_plugin_mgr)

View File

@ -27,41 +27,41 @@ class TestSplitterPlugin(base.BaseTestCase):
param = dict(separator='-', segment=0)
plugin = self.pclass(**param)
match_list = [('test.thing', 'test-foobar-baz')]
value = plugin.trait_value(match_list)
value = plugin.trait_values(match_list)[0]
self.assertEqual('test', value)
param = dict(separator='-', segment=1)
plugin = self.pclass(**param)
match_list = [('test.thing', 'test-foobar-baz')]
value = plugin.trait_value(match_list)
value = plugin.trait_values(match_list)[0]
self.assertEqual('foobar', value)
param = dict(separator='-', segment=1, max_split=1)
plugin = self.pclass(**param)
match_list = [('test.thing', 'test-foobar-baz')]
value = plugin.trait_value(match_list)
value = plugin.trait_values(match_list)[0]
self.assertEqual('foobar-baz', value)
def test_no_sep(self):
param = dict(separator='-', segment=0)
plugin = self.pclass(**param)
match_list = [('test.thing', 'test.foobar.baz')]
value = plugin.trait_value(match_list)
value = plugin.trait_values(match_list)[0]
self.assertEqual('test.foobar.baz', value)
def test_no_segment(self):
param = dict(separator='-', segment=5)
plugin = self.pclass(**param)
match_list = [('test.thing', 'test-foobar-baz')]
value = plugin.trait_value(match_list)
value = plugin.trait_values(match_list)[0]
self.assertIs(None, value)
def test_no_match(self):
param = dict(separator='-', segment=0)
plugin = self.pclass(**param)
match_list = []
value = plugin.trait_value(match_list)
self.assertIs(None, value)
value = plugin.trait_values(match_list)
self.assertEqual([], value)
class TestBitfieldPlugin(base.BaseTestCase):
@ -86,8 +86,8 @@ class TestBitfieldPlugin(base.BaseTestCase):
('thingy.boink', 'testagain')]
plugin = self.pclass(**self.params)
value = plugin.trait_value(match_list)
self.assertEqual(0x412, value)
value = plugin.trait_values(match_list)
self.assertEqual(0x412, value[0])
def test_initial(self):
match_list = [('payload.foo', 12),
@ -95,14 +95,14 @@ class TestBitfieldPlugin(base.BaseTestCase):
('thingy.boink', 'testagain')]
self.params['initial_bitfield'] = 0x2000
plugin = self.pclass(**self.params)
value = plugin.trait_value(match_list)
self.assertEqual(0x2412, value)
value = plugin.trait_values(match_list)
self.assertEqual(0x2412, value[0])
def test_no_match(self):
match_list = []
plugin = self.pclass(**self.params)
value = plugin.trait_value(match_list)
self.assertEqual(self.init, value)
value = plugin.trait_values(match_list)
self.assertEqual(self.init, value[0])
def test_multi(self):
match_list = [('payload.foo', 12),
@ -111,5 +111,5 @@ class TestBitfieldPlugin(base.BaseTestCase):
('thingy.boink', 'testagain')]
plugin = self.pclass(**self.params)
value = plugin.trait_value(match_list)
self.assertEqual(0x412, value)
value = plugin.trait_values(match_list)
self.assertEqual(0x412, value[0])

View File

@ -18,9 +18,11 @@ import six
import yaml
from oslo_config import fixture as fixture_config
from oslo_utils import encodeutils
from oslo_utils import fileutils
from oslotest import mockpatch
from ceilometer import declarative
from ceilometer.meter import notifications
from ceilometer import service as ceilometer_service
from ceilometer.tests import base as test
@ -223,31 +225,34 @@ class TestMeterDefinition(test.BaseTestCase):
volume="$.payload.volume",
resource_id="$.payload.resource_id",
project_id="$.payload.project_id")
handler = notifications.MeterDefinition(cfg)
handler = notifications.MeterDefinition(cfg, mock.Mock())
self.assertTrue(handler.match_type("test.create"))
self.assertEqual(1.0, handler.parse_fields("volume", NOTIFICATION))
sample = list(handler.to_samples(NOTIFICATION))[0]
self.assertEqual(1.0, sample["volume"])
self.assertEqual("bea70e51c7340cb9d555b15cbfcaec23",
handler.parse_fields("resource_id", NOTIFICATION))
sample["resource_id"])
self.assertEqual("30be1fc9a03c4e94ab05c403a8a377f2",
handler.parse_fields("project_id", NOTIFICATION))
sample["project_id"])
def test_config_required_missing_fields(self):
cfg = dict()
try:
notifications.MeterDefinition(cfg)
except notifications.MeterDefinitionException as e:
notifications.MeterDefinition(cfg, mock.Mock())
except declarative.DefinitionException as e:
self.assertEqual("Required fields ['name', 'type', 'event_type',"
" 'unit', 'volume', 'resource_id']"
" not specified", e.message)
" not specified",
encodeutils.exception_to_unicode(e))
def test_bad_type_cfg_definition(self):
cfg = dict(name="test", type="foo", event_type="bar.create",
unit="foo", volume="bar",
resource_id="bea70e51c7340cb9d555b15cbfcaec23")
try:
notifications.MeterDefinition(cfg)
except notifications.MeterDefinitionException as e:
self.assertEqual("Invalid type foo specified", e.message)
notifications.MeterDefinition(cfg, mock.Mock())
except declarative.DefinitionException as e:
self.assertEqual("Invalid type foo specified",
encodeutils.exception_to_unicode(e))
class TestMeterProcessing(test.BaseTestCase):
@ -601,7 +606,8 @@ class TestMeterProcessing(test.BaseTestCase):
self.__setup_meter_def_file(cfg))
c = list(self.handler.process_notification(event))
self.assertEqual(0, len(c))
LOG.warning.assert_called_with('Could not find %s values', 'volume')
LOG.warning.assert_called_with('Only 0 fetched meters contain '
'"volume" field instead of 2.')
@mock.patch('ceilometer.meter.notifications.LOG')
def test_multi_meter_payload_invalid_short(self, LOG):
@ -620,8 +626,8 @@ class TestMeterProcessing(test.BaseTestCase):
self.__setup_meter_def_file(cfg))
c = list(self.handler.process_notification(event))
self.assertEqual(0, len(c))
LOG.warning.assert_called_with('Not all fetched meters contain "%s" '
'field', 'volume')
LOG.warning.assert_called_with('Only 1 fetched meters contain '
'"volume" field instead of 2.')
def test_arithmetic_expr_meter(self):
cfg = yaml.dump(