Commit 7820a43b authored by Scott Moser's avatar Scott Moser

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.
parent 50bcb0f7
......@@ -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
......@@ -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')
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment