summaryrefslogtreecommitdiff
path: root/cloudinit/reporting
diff options
context:
space:
mode:
Diffstat (limited to 'cloudinit/reporting')
-rw-r--r--cloudinit/reporting/__init__.py42
-rw-r--r--cloudinit/reporting/events.py246
-rw-r--r--cloudinit/reporting/handlers.py91
3 files changed, 379 insertions, 0 deletions
diff --git a/cloudinit/reporting/__init__.py b/cloudinit/reporting/__init__.py
new file mode 100644
index 00000000..6b41ae61
--- /dev/null
+++ b/cloudinit/reporting/__init__.py
@@ -0,0 +1,42 @@
+# Copyright 2015 Canonical Ltd.
+# This file is part of cloud-init. See LICENCE file for license information.
+#
+"""
+cloud-init reporting framework
+
+The reporting framework is intended to allow all parts of cloud-init to
+report events in a structured manner.
+"""
+
+from ..registry import DictRegistry
+from .handlers import available_handlers
+
+DEFAULT_CONFIG = {
+ 'logging': {'type': 'log'},
+}
+
+
+def update_configuration(config):
+ """Update the instanciated_handler_registry.
+
+ :param config:
+ The dictionary containing changes to apply. If a key is given
+ with a False-ish value, the registered handler matching that name
+ will be unregistered.
+ """
+ for handler_name, handler_config in config.items():
+ if not handler_config:
+ instantiated_handler_registry.unregister_item(
+ handler_name, force=True)
+ continue
+ handler_config = handler_config.copy()
+ cls = available_handlers.registered_items[handler_config.pop('type')]
+ instantiated_handler_registry.unregister_item(handler_name)
+ instance = cls(**handler_config)
+ instantiated_handler_registry.register_item(handler_name, instance)
+
+
+instantiated_handler_registry = DictRegistry()
+update_configuration(DEFAULT_CONFIG)
+
+# vi: ts=4 expandtab
diff --git a/cloudinit/reporting/events.py b/cloudinit/reporting/events.py
new file mode 100644
index 00000000..2f767f64
--- /dev/null
+++ b/cloudinit/reporting/events.py
@@ -0,0 +1,246 @@
+# Copyright 2015 Canonical Ltd.
+# This file is part of cloud-init. See LICENCE file for license information.
+#
+"""
+events for reporting.
+
+The events here are designed to be used with reporting.
+They can be published to registered handlers with report_event.
+"""
+import base64
+import os.path
+import time
+
+from . import instantiated_handler_registry
+
+FINISH_EVENT_TYPE = 'finish'
+START_EVENT_TYPE = 'start'
+
+DEFAULT_EVENT_ORIGIN = 'cloudinit'
+
+
+class _nameset(set):
+ def __getattr__(self, name):
+ if name in self:
+ return name
+ raise AttributeError("%s not a valid value" % name)
+
+
+status = _nameset(("SUCCESS", "WARN", "FAIL"))
+
+
+class ReportingEvent(object):
+ """Encapsulation of event formatting."""
+
+ def __init__(self, event_type, name, description,
+ origin=DEFAULT_EVENT_ORIGIN, timestamp=time.time()):
+ self.event_type = event_type
+ self.name = name
+ self.description = description
+ self.origin = origin
+ self.timestamp = timestamp
+
+ def as_string(self):
+ """The event represented as a string."""
+ return '{0}: {1}: {2}'.format(
+ self.event_type, self.name, self.description)
+
+ def as_dict(self):
+ """The event represented as a dictionary."""
+ return {'name': self.name, 'description': self.description,
+ 'event_type': self.event_type, 'origin': self.origin,
+ 'timestamp': self.timestamp}
+
+
+class FinishReportingEvent(ReportingEvent):
+
+ def __init__(self, name, description, result=status.SUCCESS,
+ post_files=None):
+ super(FinishReportingEvent, self).__init__(
+ FINISH_EVENT_TYPE, name, description)
+ self.result = result
+ if post_files is None:
+ post_files = []
+ self.post_files = post_files
+ if result not in status:
+ raise ValueError("Invalid result: %s" % result)
+
+ def as_string(self):
+ return '{0}: {1}: {2}: {3}'.format(
+ self.event_type, self.name, self.result, self.description)
+
+ def as_dict(self):
+ """The event represented as json friendly."""
+ data = super(FinishReportingEvent, self).as_dict()
+ data['result'] = self.result
+ if self.post_files:
+ data['files'] = _collect_file_info(self.post_files)
+ return data
+
+
+def report_event(event):
+ """Report an event to all registered event handlers.
+
+ This should generally be called via one of the other functions in
+ the reporting module.
+
+ :param event_type:
+ The type of the event; this should be a constant from the
+ reporting module.
+ """
+ for _, handler in instantiated_handler_registry.registered_items.items():
+ handler.publish_event(event)
+
+
+def report_finish_event(event_name, event_description,
+ result=status.SUCCESS, post_files=None):
+ """Report a "finish" event.
+
+ See :py:func:`.report_event` for parameter details.
+ """
+ event = FinishReportingEvent(event_name, event_description, result,
+ post_files=post_files)
+ return report_event(event)
+
+
+def report_start_event(event_name, event_description):
+ """Report a "start" event.
+
+ :param event_name:
+ The name of the event; this should be a topic which events would
+ share (e.g. it will be the same for start and finish events).
+
+ :param event_description:
+ A human-readable description of the event that has occurred.
+ """
+ event = ReportingEvent(START_EVENT_TYPE, event_name, event_description)
+ return report_event(event)
+
+
+class ReportEventStack(object):
+ """Context Manager for using :py:func:`report_event`
+
+ This enables calling :py:func:`report_start_event` and
+ :py:func:`report_finish_event` through a context manager.
+
+ :param name:
+ the name of the event
+
+ :param description:
+ the event's description, passed on to :py:func:`report_start_event`
+
+ :param message:
+ the description to use for the finish event. defaults to
+ :param:description.
+
+ :param parent:
+ :type parent: :py:class:ReportEventStack or None
+ The parent of this event. The parent is populated with
+ results of all its children. The name used in reporting
+ is <parent.name>/<name>
+
+ :param reporting_enabled:
+ Indicates if reporting events should be generated.
+ If not provided, defaults to the parent's value, or True if no parent
+ is provided.
+
+ :param result_on_exception:
+ The result value to set if an exception is caught. default
+ value is FAIL.
+ """
+ def __init__(self, name, description, message=None, parent=None,
+ reporting_enabled=None, result_on_exception=status.FAIL,
+ post_files=None):
+ self.parent = parent
+ self.name = name
+ self.description = description
+ self.message = message
+ self.result_on_exception = result_on_exception
+ self.result = status.SUCCESS
+ if post_files is None:
+ post_files = []
+ self.post_files = post_files
+
+ # use parents reporting value if not provided
+ if reporting_enabled is None:
+ if parent:
+ reporting_enabled = parent.reporting_enabled
+ else:
+ reporting_enabled = True
+ self.reporting_enabled = reporting_enabled
+
+ if parent:
+ self.fullname = '/'.join((parent.fullname, name,))
+ else:
+ self.fullname = self.name
+ self.children = {}
+
+ def __repr__(self):
+ return ("ReportEventStack(%s, %s, reporting_enabled=%s)" %
+ (self.name, self.description, self.reporting_enabled))
+
+ def __enter__(self):
+ self.result = status.SUCCESS
+ if self.reporting_enabled:
+ report_start_event(self.fullname, self.description)
+ if self.parent:
+ self.parent.children[self.name] = (None, None)
+ return self
+
+ def _childrens_finish_info(self):
+ for cand_result in (status.FAIL, status.WARN):
+ for name, (value, msg) in self.children.items():
+ if value == cand_result:
+ return (value, self.message)
+ return (self.result, self.message)
+
+ @property
+ def result(self):
+ return self._result
+
+ @result.setter
+ def result(self, value):
+ if value not in status:
+ raise ValueError("'%s' not a valid result" % value)
+ self._result = value
+
+ @property
+ def message(self):
+ if self._message is not None:
+ return self._message
+ return self.description
+
+ @message.setter
+ def message(self, value):
+ self._message = value
+
+ def _finish_info(self, exc):
+ # return tuple of description, and value
+ if exc:
+ return (self.result_on_exception, self.message)
+ return self._childrens_finish_info()
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ (result, msg) = self._finish_info(exc_value)
+ if self.parent:
+ self.parent.children[self.name] = (result, msg)
+ if self.reporting_enabled:
+ report_finish_event(self.fullname, msg, result,
+ post_files=self.post_files)
+
+
+def _collect_file_info(files):
+ if not files:
+ return None
+ ret = []
+ for fname in files:
+ if not os.path.isfile(fname):
+ content = None
+ else:
+ with open(fname, "rb") as fp:
+ content = base64.b64encode(fp.read()).decode()
+ ret.append({'path': fname, 'content': content,
+ 'encoding': 'base64'})
+ return ret
+
+# vi: ts=4 expandtab syntax=python
diff --git a/cloudinit/reporting/handlers.py b/cloudinit/reporting/handlers.py
new file mode 100644
index 00000000..3212d173
--- /dev/null
+++ b/cloudinit/reporting/handlers.py
@@ -0,0 +1,91 @@
+# vi: ts=4 expandtab
+
+import abc
+import json
+import six
+
+from ..registry import DictRegistry
+from .. import (url_helper, util)
+from .. import log as logging
+
+
+LOG = logging.getLogger(__name__)
+
+
+@six.add_metaclass(abc.ABCMeta)
+class ReportingHandler(object):
+ """Base class for report handlers.
+
+ Implement :meth:`~publish_event` for controlling what
+ the handler does with an event.
+ """
+
+ @abc.abstractmethod
+ def publish_event(self, event):
+ """Publish an event."""
+
+
+class LogHandler(ReportingHandler):
+ """Publishes events to the cloud-init log at the ``DEBUG`` log level."""
+
+ def __init__(self, level="DEBUG"):
+ super(LogHandler, self).__init__()
+ if isinstance(level, int):
+ pass
+ else:
+ input_level = level
+ try:
+ level = getattr(logging, level.upper())
+ except:
+ LOG.warn("invalid level '%s', using WARN", input_level)
+ level = logging.WARN
+ self.level = level
+
+ def publish_event(self, event):
+ logger = logging.getLogger(
+ '.'.join(['cloudinit', 'reporting', event.event_type, event.name]))
+ logger.log(self.level, event.as_string())
+
+
+class PrintHandler(ReportingHandler):
+ """Print the event as a string."""
+
+ def publish_event(self, event):
+ print(event.as_string())
+
+
+class WebHookHandler(ReportingHandler):
+ def __init__(self, endpoint, consumer_key=None, token_key=None,
+ token_secret=None, consumer_secret=None, timeout=None,
+ retries=None):
+ super(WebHookHandler, self).__init__()
+
+ if any([consumer_key, token_key, token_secret, consumer_secret]):
+ self.oauth_helper = url_helper.OauthUrlHelper(
+ consumer_key=consumer_key, token_key=token_key,
+ token_secret=token_secret, consumer_secret=consumer_secret)
+ else:
+ self.oauth_helper = None
+ self.endpoint = endpoint
+ self.timeout = timeout
+ self.retries = retries
+ self.ssl_details = util.fetch_ssl_details()
+
+ def publish_event(self, event):
+ if self.oauth_helper:
+ readurl = self.oauth_helper.readurl
+ else:
+ readurl = url_helper.readurl
+ try:
+ return readurl(
+ self.endpoint, data=json.dumps(event.as_dict()),
+ timeout=self.timeout,
+ retries=self.retries, ssl_details=self.ssl_details)
+ except:
+ LOG.warn("failed posting event: %s" % event.as_string())
+
+
+available_handlers = DictRegistry()
+available_handlers.register_item('log', LogHandler)
+available_handlers.register_item('print', PrintHandler)
+available_handlers.register_item('webhook', WebHookHandler)