~curtin-dev/curtin/trunk

« back to all changes in this revision

Viewing changes to curtin/reporter/events.py

  • Committer: Scott Moser
  • Date: 2017-12-20 17:33:03 UTC
  • Revision ID: smoser@ubuntu.com-20171220173303-29gha5qb8wpqrd40
README: Mention move of revision control to git.

curtin development has moved its revision control to git.
It is available at
  https://code.launchpad.net/curtin

Clone with
  git clone https://git.launchpad.net/curtin
or
  git clone git+ssh://git.launchpad.net/curtin

For more information see
  http://curtin.readthedocs.io/en/latest/topics/development.html

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#   Copyright (C) 2015 Canonical Ltd.
2
 
#
3
 
#   Author: Scott Moser <scott.moser@canonical.com>
4
 
#
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.
9
 
#
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
13
 
#   more details.
14
 
#
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/>.
17
 
"""
18
 
cloud-init reporting framework
19
 
 
20
 
The reporting framework is intended to allow all parts of cloud-init to
21
 
report events in a structured manner.
22
 
"""
23
 
import base64
24
 
import os.path
25
 
import time
26
 
 
27
 
from . import instantiated_handler_registry
28
 
 
29
 
FINISH_EVENT_TYPE = 'finish'
30
 
START_EVENT_TYPE = 'start'
31
 
RESULT_EVENT_TYPE = 'result'
32
 
 
33
 
DEFAULT_EVENT_ORIGIN = 'curtin'
34
 
 
35
 
 
36
 
class _nameset(set):
37
 
    def __getattr__(self, name):
38
 
        if name in self:
39
 
            return name
40
 
        raise AttributeError("%s not a valid value" % name)
41
 
 
42
 
 
43
 
status = _nameset(("SUCCESS", "WARN", "FAIL"))
44
 
 
45
 
 
46
 
class ReportingEvent(object):
47
 
    """Encapsulation of event formatting."""
48
 
 
49
 
    def __init__(self, event_type, name, description,
50
 
                 origin=DEFAULT_EVENT_ORIGIN, timestamp=None,
51
 
                 level=None):
52
 
        self.event_type = event_type
53
 
        self.name = name
54
 
        self.description = description
55
 
        self.origin = origin
56
 
        if timestamp is None:
57
 
            timestamp = time.time()
58
 
        self.timestamp = timestamp
59
 
        if level is None:
60
 
            level = "INFO"
61
 
        self.level = level
62
 
 
63
 
    def as_string(self):
64
 
        """The event represented as a string."""
65
 
        return '{0}: {1}: {2}'.format(
66
 
            self.event_type, self.name, self.description)
67
 
 
68
 
    def as_dict(self):
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}
73
 
 
74
 
 
75
 
class FinishReportingEvent(ReportingEvent):
76
 
 
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)
81
 
        self.result = result
82
 
        if post_files is None:
83
 
            post_files = []
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:
88
 
            self.level = "WARN"
89
 
        elif self.result == status.FAIL:
90
 
            self.level = "ERROR"
91
 
 
92
 
    def as_string(self):
93
 
        return '{0}: {1}: {2}: {3}'.format(
94
 
            self.event_type, self.name, self.result, self.description)
95
 
 
96
 
    def as_dict(self):
97
 
        """The event represented as json friendly."""
98
 
        data = super(FinishReportingEvent, self).as_dict()
99
 
        data['result'] = self.result
100
 
        if self.post_files:
101
 
            data['files'] = _collect_file_info(self.post_files)
102
 
        return data
103
 
 
104
 
 
105
 
def report_event(event):
106
 
    """Report an event to all registered event handlers.
107
 
 
108
 
    This should generally be called via one of the other functions in
109
 
    the reporting module.
110
 
 
111
 
    :param event_type:
112
 
        The type of the event; this should be a constant from the
113
 
        reporting module.
114
 
    """
115
 
    for _, handler in instantiated_handler_registry.registered_items.items():
116
 
        handler.publish_event(event)
117
 
 
118
 
 
119
 
def report_finish_event(event_name, event_description,
120
 
                        result=status.SUCCESS, post_files=None, level=None):
121
 
    """Report a "finish" event.
122
 
 
123
 
    See :py:func:`.report_event` for parameter details.
124
 
    """
125
 
    event = FinishReportingEvent(event_name, event_description, result,
126
 
                                 post_files=post_files, level=level)
127
 
    return report_event(event)
128
 
 
129
 
 
130
 
def report_start_event(event_name, event_description, level=None):
131
 
    """Report a "start" event.
132
 
 
133
 
    :param event_name:
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).
136
 
 
137
 
    :param event_description:
138
 
        A human-readable description of the event that has occurred.
139
 
    """
140
 
    event = ReportingEvent(START_EVENT_TYPE, event_name, event_description,
141
 
                           level=level)
142
 
    return report_event(event)
143
 
 
144
 
 
145
 
class ReportEventStack(object):
146
 
    """Context Manager for using :py:func:`report_event`
147
 
 
148
 
    This enables calling :py:func:`report_start_event` and
149
 
    :py:func:`report_finish_event` through a context manager.
150
 
 
151
 
    :param name:
152
 
        the name of the event
153
 
 
154
 
    :param description:
155
 
        the event's description, passed on to :py:func:`report_start_event`
156
 
 
157
 
    :param message:
158
 
        the description to use for the finish event. defaults to
159
 
        :param:description.
160
 
 
161
 
    :param parent:
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>
166
 
 
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
170
 
        is provided.
171
 
 
172
 
    :param result_on_exception:
173
 
        The result value to set if an exception is caught. default
174
 
        value is FAIL.
175
 
 
176
 
    :param level:
177
 
        The priority level of the enter and exit messages sent. Default value
178
 
        is INFO.
179
 
    """
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"):
183
 
        self.parent = parent
184
 
        self.name = name
185
 
        self.description = description
186
 
        self.message = message
187
 
        self.result_on_exception = result_on_exception
188
 
        self.result = status.SUCCESS
189
 
        self.level = level
190
 
        if post_files is None:
191
 
            post_files = []
192
 
        self.post_files = post_files
193
 
 
194
 
        # use parents reporting value if not provided
195
 
        if reporting_enabled is None:
196
 
            if parent:
197
 
                reporting_enabled = parent.reporting_enabled
198
 
            else:
199
 
                reporting_enabled = True
200
 
        self.reporting_enabled = reporting_enabled
201
 
 
202
 
        if parent:
203
 
            self.fullname = '/'.join((parent.fullname, name,))
204
 
        else:
205
 
            self.fullname = self.name
206
 
        self.children = {}
207
 
 
208
 
    def __repr__(self):
209
 
        return ("ReportEventStack(%s, %s, reporting_enabled=%s)" %
210
 
                (self.name, self.description, self.reporting_enabled))
211
 
 
212
 
    def __enter__(self):
213
 
        self.result = status.SUCCESS
214
 
        if self.reporting_enabled:
215
 
            report_start_event(self.fullname, self.description,
216
 
                               level=self.level)
217
 
        if self.parent:
218
 
            self.parent.children[self.name] = (None, None)
219
 
        return self
220
 
 
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)
227
 
 
228
 
    @property
229
 
    def result(self):
230
 
        return self._result
231
 
 
232
 
    @result.setter
233
 
    def result(self, value):
234
 
        if value not in status:
235
 
            raise ValueError("'%s' not a valid result" % value)
236
 
        self._result = value
237
 
 
238
 
    @property
239
 
    def message(self):
240
 
        if self._message is not None:
241
 
            return self._message
242
 
        return self.description
243
 
 
244
 
    @message.setter
245
 
    def message(self, value):
246
 
        self._message = value
247
 
 
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()
254
 
 
255
 
    def __exit__(self, exc_type, exc_value, traceback):
256
 
        (result, msg) = self._finish_info(exc_value)
257
 
        if self.parent:
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)
262
 
 
263
 
 
264
 
def _collect_file_info(files):
265
 
    if not files:
266
 
        return None
267
 
    ret = []
268
 
    for fname in files:
269
 
        if not os.path.isfile(fname):
270
 
            content = None
271
 
        else:
272
 
            with open(fname, "rb") as fp:
273
 
                content = base64.b64encode(fp.read()).decode()
274
 
        ret.append({'path': fname, 'content': content,
275
 
                    'encoding': 'base64'})
276
 
    return ret
277
 
 
278
 
# vi: ts=4 expandtab syntax=python