summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xbin/cloud-init28
-rw-r--r--cloudinit/reporting.py91
-rw-r--r--cloudinit/sources/__init__.py16
-rw-r--r--cloudinit/stages.py10
-rw-r--r--tests/unittests/test_reporting.py14
5 files changed, 134 insertions, 25 deletions
diff --git a/bin/cloud-init b/bin/cloud-init
index 1d3e7ee3..7f21e49f 100755
--- a/bin/cloud-init
+++ b/bin/cloud-init
@@ -46,6 +46,7 @@ from cloudinit import sources
from cloudinit import stages
from cloudinit import templater
from cloudinit import util
+from cloudinit import reporting
from cloudinit import version
from cloudinit.settings import (PER_INSTANCE, PER_ALWAYS, PER_ONCE,
@@ -313,7 +314,7 @@ def main_modules(action_name, args):
# 5. Run the modules for the given stage name
# 6. Done!
w_msg = welcome_format("%s:%s" % (action_name, name))
- init = stages.Init(ds_deps=[])
+ init = stages.Init(ds_deps=[], reporter=args.reporter)
# Stage 1
init.read_cfg(extract_fns(args))
# Stage 2
@@ -549,6 +550,8 @@ def main():
' found (use at your own risk)'),
dest='force',
default=False)
+
+ parser.set_defaults(reporter=None)
subparsers = parser.add_subparsers()
# Each action and its sub-options (if any)
@@ -595,6 +598,9 @@ def main():
help=("frequency of the module"),
required=False,
choices=list(FREQ_SHORT_NAMES.keys()))
+ parser_single.add_argument("--report", action="store_true",
+ help="enable reporting",
+ required=False)
parser_single.add_argument("module_args", nargs="*",
metavar='argument',
help=('any additional arguments to'
@@ -617,8 +623,24 @@ def main():
if name in ("modules", "init"):
functor = status_wrapper
- return util.log_time(logfunc=LOG.debug, msg="cloud-init mode '%s'" % name,
- get_uptime=True, func=functor, args=(name, args))
+ reporting = True
+ if name == "init":
+ if args.local:
+ rname, rdesc = ("init-local", "searching for local datasources")
+ else:
+ rname, rdesc = ("init-network", "searching for network datasources")
+ elif name == "modules":
+ rname, rdesc = ("modules-%s" % args.mode, "running modules for %s")
+ elif name == "single":
+ rname, rdesc = ("single/%s" % args.name,
+ "running single module %s" % args.name)
+ reporting = args.report
+
+ reporter = reporting.ReportStack(rname, rdesc, reporting=reporting)
+ with reporter:
+ return util.log_time(
+ logfunc=LOG.debug, msg="cloud-init mode '%s'" % name,
+ get_uptime=True, func=functor, args=(name, args))
if __name__ == '__main__':
diff --git a/cloudinit/reporting.py b/cloudinit/reporting.py
index d2dd4fec..c925f661 100644
--- a/cloudinit/reporting.py
+++ b/cloudinit/reporting.py
@@ -20,9 +20,18 @@ START_EVENT_TYPE = 'start'
DEFAULT_CONFIG = {
'logging': {'type': 'log'},
+ 'print': {'type': 'print'},
}
+class _nameset(set):
+ def __getattr__(self, name):
+ if name in self:
+ return name
+ raise AttributeError
+
+status = _nameset(("SUCCESS", "WARN", "FAIL"))
+
instantiated_handler_registry = DictRegistry()
available_handlers = DictRegistry()
@@ -43,17 +52,18 @@ class ReportingEvent(object):
class FinishReportingEvent(ReportingEvent):
- def __init__(self, name, description, successful=None):
+ def __init__(self, name, description, result=None):
super(FinishReportingEvent, self).__init__(
FINISH_EVENT_TYPE, name, description)
- self.successful = successful
+ if result is None:
+ result = status.SUCCESS
+ self.result = result
+ if result not in status:
+ raise ValueError("Invalid result: %s" % result)
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)
+ self.event_type, self.name, self.result, self.description)
class ReportingHandler(object):
@@ -73,6 +83,11 @@ class LogHandler(ReportingHandler):
logger.info(event.as_string())
+class PrintHandler(ReportingHandler):
+ def publish_event(self, event):
+ print(event.as_string())
+
+
def add_configuration(config):
for handler_name, handler_config in config.items():
handler_config = handler_config.copy()
@@ -95,12 +110,12 @@ def report_event(event):
handler.publish_event(event)
-def report_finish_event(event_name, event_description, successful=None):
+def report_finish_event(event_name, event_description, result):
"""Report a "finish" event.
See :py:func:`.report_event` for parameter details.
"""
- event = FinishReportingEvent(event_name, event_description, successful)
+ event = FinishReportingEvent(event_name, event_description, result)
return report_event(event)
@@ -118,5 +133,65 @@ def report_start_event(event_name, event_description):
return report_event(event)
+class ReportStack(object):
+ def __init__(self, name, description, parent=None, reporting=None,
+ exc_result=None):
+ self.parent = parent
+ self.reporting = reporting
+ self.name = name
+ self.description = description
+
+ if exc_result is None:
+ exc_result = status.FAIL
+ self.exc_result = exc_result
+
+ if reporting is None:
+ # if reporting is specified respect it, otherwise use parent's value
+ if parent:
+ reporting = parent.reporting
+ else:
+ reporting = True
+ if parent:
+ self.fullname = '/'.join((name, parent.fullname,))
+ else:
+ self.fullname = self.name
+ self.children = {}
+
+ def __enter__(self):
+ self.exception = None
+ if self.reporting:
+ report_start_event(self.fullname, self.description)
+ if self.parent:
+ self.parent.children[self.name] = (None, None)
+ return self
+
+ def childrens_finish_info(self, result=None, description=None):
+ for result in (status.FAIL, status.WARN):
+ for name, (value, msg) in self.children.items():
+ if value == result:
+ return (result, "[" + name + "]" + msg)
+ if result is None:
+ result = status.SUCCESS
+ if description is None:
+ description = self.description
+ return (result, description)
+
+ def finish_info(self, exc):
+ # return tuple of description, and value
+ if exc:
+ # by default, exceptions are fatal
+ return (self.exc_result, self.description)
+ return self.childrens_finish_info()
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ self.exception = exc_value
+ (result, msg) = self.finish_info(exc_value)
+ if self.parent:
+ self.parent.children[self.name] = (result, msg)
+ if self.reporting:
+ report_finish_event(self.fullname, msg, result)
+
+
available_handlers.register_item('log', LogHandler)
+available_handlers.register_item('print', PrintHandler)
add_configuration(DEFAULT_CONFIG)
diff --git a/cloudinit/sources/__init__.py b/cloudinit/sources/__init__.py
index a21c08c2..c4848d5d 100644
--- a/cloudinit/sources/__init__.py
+++ b/cloudinit/sources/__init__.py
@@ -27,6 +27,7 @@ import six
from cloudinit import importer
from cloudinit import log as logging
+from cloudinit import reporting
from cloudinit import type_utils
from cloudinit import user_data as ud
from cloudinit import util
@@ -246,17 +247,22 @@ def normalize_pubkey_data(pubkey_data):
return keys
-def find_source(sys_cfg, distro, paths, ds_deps, cfg_list, pkg_list):
+def find_source(sys_cfg, distro, paths, ds_deps, cfg_list, pkg_list, reporter):
ds_list = list_sources(cfg_list, ds_deps, pkg_list)
ds_names = [type_utils.obj_name(f) for f in ds_list]
LOG.debug("Searching for data source in: %s", ds_names)
for cls in ds_list:
+ myreporter = reporting.ReportStack(
+ "check-%s" % cls, "searching for %s" % cls,
+ parent=reporter, exc_result=reporting.status.WARN)
+
try:
- LOG.debug("Seeing if we can get any data from %s", cls)
- s = cls(sys_cfg, distro, paths)
- if s.get_data():
- return (s, type_utils.obj_name(cls))
+ with myreporter:
+ LOG.debug("Seeing if we can get any data from %s", cls)
+ s = cls(sys_cfg, distro, paths)
+ if s.get_data():
+ return (s, type_utils.obj_name(cls))
except Exception:
util.logexc(LOG, "Getting data from %s failed", cls)
diff --git a/cloudinit/stages.py b/cloudinit/stages.py
index d28e765b..dbcdbece 100644
--- a/cloudinit/stages.py
+++ b/cloudinit/stages.py
@@ -46,6 +46,7 @@ from cloudinit import log as logging
from cloudinit import sources
from cloudinit import type_utils
from cloudinit import util
+from cloudinit import reporting
LOG = logging.getLogger(__name__)
@@ -53,7 +54,7 @@ NULL_DATA_SOURCE = None
class Init(object):
- def __init__(self, ds_deps=None):
+ def __init__(self, reporter=None, ds_deps=None):
if ds_deps is not None:
self.ds_deps = ds_deps
else:
@@ -65,6 +66,11 @@ class Init(object):
# Changed only when a fetch occurs
self.datasource = NULL_DATA_SOURCE
+ if reporter is None:
+ reporter = reporting.ReportStack(
+ name="init-reporter", description="init-desc", reporting=False)
+ self.reporter = reporter
+
def _reset(self, reset_ds=False):
# Recreated on access
self._cfg = None
@@ -246,7 +252,7 @@ class Init(object):
self.paths,
copy.deepcopy(self.ds_deps),
cfg_list,
- pkg_list)
+ pkg_list, self.reporter)
LOG.info("Loaded datasource %s - %s", dsname, ds)
self.datasource = ds
# Ensure we adjust our path members datasource
diff --git a/tests/unittests/test_reporting.py b/tests/unittests/test_reporting.py
index f4011a79..5700118f 100644
--- a/tests/unittests/test_reporting.py
+++ b/tests/unittests/test_reporting.py
@@ -32,10 +32,10 @@ class TestReportStartEvent(TestCase):
class TestReportFinishEvent(TestCase):
- def _report_finish_event(self, successful=None):
+ def _report_finish_event(self, result=None):
event_name, event_description = 'my_test_event', 'my description'
reporting.report_finish_event(
- event_name, event_description, successful=successful)
+ event_name, event_description, result=result)
return event_name, event_description
def assertHandlersPassedObjectWithAsString(
@@ -51,7 +51,7 @@ class TestReportFinishEvent(TestCase):
self, instantiated_handler_registry):
event_name, event_description = self._report_finish_event()
expected_string_representation = ': '.join(
- ['finish', event_name, event_description])
+ ['finish', event_name, reporting.status.SUCCESS, event_description])
self.assertHandlersPassedObjectWithAsString(
instantiated_handler_registry.registered_items,
expected_string_representation)
@@ -61,9 +61,9 @@ class TestReportFinishEvent(TestCase):
def test_reporting_successful_finish_has_sensible_string_repr(
self, instantiated_handler_registry):
event_name, event_description = self._report_finish_event(
- successful=True)
+ result=reporting.status.SUCCESS)
expected_string_representation = ': '.join(
- ['finish', event_name, 'success', event_description])
+ ['finish', event_name, reporting.status.SUCCESS, event_description])
self.assertHandlersPassedObjectWithAsString(
instantiated_handler_registry.registered_items,
expected_string_representation)
@@ -73,9 +73,9 @@ class TestReportFinishEvent(TestCase):
def test_reporting_unsuccessful_finish_has_sensible_string_repr(
self, instantiated_handler_registry):
event_name, event_description = self._report_finish_event(
- successful=False)
+ result=reporting.status.FAIL)
expected_string_representation = ': '.join(
- ['finish', event_name, 'fail', event_description])
+ ['finish', event_name, reporting.status.FAIL, event_description])
self.assertHandlersPassedObjectWithAsString(
instantiated_handler_registry.registered_items,
expected_string_representation)