diff options
| author | Scott Moser <smoser@ubuntu.com> | 2015-07-28 16:15:10 -0400 | 
|---|---|---|
| committer | Scott Moser <smoser@ubuntu.com> | 2015-07-28 16:15:10 -0400 | 
| commit | c33b3becebfa7bf3f6e2ee67ea7bc3def6feeb8c (patch) | |
| tree | 19a41d3160b3536e212342154f3c10e0f35c62ec | |
| parent | 55472eb02eaa5b88676a96e006f6838020f8ffe3 (diff) | |
| download | vyos-cloud-init-c33b3becebfa7bf3f6e2ee67ea7bc3def6feeb8c.tar.gz vyos-cloud-init-c33b3becebfa7bf3f6e2ee67ea7bc3def6feeb8c.zip | |
pull from 2.0 trunk @ a433358bbcf4e8a771b80cae34468409ed5a811d
| -rw-r--r-- | cloudinit/registry.py | 23 | ||||
| -rw-r--r-- | cloudinit/reporting.py | 122 | ||||
| -rw-r--r-- | tests/unittests/test_registry.py | 28 | ||||
| -rw-r--r-- | tests/unittests/test_reporting.py | 192 | 
4 files changed, 365 insertions, 0 deletions
| 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) | 
