From 50bcb0f77d29a76a03946c6da13b15be25257402 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Mon, 31 Aug 2015 13:33:30 -0400 Subject: split 'events' portion of reporting into separate file this just separates events from other things that could conceivably be reported. --- tests/unittests/test_reporting.py | 121 +++++++++++++++++++------------------- 1 file changed, 61 insertions(+), 60 deletions(-) (limited to 'tests') diff --git a/tests/unittests/test_reporting.py b/tests/unittests/test_reporting.py index 66d4e87e..bb67ef73 100644 --- a/tests/unittests/test_reporting.py +++ b/tests/unittests/test_reporting.py @@ -5,6 +5,7 @@ from cloudinit import reporting from cloudinit.reporting import handlers +from cloudinit.reporting import events from .helpers import (mock, TestCase) @@ -16,12 +17,12 @@ def _fake_registry(): class TestReportStartEvent(TestCase): - @mock.patch('cloudinit.reporting.instantiated_handler_registry', + @mock.patch('cloudinit.reporting.events.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) + events.report_start_event(event_name, event_description) expected_string_representation = ': '.join( ['start', event_name, event_description]) for _, handler in ( @@ -33,9 +34,9 @@ class TestReportStartEvent(TestCase): class TestReportFinishEvent(TestCase): - def _report_finish_event(self, result=reporting.status.SUCCESS): + def _report_finish_event(self, result=events.status.SUCCESS): event_name, event_description = 'my_test_event', 'my description' - reporting.report_finish_event( + events.report_finish_event( event_name, event_description, result=result) return event_name, event_description @@ -46,39 +47,39 @@ class TestReportFinishEvent(TestCase): event = handler.publish_event.call_args[0][0] self.assertEqual(expected_as_string, event.as_string()) - @mock.patch('cloudinit.reporting.instantiated_handler_registry', + @mock.patch('cloudinit.reporting.events.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, reporting.status.SUCCESS, + ['finish', event_name, events.status.SUCCESS, event_description]) self.assertHandlersPassedObjectWithAsString( instantiated_handler_registry.registered_items, expected_string_representation) - @mock.patch('cloudinit.reporting.instantiated_handler_registry', + @mock.patch('cloudinit.reporting.events.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( - result=reporting.status.SUCCESS) + result=events.status.SUCCESS) expected_string_representation = ': '.join( - ['finish', event_name, reporting.status.SUCCESS, + ['finish', event_name, events.status.SUCCESS, event_description]) self.assertHandlersPassedObjectWithAsString( instantiated_handler_registry.registered_items, expected_string_representation) - @mock.patch('cloudinit.reporting.instantiated_handler_registry', + @mock.patch('cloudinit.reporting.events.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( - result=reporting.status.FAIL) + result=events.status.FAIL) expected_string_representation = ': '.join( - ['finish', event_name, reporting.status.FAIL, event_description]) + ['finish', event_name, events.status.FAIL, event_description]) self.assertHandlersPassedObjectWithAsString( instantiated_handler_registry.registered_items, expected_string_representation) @@ -91,14 +92,14 @@ 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) + event = events.ReportingEvent(event_type, name, description) expected_string_representation = ': '.join( [event_type, name, description]) self.assertEqual(expected_string_representation, event.as_string()) def test_as_dict(self): event_type, name, desc = 'test_type', 'test_name', 'test_desc' - event = reporting.ReportingEvent(event_type, name, desc) + event = events.ReportingEvent(event_type, name, desc) self.assertEqual( {'event_type': event_type, 'name': name, 'description': desc}, event.as_dict()) @@ -106,9 +107,9 @@ class TestReportingEvent(TestCase): class TestFinishReportingEvent(TestCase): def test_as_has_result(self): - result = reporting.status.SUCCESS + result = events.status.SUCCESS name, desc = 'test_name', 'test_desc' - event = reporting.FinishReportingEvent(name, desc, result) + event = events.FinishReportingEvent(name, desc, result) ret = event.as_dict() self.assertTrue('result' in ret) self.assertEqual(ret['result'], result) @@ -126,7 +127,7 @@ class TestLogHandler(TestCase): @mock.patch.object(reporting.handlers.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') + event = events.ReportingEvent(event_type, event_name, 'description') reporting.handlers.LogHandler().publish_event(event) self.assertEqual( [mock.call( @@ -135,13 +136,13 @@ class TestLogHandler(TestCase): @mock.patch.object(reporting.handlers.logging, 'getLogger') def test_single_log_message_at_info_published(self, getLogger): - event = reporting.ReportingEvent('type', 'name', 'description') + event = events.ReportingEvent('type', 'name', 'description') reporting.handlers.LogHandler().publish_event(event) self.assertEqual(1, getLogger.return_value.log.call_count) @mock.patch.object(reporting.handlers.logging, 'getLogger') def test_log_message_uses_event_as_string(self, getLogger): - event = reporting.ReportingEvent('type', 'name', 'description') + event = events.ReportingEvent('type', 'name', 'description') reporting.handlers.LogHandler(level="INFO").publish_event(event) self.assertIn(event.as_string(), getLogger.return_value.log.call_args[0][1]) @@ -232,49 +233,49 @@ class TestReportingConfiguration(TestCase): class TestReportingEventStack(TestCase): - @mock.patch('cloudinit.reporting.report_finish_event') - @mock.patch('cloudinit.reporting.report_start_event') + @mock.patch('cloudinit.reporting.events.report_finish_event') + @mock.patch('cloudinit.reporting.events.report_start_event') def test_start_and_finish_success(self, report_start, report_finish): - with reporting.ReportEventStack(name="myname", description="mydesc"): + with events.ReportEventStack(name="myname", description="mydesc"): pass self.assertEqual( [mock.call('myname', 'mydesc')], report_start.call_args_list) self.assertEqual( - [mock.call('myname', 'mydesc', reporting.status.SUCCESS)], + [mock.call('myname', 'mydesc', events.status.SUCCESS)], report_finish.call_args_list) - @mock.patch('cloudinit.reporting.report_finish_event') - @mock.patch('cloudinit.reporting.report_start_event') + @mock.patch('cloudinit.reporting.events.report_finish_event') + @mock.patch('cloudinit.reporting.events.report_start_event') def test_finish_exception_defaults_fail(self, report_start, report_finish): name = "myname" desc = "mydesc" try: - with reporting.ReportEventStack(name, description=desc): + with events.ReportEventStack(name, description=desc): raise ValueError("This didnt work") except ValueError: pass self.assertEqual([mock.call(name, desc)], report_start.call_args_list) self.assertEqual( - [mock.call(name, desc, reporting.status.FAIL)], + [mock.call(name, desc, events.status.FAIL)], report_finish.call_args_list) - @mock.patch('cloudinit.reporting.report_finish_event') - @mock.patch('cloudinit.reporting.report_start_event') + @mock.patch('cloudinit.reporting.events.report_finish_event') + @mock.patch('cloudinit.reporting.events.report_start_event') def test_result_on_exception_used(self, report_start, report_finish): name = "myname" desc = "mydesc" try: - with reporting.ReportEventStack( - name, desc, result_on_exception=reporting.status.WARN): + with events.ReportEventStack( + name, desc, result_on_exception=events.status.WARN): raise ValueError("This didnt work") except ValueError: pass self.assertEqual([mock.call(name, desc)], report_start.call_args_list) self.assertEqual( - [mock.call(name, desc, reporting.status.WARN)], + [mock.call(name, desc, events.status.WARN)], report_finish.call_args_list) - @mock.patch('cloudinit.reporting.report_start_event') + @mock.patch('cloudinit.reporting.events.report_start_event') def test_child_fullname_respects_parent(self, report_start): parent_name = "topname" c1_name = "c1name" @@ -282,59 +283,59 @@ class TestReportingEventStack(TestCase): c2_expected_fullname = '/'.join([parent_name, c1_name, c2_name]) c1_expected_fullname = '/'.join([parent_name, c1_name]) - parent = reporting.ReportEventStack(parent_name, "topdesc") - c1 = reporting.ReportEventStack(c1_name, "c1desc", parent=parent) - c2 = reporting.ReportEventStack(c2_name, "c2desc", parent=c1) + parent = events.ReportEventStack(parent_name, "topdesc") + c1 = events.ReportEventStack(c1_name, "c1desc", parent=parent) + c2 = events.ReportEventStack(c2_name, "c2desc", parent=c1) with c1: report_start.assert_called_with(c1_expected_fullname, "c1desc") with c2: report_start.assert_called_with(c2_expected_fullname, "c2desc") - @mock.patch('cloudinit.reporting.report_finish_event') - @mock.patch('cloudinit.reporting.report_start_event') + @mock.patch('cloudinit.reporting.events.report_finish_event') + @mock.patch('cloudinit.reporting.events.report_start_event') def test_child_result_bubbles_up(self, report_start, report_finish): - parent = reporting.ReportEventStack("topname", "topdesc") - child = reporting.ReportEventStack("c_name", "c_desc", parent=parent) + parent = events.ReportEventStack("topname", "topdesc") + child = events.ReportEventStack("c_name", "c_desc", parent=parent) with parent: with child: - child.result = reporting.status.WARN + child.result = events.status.WARN report_finish.assert_called_with( - "topname", "topdesc", reporting.status.WARN) + "topname", "topdesc", events.status.WARN) - @mock.patch('cloudinit.reporting.report_finish_event') + @mock.patch('cloudinit.reporting.events.report_finish_event') def test_message_used_in_finish(self, report_finish): - with reporting.ReportEventStack("myname", "mydesc", - message="mymessage"): + with events.ReportEventStack("myname", "mydesc", + message="mymessage"): pass self.assertEqual( - [mock.call("myname", "mymessage", reporting.status.SUCCESS)], + [mock.call("myname", "mymessage", events.status.SUCCESS)], report_finish.call_args_list) - @mock.patch('cloudinit.reporting.report_finish_event') + @mock.patch('cloudinit.reporting.events.report_finish_event') def test_message_updatable(self, report_finish): - with reporting.ReportEventStack("myname", "mydesc") as c: + with events.ReportEventStack("myname", "mydesc") as c: c.message = "all good" self.assertEqual( - [mock.call("myname", "all good", reporting.status.SUCCESS)], + [mock.call("myname", "all good", events.status.SUCCESS)], report_finish.call_args_list) - @mock.patch('cloudinit.reporting.report_start_event') - @mock.patch('cloudinit.reporting.report_finish_event') + @mock.patch('cloudinit.reporting.events.report_start_event') + @mock.patch('cloudinit.reporting.events.report_finish_event') def test_reporting_disabled_does_not_report_events( self, report_start, report_finish): - with reporting.ReportEventStack("a", "b", reporting_enabled=False): + with events.ReportEventStack("a", "b", reporting_enabled=False): pass self.assertEqual(report_start.call_count, 0) self.assertEqual(report_finish.call_count, 0) - @mock.patch('cloudinit.reporting.report_start_event') - @mock.patch('cloudinit.reporting.report_finish_event') + @mock.patch('cloudinit.reporting.events.report_start_event') + @mock.patch('cloudinit.reporting.events.report_finish_event') def test_reporting_child_default_to_parent( self, report_start, report_finish): - parent = reporting.ReportEventStack( + parent = events.ReportEventStack( "pname", "pdesc", reporting_enabled=False) - child = reporting.ReportEventStack("cname", "cdesc", parent=parent) + child = events.ReportEventStack("cname", "cdesc", parent=parent) with parent: with child: pass @@ -343,17 +344,17 @@ class TestReportingEventStack(TestCase): self.assertEqual(report_finish.call_count, 0) def test_reporting_event_has_sane_repr(self): - myrep = reporting.ReportEventStack("fooname", "foodesc", - reporting_enabled=True).__repr__() + myrep = events.ReportEventStack("fooname", "foodesc", + reporting_enabled=True).__repr__() self.assertIn("fooname", myrep) self.assertIn("foodesc", myrep) self.assertIn("True", myrep) def test_set_invalid_result_raises_value_error(self): - f = reporting.ReportEventStack("myname", "mydesc") + f = events.ReportEventStack("myname", "mydesc") self.assertRaises(ValueError, setattr, f, "result", "BOGUS") class TestStatusAccess(TestCase): def test_invalid_status_access_raises_value_error(self): - self.assertRaises(AttributeError, getattr, reporting.status, "BOGUS") + self.assertRaises(AttributeError, getattr, events.status, "BOGUS") -- cgit v1.2.3 From 7820a43baf94e11ce458476a442edd726a406aba Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Mon, 31 Aug 2015 13:57:05 -0400 Subject: events: add timestamp and origin, support file posting This adds 'timestamp' and 'origin' to events. The timestamp is simply that, a floating point timestamp of when the event occurred. The origin indicates the source / reporter of this. It is useful to have a single endpoint with multiple different things reporting to it. For example, MAAS will configure cloud-init and curtin to report to the same endpoint and then it can differenciate who made the post. Admittedly, they could use multiple endpoints, but this this seems sane. Also, add support for posting files at the close of an event. This is utilized in curtin to post a log file when the install is done. files are posted on success or fail of the event. --- cloudinit/reporting/events.py | 58 +++++++++++++++++++++++++++++++-------- tests/unittests/test_reporting.py | 15 ++++++---- 2 files changed, 56 insertions(+), 17 deletions(-) (limited to 'tests') diff --git a/cloudinit/reporting/events.py b/cloudinit/reporting/events.py index e35e41dd..2f767f64 100644 --- a/cloudinit/reporting/events.py +++ b/cloudinit/reporting/events.py @@ -2,17 +2,22 @@ # This file is part of cloud-init. See LICENCE file for license information. # """ -cloud-init events +events for reporting. -Report events in a structured manner. -The events here are most likely used via 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): @@ -27,10 +32,13 @@ status = _nameset(("SUCCESS", "WARN", "FAIL")) class ReportingEvent(object): """Encapsulation of event formatting.""" - def __init__(self, event_type, name, description): + 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.""" @@ -40,15 +48,20 @@ class ReportingEvent(object): def as_dict(self): """The event represented as a dictionary.""" return {'name': self.name, 'description': self.description, - 'event_type': self.event_type} + 'event_type': self.event_type, 'origin': self.origin, + 'timestamp': self.timestamp} class FinishReportingEvent(ReportingEvent): - def __init__(self, name, description, result=status.SUCCESS): + 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) @@ -60,6 +73,8 @@ class FinishReportingEvent(ReportingEvent): """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 @@ -78,12 +93,13 @@ def report_event(event): def report_finish_event(event_name, event_description, - result=status.SUCCESS): + 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) + event = FinishReportingEvent(event_name, event_description, result, + post_files=post_files) return report_event(event) @@ -133,13 +149,17 @@ class ReportEventStack(object): value is FAIL. """ def __init__(self, name, description, message=None, parent=None, - reporting_enabled=None, result_on_exception=status.FAIL): + 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: @@ -205,6 +225,22 @@ class ReportEventStack(object): if self.parent: self.parent.children[self.name] = (result, msg) if self.reporting_enabled: - report_finish_event(self.fullname, msg, result) + 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 +# vi: ts=4 expandtab syntax=python diff --git a/tests/unittests/test_reporting.py b/tests/unittests/test_reporting.py index bb67ef73..0a441adf 100644 --- a/tests/unittests/test_reporting.py +++ b/tests/unittests/test_reporting.py @@ -241,7 +241,8 @@ class TestReportingEventStack(TestCase): self.assertEqual( [mock.call('myname', 'mydesc')], report_start.call_args_list) self.assertEqual( - [mock.call('myname', 'mydesc', events.status.SUCCESS)], + [mock.call('myname', 'mydesc', events.status.SUCCESS, + post_files=[])], report_finish.call_args_list) @mock.patch('cloudinit.reporting.events.report_finish_event') @@ -256,7 +257,7 @@ class TestReportingEventStack(TestCase): pass self.assertEqual([mock.call(name, desc)], report_start.call_args_list) self.assertEqual( - [mock.call(name, desc, events.status.FAIL)], + [mock.call(name, desc, events.status.FAIL, post_files=[])], report_finish.call_args_list) @mock.patch('cloudinit.reporting.events.report_finish_event') @@ -272,7 +273,7 @@ class TestReportingEventStack(TestCase): pass self.assertEqual([mock.call(name, desc)], report_start.call_args_list) self.assertEqual( - [mock.call(name, desc, events.status.WARN)], + [mock.call(name, desc, events.status.WARN, post_files=[])], report_finish.call_args_list) @mock.patch('cloudinit.reporting.events.report_start_event') @@ -301,7 +302,7 @@ class TestReportingEventStack(TestCase): child.result = events.status.WARN report_finish.assert_called_with( - "topname", "topdesc", events.status.WARN) + "topname", "topdesc", events.status.WARN, post_files=[]) @mock.patch('cloudinit.reporting.events.report_finish_event') def test_message_used_in_finish(self, report_finish): @@ -309,7 +310,8 @@ class TestReportingEventStack(TestCase): message="mymessage"): pass self.assertEqual( - [mock.call("myname", "mymessage", events.status.SUCCESS)], + [mock.call("myname", "mymessage", events.status.SUCCESS, + post_files=[])], report_finish.call_args_list) @mock.patch('cloudinit.reporting.events.report_finish_event') @@ -317,7 +319,8 @@ class TestReportingEventStack(TestCase): with events.ReportEventStack("myname", "mydesc") as c: c.message = "all good" self.assertEqual( - [mock.call("myname", "all good", events.status.SUCCESS)], + [mock.call("myname", "all good", events.status.SUCCESS, + post_files=[])], report_finish.call_args_list) @mock.patch('cloudinit.reporting.events.report_start_event') -- cgit v1.2.3 From cba8282acc1c957698480bae2d0c2032b884d80d Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Wed, 2 Sep 2015 16:44:51 -0400 Subject: fix test_as_dict --- tests/unittests/test_reporting.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) (limited to 'tests') diff --git a/tests/unittests/test_reporting.py b/tests/unittests/test_reporting.py index 0a441adf..32356ef9 100644 --- a/tests/unittests/test_reporting.py +++ b/tests/unittests/test_reporting.py @@ -100,9 +100,15 @@ class TestReportingEvent(TestCase): def test_as_dict(self): event_type, name, desc = 'test_type', 'test_name', 'test_desc' event = events.ReportingEvent(event_type, name, desc) - self.assertEqual( - {'event_type': event_type, 'name': name, 'description': desc}, - event.as_dict()) + expected = {'event_type': event_type, 'name': name, + 'description': desc, 'origin': 'cloudinit'} + + # allow for timestamp to differ, but must be present + as_dict = event.as_dict() + self.assertIn('timestamp', as_dict) + del as_dict['timestamp'] + + self.assertEqual(expected, as_dict) class TestFinishReportingEvent(TestCase): -- cgit v1.2.3