~ubuntu-branches/ubuntu/trusty/ceilometer/trusty-proposed

« back to all changes in this revision

Viewing changes to ceilometer/event/converter.py

  • Committer: Package Import Robot
  • Author(s): Chuck Short, James Page, Chuck Short
  • Date: 2014-01-23 15:08:11 UTC
  • mfrom: (1.1.11)
  • Revision ID: package-import@ubuntu.com-20140123150811-1zaismsuyh1hcl8y
Tags: 2014.1~b2-0ubuntu1
[ James Page ]
* d/control: Add python-jsonpath-rw to BD's.
* d/p/fix-setup-requirements.patch: Bump WebOb to support < 1.4.
 (LP: #1261101)

[ Chuck Short ]
* New upstream version.
* debian/control, debian/ceilometer-common.install: Split out
  ceilometer-alarm-evaluator and ceilometer-alarm-notifier into their
  own packages. (LP: #1250002)
* debian/ceilometer-agent-central.logrotate,
  debian/ceilometer-agent-compute.logrotate,
  debian/ceilometer-api.logrotate,
  debian/ceilometer-collector.logrotate: Add logrotate files, 
  thanks to Ahmed Rahal. (LP: #1224223)
* Fix typos in upstart files.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- encoding: utf-8 -*-
 
2
#
 
3
# Copyright © 2013 Rackspace Hosting.
 
4
#
 
5
# Author: Monsyne Dragon <mdragon@rackspace.com>
 
6
#
 
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
 
10
#
 
11
#      http://www.apache.org/licenses/LICENSE-2.0
 
12
#
 
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
 
17
# under the License.
 
18
 
 
19
import fnmatch
 
20
import os
 
21
 
 
22
import jsonpath_rw
 
23
from oslo.config import cfg
 
24
import six
 
25
import yaml
 
26
 
 
27
from ceilometer.openstack.common import log
 
28
from ceilometer.openstack.common import timeutils
 
29
from ceilometer.storage import models
 
30
 
 
31
OPTS = [
 
32
    cfg.StrOpt('definitions_cfg_file',
 
33
               default="event_definitions.yaml",
 
34
               help="Configuration file for event definitions"
 
35
               ),
 
36
    cfg.BoolOpt('drop_unmatched_notifications',
 
37
                default=False,
 
38
                help='Drop notifications if no event definition matches. '
 
39
                '(Otherwise, we convert them with just the default traits)'),
 
40
 
 
41
]
 
42
 
 
43
cfg.CONF.register_opts(OPTS, group='event')
 
44
 
 
45
LOG = log.getLogger(__name__)
 
46
 
 
47
 
 
48
class EventDefinitionException(Exception):
 
49
    def __init__(self, message, definition_cfg):
 
50
        super(EventDefinitionException, self).__init__(message)
 
51
        self.definition_cfg = definition_cfg
 
52
 
 
53
    def __str__(self):
 
54
        return '%s %s: %s' % (self.__class__.__name__,
 
55
                              self.definition_cfg, self.message)
 
56
 
 
57
 
 
58
class TraitDefinition(object):
 
59
 
 
60
    def __init__(self, name, trait_cfg, plugin_manager):
 
61
        self.cfg = trait_cfg
 
62
        self.name = name
 
63
 
 
64
        type_name = trait_cfg.get('type', 'text')
 
65
 
 
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
 
70
                plugin_params = {}
 
71
            else:
 
72
                try:
 
73
                    plugin_name = plugin_cfg['name']
 
74
                except KeyError:
 
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:
 
80
                    plugin_params = {}
 
81
            try:
 
82
                plugin_ext = plugin_manager[plugin_name]
 
83
            except KeyError:
 
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)
 
90
        else:
 
91
            self.plugin = None
 
92
 
 
93
        if 'fields' not in trait_cfg:
 
94
            raise EventDefinitionException(
 
95
                _("Required field in trait definition not specified: "
 
96
                  "'%s'") % 'fields',
 
97
                self.cfg)
 
98
 
 
99
        fields = trait_cfg['fields']
 
100
        if not isinstance(fields, six.string_types):
 
101
            # NOTE(mdragon): if not a string, we assume a list.
 
102
            if len(fields) == 1:
 
103
                fields = fields[0]
 
104
            else:
 
105
                fields = '|'.join('(%s)' % path for path in fields)
 
106
        try:
 
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)
 
118
 
 
119
    def _get_path(self, match):
 
120
        if match.context is not None:
 
121
            for path_element in self._get_path(match.context):
 
122
                yield path_element
 
123
            yield str(match.path)
 
124
 
 
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]
 
128
 
 
129
        if self.plugin is not None:
 
130
            value_map = [('.'.join(self._get_path(match)), match.value) for
 
131
                         match in values]
 
132
            value = self.plugin.trait_value(value_map)
 
133
        else:
 
134
            value = values[0].value if values else None
 
135
 
 
136
        if value is None:
 
137
            return None
 
138
 
 
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 == '':
 
142
            return None
 
143
 
 
144
        value = models.Trait.convert_value(self.trait_type, value)
 
145
        return models.Trait(self.name, self.trait_type, value)
 
146
 
 
147
 
 
148
class EventDefinition(object):
 
149
 
 
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',
 
154
                                            '_context_tenant']),
 
155
    )
 
156
 
 
157
    def __init__(self, definition_cfg, trait_plugin_mgr):
 
158
        self._included_types = []
 
159
        self._excluded_types = []
 
160
        self.traits = dict()
 
161
        self.cfg = definition_cfg
 
162
 
 
163
        try:
 
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)
 
169
 
 
170
        if isinstance(event_type, six.string_types):
 
171
            event_type = [event_type]
 
172
 
 
173
        for t in event_type:
 
174
            if t.startswith('!'):
 
175
                self._excluded_types.append(t[1:])
 
176
            else:
 
177
                self._included_types.append(t)
 
178
 
 
179
        if self._excluded_types and not self._included_types:
 
180
            self._included_types.append('*')
 
181
 
 
182
        for trait_name in self.DEFAULT_TRAITS:
 
183
            self.traits[trait_name] = TraitDefinition(
 
184
                trait_name,
 
185
                self.DEFAULT_TRAITS[trait_name],
 
186
                trait_plugin_mgr)
 
187
        for trait_name in traits:
 
188
            self.traits[trait_name] = TraitDefinition(
 
189
                trait_name,
 
190
                traits[trait_name],
 
191
                trait_plugin_mgr)
 
192
 
 
193
    def included_type(self, event_type):
 
194
        for t in self._included_types:
 
195
            if fnmatch.fnmatch(event_type, t):
 
196
                return True
 
197
        return False
 
198
 
 
199
    def excluded_type(self, event_type):
 
200
        for t in self._excluded_types:
 
201
            if fnmatch.fnmatch(event_type, t):
 
202
                return True
 
203
        return False
 
204
 
 
205
    def match_type(self, event_type):
 
206
        return (self.included_type(event_type)
 
207
                and not self.excluded_type(event_type))
 
208
 
 
209
    @property
 
210
    def is_catchall(self):
 
211
        return '*' in self._included_types and not self._excluded_types
 
212
 
 
213
    @staticmethod
 
214
    def _extract_when(body):
 
215
        """Extract the generated datetime from the notification.
 
216
        """
 
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
 
222
        # project (mdragon)
 
223
        when = body.get('timestamp', body.get('_context_timestamp'))
 
224
        if when:
 
225
            return timeutils.normalize_time(timeutils.parse_isotime(when))
 
226
 
 
227
        return timeutils.utcnow()
 
228
 
 
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)
 
233
 
 
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)
 
239
        return event
 
240
 
 
241
 
 
242
class NotificationEventsConverter(object):
 
243
    """Notification Event Converter
 
244
 
 
245
    The NotificationEventsConverter handles the conversion of Notifications
 
246
    from openstack systems into Ceilometer Events.
 
247
 
 
248
    The conversion is handled according to event definitions in a config file.
 
249
 
 
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
 
261
                    exclusions.
 
262
                    This item can also be a string, which will be taken as
 
263
                    equivalent to 1 item list.
 
264
 
 
265
                    Examples:
 
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
 
278
                        notifications.
 
279
                    *   ['*', '!image.*']  same as above.
 
280
        traits:  dictionary, The keys are trait names, the values are the trait
 
281
                 definitions
 
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:
 
293
 
 
294
                          "payload.image_meta.'org.openstack__1__architecture'"
 
295
 
 
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)
 
305
 
 
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.
 
319
 
 
320
    """
 
321
 
 
322
    def __init__(self, events_config, trait_plugin_mgr, add_catchall=True):
 
323
        self.definitions = [
 
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,
 
329
                                                    trait_plugin_mgr))
 
330
 
 
331
    def to_event(self, notification_body):
 
332
        event_type = notification_body['event_type']
 
333
        message_id = notification_body['message_id']
 
334
        edef = None
 
335
        for d in self.definitions:
 
336
            if d.match_type(event_type):
 
337
                edef = d
 
338
                break
 
339
 
 
340
        if edef is None:
 
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:
 
344
                LOG.debug(msg)
 
345
            else:
 
346
                # If drop_unmatched_notifications is False, this should
 
347
                # never happen. (mdragon)
 
348
                LOG.error(msg)
 
349
            return None
 
350
 
 
351
        return edef.to_event(notification_body)
 
352
 
 
353
 
 
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)
 
358
    return config_file
 
359
 
 
360
 
 
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)
 
366
 
 
367
        with open(config_file) as cf:
 
368
            config = cf.read()
 
369
 
 
370
        try:
 
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,
 
378
                                 line=mark.line + 1,
 
379
                                 column=mark.column + 1))
 
380
            else:
 
381
                errmsg = (_("YAML error reading Event Definitions file "
 
382
                            "%(file)s")
 
383
                          % dict(file=config_file))
 
384
            LOG.error(errmsg)
 
385
            raise
 
386
 
 
387
    else:
 
388
        LOG.debug(_("No Event Definitions configuration file found!"
 
389
                  " Using default config."))
 
390
        events_config = []
 
391
 
 
392
    LOG.info(_("Event Definitions: %s"), events_config)
 
393
 
 
394
    allow_drop = cfg.CONF.event.drop_unmatched_notifications
 
395
    return NotificationEventsConverter(events_config,
 
396
                                       trait_plugin_mgr,
 
397
                                       add_catchall=not allow_drop)