diff options
Diffstat (limited to 'cloudinit/reporting')
| -rw-r--r-- | cloudinit/reporting/__init__.py | 240 | ||||
| -rw-r--r-- | cloudinit/reporting/handlers.py | 90 | 
2 files changed, 330 insertions, 0 deletions
| diff --git a/cloudinit/reporting/__init__.py b/cloudinit/reporting/__init__.py new file mode 100644 index 00000000..e23fab32 --- /dev/null +++ b/cloudinit/reporting/__init__.py @@ -0,0 +1,240 @@ +# Copyright 2015 Canonical Ltd. +# This file is part of cloud-init.  See LICENCE file for license information. +# +# vi: ts=4 expandtab +""" +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 ..reporting.handlers import available_handlers + + +FINISH_EVENT_TYPE = 'finish' +START_EVENT_TYPE = 'start' + +DEFAULT_CONFIG = { +    'logging': {'type': 'log'}, +} + + +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): +        self.event_type = event_type +        self.name = name +        self.description = description + +    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} + + +class FinishReportingEvent(ReportingEvent): + +    def __init__(self, name, description, result=status.SUCCESS): +        super(FinishReportingEvent, self).__init__( +            FINISH_EVENT_TYPE, name, description) +        self.result = result +        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 +        return data + + +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 +        registered = instantiated_handler_registry.registered_items +        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) + + +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): +    """Report a "finish" event. + +    See :py:func:`.report_event` for parameter details. +    """ +    event = FinishReportingEvent(event_name, event_description, result) +    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): +        self.parent = parent +        self.name = name +        self.description = description +        self.message = message +        self.result_on_exception = result_on_exception +        self.result = status.SUCCESS + +        # 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) + + +instantiated_handler_registry = DictRegistry() +update_configuration(DEFAULT_CONFIG) diff --git a/cloudinit/reporting/handlers.py b/cloudinit/reporting/handlers.py new file mode 100644 index 00000000..1343311f --- /dev/null +++ b/cloudinit/reporting/handlers.py @@ -0,0 +1,90 @@ +# vi: ts=4 expandtab + +import abc +import oauthlib.oauth1 as oauth1 +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 to the ``INFO`` log level.""" + + +class LogHandler(ReportingHandler): +    """Publishes events to the cloud-init log at the ``INFO`` log level.""" + +    def __init__(self, level="DEBUG"): +        super(LogHandler, self).__init__() +        if isinstance(level, int): +            pass +        else: +            input_level = level +            try: +                level = gettattr(logging, level.upper()) +            except: +                LOG.warn("invalid level '%s', using WARN", input_level) +                level = logging.WARN +        self.level = level + +    def publish_event(self, event): +        """Publish an event to the ``INFO`` log level.""" +        logger = logging.getLogger( +            '.'.join(['cloudinit', 'reporting', event.event_type, event.name])) +        logger.log(self.level, event.as_string()) + + +class PrintHandler(ReportingHandler): +    def publish_event(self, event): +        """Publish an event to the ``INFO`` log level.""" + + +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=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) | 
