1
# -*- encoding: utf-8 -*-
3
# Copyright © 2013 Rackspace Hosting.
5
# Author: Monsyne Dragon <mdragon@rackspace.com>
7
# Licensed under the Apache License, Version 2.0 (the "License"); you may
8
# not use this file except in compliance with the License. You may obtain
9
# a copy of the License at
11
# http://www.apache.org/licenses/LICENSE-2.0
13
# Unless required by applicable law or agreed to in writing, software
14
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16
# License for the specific language governing permissions and limitations
23
from oslo.config import cfg
27
from ceilometer.openstack.common import log
28
from ceilometer.openstack.common import timeutils
29
from ceilometer.storage import models
32
cfg.StrOpt('definitions_cfg_file',
33
default="event_definitions.yaml",
34
help="Configuration file for event definitions"
36
cfg.BoolOpt('drop_unmatched_notifications',
38
help='Drop notifications if no event definition matches. '
39
'(Otherwise, we convert them with just the default traits)'),
43
cfg.CONF.register_opts(OPTS, group='event')
45
LOG = log.getLogger(__name__)
48
class EventDefinitionException(Exception):
49
def __init__(self, message, definition_cfg):
50
super(EventDefinitionException, self).__init__(message)
51
self.definition_cfg = definition_cfg
54
return '%s %s: %s' % (self.__class__.__name__,
55
self.definition_cfg, self.message)
58
class TraitDefinition(object):
60
def __init__(self, name, trait_cfg, plugin_manager):
64
type_name = trait_cfg.get('type', 'text')
66
if 'plugin' in trait_cfg:
67
plugin_cfg = trait_cfg['plugin']
68
if isinstance(plugin_cfg, six.string_types):
69
plugin_name = plugin_cfg
73
plugin_name = plugin_cfg['name']
75
raise EventDefinitionException(
76
_('Plugin specified, but no plugin name supplied for '
77
'trait %s') % name, self.cfg)
78
plugin_params = plugin_cfg.get('parameters')
79
if plugin_params is None:
82
plugin_ext = plugin_manager[plugin_name]
84
raise EventDefinitionException(
85
_('No plugin named %(plugin)s available for '
86
'trait %(trait)s') % dict(plugin=plugin_name,
87
trait=name), self.cfg)
88
plugin_class = plugin_ext.plugin
89
self.plugin = plugin_class(**plugin_params)
93
if 'fields' not in trait_cfg:
94
raise EventDefinitionException(
95
_("Required field in trait definition not specified: "
99
fields = trait_cfg['fields']
100
if not isinstance(fields, six.string_types):
101
# NOTE(mdragon): if not a string, we assume a list.
105
fields = '|'.join('(%s)' % path for path in fields)
107
self.fields = jsonpath_rw.parse(fields)
108
except Exception as e:
109
raise EventDefinitionException(
110
_("Parse error in JSONPath specification "
111
"'%(jsonpath)s' for %(trait)s: %(err)s")
112
% dict(jsonpath=fields, trait=name, err=e), self.cfg)
113
self.trait_type = models.Trait.get_type_by_name(type_name)
114
if self.trait_type is None:
115
raise EventDefinitionException(
116
_("Invalid trait type '%(type)s' for trait %(trait)s")
117
% dict(type=type_name, trait=name), self.cfg)
119
def _get_path(self, match):
120
if match.context is not None:
121
for path_element in self._get_path(match.context):
123
yield str(match.path)
125
def to_trait(self, notification_body):
126
values = [match for match in self.fields.find(notification_body)
127
if match.value is not None]
129
if self.plugin is not None:
130
value_map = [('.'.join(self._get_path(match)), match.value) for
132
value = self.plugin.trait_value(value_map)
134
value = values[0].value if values else None
139
# NOTE(mdragon): some openstack projects (mostly Nova) emit ''
140
# for null fields for things like dates.
141
if self.trait_type != models.Trait.TEXT_TYPE and value == '':
144
value = models.Trait.convert_value(self.trait_type, value)
145
return models.Trait(self.name, self.trait_type, value)
148
class EventDefinition(object):
150
DEFAULT_TRAITS = dict(
151
service=dict(type='text', fields='publisher_id'),
152
request_id=dict(type='text', fields='_context_request_id'),
153
tenant_id=dict(type='text', fields=['payload.tenant_id',
157
def __init__(self, definition_cfg, trait_plugin_mgr):
158
self._included_types = []
159
self._excluded_types = []
161
self.cfg = definition_cfg
164
event_type = definition_cfg['event_type']
165
traits = definition_cfg['traits']
166
except KeyError as err:
167
raise EventDefinitionException(
168
_("Required field %s not specified") % err.args[0], self.cfg)
170
if isinstance(event_type, six.string_types):
171
event_type = [event_type]
174
if t.startswith('!'):
175
self._excluded_types.append(t[1:])
177
self._included_types.append(t)
179
if self._excluded_types and not self._included_types:
180
self._included_types.append('*')
182
for trait_name in self.DEFAULT_TRAITS:
183
self.traits[trait_name] = TraitDefinition(
185
self.DEFAULT_TRAITS[trait_name],
187
for trait_name in traits:
188
self.traits[trait_name] = TraitDefinition(
193
def included_type(self, event_type):
194
for t in self._included_types:
195
if fnmatch.fnmatch(event_type, t):
199
def excluded_type(self, event_type):
200
for t in self._excluded_types:
201
if fnmatch.fnmatch(event_type, t):
205
def match_type(self, event_type):
206
return (self.included_type(event_type)
207
and not self.excluded_type(event_type))
210
def is_catchall(self):
211
return '*' in self._included_types and not self._excluded_types
214
def _extract_when(body):
215
"""Extract the generated datetime from the notification.
217
# NOTE: I am keeping the logic the same as it was in the collector,
218
# However, *ALL* notifications should have a 'timestamp' field, it's
219
# part of the notification envelope spec. If this was put here because
220
# some openstack project is generating notifications without a
221
# timestamp, then that needs to be filed as a bug with the offending
223
when = body.get('timestamp', body.get('_context_timestamp'))
225
return timeutils.normalize_time(timeutils.parse_isotime(when))
227
return timeutils.utcnow()
229
def to_event(self, notification_body):
230
event_type = notification_body['event_type']
231
message_id = notification_body['message_id']
232
when = self._extract_when(notification_body)
234
traits = (self.traits[t].to_trait(notification_body)
235
for t in self.traits)
236
# Only accept non-None value traits ...
237
traits = [trait for trait in traits if trait is not None]
238
event = models.Event(message_id, event_type, when, traits)
242
class NotificationEventsConverter(object):
243
"""Notification Event Converter
245
The NotificationEventsConverter handles the conversion of Notifications
246
from openstack systems into Ceilometer Events.
248
The conversion is handled according to event definitions in a config file.
250
The config is a list of event definitions. Order is significant, a
251
notification will be processed according to the LAST definition that
252
matches it's event_type. (We use the last matching definition because that
253
allows you to use YAML merge syntax in the definitions file.)
254
Each definition is a dictionary with the following keys (all are required):
255
event_type: this is a list of notification event_types this definition
256
will handle. These can be wildcarded with unix shell glob
257
(not regex!) wildcards.
258
An exclusion listing (starting with a '!') will exclude any
259
types listed from matching. If ONLY exclusions are listed,
260
the definition will match anything not matching the
262
This item can also be a string, which will be taken as
263
equivalent to 1 item list.
266
* ['compute.instance.exists'] will only match
267
compute.intance.exists notifications
268
* "compute.instance.exists" Same as above.
269
* ["image.create", "image.delete"] will match
270
image.create and image.delete, but not anything else.
271
* 'compute.instance.*" will match
272
compute.instance.create.start but not image.upload
273
* ['*.start','*.end', '!scheduler.*'] will match
274
compute.instance.create.start, and image.delete.end,
275
but NOT compute.instance.exists or
276
scheduler.run_instance.start
277
* '!image.*' matches any notification except image
279
* ['*', '!image.*'] same as above.
280
traits: dictionary, The keys are trait names, the values are the trait
282
Each trait definition is a dictionary with the following keys:
283
type (optional): The data type for this trait. (as a string)
284
Valid options are: 'text', 'int', 'float' and 'datetime'
285
defaults to 'text' if not specified.
286
fields: a path specification for the field(s) in the
287
notification you wish to extract. The paths can be
288
specified with a dot syntax (e.g. 'payload.host').
289
dictionary syntax (e.g. 'payload[host]') is also supported.
290
in either case, if the key for the field you are looking
291
for contains special charecters, like '.', it will need to
292
be quoted (with double or single quotes) like so:
294
"payload.image_meta.'org.openstack__1__architecture'"
296
The syntax used for the field specification is a variant
297
of JSONPath, and is fairly flexible.
298
(see: https://github.com/kennknowles/python-jsonpath-rw
299
for more info) Specifications can be written to match
300
multiple possible fields, the value for the trait will
301
be derived from the matching fields that exist and have
302
a non-null (i.e. is not None) values in the notification.
303
By default the value will be the first such field.
304
(plugins can alter that, if they wish)
306
This configuration value is normally a string, for
307
convenience, it can be specified as a list of
308
specifications, which will be OR'ed together (a union
309
query in jsonpath terms)
310
plugin (optional): (dictionary) with the following keys:
311
name: (string) name of a plugin to load
312
parameters: (optional) Dictionary of keyword args to pass
313
to the plugin on initialization.
314
See documentation on each plugin to see what
315
arguments it accepts.
316
For convenience, this value can also be specified as a
317
string, which is interpreted as a plugin name, which will
318
be loaded with no parameters.
322
def __init__(self, events_config, trait_plugin_mgr, add_catchall=True):
324
EventDefinition(event_def, trait_plugin_mgr)
325
for event_def in reversed(events_config)]
326
if add_catchall and not any(d.is_catchall for d in self.definitions):
327
event_def = dict(event_type='*', traits={})
328
self.definitions.append(EventDefinition(event_def,
331
def to_event(self, notification_body):
332
event_type = notification_body['event_type']
333
message_id = notification_body['message_id']
335
for d in self.definitions:
336
if d.match_type(event_type):
341
msg = (_('Dropping Notification %(type)s (uuid:%(msgid)s)')
342
% dict(type=event_type, msgid=message_id))
343
if cfg.CONF.event.drop_unmatched_notifications:
346
# If drop_unmatched_notifications is False, this should
347
# never happen. (mdragon)
351
return edef.to_event(notification_body)
354
def get_config_file():
355
config_file = cfg.CONF.event.definitions_cfg_file
356
if not os.path.exists(config_file):
357
config_file = cfg.CONF.find_file(config_file)
361
def setup_events(trait_plugin_mgr):
362
"""Setup the event definitions from yaml config file."""
363
config_file = get_config_file()
364
if config_file is not None:
365
LOG.debug(_("Event Definitions configuration file: %s"), config_file)
367
with open(config_file) as cf:
371
events_config = yaml.safe_load(config)
372
except yaml.YAMLError as err:
373
if hasattr(err, 'problem_mark'):
374
mark = err.problem_mark
375
errmsg = (_("Invalid YAML syntax in Event Definitions file "
376
"%(file)s at line: %(line)s, column: %(column)s.")
377
% dict(file=config_file,
379
column=mark.column + 1))
381
errmsg = (_("YAML error reading Event Definitions file "
383
% dict(file=config_file))
388
LOG.debug(_("No Event Definitions configuration file found!"
389
" Using default config."))
392
LOG.info(_("Event Definitions: %s"), events_config)
394
allow_drop = cfg.CONF.event.drop_unmatched_notifications
395
return NotificationEventsConverter(events_config,
397
add_catchall=not allow_drop)