1
# Copyright (C) 2015 Canonical Ltd.
3
# Author: Scott Moser <scott.moser@canonical.com>
5
# Curtin is free software: you can redistribute it and/or modify it under
6
# the terms of the GNU Affero General Public License as published by the
7
# Free Software Foundation, either version 3 of the License, or (at your
8
# option) any later version.
10
# Curtin is distributed in the hope that it will be useful, but WITHOUT ANY
11
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
15
# You should have received a copy of the GNU Affero General Public License
16
# along with Curtin. If not, see <http://www.gnu.org/licenses/>.
18
cloud-init reporting framework
20
The reporting framework is intended to allow all parts of cloud-init to
21
report events in a structured manner.
27
from . import instantiated_handler_registry
29
FINISH_EVENT_TYPE = 'finish'
30
START_EVENT_TYPE = 'start'
31
RESULT_EVENT_TYPE = 'result'
33
DEFAULT_EVENT_ORIGIN = 'curtin'
37
def __getattr__(self, name):
40
raise AttributeError("%s not a valid value" % name)
43
status = _nameset(("SUCCESS", "WARN", "FAIL"))
46
class ReportingEvent(object):
47
"""Encapsulation of event formatting."""
49
def __init__(self, event_type, name, description,
50
origin=DEFAULT_EVENT_ORIGIN, timestamp=None,
52
self.event_type = event_type
54
self.description = description
57
timestamp = time.time()
58
self.timestamp = timestamp
64
"""The event represented as a string."""
65
return '{0}: {1}: {2}'.format(
66
self.event_type, self.name, self.description)
69
"""The event represented as a dictionary."""
70
return {'name': self.name, 'description': self.description,
71
'event_type': self.event_type, 'origin': self.origin,
72
'timestamp': self.timestamp, 'level': self.level}
75
class FinishReportingEvent(ReportingEvent):
77
def __init__(self, name, description, result=status.SUCCESS,
78
post_files=None, level=None):
79
super(FinishReportingEvent, self).__init__(
80
FINISH_EVENT_TYPE, name, description, level=level)
82
if post_files is None:
84
self.post_files = post_files
85
if result not in status:
86
raise ValueError("Invalid result: %s" % result)
87
if self.result == status.WARN:
89
elif self.result == status.FAIL:
93
return '{0}: {1}: {2}: {3}'.format(
94
self.event_type, self.name, self.result, self.description)
97
"""The event represented as json friendly."""
98
data = super(FinishReportingEvent, self).as_dict()
99
data['result'] = self.result
101
data['files'] = _collect_file_info(self.post_files)
105
def report_event(event):
106
"""Report an event to all registered event handlers.
108
This should generally be called via one of the other functions in
109
the reporting module.
112
The type of the event; this should be a constant from the
115
for _, handler in instantiated_handler_registry.registered_items.items():
116
handler.publish_event(event)
119
def report_finish_event(event_name, event_description,
120
result=status.SUCCESS, post_files=None, level=None):
121
"""Report a "finish" event.
123
See :py:func:`.report_event` for parameter details.
125
event = FinishReportingEvent(event_name, event_description, result,
126
post_files=post_files, level=level)
127
return report_event(event)
130
def report_start_event(event_name, event_description, level=None):
131
"""Report a "start" event.
134
The name of the event; this should be a topic which events would
135
share (e.g. it will be the same for start and finish events).
137
:param event_description:
138
A human-readable description of the event that has occurred.
140
event = ReportingEvent(START_EVENT_TYPE, event_name, event_description,
142
return report_event(event)
145
class ReportEventStack(object):
146
"""Context Manager for using :py:func:`report_event`
148
This enables calling :py:func:`report_start_event` and
149
:py:func:`report_finish_event` through a context manager.
152
the name of the event
155
the event's description, passed on to :py:func:`report_start_event`
158
the description to use for the finish event. defaults to
162
:type parent: :py:class:ReportEventStack or None
163
The parent of this event. The parent is populated with
164
results of all its children. The name used in reporting
165
is <parent.name>/<name>
167
:param reporting_enabled:
168
Indicates if reporting events should be generated.
169
If not provided, defaults to the parent's value, or True if no parent
172
:param result_on_exception:
173
The result value to set if an exception is caught. default
177
The priority level of the enter and exit messages sent. Default value
180
def __init__(self, name, description, message=None, parent=None,
181
reporting_enabled=None, result_on_exception=status.FAIL,
182
post_files=None, level="INFO"):
185
self.description = description
186
self.message = message
187
self.result_on_exception = result_on_exception
188
self.result = status.SUCCESS
190
if post_files is None:
192
self.post_files = post_files
194
# use parents reporting value if not provided
195
if reporting_enabled is None:
197
reporting_enabled = parent.reporting_enabled
199
reporting_enabled = True
200
self.reporting_enabled = reporting_enabled
203
self.fullname = '/'.join((parent.fullname, name,))
205
self.fullname = self.name
209
return ("ReportEventStack(%s, %s, reporting_enabled=%s)" %
210
(self.name, self.description, self.reporting_enabled))
213
self.result = status.SUCCESS
214
if self.reporting_enabled:
215
report_start_event(self.fullname, self.description,
218
self.parent.children[self.name] = (None, None)
221
def _childrens_finish_info(self):
222
for cand_result in (status.FAIL, status.WARN):
223
for name, (value, msg) in self.children.items():
224
if value == cand_result:
225
return (value, self.message)
226
return (self.result, self.message)
233
def result(self, value):
234
if value not in status:
235
raise ValueError("'%s' not a valid result" % value)
240
if self._message is not None:
242
return self.description
245
def message(self, value):
246
self._message = value
248
def _finish_info(self, exc):
249
# return tuple of description, and value
250
# explicitly handle sys.exit(0) as not an error
251
if exc and not(isinstance(exc, SystemExit) and exc.code == 0):
252
return (self.result_on_exception, self.message)
253
return self._childrens_finish_info()
255
def __exit__(self, exc_type, exc_value, traceback):
256
(result, msg) = self._finish_info(exc_value)
258
self.parent.children[self.name] = (result, msg)
259
if self.reporting_enabled:
260
report_finish_event(self.fullname, msg, result,
261
post_files=self.post_files, level=self.level)
264
def _collect_file_info(files):
269
if not os.path.isfile(fname):
272
with open(fname, "rb") as fp:
273
content = base64.b64encode(fp.read()).decode()
274
ret.append({'path': fname, 'content': content,
275
'encoding': 'base64'})
278
# vi: ts=4 expandtab syntax=python