8892801205
- Add PEP8 section to tox.ini - Add hacking to requirements to enforce OpenStack style requirements - Fix large number of formatting issues flagged by flake8 check - Add copyright notices to all remaining files - Fix bug in trigger_manager related to logging calls - Add .gitignore file Change-Id: I755ab9c8bcc436836f9006fcd671408cc77214c4
285 lines
11 KiB
Python
285 lines
11 KiB
Python
# Copyright (c) 2014 Dark Secret Software Inc.
|
|
# Copyright (c) 2015 Rackspace
|
|
#
|
|
# 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.
|
|
|
|
import collections
|
|
import datetime
|
|
import fnmatch
|
|
import logging
|
|
import six
|
|
import timex
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class DefinitionError(Exception):
|
|
pass
|
|
|
|
|
|
def filter_event_timestamps(event):
|
|
return dict((trait, value) for trait, value in event.items()
|
|
if isinstance(value, datetime.datetime))
|
|
|
|
|
|
class Criterion(object):
|
|
@classmethod
|
|
def get_from_expression(cls, expression, trait_name):
|
|
if isinstance(expression, collections.Mapping):
|
|
if len(expression) != 1:
|
|
raise DefinitionError("Only exactly one type of match is "
|
|
"allowed per criterion expression")
|
|
ctype = expression.keys()[0]
|
|
expr = expression[ctype]
|
|
if ctype == 'int':
|
|
return NumericCriterion(expr, trait_name)
|
|
elif ctype == 'float':
|
|
return FloatCriterion(expr, trait_name)
|
|
elif ctype == 'datetime':
|
|
return TimeCriterion(expr, trait_name)
|
|
elif ctype == 'string' or ctype == 'text':
|
|
return Criterion(expr, trait_name)
|
|
else:
|
|
# A constant. -mdragon
|
|
return Criterion(expression, trait_name)
|
|
|
|
def __init__(self, expr, trait_name):
|
|
self.trait_name = trait_name
|
|
# match a constant
|
|
self.op = '='
|
|
self.value = expr
|
|
|
|
def match(self, event, debug_group):
|
|
if self.trait_name not in event:
|
|
return debug_group.mismatch("not %s" % self.trait_name)
|
|
value = event[self.trait_name]
|
|
if self.op == '=':
|
|
return debug_group.check(value == self.value, "== failed")
|
|
elif self.op == '>':
|
|
return debug_group.check(value > self.value, "> failed")
|
|
elif self.op == '<':
|
|
return debug_group.check(value < self.value, "< failed")
|
|
return debug_group.mismatch("Criterion match() fall-thru")
|
|
|
|
|
|
class NumericCriterion(Criterion):
|
|
|
|
def __init__(self, expr, trait_name):
|
|
self.trait_name = trait_name
|
|
if not isinstance(expr, six.string_types):
|
|
self.op = '='
|
|
self.value = expr
|
|
else:
|
|
expr = expr.strip().split(None, 1)
|
|
if len(expr) == 2:
|
|
self.op = expr[0]
|
|
value = expr[1].strip()
|
|
elif len(expr) == 1:
|
|
self.op = '='
|
|
value = expr[0]
|
|
else:
|
|
raise DefinitionError('Invalid numeric criterion.')
|
|
try:
|
|
self.value = self._convert(value)
|
|
except ValueError:
|
|
raise DefinitionError('Invalid numeric criterion.')
|
|
|
|
def _convert(self, value):
|
|
return int(value)
|
|
|
|
|
|
class FloatCriterion(NumericCriterion):
|
|
|
|
def _convert(self, value):
|
|
return float(value)
|
|
|
|
|
|
class TimeCriterion(Criterion):
|
|
|
|
def __init__(self, expression, trait_name):
|
|
self.trait_name = trait_name
|
|
self.time_expr = timex.parse(expression)
|
|
|
|
def match(self, event, debug_group):
|
|
if self.trait_name not in event:
|
|
return debug_group.mismatch("Time: not '%s'" % self.trait_name)
|
|
value = event[self.trait_name]
|
|
try:
|
|
timerange = self.time_expr(**filter_event_timestamps(event))
|
|
except timex.TimexExpressionError:
|
|
# the event doesn't contain a trait referenced in the expression.
|
|
return debug_group.mismatch("Time: no referenced trait")
|
|
return debug_group.check(value in timerange, "Not in timerange")
|
|
|
|
|
|
class Criteria(object):
|
|
def __init__(self, config):
|
|
self.included_types = []
|
|
self.excluded_types = []
|
|
if 'event_type' in config:
|
|
event_types = config['event_type']
|
|
if isinstance(event_types, six.string_types):
|
|
event_types = [event_types]
|
|
for t in event_types:
|
|
if t.startswith('!'):
|
|
self.excluded_types.append(t[1:])
|
|
else:
|
|
self.included_types.append(t)
|
|
else:
|
|
self.included_types.append('*')
|
|
if self.excluded_types and not self.included_types:
|
|
self.included_types.append('*')
|
|
if 'number' in config:
|
|
self.number = config['number']
|
|
else:
|
|
self.number = 1
|
|
if 'timestamp' in config:
|
|
self.timestamp = timex.parse(config['timestamp'])
|
|
else:
|
|
self.timestamp = None
|
|
self.map_distinguished_by = dict()
|
|
if 'map_distinguished_by' in config:
|
|
self.map_distinguished_by = config['map_distinguished_by']
|
|
self.traits = dict()
|
|
if 'traits' in config:
|
|
for trait, criterion in config['traits'].items():
|
|
self.traits[trait] = Criterion.get_from_expression(criterion,
|
|
trait)
|
|
|
|
def included_type(self, event_type):
|
|
return any(fnmatch.fnmatch(event_type, t) for t in self.included_types)
|
|
|
|
def excluded_type(self, event_type):
|
|
return any(fnmatch.fnmatch(event_type, t) for t in self.excluded_types)
|
|
|
|
def match_type(self, event_type):
|
|
return (self.included_type(event_type)
|
|
and not self.excluded_type(event_type))
|
|
|
|
def match(self, event, debug_group):
|
|
if not self.match_type(event['event_type']):
|
|
return debug_group.mismatch("Wrong event type")
|
|
if self.timestamp:
|
|
try:
|
|
t = self.timestamp(**filter_event_timestamps(event))
|
|
except timex.TimexExpressionError:
|
|
# the event doesn't contain a trait referenced
|
|
# in the expression.
|
|
return debug_group.mismatch("No timestamp trait")
|
|
if event['timestamp'] not in t:
|
|
return debug_group.mismatch("Not time yet.")
|
|
if not self.traits:
|
|
return debug_group.match()
|
|
return all(criterion.match(event, debug_group) for
|
|
criterion in self.traits.values())
|
|
|
|
|
|
class TriggerDefinition(object):
|
|
def __init__(self, config, debug_manager):
|
|
if 'name' not in config:
|
|
raise DefinitionError("Required field in trigger definition not "
|
|
"specified 'name'")
|
|
self.name = config['name']
|
|
self.debug_level = int(config.get('debug_level', 0))
|
|
self.distinguished_by = config.get('distinguished_by', [])
|
|
for dt in self.distinguished_by:
|
|
if isinstance(dt, collections.Mapping):
|
|
if len(dt) > 1:
|
|
raise DefinitionError(
|
|
"Invalid distinguising expression "
|
|
"%s. Only one trait allowed in an expression"
|
|
% str(dt))
|
|
self.fire_delay = config.get('fire_delay', 0)
|
|
if 'expiration' not in config:
|
|
raise DefinitionError("Required field in trigger definition not "
|
|
"specified 'expiration'")
|
|
self.expiration = timex.parse(config['expiration'])
|
|
self.fire_pipeline = config.get('fire_pipeline')
|
|
self.expire_pipeline = config.get('expire_pipeline')
|
|
if not self.fire_pipeline and not self.expire_pipeline:
|
|
raise DefinitionError("At least one of: 'fire_pipeline' or "
|
|
"'expire_pipeline' must be specified in a "
|
|
"trigger definition.")
|
|
if 'fire_criteria' not in config:
|
|
raise DefinitionError("Required criteria in trigger definition "
|
|
"not specified 'fire_criteria'")
|
|
self.fire_criteria = [Criteria(c) for c in config['fire_criteria']]
|
|
if 'match_criteria' not in config:
|
|
raise DefinitionError("Required criteria in trigger definition "
|
|
"not specified 'match_criteria'")
|
|
self.match_criteria = [Criteria(c) for c in config['match_criteria']]
|
|
self.load_criteria = []
|
|
if 'load_criteria' in config:
|
|
self.load_criteria = [Criteria(c) for c in config['load_criteria']]
|
|
if debug_manager:
|
|
self.set_debugger(debug_manager)
|
|
|
|
def set_debugger(self, debug_manager):
|
|
self.debugger = debug_manager.get_debugger(self)
|
|
|
|
def match(self, event):
|
|
# all distinguishing traits must exist to match.
|
|
group = self.debugger.get_group("Match")
|
|
for dt in self.distinguished_by:
|
|
if isinstance(dt, collections.Mapping):
|
|
trait_name = dt.keys()[0]
|
|
else:
|
|
trait_name = dt
|
|
if trait_name not in event:
|
|
group.mismatch("not '%s'" % trait_name)
|
|
return None
|
|
|
|
for criteria in self.match_criteria:
|
|
if criteria.match(event, group):
|
|
group.match()
|
|
return criteria
|
|
|
|
group.mismatch("No matching criteria")
|
|
return None
|
|
|
|
def get_distinguishing_traits(self, event, matching_criteria):
|
|
dist_traits = dict()
|
|
for dt in self.distinguished_by:
|
|
d_expr = None
|
|
if isinstance(dt, collections.Mapping):
|
|
trait_name = dt.keys()[0]
|
|
d_expr = timex.parse(dt[trait_name])
|
|
else:
|
|
trait_name = dt
|
|
event_trait_name = matching_criteria.map_distinguished_by.get(
|
|
trait_name, trait_name)
|
|
if d_expr is not None:
|
|
dist_traits[trait_name] = d_expr(
|
|
timestamp=event[event_trait_name])
|
|
else:
|
|
dist_traits[trait_name] = event[event_trait_name]
|
|
return dist_traits
|
|
|
|
def get_fire_timestamp(self, timestamp):
|
|
return timestamp + datetime.timedelta(seconds=self.fire_delay)
|
|
|
|
def should_fire(self, events):
|
|
group = self.debugger.get_group("Fire")
|
|
for criteria in self.fire_criteria:
|
|
matches = 0
|
|
for event in events:
|
|
if criteria.match(event, group):
|
|
matches += 1
|
|
if matches >= criteria.number:
|
|
break
|
|
if matches < criteria.number:
|
|
return group.mismatch("Not enough matching criteria")
|
|
return group.match()
|