From c33b3becebfa7bf3f6e2ee67ea7bc3def6feeb8c Mon Sep 17 00:00:00 2001
From: Scott Moser <smoser@ubuntu.com>
Date: Tue, 28 Jul 2015 16:15:10 -0400
Subject: pull from 2.0 trunk @ a433358bbcf4e8a771b80cae34468409ed5a811d

---
 cloudinit/registry.py             |  23 +++++
 cloudinit/reporting.py            | 122 ++++++++++++++++++++++++
 tests/unittests/test_registry.py  |  28 ++++++
 tests/unittests/test_reporting.py | 192 ++++++++++++++++++++++++++++++++++++++
 4 files changed, 365 insertions(+)
 create mode 100644 cloudinit/registry.py
 create mode 100644 cloudinit/reporting.py
 create mode 100644 tests/unittests/test_registry.py
 create mode 100644 tests/unittests/test_reporting.py

diff --git a/cloudinit/registry.py b/cloudinit/registry.py
new file mode 100644
index 00000000..46cf0585
--- /dev/null
+++ b/cloudinit/registry.py
@@ -0,0 +1,23 @@
+import copy
+
+
+class DictRegistry(object):
+    """A simple registry for a mapping of objects."""
+
+    def __init__(self):
+        self._items = {}
+
+    def register_item(self, key, item):
+        """Add item to the registry."""
+        if key in self._items:
+            raise ValueError(
+                'Item already registered with key {0}'.format(key))
+        self._items[key] = item
+
+    @property
+    def registered_items(self):
+        """All the items that have been registered.
+
+        This cannot be used to modify the contents of the registry.
+        """
+        return copy.copy(self._items)
diff --git a/cloudinit/reporting.py b/cloudinit/reporting.py
new file mode 100644
index 00000000..d2dd4fec
--- /dev/null
+++ b/cloudinit/reporting.py
@@ -0,0 +1,122 @@
+# 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.
+"""
+
+import abc
+import logging
+
+from cloudinit.registry import DictRegistry
+
+
+FINISH_EVENT_TYPE = 'finish'
+START_EVENT_TYPE = 'start'
+
+DEFAULT_CONFIG = {
+    'logging': {'type': 'log'},
+}
+
+
+instantiated_handler_registry = DictRegistry()
+available_handlers = DictRegistry()
+
+
+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)
+
+
+class FinishReportingEvent(ReportingEvent):
+
+    def __init__(self, name, description, successful=None):
+        super(FinishReportingEvent, self).__init__(
+            FINISH_EVENT_TYPE, name, description)
+        self.successful = successful
+
+    def as_string(self):
+        if self.successful is None:
+            return super(FinishReportingEvent, self).as_string()
+        success_string = 'success' if self.successful else 'fail'
+        return '{0}: {1}: {2}: {3}'.format(
+            self.event_type, self.name, success_string, self.description)
+
+
+class ReportingHandler(object):
+
+    @abc.abstractmethod
+    def publish_event(self, event):
+        raise NotImplementedError
+
+
+class LogHandler(ReportingHandler):
+    """Publishes events to the cloud-init log at the ``INFO`` log level."""
+
+    def publish_event(self, event):
+        """Publish an event to the ``INFO`` log level."""
+        logger = logging.getLogger(
+            '.'.join([__name__, event.event_type, event.name]))
+        logger.info(event.as_string())
+
+
+def add_configuration(config):
+    for handler_name, handler_config in config.items():
+        handler_config = handler_config.copy()
+        cls = available_handlers.registered_items[handler_config.pop('type')]
+        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, successful=None):
+    """Report a "finish" event.
+
+    See :py:func:`.report_event` for parameter details.
+    """
+    event = FinishReportingEvent(event_name, event_description, successful)
+    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)
+
+
+available_handlers.register_item('log', LogHandler)
+add_configuration(DEFAULT_CONFIG)
diff --git a/tests/unittests/test_registry.py b/tests/unittests/test_registry.py
new file mode 100644
index 00000000..bcf01475
--- /dev/null
+++ b/tests/unittests/test_registry.py
@@ -0,0 +1,28 @@
+from cloudinit.registry import DictRegistry
+
+from .helpers import (mock, TestCase)
+
+
+class TestDictRegistry(TestCase):
+
+    def test_added_item_included_in_output(self):
+        registry = DictRegistry()
+        item_key, item_to_register = 'test_key', mock.Mock()
+        registry.register_item(item_key, item_to_register)
+        self.assertEqual({item_key: item_to_register},
+                         registry.registered_items)
+
+    def test_registry_starts_out_empty(self):
+        self.assertEqual({}, DictRegistry().registered_items)
+
+    def test_modifying_registered_items_isnt_exposed_to_other_callers(self):
+        registry = DictRegistry()
+        registry.registered_items['test_item'] = mock.Mock()
+        self.assertEqual({}, registry.registered_items)
+
+    def test_keys_cannot_be_replaced(self):
+        registry = DictRegistry()
+        item_key = 'test_key'
+        registry.register_item(item_key, mock.Mock())
+        self.assertRaises(ValueError,
+                          registry.register_item, item_key, mock.Mock())
diff --git a/tests/unittests/test_reporting.py b/tests/unittests/test_reporting.py
new file mode 100644
index 00000000..f4011a79
--- /dev/null
+++ b/tests/unittests/test_reporting.py
@@ -0,0 +1,192 @@
+# Copyright 2015 Canonical Ltd.
+# This file is part of cloud-init.  See LICENCE file for license information.
+#
+# vi: ts=4 expandtab
+
+from cloudinit import reporting
+
+from .helpers import (mock, TestCase)
+
+
+def _fake_registry():
+    return mock.Mock(registered_items={'a': mock.MagicMock(),
+                                       'b': mock.MagicMock()})
+
+
+class TestReportStartEvent(TestCase):
+
+    @mock.patch('cloudinit.reporting.instantiated_handler_registry',
+                new_callable=_fake_registry)
+    def test_report_start_event_passes_something_with_as_string_to_handlers(
+            self, instantiated_handler_registry):
+        event_name, event_description = 'my_test_event', 'my description'
+        reporting.report_start_event(event_name, event_description)
+        expected_string_representation = ': '.join(
+            ['start', event_name, event_description])
+        for _, handler in (
+                instantiated_handler_registry.registered_items.items()):
+            self.assertEqual(1, handler.publish_event.call_count)
+            event = handler.publish_event.call_args[0][0]
+            self.assertEqual(expected_string_representation, event.as_string())
+
+
+class TestReportFinishEvent(TestCase):
+
+    def _report_finish_event(self, successful=None):
+        event_name, event_description = 'my_test_event', 'my description'
+        reporting.report_finish_event(
+            event_name, event_description, successful=successful)
+        return event_name, event_description
+
+    def assertHandlersPassedObjectWithAsString(
+            self, handlers, expected_as_string):
+        for _, handler in handlers.items():
+            self.assertEqual(1, handler.publish_event.call_count)
+            event = handler.publish_event.call_args[0][0]
+            self.assertEqual(expected_as_string, event.as_string())
+
+    @mock.patch('cloudinit.reporting.instantiated_handler_registry',
+                new_callable=_fake_registry)
+    def test_report_finish_event_passes_something_with_as_string_to_handlers(
+            self, instantiated_handler_registry):
+        event_name, event_description = self._report_finish_event()
+        expected_string_representation = ': '.join(
+            ['finish', event_name, event_description])
+        self.assertHandlersPassedObjectWithAsString(
+            instantiated_handler_registry.registered_items,
+            expected_string_representation)
+
+    @mock.patch('cloudinit.reporting.instantiated_handler_registry',
+                new_callable=_fake_registry)
+    def test_reporting_successful_finish_has_sensible_string_repr(
+            self, instantiated_handler_registry):
+        event_name, event_description = self._report_finish_event(
+            successful=True)
+        expected_string_representation = ': '.join(
+            ['finish', event_name, 'success', event_description])
+        self.assertHandlersPassedObjectWithAsString(
+            instantiated_handler_registry.registered_items,
+            expected_string_representation)
+
+    @mock.patch('cloudinit.reporting.instantiated_handler_registry',
+                new_callable=_fake_registry)
+    def test_reporting_unsuccessful_finish_has_sensible_string_repr(
+            self, instantiated_handler_registry):
+        event_name, event_description = self._report_finish_event(
+            successful=False)
+        expected_string_representation = ': '.join(
+            ['finish', event_name, 'fail', event_description])
+        self.assertHandlersPassedObjectWithAsString(
+            instantiated_handler_registry.registered_items,
+            expected_string_representation)
+
+
+class TestReportingEvent(TestCase):
+
+    def test_as_string(self):
+        event_type, name, description = 'test_type', 'test_name', 'test_desc'
+        event = reporting.ReportingEvent(event_type, name, description)
+        expected_string_representation = ': '.join(
+            [event_type, name, description])
+        self.assertEqual(expected_string_representation, event.as_string())
+
+
+class TestReportingHandler(TestCase):
+
+    def test_no_default_publish_event_implementation(self):
+        self.assertRaises(NotImplementedError,
+                          reporting.ReportingHandler().publish_event, None)
+
+
+class TestLogHandler(TestCase):
+
+    @mock.patch.object(reporting.logging, 'getLogger')
+    def test_appropriate_logger_used(self, getLogger):
+        event_type, event_name = 'test_type', 'test_name'
+        event = reporting.ReportingEvent(event_type, event_name, 'description')
+        reporting.LogHandler().publish_event(event)
+        self.assertEqual(
+            [mock.call(
+                'cloudinit.reporting.{0}.{1}'.format(event_type, event_name))],
+            getLogger.call_args_list)
+
+    @mock.patch.object(reporting.logging, 'getLogger')
+    def test_single_log_message_at_info_published(self, getLogger):
+        event = reporting.ReportingEvent('type', 'name', 'description')
+        reporting.LogHandler().publish_event(event)
+        self.assertEqual(1, getLogger.return_value.info.call_count)
+
+    @mock.patch.object(reporting.logging, 'getLogger')
+    def test_log_message_uses_event_as_string(self, getLogger):
+        event = reporting.ReportingEvent('type', 'name', 'description')
+        reporting.LogHandler().publish_event(event)
+        self.assertIn(event.as_string(),
+                      getLogger.return_value.info.call_args[0][0])
+
+
+class TestDefaultRegisteredHandler(TestCase):
+
+    def test_log_handler_registered_by_default(self):
+        registered_items = (
+            reporting.instantiated_handler_registry.registered_items)
+        for _, item in registered_items.items():
+            if isinstance(item, reporting.LogHandler):
+                break
+        else:
+            self.fail('No reporting LogHandler registered by default.')
+
+
+class TestReportingConfiguration(TestCase):
+
+    @mock.patch.object(reporting, 'instantiated_handler_registry')
+    def test_empty_configuration_doesnt_add_handlers(
+            self, instantiated_handler_registry):
+        reporting.add_configuration({})
+        self.assertEqual(
+            0, instantiated_handler_registry.register_item.call_count)
+
+    @mock.patch.object(
+        reporting, 'instantiated_handler_registry', reporting.DictRegistry())
+    @mock.patch.object(reporting, 'available_handlers')
+    def test_looks_up_handler_by_type_and_adds_it(self, available_handlers):
+        handler_type_name = 'test_handler'
+        handler_cls = mock.Mock()
+        available_handlers.registered_items = {handler_type_name: handler_cls}
+        handler_name = 'my_test_handler'
+        reporting.add_configuration(
+            {handler_name: {'type': handler_type_name}})
+        self.assertEqual(
+            {handler_name: handler_cls.return_value},
+            reporting.instantiated_handler_registry.registered_items)
+
+    @mock.patch.object(
+        reporting, 'instantiated_handler_registry', reporting.DictRegistry())
+    @mock.patch.object(reporting, 'available_handlers')
+    def test_uses_non_type_parts_of_config_dict_as_kwargs(
+            self, available_handlers):
+        handler_type_name = 'test_handler'
+        handler_cls = mock.Mock()
+        available_handlers.registered_items = {handler_type_name: handler_cls}
+        extra_kwargs = {'foo': 'bar', 'bar': 'baz'}
+        handler_config = extra_kwargs.copy()
+        handler_config.update({'type': handler_type_name})
+        handler_name = 'my_test_handler'
+        reporting.add_configuration({handler_name: handler_config})
+        self.assertEqual(
+            handler_cls.return_value,
+            reporting.instantiated_handler_registry.registered_items[
+                handler_name])
+        self.assertEqual([mock.call(**extra_kwargs)],
+                         handler_cls.call_args_list)
+
+    @mock.patch.object(
+        reporting, 'instantiated_handler_registry', reporting.DictRegistry())
+    @mock.patch.object(reporting, 'available_handlers')
+    def test_handler_config_not_modified(self, available_handlers):
+        handler_type_name = 'test_handler'
+        handler_cls = mock.Mock()
+        available_handlers.registered_items = {handler_type_name: handler_cls}
+        handler_config = {'type': handler_type_name, 'foo': 'bar'}
+        expected_handler_config = handler_config.copy()
+        reporting.add_configuration({'my_test_handler': handler_config})
+        self.assertEqual(expected_handler_config, handler_config)
-- 
cgit v1.2.3