Inital commit.
This commit is contained in:
commit
fe164f59d0
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
*.py[cod]
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Packages
|
||||
*.egg
|
||||
*.egg-info
|
||||
dist
|
||||
build
|
||||
eggs
|
||||
parts
|
||||
var
|
||||
sdist
|
||||
develop-eggs
|
||||
.installed.cfg
|
||||
lib
|
||||
lib64
|
||||
__pycache__
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
.coverage
|
||||
.tox
|
||||
nosetests.xml
|
||||
|
||||
# Translations
|
||||
*.mo
|
202
LICENSE
Normal file
202
LICENSE
Normal file
@ -0,0 +1,202 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
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.
|
||||
|
2
MANIFEST.in
Normal file
2
MANIFEST.in
Normal file
@ -0,0 +1,2 @@
|
||||
include README.md
|
||||
include requirements.txt
|
84
README.md
Normal file
84
README.md
Normal file
@ -0,0 +1,84 @@
|
||||
stackdistiller
|
||||
==============
|
||||
|
||||
A data extraction and transformation library for OpenStack notifications.
|
||||
|
||||
Stackdistiller is designed to extract data from openstack notifications
|
||||
and convert it into a form relevant to the application consuming the
|
||||
notification. It consists of two components, the Distiller, which extracts
|
||||
data from notifications according to a YAML config file, and the Condenser,
|
||||
which receives the data extracted by the Distiller, and formats it into
|
||||
an application-specific object, referred to as an Event. This could be a
|
||||
simple python dictionary, an XML document tree, or a set of ORM model
|
||||
objects.
|
||||
|
||||
## Distiller
|
||||
|
||||
The Distiller reads a YAML config file to determine what data to extract
|
||||
from each notification, according to it's event type. event types can be
|
||||
wildcarded using shell glob syntax. The distiller will extract two types of
|
||||
data from each notification:
|
||||
|
||||
* Metadata from the notifications envelope, including the event type,
|
||||
message id (uuid of the notification) and the timestamp showing when
|
||||
notification was generated by the source system.
|
||||
* A series of data items extracted from the notification's body. These
|
||||
are called Traits. Traits are basically just typed name-value pairs.
|
||||
|
||||
The distiller can also do some basic data massaging on traits extracted
|
||||
from the notification, such as splitting a value from a string. This is
|
||||
handled by trait plugins. These are just classes that implement the
|
||||
TraitPluginBase interface. They are referred to by name in the config, and
|
||||
are looked up in a trait\_plugin\_map passed to the distiller on init.
|
||||
The plugin map is just a dictionary, or dictionary-like object (such as a
|
||||
plugin manager) that maps names to plugin classes. If no map is passed to
|
||||
the distiller, it will use a default that just contains the builtin plugins
|
||||
bundled with stackdistiller.
|
||||
|
||||
If a notification does not match any event definition in the distiller's
|
||||
config file, the distiller's to\_event method will return None, indicating
|
||||
it cannot extract that notification. This may be what you want (i.e. your
|
||||
application may only be interested in certain notifications.), but if you
|
||||
want to record basic informaton from *any* event type, you can pass
|
||||
"catchall=True" to the distiller, and it will generate a minimal event from
|
||||
any notification.
|
||||
|
||||
## Condenser
|
||||
|
||||
The Condenser receives the data extracted from the notification by the
|
||||
Distiller and formats it into an appropriate type of Event object.
|
||||
An instance of a Condenser class is passed, along with the raw,
|
||||
deserialized notification, to the distiller object's to\_event method.
|
||||
To create your own type of Event from the data extracted by the distiller,
|
||||
you just need to create a Condenser class to receive the data.
|
||||
Condenser classes don't have to subclass any particular class, as long as
|
||||
they implement the methods defined in stackdistiller.condenser.CondenserBase.
|
||||
If you do not pass a condenser to the distiller when you call to\_event,
|
||||
it will create an instance of the default DictionaryCondenser for you.
|
||||
This just formats the event as a plain python dictionary.
|
||||
|
||||
## Example:
|
||||
|
||||
import json
|
||||
from stackdistiller import distiller
|
||||
from stackdistiller import condenser
|
||||
|
||||
config_file_name = "events_i_want.yaml"
|
||||
notification_string = open('a_notification_here.json', 'r').read()
|
||||
|
||||
notification = json.loads(notification_string)
|
||||
config = distiller.load_config(config_file_name)
|
||||
|
||||
d = distiller.Distiller(config, catchall=False)
|
||||
|
||||
#this is the default condenser.
|
||||
cond = condenser.DictionaryCondenser()
|
||||
|
||||
if dist.to_event(notification, cond):
|
||||
# What get_event() returns is up to the condenser class. In this
|
||||
# case, it's a dictionary.
|
||||
event = cond.get_event()
|
||||
print "Yay! An Event: %s" % str(event)
|
||||
else:
|
||||
print "Not something we are interested in. Ignoring."
|
||||
|
128
bin/test-distiller.py
Executable file
128
bin/test-distiller.py
Executable file
@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Copyright © 2014 Rackspace Hosting.
|
||||
#
|
||||
# Author: Monsyne Dragon <mdragon@rackspace.com>
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Command line tool help you debug your distiller event definitions.
|
||||
|
||||
Feed it a list of test notifications in json format, and it will show
|
||||
you what events will be generated.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
|
||||
from stackdistiller import distiller
|
||||
from stackdistiller import condenser
|
||||
|
||||
|
||||
class TestCondenser(condenser.CondenserBase):
|
||||
def __init__(self):
|
||||
self.clear()
|
||||
|
||||
def add_trait(self, name, trait_type, value):
|
||||
self.traits.append(distiller.Trait(name, trait_type, value))
|
||||
|
||||
def add_envelope_info(self, event_type, message_id, when):
|
||||
self.event_type = event_type
|
||||
self.message_id = message_id
|
||||
self.when = when
|
||||
|
||||
def get_event(self):
|
||||
return self
|
||||
|
||||
def clear(self):
|
||||
self.event_type = None
|
||||
self.message_id = None
|
||||
self.when = None
|
||||
self.traits = []
|
||||
|
||||
def __str__(self):
|
||||
text = ["Event: %s (id: %s) at %s" % (self.event_type,
|
||||
self.message_id,
|
||||
self.when)]
|
||||
for trait in sorted(self.traits):
|
||||
text.append(" Trait: name: %s, type: %s, value: %s" % trait)
|
||||
text.append('')
|
||||
return "\n".join(text)
|
||||
|
||||
|
||||
def test_data(args):
|
||||
if not args.test_data:
|
||||
n = json.load(sys.stdin)
|
||||
if args.list:
|
||||
for notif in n:
|
||||
yield notif
|
||||
else:
|
||||
yield n
|
||||
else:
|
||||
for f in args.test_data:
|
||||
with open(f, 'r') as data:
|
||||
n = json.load(data)
|
||||
if args.list:
|
||||
for notif in n:
|
||||
yield notif
|
||||
else:
|
||||
yield n
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(description="Test Distiller configuration")
|
||||
parser.add_argument('-c', '--config',
|
||||
default='event_definitions.yaml',
|
||||
help='Name of event definitions file '
|
||||
'to test (Default: %(default)s)')
|
||||
parser.add_argument('-l', '--list', action='store_true',
|
||||
help='Test data files contain JSON list of notifications.'
|
||||
' (By default data files should contain a single '
|
||||
'notification.)')
|
||||
parser.add_argument('-d', '--add_default_definition', action='store_true',
|
||||
help='Add default event definition. Normally, '
|
||||
'notifications are dropped if there is no event '
|
||||
'definition for their event_type. Setting this adds a '
|
||||
'"catchall" that converts unknown notifications to Events'
|
||||
' with a few basic traits.')
|
||||
parser.add_argument('-o', '--output', type=argparse.FileType('w'),
|
||||
default=sys.stdout, help="Output file. Default stdout")
|
||||
parser.add_argument('test_data', nargs='*', metavar='JSON_FILE',
|
||||
help="Test notifications in JSON format. Defaults to stdin")
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
config = distiller.load_config(args.config)
|
||||
|
||||
out = args.output
|
||||
out.write("Definitions file: %s\n" % args.config)
|
||||
notifications = test_data(args)
|
||||
|
||||
dist = distiller.Distiller(config, catchall=args.add_default_definition)
|
||||
nct = 0
|
||||
drops = 0
|
||||
cond = TestCondenser()
|
||||
for notification in notifications:
|
||||
cond.clear()
|
||||
nct +=1
|
||||
if dist.to_event(notification, cond) is None:
|
||||
out.write("Dropped notification: %s\n" %
|
||||
notification['message_id'])
|
||||
drops += 1
|
||||
else:
|
||||
event = cond.get_event()
|
||||
out.write(str(event))
|
||||
out.write("--\n")
|
||||
|
||||
out.write("Notifications tested: %s (%s dropped)\n" % (nct, drops))
|
4
doc/event_definitions_config.md
Normal file
4
doc/event_definitions_config.md
Normal file
@ -0,0 +1,4 @@
|
||||
Event Definitions YAML Config Format
|
||||
====================================
|
||||
|
||||
Documentation to go here soon.
|
63
etc/event_definitions.yaml
Normal file
63
etc/event_definitions.yaml
Normal file
@ -0,0 +1,63 @@
|
||||
---
|
||||
- event_type: compute.instance.*
|
||||
traits: &instance_traits
|
||||
tenant_id:
|
||||
fields: payload.tenant_id
|
||||
user_id:
|
||||
fields: payload.user_id
|
||||
instance_id:
|
||||
fields: payload.instance_id
|
||||
host:
|
||||
fields: publisher_id
|
||||
plugin:
|
||||
name: split
|
||||
parameters:
|
||||
segment: 1
|
||||
max_split: 1
|
||||
service:
|
||||
fields: publisher_id
|
||||
plugin: split
|
||||
memory_mb:
|
||||
type: int
|
||||
fields: payload.memory_mb
|
||||
disk_gb:
|
||||
type: int
|
||||
fields: payload.disk_gb
|
||||
root_gb:
|
||||
type: int
|
||||
fields: payload.root_gb
|
||||
ephemeral_gb:
|
||||
type: int
|
||||
fields: payload.ephemeral_gb
|
||||
vcpus:
|
||||
type: int
|
||||
fields: payload.vcpus
|
||||
instance_type_id:
|
||||
type: int
|
||||
fields: payload.instance_type_id
|
||||
instance_type:
|
||||
fields: payload.instance_type
|
||||
state:
|
||||
fields: payload.state
|
||||
os_architecture:
|
||||
fields: payload.image_meta.'org.openstack__1__architecture'
|
||||
os_version:
|
||||
fields: payload.image_meta.'org.openstack__1__os_version'
|
||||
os_distro:
|
||||
fields: payload.image_meta.'org.openstack__1__os_distro'
|
||||
launched_at:
|
||||
type: datetime
|
||||
fields: payload.launched_at
|
||||
deleted_at:
|
||||
type: datetime
|
||||
fields: payload.deleted_at
|
||||
- event_type: compute.instance.exists
|
||||
traits:
|
||||
<<: *instance_traits
|
||||
audit_period_beginning:
|
||||
type: datetime
|
||||
fields: payload.audit_period_beginning
|
||||
audit_period_ending:
|
||||
type: datetime
|
||||
fields: payload.audit_period_ending
|
||||
|
6
requirements.txt
Normal file
6
requirements.txt
Normal file
@ -0,0 +1,6 @@
|
||||
enum34>=1.0
|
||||
iso8601>=0.1.10
|
||||
jsonpath-rw>=1.2.0,<2.0
|
||||
PyYAML>=3.1.0
|
||||
six>=1.5.2
|
||||
stevedore>=0.14
|
37
setup.py
Normal file
37
setup.py
Normal file
@ -0,0 +1,37 @@
|
||||
import os
|
||||
from pip.req import parse_requirements
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
|
||||
def read(fname):
|
||||
return open(os.path.join(os.path.dirname(__file__), fname)).read()
|
||||
|
||||
|
||||
req_file = os.path.join(os.path.dirname(__file__), "requirements.txt")
|
||||
install_reqs = [str(r.req) for r in parse_requirements(req_file)]
|
||||
|
||||
|
||||
setup(
|
||||
name='stackdistiller',
|
||||
version='0.10',
|
||||
author='Monsyne Dragon',
|
||||
author_email='mdragon@rackspace.com',
|
||||
description=("A data extraction and transformation library for "
|
||||
"OpenStack notifications"),
|
||||
license='Apache License (2.0)',
|
||||
keywords='OpenStack notifications events extraction transformation',
|
||||
packages=find_packages(exclude=['tests']),
|
||||
classifiers=[
|
||||
'Development Status :: 3 - Alpha',
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Operating System :: POSIX :: Linux',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
],
|
||||
url='https://github.com/StackTach/stackdistiller',
|
||||
scripts=['bin/test-distiller.py'],
|
||||
long_description=read('README.md'),
|
||||
install_requires=install_reqs,
|
||||
|
||||
zip_safe=False
|
||||
)
|
0
stackdistiller/__init__.py
Normal file
0
stackdistiller/__init__.py
Normal file
102
stackdistiller/condenser.py
Normal file
102
stackdistiller/condenser.py
Normal file
@ -0,0 +1,102 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Copyright © 2014 Rackspace Hosting.
|
||||
#
|
||||
# Author: Monsyne Dragon <mdragon@rackspace.com>
|
||||
#
|
||||
# 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 abc
|
||||
import six
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class CondenserBase(object):
|
||||
"""Base class for Condenser objects that collect data extracted from a
|
||||
Notification by the Distiller, and format it into a usefull datastructure.
|
||||
|
||||
A simple Condenser may just colect all the traits received into a dictionary.
|
||||
More complex ones may build collections of application or ORM model objects,
|
||||
or XML document trees.
|
||||
|
||||
Condensers also have hooks for verification logic, to check that all needed
|
||||
traits are present."""
|
||||
|
||||
def __init__(self, **kw):
|
||||
"""Setup the condenser. A new instance of the condenser is passed to the
|
||||
distiller for each notification extracted.
|
||||
|
||||
:param kw: keyword parameters for condenser.
|
||||
|
||||
"""
|
||||
super(CondenserBase, self).__init__()
|
||||
|
||||
@abc.abstractmethod
|
||||
def add_trait(self, name, trait_type, value):
|
||||
"""Add a trait to the Event datastructure being built by this
|
||||
condenser. The distiller will call this for each extracted trait.
|
||||
|
||||
:param name: (string) name of the trait
|
||||
:param trait_type: (distiller.Datatype) data type of the trait.
|
||||
:param value: Value of the trait (of datatype indicated by trait_type)
|
||||
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def add_envelope_info(self, event_type, message_id, when):
|
||||
"""Add the metadata for this event, extracted from the notification's
|
||||
envelope. The distiller will call this once.
|
||||
|
||||
:param event_type: (string) Type of event, as a dotted string such as
|
||||
"compute.instance.update".
|
||||
:param message_id: (string) UUID of notification.
|
||||
:param when: (datetime) Timestamp of notification from source system.
|
||||
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_event(self):
|
||||
"""Return the Event datastructure constructed by this condenser."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def clear(self):
|
||||
"""Clear condenser state."""
|
||||
|
||||
def validate(self):
|
||||
"""Check Event against whatever validation logic this condenser may have
|
||||
|
||||
:returns: (bool) True if valid.
|
||||
|
||||
"""
|
||||
return True
|
||||
|
||||
|
||||
class DictionaryCondenser(CondenserBase):
|
||||
"""Return event data as a simple python dictionary"""
|
||||
def __init__(self, **kw):
|
||||
self.clear()
|
||||
super(DictionaryCondenser, self).__init__(**kw)
|
||||
|
||||
def get_event(self):
|
||||
return self.event
|
||||
|
||||
def clear(self):
|
||||
self.event = dict()
|
||||
|
||||
def add_envelope_info(self, event_type, message_id, when):
|
||||
self.event['event_type'] = event_type
|
||||
self.event['message_id'] = message_id
|
||||
self.event['when'] = when
|
||||
|
||||
def add_trait(self, name, trait_type, value):
|
||||
self.event[name] = value
|
343
stackdistiller/distiller.py
Normal file
343
stackdistiller/distiller.py
Normal file
@ -0,0 +1,343 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Copyright © 2013 Rackspace Hosting.
|
||||
#
|
||||
# Author: Monsyne Dragon <mdragon@rackspace.com>
|
||||
#
|
||||
# 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 os
|
||||
|
||||
from enum import Enum
|
||||
import iso8601
|
||||
import jsonpath_rw
|
||||
import six
|
||||
import yaml
|
||||
|
||||
from stackdistiller.condenser import DictionaryCondenser
|
||||
from stackdistiller.trait_plugins import DEFAULT_PLUGINMAP
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def utcnow():
|
||||
# defined here so the call can be mocked out in unittests.
|
||||
dt = datetime.datetime.utcnow()
|
||||
return dt.replace(tzinfo=iso8601.iso8601.UTC)
|
||||
|
||||
|
||||
def convert_datetime(value):
|
||||
value = iso8601.parse_date(value)
|
||||
tz = iso8601.iso8601.UTC
|
||||
return value.astimezone(tz)
|
||||
|
||||
|
||||
def load_config(filename):
|
||||
"""Load the event definitions from yaml config file."""
|
||||
logger.debug("Event Definitions configuration file: %s", filename)
|
||||
|
||||
with open(filename, 'r') as cf:
|
||||
config = cf.read()
|
||||
|
||||
try:
|
||||
events_config = yaml.safe_load(config)
|
||||
except yaml.YAMLError as err:
|
||||
if hasattr(err, 'problem_mark'):
|
||||
mark = err.problem_mark
|
||||
errmsg = ("Invalid YAML syntax in Event Definitions file "
|
||||
"%(file)s at line: %(line)s, column: %(column)s."
|
||||
% dict(file=filename,
|
||||
line=mark.line + 1,
|
||||
column=mark.column + 1))
|
||||
else:
|
||||
errmsg = ("YAML error reading Event Definitions file "
|
||||
"%(file)s"
|
||||
% dict(file=filename))
|
||||
logger.error(errmsg)
|
||||
raise
|
||||
|
||||
logger.info("Event Definitions: %s", events_config)
|
||||
return events_config
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class Datatype(Enum):
|
||||
text = (1, str)
|
||||
int = (2, int)
|
||||
float = (3, float)
|
||||
datetime = (4, convert_datetime)
|
||||
|
||||
def convert(self, value):
|
||||
f = self.value[1]
|
||||
return f(value)
|
||||
|
||||
|
||||
Trait = collections.namedtuple('Trait', ('name', 'trait_type', 'value'))
|
||||
|
||||
|
||||
class TraitDefinition(object):
|
||||
|
||||
def __init__(self, name, trait_cfg, plugin_map):
|
||||
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_class = plugin_map[plugin_name]
|
||||
except KeyError:
|
||||
raise EventDefinitionException(
|
||||
'No plugin named %(plugin)s available for '
|
||||
'trait %(trait)s' % dict(plugin=plugin_name,
|
||||
trait=name), self.cfg)
|
||||
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 = jsonpath_rw.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)
|
||||
try:
|
||||
self.trait_type = Datatype[type_name]
|
||||
except KeyError:
|
||||
raise EventDefinitionException(
|
||||
"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
|
||||
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
# NOTE(mdragon): some openstack projects (mostly Nova) emit ''
|
||||
# for null fields for things like dates.
|
||||
if self.trait_type != Datatype.text and value == '':
|
||||
return None
|
||||
|
||||
value = self.trait_type.convert(value)
|
||||
return Trait(self.name, self.trait_type, value)
|
||||
|
||||
|
||||
class EventDefinition(object):
|
||||
|
||||
DEFAULT_TRAITS = dict(
|
||||
service=dict(type='text', fields='publisher_id'),
|
||||
request_id=dict(type='text', fields='_context_request_id'),
|
||||
tenant_id=dict(type='text', fields=['payload.tenant_id',
|
||||
'_context_tenant']),
|
||||
)
|
||||
|
||||
def __init__(self, definition_cfg, trait_plugin_map):
|
||||
self._included_types = []
|
||||
self._excluded_types = []
|
||||
self.traits = dict()
|
||||
self.cfg = definition_cfg
|
||||
|
||||
try:
|
||||
event_type = definition_cfg['event_type']
|
||||
traits = definition_cfg['traits']
|
||||
except KeyError as err:
|
||||
raise EventDefinitionException(
|
||||
"Required field %s not specified" % err.args[0], self.cfg)
|
||||
|
||||
if isinstance(event_type, six.string_types):
|
||||
event_type = [event_type]
|
||||
|
||||
for t in event_type:
|
||||
if t.startswith('!'):
|
||||
self._excluded_types.append(t[1:])
|
||||
else:
|
||||
self._included_types.append(t)
|
||||
|
||||
if self._excluded_types and not self._included_types:
|
||||
self._included_types.append('*')
|
||||
|
||||
for trait_name in self.DEFAULT_TRAITS:
|
||||
self.traits[trait_name] = TraitDefinition(
|
||||
trait_name,
|
||||
self.DEFAULT_TRAITS[trait_name],
|
||||
trait_plugin_map)
|
||||
for trait_name in traits:
|
||||
self.traits[trait_name] = TraitDefinition(
|
||||
trait_name,
|
||||
traits[trait_name],
|
||||
trait_plugin_map)
|
||||
|
||||
def included_type(self, event_type):
|
||||
for t in self._included_types:
|
||||
if fnmatch.fnmatch(event_type, t):
|
||||
return True
|
||||
return False
|
||||
|
||||
def excluded_type(self, event_type):
|
||||
for t in self._excluded_types:
|
||||
if fnmatch.fnmatch(event_type, t):
|
||||
return True
|
||||
return False
|
||||
|
||||
def match_type(self, event_type):
|
||||
return (self.included_type(event_type)
|
||||
and not self.excluded_type(event_type))
|
||||
|
||||
@property
|
||||
def is_catchall(self):
|
||||
return '*' in self._included_types and not self._excluded_types
|
||||
|
||||
@staticmethod
|
||||
def _extract_when(body):
|
||||
"""Extract the generated datetime from the notification.
|
||||
"""
|
||||
# NOTE: I am keeping the logic the same as it was in openstack
|
||||
# code, However, *ALL* notifications should have a 'timestamp'
|
||||
# field, it's part of the notification envelope spec. If this was
|
||||
# put here because some openstack project is generating notifications
|
||||
# without a timestamp, then that needs to be filed as a bug with the
|
||||
# offending project (mdragon)
|
||||
when = body.get('timestamp', body.get('_context_timestamp'))
|
||||
if when:
|
||||
return Datatype.datetime.convert(when)
|
||||
return utcnow()
|
||||
|
||||
def to_event(self, notification_body, condenser):
|
||||
event_type = notification_body['event_type']
|
||||
message_id = notification_body['message_id']
|
||||
when = self._extract_when(notification_body)
|
||||
|
||||
condenser.add_envelope_info(event_type, message_id, when)
|
||||
for t in self.traits:
|
||||
trait_info = self.traits[t].to_trait(notification_body)
|
||||
# Only accept non-None value traits ...
|
||||
if trait_info is not None:
|
||||
condenser.add_trait(*trait_info)
|
||||
return condenser
|
||||
|
||||
|
||||
class Distiller(object):
|
||||
"""Distiller
|
||||
|
||||
The Distiller extracts relevent information from an OpenStack Notification,
|
||||
with optional minor data massaging, and hands the information to a
|
||||
Condenser object, which formats it into an Event which only contains
|
||||
information you need, in a format relevent to your application.
|
||||
|
||||
The extraction is handled according to event definitions in a config file.
|
||||
|
||||
:param events_config: event-definitions configuration deserializerd
|
||||
from the YAML config-file.
|
||||
:param trait_plugin_map: Dictionary, or dictionary-like object mapping
|
||||
names to plugin classes. Defaults to default
|
||||
map of builtin plugins.
|
||||
:param catchall: Boolean. Add basic event definition to cover
|
||||
any notifications not otherwise described by
|
||||
the loaded config. The basic event definion
|
||||
only has envelope metadata, and a few really
|
||||
basic traits. Defaults to False.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, events_config, trait_plugin_map=None, catchall=False):
|
||||
if trait_plugin_map is None:
|
||||
trait_plugin_map = DEFAULT_PLUGINMAP
|
||||
self.definitions = [
|
||||
EventDefinition(event_def, trait_plugin_map)
|
||||
for event_def in reversed(events_config)]
|
||||
self.catchall = catchall
|
||||
if catchall and not any(d.is_catchall for d in self.definitions):
|
||||
event_def = dict(event_type='*', traits={})
|
||||
self.definitions.append(EventDefinition(event_def,
|
||||
trait_plugin_map))
|
||||
|
||||
def to_event(self, notification_body, condenser=None):
|
||||
if condenser is None:
|
||||
condenser = DictionaryCondenser()
|
||||
event_type = notification_body['event_type']
|
||||
message_id = notification_body['message_id']
|
||||
edef = None
|
||||
for d in self.definitions:
|
||||
if d.match_type(event_type):
|
||||
edef = d
|
||||
break
|
||||
|
||||
if edef is None:
|
||||
msg = ('Dropping Notification %(type)s (uuid:%(msgid)s)'
|
||||
% dict(type=event_type, msgid=message_id))
|
||||
if self.catchall:
|
||||
# If catchall is True, this should
|
||||
# never happen. (mdragon)
|
||||
logger.error(msg)
|
||||
else:
|
||||
logger.debug(msg)
|
||||
return None
|
||||
|
||||
return edef.to_event(notification_body, condenser)
|
162
stackdistiller/trait_plugins.py
Normal file
162
stackdistiller/trait_plugins.py
Normal file
@ -0,0 +1,162 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Copyright © 2013 Rackspace Hosting.
|
||||
#
|
||||
# Author: Monsyne Dragon <mdragon@rackspace.com>
|
||||
#
|
||||
# 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 abc
|
||||
import six
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class TraitPluginBase(object):
|
||||
"""Base class for plugins that convert notification fields to
|
||||
Trait values.
|
||||
"""
|
||||
|
||||
def __init__(self, **kw):
|
||||
"""Setup the trait plugin.
|
||||
|
||||
For each Trait definition a plugin is used on in a conversion
|
||||
definition, a new instance of the plugin will be created, and
|
||||
initialized with the parameters (if any) specified in the
|
||||
config file.
|
||||
|
||||
:param kw: the parameters specified in the event definitions file.
|
||||
|
||||
"""
|
||||
super(TraitPluginBase, self).__init__()
|
||||
|
||||
@abc.abstractmethod
|
||||
def trait_value(self, match_list):
|
||||
"""Convert a set of fields to a Trait value.
|
||||
|
||||
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
|
||||
are found in the notification (in that case, the match_list will be
|
||||
empty). If this method returns None, the trait *will not* be added to
|
||||
the event. Any other value returned by this method will be used as
|
||||
the value for the trait. Values returned will be coerced to the
|
||||
appropriate type for the trait.
|
||||
|
||||
:param match_list: A list (may be empty if no matches) of *tuples*.
|
||||
Each tuple is (field_path, value) where field_path
|
||||
is the jsonpath for that specific field,
|
||||
|
||||
Example:
|
||||
trait's fields definition: ['payload.foobar',
|
||||
'payload.baz',
|
||||
'payload.thing.*']
|
||||
notification body:
|
||||
{
|
||||
'message_id': '12345',
|
||||
'publisher': 'someservice.host',
|
||||
'payload': {
|
||||
'foobar': 'test',
|
||||
'thing': {
|
||||
'bar': 12,
|
||||
'boing': 13,
|
||||
}
|
||||
}
|
||||
}
|
||||
match_list will be: [('payload.foobar','test'),
|
||||
('payload.thing.bar',12),
|
||||
('payload.thing.boing',13)]
|
||||
|
||||
Here is a plugin that emulates the default (no plugin) behavior:
|
||||
|
||||
class DefaultPlugin(TraitPluginBase):
|
||||
"Plugin that returns the first field value"
|
||||
|
||||
def __init__(self, **kw):
|
||||
super(DefaultPlugin, self).__init__()
|
||||
|
||||
def trait_value(self, match_list):
|
||||
if not match_list:
|
||||
return None
|
||||
return match_list[0][1]
|
||||
"""
|
||||
|
||||
|
||||
class SplitterTraitPlugin(TraitPluginBase):
|
||||
"""Plugin that splits a piece off of a string value."""
|
||||
|
||||
def __init__(self, separator=".", segment=0, max_split=None, **kw):
|
||||
"""Setup how do split the field.
|
||||
|
||||
:param separator: String to split on. default "."
|
||||
:param segment: Which segment to return. (int) default 0
|
||||
:param max_split: Limit number of splits. Default: None (no limit)
|
||||
"""
|
||||
self.separator = separator
|
||||
self.segment = segment
|
||||
self.max_split = max_split
|
||||
super(SplitterTraitPlugin, self).__init__(**kw)
|
||||
|
||||
def trait_value(self, match_list):
|
||||
if not match_list:
|
||||
return None
|
||||
value = str(match_list[0][1])
|
||||
if self.max_split is not None:
|
||||
values = value.split(self.separator, self.max_split)
|
||||
else:
|
||||
values = value.split(self.separator)
|
||||
try:
|
||||
return values[self.segment]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
|
||||
class BitfieldTraitPlugin(TraitPluginBase):
|
||||
"""Plugin to set flags on a bitfield."""
|
||||
def __init__(self, initial_bitfield=0, flags=None, **kw):
|
||||
"""Setup bitfield trait.
|
||||
|
||||
:param initial_bitfield: (int) initial value for the bitfield
|
||||
Flags that are set will be OR'ed with this.
|
||||
:param flags: List of dictionaries defining bitflags to set depending
|
||||
on data in the notification. Each one has the following
|
||||
keys:
|
||||
path: jsonpath of field to match.
|
||||
bit: (int) number of bit to set (lsb is bit 0)
|
||||
value: set bit if corresponding field's value
|
||||
matches this. If value is not provided,
|
||||
bit will be set if the field exists (and
|
||||
is non-null), regardless of it's value.
|
||||
|
||||
"""
|
||||
self.initial_bitfield = initial_bitfield
|
||||
if flags is None:
|
||||
flags = []
|
||||
self.flags = flags
|
||||
super(BitfieldTraitPlugin, self).__init__(**kw)
|
||||
|
||||
def trait_value(self, match_list):
|
||||
matches = dict(match_list)
|
||||
bitfield = self.initial_bitfield
|
||||
for flagdef in self.flags:
|
||||
path = flagdef['path']
|
||||
bit = 2 ** int(flagdef['bit'])
|
||||
if path in matches:
|
||||
if 'value' in flagdef:
|
||||
if matches[path] == flagdef['value']:
|
||||
bitfield |= bit
|
||||
else:
|
||||
bitfield |= bit
|
||||
return bitfield
|
||||
|
||||
|
||||
DEFAULT_PLUGINMAP = dict(split=SplitterTraitPlugin,
|
||||
bitfield=BitfieldTraitPlugin)
|
3
test-requirements.txt
Normal file
3
test-requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
mock>=1.0
|
||||
nose
|
||||
unittest2
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
725
tests/test_distiller.py
Normal file
725
tests/test_distiller.py
Normal file
@ -0,0 +1,725 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Copyright © 2013 Rackspace Hosting.
|
||||
#
|
||||
# Author: Monsyne Dragon <mdragon@rackspace.com>
|
||||
#
|
||||
# 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 datetime
|
||||
|
||||
#for Python2.6 compatability.
|
||||
import unittest2 as unittest
|
||||
|
||||
import iso8601
|
||||
import jsonpath_rw
|
||||
import mock
|
||||
import six
|
||||
|
||||
from stackdistiller import distiller
|
||||
|
||||
|
||||
class TestCondenser(object):
|
||||
def __init__(self):
|
||||
self.event_type = None
|
||||
self.message_id = None
|
||||
self.when = None
|
||||
self.traits = []
|
||||
|
||||
def add_trait(self, name, trait_type, value):
|
||||
self.traits.append(distiller.Trait(name, trait_type, value))
|
||||
|
||||
def add_envelope_info(self, event_type, message_id, when):
|
||||
self.event_type = event_type
|
||||
self.message_id = message_id
|
||||
self.when = when
|
||||
|
||||
def get_event(self):
|
||||
return self
|
||||
|
||||
|
||||
class DistillerTestBase(unittest.TestCase):
|
||||
def _create_test_notification(self, event_type, message_id, **kw):
|
||||
return dict(event_type=event_type,
|
||||
message_id=message_id,
|
||||
priority="INFO",
|
||||
publisher_id="compute.host-1-2-3",
|
||||
timestamp="2013-08-08 21:06:37.803826",
|
||||
payload=kw,
|
||||
)
|
||||
|
||||
def assertIsValidEvent(self, event, notification):
|
||||
self.assertIsNot(
|
||||
None, event,
|
||||
"Notification dropped unexpectedly:"
|
||||
" %s" % str(notification))
|
||||
|
||||
def assertIsNotValidEvent(self, event, notification):
|
||||
self.assertIs(
|
||||
None, event,
|
||||
"Notification NOT dropped when expected to be dropped:"
|
||||
" %s" % str(notification))
|
||||
|
||||
def assertHasTrait(self, event, name, value=None, trait_type=None):
|
||||
traits = [trait for trait in event.traits if trait.name == name]
|
||||
self.assertTrue(
|
||||
len(traits) > 0,
|
||||
"Trait %s not found in event %s" % (name, event))
|
||||
trait = traits[0]
|
||||
if value is not None:
|
||||
self.assertEqual(value, trait.value)
|
||||
if trait_type is not None:
|
||||
self.assertEqual(trait_type, trait.trait_type)
|
||||
if trait_type == distiller.Datatype.int:
|
||||
self.assertIsInstance(trait.value, int)
|
||||
elif trait_type == distiller.Datatype.float:
|
||||
self.assertIsInstance(trait.value, float)
|
||||
elif trait_type == distiller.Datatype.datetime:
|
||||
self.assertIsInstance(trait.value, datetime.datetime)
|
||||
elif trait_type == distiller.Datatype.text:
|
||||
self.assertIsInstance(trait.value, six.string_types)
|
||||
|
||||
def assertDoesNotHaveTrait(self, event, name):
|
||||
traits = [trait for trait in event.traits if trait.name == name]
|
||||
self.assertEqual(
|
||||
len(traits), 0,
|
||||
"Extra Trait %s found in event %s" % (name, event))
|
||||
|
||||
def assertHasDefaultTraits(self, event):
|
||||
text = distiller.Datatype.text
|
||||
self.assertHasTrait(event, 'service', trait_type=text)
|
||||
|
||||
def _cmp_tree(self, this, other):
|
||||
if hasattr(this, 'right') and hasattr(other, 'right'):
|
||||
return (self._cmp_tree(this.right, other.right) and
|
||||
self._cmp_tree(this.left, other.left))
|
||||
if not hasattr(this, 'right') and not hasattr(other, 'right'):
|
||||
return this == other
|
||||
return False
|
||||
|
||||
def assertPathsEqual(self, path1, path2):
|
||||
self.assertTrue(self._cmp_tree(path1, path2),
|
||||
'JSONPaths not equivalent %s %s' % (path1, path2))
|
||||
|
||||
|
||||
class TestTraitDefinition(DistillerTestBase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestTraitDefinition, self).setUp()
|
||||
self.n1 = self._create_test_notification(
|
||||
"test.thing",
|
||||
"uuid-for-notif-0001",
|
||||
instance_uuid="uuid-for-instance-0001",
|
||||
instance_id="id-for-instance-0001",
|
||||
instance_uuid2=None,
|
||||
instance_id2=None,
|
||||
host='host-1-2-3',
|
||||
bogus_date='',
|
||||
image_meta=dict(
|
||||
disk_gb='20',
|
||||
thing='whatzit'),
|
||||
foobar=50)
|
||||
|
||||
self.test_plugin_class = mock.MagicMock(name='mock_test_plugin')
|
||||
self.test_plugin = self.test_plugin_class()
|
||||
self.test_plugin.trait_value.return_value = 'foobar'
|
||||
self.test_plugin_class.reset_mock()
|
||||
|
||||
self.nothing_plugin_class = mock.MagicMock(name='mock_nothing_plugin')
|
||||
self.nothing_plugin = self.nothing_plugin_class()
|
||||
self.nothing_plugin.trait_value.return_value = None
|
||||
self.nothing_plugin_class.reset_mock()
|
||||
|
||||
self.fake_plugin_map = dict(test=self.test_plugin_class,
|
||||
nothing=self.nothing_plugin_class)
|
||||
|
||||
def test_to_trait_with_plugin(self):
|
||||
cfg = dict(type='text',
|
||||
fields=['payload.instance_id', 'payload.instance_uuid'],
|
||||
plugin=dict(name='test'))
|
||||
|
||||
tdef = distiller.TraitDefinition('test_trait', cfg,
|
||||
self.fake_plugin_map)
|
||||
tname, trait_type, value = tdef.to_trait(self.n1)
|
||||
self.assertEqual('test_trait', tname)
|
||||
self.assertEqual(distiller.Datatype.text, trait_type)
|
||||
self.assertEqual('foobar', value)
|
||||
self.test_plugin_class.assert_called_once_with()
|
||||
self.test_plugin.trait_value.assert_called_once_with([
|
||||
('payload.instance_id', 'id-for-instance-0001'),
|
||||
('payload.instance_uuid', 'uuid-for-instance-0001')])
|
||||
|
||||
def test_to_trait_null_match_with_plugin(self):
|
||||
cfg = dict(type='text',
|
||||
fields=['payload.nothere', 'payload.bogus'],
|
||||
plugin=dict(name='test'))
|
||||
|
||||
tdef = distiller.TraitDefinition('test_trait', cfg,
|
||||
self.fake_plugin_map)
|
||||
tname, trait_type, value = tdef.to_trait(self.n1)
|
||||
self.assertEqual('test_trait', tname)
|
||||
self.assertEqual(distiller.Datatype.text, trait_type)
|
||||
self.assertEqual('foobar', value)
|
||||
self.test_plugin_class.assert_called_once_with()
|
||||
self.test_plugin.trait_value.assert_called_once_with([])
|
||||
|
||||
def test_to_trait_with_plugin_null(self):
|
||||
cfg = dict(type='text',
|
||||
fields=['payload.instance_id', 'payload.instance_uuid'],
|
||||
plugin=dict(name='nothing'))
|
||||
|
||||
tdef = distiller.TraitDefinition('test_trait', cfg,
|
||||
self.fake_plugin_map)
|
||||
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([
|
||||
('payload.instance_id', 'id-for-instance-0001'),
|
||||
('payload.instance_uuid', 'uuid-for-instance-0001')])
|
||||
|
||||
def test_to_trait_with_plugin_with_parameters(self):
|
||||
cfg = dict(type='text',
|
||||
fields=['payload.instance_id', 'payload.instance_uuid'],
|
||||
plugin=dict(name='test', parameters=dict(a=1, b='foo')))
|
||||
|
||||
tdef = distiller.TraitDefinition('test_trait', cfg,
|
||||
self.fake_plugin_map)
|
||||
tname, trait_type, value = tdef.to_trait(self.n1)
|
||||
self.assertEqual('test_trait', tname)
|
||||
self.assertEqual(distiller.Datatype.text, trait_type)
|
||||
self.assertEqual('foobar', value)
|
||||
self.test_plugin_class.assert_called_once_with(a=1, b='foo')
|
||||
self.test_plugin.trait_value.assert_called_once_with([
|
||||
('payload.instance_id', 'id-for-instance-0001'),
|
||||
('payload.instance_uuid', 'uuid-for-instance-0001')])
|
||||
|
||||
def test_to_trait(self):
|
||||
cfg = dict(type='text', fields='payload.instance_id')
|
||||
tdef = distiller.TraitDefinition('test_trait', cfg,
|
||||
self.fake_plugin_map)
|
||||
t = tdef.to_trait(self.n1)
|
||||
self.assertEqual('test_trait', t.name)
|
||||
self.assertEqual(distiller.Datatype.text, t.trait_type)
|
||||
self.assertEqual('id-for-instance-0001', t.value)
|
||||
|
||||
cfg = dict(type='int', fields='payload.image_meta.disk_gb')
|
||||
tdef = distiller.TraitDefinition('test_trait', cfg,
|
||||
self.fake_plugin_map)
|
||||
t = tdef.to_trait(self.n1)
|
||||
self.assertEqual('test_trait', t.name)
|
||||
self.assertEqual(distiller.Datatype.int, t.trait_type)
|
||||
self.assertEqual(20, t.value)
|
||||
|
||||
def test_to_trait_multiple(self):
|
||||
cfg = dict(type='text', fields=['payload.instance_id',
|
||||
'payload.instance_uuid'])
|
||||
tdef = distiller.TraitDefinition('test_trait', cfg,
|
||||
self.fake_plugin_map)
|
||||
t = tdef.to_trait(self.n1)
|
||||
self.assertEqual('id-for-instance-0001', t.value)
|
||||
|
||||
cfg = dict(type='text', fields=['payload.instance_uuid',
|
||||
'payload.instance_id'])
|
||||
tdef = distiller.TraitDefinition('test_trait', cfg,
|
||||
self.fake_plugin_map)
|
||||
t = tdef.to_trait(self.n1)
|
||||
self.assertEqual('uuid-for-instance-0001', t.value)
|
||||
|
||||
def test_to_trait_multiple_different_nesting(self):
|
||||
cfg = dict(type='int', fields=['payload.foobar',
|
||||
'payload.image_meta.disk_gb'])
|
||||
tdef = distiller.TraitDefinition('test_trait', cfg,
|
||||
self.fake_plugin_map)
|
||||
t = tdef.to_trait(self.n1)
|
||||
self.assertEqual(50, t.value)
|
||||
|
||||
cfg = dict(type='int', fields=['payload.image_meta.disk_gb',
|
||||
'payload.foobar'])
|
||||
tdef = distiller.TraitDefinition('test_trait', cfg,
|
||||
self.fake_plugin_map)
|
||||
t = tdef.to_trait(self.n1)
|
||||
self.assertEqual(20, t.value)
|
||||
|
||||
def test_to_trait_some_null_multiple(self):
|
||||
cfg = dict(type='text', fields=['payload.instance_id2',
|
||||
'payload.instance_uuid'])
|
||||
tdef = distiller.TraitDefinition('test_trait', cfg,
|
||||
self.fake_plugin_map)
|
||||
t = tdef.to_trait(self.n1)
|
||||
self.assertEqual('uuid-for-instance-0001', t.value)
|
||||
|
||||
def test_to_trait_some_missing_multiple(self):
|
||||
cfg = dict(type='text', fields=['payload.not_here_boss',
|
||||
'payload.instance_uuid'])
|
||||
tdef = distiller.TraitDefinition('test_trait', cfg,
|
||||
self.fake_plugin_map)
|
||||
t = tdef.to_trait(self.n1)
|
||||
self.assertEqual('uuid-for-instance-0001', t.value)
|
||||
|
||||
def test_to_trait_missing(self):
|
||||
cfg = dict(type='text', fields='payload.not_here_boss')
|
||||
tdef = distiller.TraitDefinition('test_trait', cfg,
|
||||
self.fake_plugin_map)
|
||||
t = tdef.to_trait(self.n1)
|
||||
self.assertIs(None, t)
|
||||
|
||||
def test_to_trait_null(self):
|
||||
cfg = dict(type='text', fields='payload.instance_id2')
|
||||
tdef = distiller.TraitDefinition('test_trait', cfg,
|
||||
self.fake_plugin_map)
|
||||
t = tdef.to_trait(self.n1)
|
||||
self.assertIs(None, t)
|
||||
|
||||
def test_to_trait_empty_nontext(self):
|
||||
cfg = dict(type='datetime', fields='payload.bogus_date')
|
||||
tdef = distiller.TraitDefinition('test_trait', cfg,
|
||||
self.fake_plugin_map)
|
||||
t = tdef.to_trait(self.n1)
|
||||
self.assertIs(None, t)
|
||||
|
||||
def test_to_trait_multiple_null_missing(self):
|
||||
cfg = dict(type='text', fields=['payload.not_here_boss',
|
||||
'payload.instance_id2'])
|
||||
tdef = distiller.TraitDefinition('test_trait', cfg,
|
||||
self.fake_plugin_map)
|
||||
t = tdef.to_trait(self.n1)
|
||||
self.assertIs(None, t)
|
||||
|
||||
def test_missing_fields_config(self):
|
||||
self.assertRaises(distiller.EventDefinitionException,
|
||||
distiller.TraitDefinition,
|
||||
'bogus_trait',
|
||||
dict(),
|
||||
self.fake_plugin_map)
|
||||
|
||||
def test_string_fields_config(self):
|
||||
cfg = dict(fields='payload.test')
|
||||
t = distiller.TraitDefinition('test_trait', cfg, self.fake_plugin_map)
|
||||
self.assertPathsEqual(t.fields, jsonpath_rw.parse('payload.test'))
|
||||
|
||||
def test_list_fields_config(self):
|
||||
cfg = dict(fields=['payload.test', 'payload.other'])
|
||||
t = distiller.TraitDefinition('test_trait', cfg, self.fake_plugin_map)
|
||||
self.assertPathsEqual(
|
||||
t.fields,
|
||||
jsonpath_rw.parse('(payload.test)|(payload.other)'))
|
||||
|
||||
def test_invalid_path_config(self):
|
||||
#test invalid jsonpath...
|
||||
cfg = dict(fields='payload.bogus(')
|
||||
self.assertRaises(distiller.EventDefinitionException,
|
||||
distiller.TraitDefinition,
|
||||
'bogus_trait',
|
||||
cfg,
|
||||
self.fake_plugin_map)
|
||||
|
||||
def test_invalid_plugin_config(self):
|
||||
#test invalid jsonpath...
|
||||
cfg = dict(fields='payload.test', plugin=dict(bogus="true"))
|
||||
self.assertRaises(distiller.EventDefinitionException,
|
||||
distiller.TraitDefinition,
|
||||
'test_trait',
|
||||
cfg,
|
||||
self.fake_plugin_map)
|
||||
|
||||
def test_unknown_plugin(self):
|
||||
#test invalid jsonpath...
|
||||
cfg = dict(fields='payload.test', plugin=dict(name='bogus'))
|
||||
self.assertRaises(distiller.EventDefinitionException,
|
||||
distiller.TraitDefinition,
|
||||
'test_trait',
|
||||
cfg,
|
||||
self.fake_plugin_map)
|
||||
|
||||
def test_type_config(self):
|
||||
cfg = dict(type='text', fields='payload.test')
|
||||
t = distiller.TraitDefinition('test_trait', cfg, self.fake_plugin_map)
|
||||
self.assertEqual(distiller.Datatype.text, t.trait_type)
|
||||
|
||||
cfg = dict(type='int', fields='payload.test')
|
||||
t = distiller.TraitDefinition('test_trait', cfg, self.fake_plugin_map)
|
||||
self.assertEqual(distiller.Datatype.int, t.trait_type)
|
||||
|
||||
cfg = dict(type='float', fields='payload.test')
|
||||
t = distiller.TraitDefinition('test_trait', cfg, self.fake_plugin_map)
|
||||
self.assertEqual(distiller.Datatype.float, t.trait_type)
|
||||
|
||||
cfg = dict(type='datetime', fields='payload.test')
|
||||
t = distiller.TraitDefinition('test_trait', cfg, self.fake_plugin_map)
|
||||
self.assertEqual(distiller.Datatype.datetime, t.trait_type)
|
||||
|
||||
def test_invalid_type_config(self):
|
||||
#test invalid jsonpath...
|
||||
cfg = dict(type='bogus', fields='payload.test')
|
||||
self.assertRaises(distiller.EventDefinitionException,
|
||||
distiller.TraitDefinition,
|
||||
'bogus_trait',
|
||||
cfg,
|
||||
self.fake_plugin_map)
|
||||
|
||||
|
||||
class TestEventDefinition(DistillerTestBase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestEventDefinition, self).setUp()
|
||||
|
||||
self.traits_cfg = {
|
||||
'instance_id': {
|
||||
'type': 'text',
|
||||
'fields': ['payload.instance_uuid',
|
||||
'payload.instance_id'],
|
||||
},
|
||||
'host': {
|
||||
'type': 'text',
|
||||
'fields': 'payload.host',
|
||||
},
|
||||
}
|
||||
|
||||
self.test_notification1 = self._create_test_notification(
|
||||
"test.thing",
|
||||
"uuid-for-notif-0001",
|
||||
instance_id="uuid-for-instance-0001",
|
||||
host='host-1-2-3')
|
||||
|
||||
self.test_notification2 = self._create_test_notification(
|
||||
"test.thing",
|
||||
"uuid-for-notif-0002",
|
||||
instance_id="uuid-for-instance-0002")
|
||||
|
||||
self.test_notification3 = self._create_test_notification(
|
||||
"test.thing",
|
||||
"uuid-for-notif-0003",
|
||||
instance_id="uuid-for-instance-0003",
|
||||
host=None)
|
||||
self.fake_plugin_map = {}
|
||||
self.condenser = TestCondenser()
|
||||
|
||||
def test_to_event(self):
|
||||
trait_type = distiller.Datatype.text
|
||||
cfg = dict(event_type='test.thing', traits=self.traits_cfg)
|
||||
edef = distiller.EventDefinition(cfg, self.fake_plugin_map)
|
||||
|
||||
e = edef.to_event(self.test_notification1, self.condenser)
|
||||
self.assertTrue(e is self.condenser)
|
||||
self.assertEqual('test.thing', e.event_type)
|
||||
self.assertEqual(datetime.datetime(2013, 8, 8, 21, 6, 37, 803826, iso8601.iso8601.UTC),
|
||||
e.when)
|
||||
|
||||
self.assertHasDefaultTraits(e)
|
||||
self.assertHasTrait(e, 'host', value='host-1-2-3', trait_type=trait_type)
|
||||
self.assertHasTrait(e, 'instance_id',
|
||||
value='uuid-for-instance-0001',
|
||||
trait_type=trait_type)
|
||||
|
||||
def test_to_event_missing_trait(self):
|
||||
trait_type = distiller.Datatype.text
|
||||
cfg = dict(event_type='test.thing', traits=self.traits_cfg)
|
||||
edef = distiller.EventDefinition(cfg, self.fake_plugin_map)
|
||||
|
||||
e = edef.to_event(self.test_notification2, self.condenser)
|
||||
|
||||
self.assertHasDefaultTraits(e)
|
||||
self.assertHasTrait(e, 'instance_id',
|
||||
value='uuid-for-instance-0002',
|
||||
trait_type=trait_type)
|
||||
self.assertDoesNotHaveTrait(e, 'host')
|
||||
|
||||
def test_to_event_null_trait(self):
|
||||
trait_type = distiller.Datatype.text
|
||||
cfg = dict(event_type='test.thing', traits=self.traits_cfg)
|
||||
edef = distiller.EventDefinition(cfg, self.fake_plugin_map)
|
||||
|
||||
e = edef.to_event(self.test_notification3, self.condenser)
|
||||
|
||||
self.assertHasDefaultTraits(e)
|
||||
self.assertHasTrait(e, 'instance_id',
|
||||
value='uuid-for-instance-0003',
|
||||
trait_type=trait_type)
|
||||
self.assertDoesNotHaveTrait(e, 'host')
|
||||
|
||||
def test_bogus_cfg_no_traits(self):
|
||||
bogus = dict(event_type='test.foo')
|
||||
self.assertRaises(distiller.EventDefinitionException,
|
||||
distiller.EventDefinition,
|
||||
bogus,
|
||||
self.fake_plugin_map)
|
||||
|
||||
def test_bogus_cfg_no_type(self):
|
||||
bogus = dict(traits=self.traits_cfg)
|
||||
self.assertRaises(distiller.EventDefinitionException,
|
||||
distiller.EventDefinition,
|
||||
bogus,
|
||||
self.fake_plugin_map)
|
||||
|
||||
def test_included_type_string(self):
|
||||
cfg = dict(event_type='test.thing', traits=self.traits_cfg)
|
||||
edef = distiller.EventDefinition(cfg, self.fake_plugin_map)
|
||||
self.assertEqual(1, len(edef._included_types))
|
||||
self.assertEqual('test.thing', edef._included_types[0])
|
||||
self.assertEqual(0, len(edef._excluded_types))
|
||||
self.assertTrue(edef.included_type('test.thing'))
|
||||
self.assertFalse(edef.excluded_type('test.thing'))
|
||||
self.assertTrue(edef.match_type('test.thing'))
|
||||
self.assertFalse(edef.match_type('random.thing'))
|
||||
|
||||
def test_included_type_list(self):
|
||||
cfg = dict(event_type=['test.thing', 'other.thing'],
|
||||
traits=self.traits_cfg)
|
||||
edef = distiller.EventDefinition(cfg, self.fake_plugin_map)
|
||||
self.assertEqual(2, len(edef._included_types))
|
||||
self.assertEqual(0, len(edef._excluded_types))
|
||||
self.assertTrue(edef.included_type('test.thing'))
|
||||
self.assertTrue(edef.included_type('other.thing'))
|
||||
self.assertFalse(edef.excluded_type('test.thing'))
|
||||
self.assertTrue(edef.match_type('test.thing'))
|
||||
self.assertTrue(edef.match_type('other.thing'))
|
||||
self.assertFalse(edef.match_type('random.thing'))
|
||||
|
||||
def test_excluded_type_string(self):
|
||||
cfg = dict(event_type='!test.thing', traits=self.traits_cfg)
|
||||
edef = distiller.EventDefinition(cfg, self.fake_plugin_map)
|
||||
self.assertEqual(1, len(edef._included_types))
|
||||
self.assertEqual('*', edef._included_types[0])
|
||||
self.assertEqual('test.thing', edef._excluded_types[0])
|
||||
self.assertEqual(1, len(edef._excluded_types))
|
||||
self.assertEqual('test.thing', edef._excluded_types[0])
|
||||
self.assertTrue(edef.excluded_type('test.thing'))
|
||||
self.assertTrue(edef.included_type('random.thing'))
|
||||
self.assertFalse(edef.match_type('test.thing'))
|
||||
self.assertTrue(edef.match_type('random.thing'))
|
||||
|
||||
def test_excluded_type_list(self):
|
||||
cfg = dict(event_type=['!test.thing', '!other.thing'],
|
||||
traits=self.traits_cfg)
|
||||
edef = distiller.EventDefinition(cfg, self.fake_plugin_map)
|
||||
self.assertEqual(1, len(edef._included_types))
|
||||
self.assertEqual(2, len(edef._excluded_types))
|
||||
self.assertTrue(edef.excluded_type('test.thing'))
|
||||
self.assertTrue(edef.excluded_type('other.thing'))
|
||||
self.assertFalse(edef.excluded_type('random.thing'))
|
||||
self.assertFalse(edef.match_type('test.thing'))
|
||||
self.assertFalse(edef.match_type('other.thing'))
|
||||
self.assertTrue(edef.match_type('random.thing'))
|
||||
|
||||
def test_mixed_type_list(self):
|
||||
cfg = dict(event_type=['*.thing', '!test.thing', '!other.thing'],
|
||||
traits=self.traits_cfg)
|
||||
edef = distiller.EventDefinition(cfg, self.fake_plugin_map)
|
||||
self.assertEqual(1, len(edef._included_types))
|
||||
self.assertEqual(2, len(edef._excluded_types))
|
||||
self.assertTrue(edef.excluded_type('test.thing'))
|
||||
self.assertTrue(edef.excluded_type('other.thing'))
|
||||
self.assertFalse(edef.excluded_type('random.thing'))
|
||||
self.assertFalse(edef.match_type('test.thing'))
|
||||
self.assertFalse(edef.match_type('other.thing'))
|
||||
self.assertFalse(edef.match_type('random.whatzit'))
|
||||
self.assertTrue(edef.match_type('random.thing'))
|
||||
|
||||
def test_catchall(self):
|
||||
cfg = dict(event_type=['*.thing', '!test.thing', '!other.thing'],
|
||||
traits=self.traits_cfg)
|
||||
edef = distiller.EventDefinition(cfg, self.fake_plugin_map)
|
||||
self.assertFalse(edef.is_catchall)
|
||||
|
||||
cfg = dict(event_type=['!other.thing'],
|
||||
traits=self.traits_cfg)
|
||||
edef = distiller.EventDefinition(cfg, self.fake_plugin_map)
|
||||
self.assertFalse(edef.is_catchall)
|
||||
|
||||
cfg = dict(event_type=['other.thing'],
|
||||
traits=self.traits_cfg)
|
||||
edef = distiller.EventDefinition(cfg, self.fake_plugin_map)
|
||||
self.assertFalse(edef.is_catchall)
|
||||
|
||||
cfg = dict(event_type=['*', '!other.thing'],
|
||||
traits=self.traits_cfg)
|
||||
edef = distiller.EventDefinition(cfg, self.fake_plugin_map)
|
||||
self.assertFalse(edef.is_catchall)
|
||||
|
||||
cfg = dict(event_type=['*'],
|
||||
traits=self.traits_cfg)
|
||||
edef = distiller.EventDefinition(cfg, self.fake_plugin_map)
|
||||
self.assertTrue(edef.is_catchall)
|
||||
|
||||
cfg = dict(event_type=['*', 'foo'],
|
||||
traits=self.traits_cfg)
|
||||
edef = distiller.EventDefinition(cfg, self.fake_plugin_map)
|
||||
self.assertTrue(edef.is_catchall)
|
||||
|
||||
@mock.patch('stackdistiller.distiller.utcnow')
|
||||
def test_extract_when(self, mock_utcnow):
|
||||
now = datetime.datetime.utcnow().replace(tzinfo=iso8601.iso8601.UTC)
|
||||
modified = now + datetime.timedelta(minutes=1)
|
||||
mock_utcnow.return_value = now
|
||||
|
||||
body = {"timestamp": str(modified)}
|
||||
when = distiller.EventDefinition._extract_when(body)
|
||||
self.assertEqual(modified, when)
|
||||
|
||||
body = {"_context_timestamp": str(modified)}
|
||||
when = distiller.EventDefinition._extract_when(body)
|
||||
self.assertEqual(modified, when)
|
||||
|
||||
then = now + datetime.timedelta(hours=1)
|
||||
body = {"timestamp": str(modified), "_context_timestamp": str(then)}
|
||||
when = distiller.EventDefinition._extract_when(body)
|
||||
self.assertEqual(modified, when)
|
||||
|
||||
when = distiller.EventDefinition._extract_when({})
|
||||
self.assertEqual(now, when)
|
||||
|
||||
def test_default_traits(self):
|
||||
cfg = dict(event_type='test.thing', traits={})
|
||||
edef = distiller.EventDefinition(cfg, self.fake_plugin_map)
|
||||
default_traits = distiller.EventDefinition.DEFAULT_TRAITS.keys()
|
||||
traits = set(edef.traits.keys())
|
||||
for dt in default_traits:
|
||||
self.assertIn(dt, traits)
|
||||
self.assertEqual(len(distiller.EventDefinition.DEFAULT_TRAITS),
|
||||
len(edef.traits))
|
||||
|
||||
def test_traits(self):
|
||||
cfg = dict(event_type='test.thing', traits=self.traits_cfg)
|
||||
edef = distiller.EventDefinition(cfg, self.fake_plugin_map)
|
||||
default_traits = distiller.EventDefinition.DEFAULT_TRAITS.keys()
|
||||
traits = set(edef.traits.keys())
|
||||
for dt in default_traits:
|
||||
self.assertIn(dt, traits)
|
||||
self.assertIn('host', traits)
|
||||
self.assertIn('instance_id', traits)
|
||||
self.assertEqual(len(distiller.EventDefinition.DEFAULT_TRAITS) + 2,
|
||||
len(edef.traits))
|
||||
|
||||
|
||||
class TestDistiller(DistillerTestBase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestDistiller, self).setUp()
|
||||
|
||||
self.valid_event_def1 = [{
|
||||
'event_type': 'compute.instance.create.*',
|
||||
'traits': {
|
||||
'instance_id': {
|
||||
'type': 'text',
|
||||
'fields': ['payload.instance_uuid',
|
||||
'payload.instance_id'],
|
||||
},
|
||||
'host': {
|
||||
'type': 'text',
|
||||
'fields': 'payload.host',
|
||||
},
|
||||
},
|
||||
}]
|
||||
|
||||
self.test_notification1 = self._create_test_notification(
|
||||
"compute.instance.create.start",
|
||||
"uuid-for-notif-0001",
|
||||
instance_id="uuid-for-instance-0001",
|
||||
host='host-1-2-3')
|
||||
self.test_notification2 = self._create_test_notification(
|
||||
"bogus.notification.from.mars",
|
||||
"uuid-for-notif-0002",
|
||||
weird='true',
|
||||
host='cydonia')
|
||||
self.fake_plugin_map = {}
|
||||
|
||||
@mock.patch('stackdistiller.distiller.utcnow')
|
||||
def test_distiller_missing_keys(self, mock_utcnow):
|
||||
# test a malformed notification
|
||||
now = datetime.datetime.utcnow().replace(tzinfo=iso8601.iso8601.UTC)
|
||||
mock_utcnow.return_value = now
|
||||
c = distiller.Distiller(
|
||||
[],
|
||||
self.fake_plugin_map,
|
||||
catchall=True)
|
||||
message = {'event_type': "foo",
|
||||
'message_id': "abc",
|
||||
'publisher_id': "1"}
|
||||
e = c.to_event(message, TestCondenser())
|
||||
self.assertIsValidEvent(e, message)
|
||||
self.assertEqual(1, len(e.traits))
|
||||
self.assertEqual("foo", e.event_type)
|
||||
self.assertEqual(now, e.when)
|
||||
|
||||
def test_distiller_with_catchall(self):
|
||||
c = distiller.Distiller(
|
||||
self.valid_event_def1,
|
||||
self.fake_plugin_map,
|
||||
catchall=True)
|
||||
self.assertEqual(2, len(c.definitions))
|
||||
e = c.to_event(self.test_notification1, TestCondenser())
|
||||
self.assertIsValidEvent(e, self.test_notification1)
|
||||
self.assertEqual(3, len(e.traits))
|
||||
self.assertHasDefaultTraits(e)
|
||||
self.assertHasTrait(e, 'instance_id')
|
||||
self.assertHasTrait(e, 'host')
|
||||
|
||||
e = c.to_event(self.test_notification2, TestCondenser())
|
||||
self.assertIsValidEvent(e, self.test_notification2)
|
||||
self.assertEqual(1, len(e.traits),
|
||||
"Wrong number of traits %s: %s" % (len(e.traits), e.traits))
|
||||
self.assertHasDefaultTraits(e)
|
||||
self.assertDoesNotHaveTrait(e, 'instance_id')
|
||||
self.assertDoesNotHaveTrait(e, 'host')
|
||||
|
||||
def test_distiller_without_catchall(self):
|
||||
c = distiller.Distiller(
|
||||
self.valid_event_def1,
|
||||
self.fake_plugin_map,
|
||||
catchall=False)
|
||||
self.assertEqual(1, len(c.definitions))
|
||||
e = c.to_event(self.test_notification1, TestCondenser())
|
||||
self.assertIsValidEvent(e, self.test_notification1)
|
||||
self.assertEqual(3, len(e.traits))
|
||||
self.assertHasDefaultTraits(e)
|
||||
self.assertHasTrait(e, 'instance_id')
|
||||
self.assertHasTrait(e, 'host')
|
||||
|
||||
e = c.to_event(self.test_notification2, TestCondenser())
|
||||
self.assertIsNotValidEvent(e, self.test_notification2)
|
||||
|
||||
def test_distiller_empty_cfg_with_catchall(self):
|
||||
c = distiller.Distiller(
|
||||
[],
|
||||
self.fake_plugin_map,
|
||||
catchall=True)
|
||||
self.assertEqual(1, len(c.definitions))
|
||||
e = c.to_event(self.test_notification1, TestCondenser())
|
||||
self.assertIsValidEvent(e, self.test_notification1)
|
||||
self.assertEqual(1, len(e.traits))
|
||||
self.assertHasDefaultTraits(e)
|
||||
|
||||
e = c.to_event(self.test_notification2, TestCondenser())
|
||||
self.assertIsValidEvent(e, self.test_notification2)
|
||||
self.assertEqual(1, len(e.traits))
|
||||
self.assertHasDefaultTraits(e)
|
||||
|
||||
def test_distiller_empty_cfg_without_catchall(self):
|
||||
c = distiller.Distiller(
|
||||
[],
|
||||
self.fake_plugin_map,
|
||||
catchall=False)
|
||||
self.assertEqual(0, len(c.definitions))
|
||||
e = c.to_event(self.test_notification1, TestCondenser())
|
||||
self.assertIsNotValidEvent(e, self.test_notification1)
|
||||
|
||||
e = c.to_event(self.test_notification2, TestCondenser())
|
||||
self.assertIsNotValidEvent(e, self.test_notification2)
|
||||
|
||||
def test_default_config(self):
|
||||
d = distiller.Distiller([], catchall=True)
|
||||
self.assertEqual(1, len(d.definitions))
|
||||
self.assertTrue(d.definitions[0].is_catchall)
|
||||
|
||||
d = distiller.Distiller([], catchall=False)
|
||||
self.assertEqual(0, len(d.definitions))
|
120
tests/test_trait_plugins.py
Normal file
120
tests/test_trait_plugins.py
Normal file
@ -0,0 +1,120 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Copyright © 2013 Rackspace Hosting.
|
||||
#
|
||||
# Author: Monsyne Dragon <mdragon@rackspace.com>
|
||||
#
|
||||
# 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.
|
||||
|
||||
#for Python2.6 compatability.
|
||||
import unittest2 as unittest
|
||||
|
||||
from stackdistiller import trait_plugins
|
||||
|
||||
|
||||
class TestSplitterPlugin(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestSplitterPlugin, self).setUp()
|
||||
self.pclass = trait_plugins.SplitterTraitPlugin
|
||||
|
||||
def test_split(self):
|
||||
param = dict(separator='-', segment=0)
|
||||
plugin = self.pclass(**param)
|
||||
match_list = [('test.thing', 'test-foobar-baz')]
|
||||
value = plugin.trait_value(match_list)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
class TestBitfieldPlugin(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestBitfieldPlugin, self).setUp()
|
||||
self.pclass = trait_plugins.BitfieldTraitPlugin
|
||||
self.init = 0
|
||||
self.params = dict(initial_bitfield=self.init,
|
||||
flags=[dict(path='payload.foo', bit=0, value=42),
|
||||
dict(path='payload.foo', bit=1, value=12),
|
||||
dict(path='payload.thud', bit=1, value=23),
|
||||
dict(path='thingy.boink', bit=4),
|
||||
dict(path='thingy.quux', bit=6,
|
||||
value="wokka"),
|
||||
dict(path='payload.bar', bit=10,
|
||||
value='test')])
|
||||
|
||||
def test_bitfield(self):
|
||||
match_list = [('payload.foo', 12),
|
||||
('payload.bar', 'test'),
|
||||
('thingy.boink', 'testagain')]
|
||||
|
||||
plugin = self.pclass(**self.params)
|
||||
value = plugin.trait_value(match_list)
|
||||
self.assertEqual(0x412, value)
|
||||
|
||||
def test_initial(self):
|
||||
match_list = [('payload.foo', 12),
|
||||
('payload.bar', 'test'),
|
||||
('thingy.boink', 'testagain')]
|
||||
self.params['initial_bitfield'] = 0x2000
|
||||
plugin = self.pclass(**self.params)
|
||||
value = plugin.trait_value(match_list)
|
||||
self.assertEqual(0x2412, value)
|
||||
|
||||
def test_no_match(self):
|
||||
match_list = []
|
||||
plugin = self.pclass(**self.params)
|
||||
value = plugin.trait_value(match_list)
|
||||
self.assertEqual(self.init, value)
|
||||
|
||||
def test_multi(self):
|
||||
match_list = [('payload.foo', 12),
|
||||
('payload.thud', 23),
|
||||
('payload.bar', 'test'),
|
||||
('thingy.boink', 'testagain')]
|
||||
|
||||
plugin = self.pclass(**self.params)
|
||||
value = plugin.trait_value(match_list)
|
||||
self.assertEqual(0x412, value)
|
Loading…
Reference in New Issue
Block a user