← Back to team overview

cloud-init-dev team mailing list archive

[Merge] ~andyliuliming/cloud-init:reporting into cloud-init:master

 

Andy has proposed merging ~andyliuliming/cloud-init:reporting into cloud-init:master.

Requested reviews:
  cloud-init commiters (cloud-init-dev)

For more details, see:
https://code.launchpad.net/~andyliuliming/cloud-init/+git/cloud-init/+merge/351742

added one option to send the cloud init events to the kvp in hyper-v.
-- 
Your team cloud-init commiters is requested to review the proposed merge of ~andyliuliming/cloud-init:reporting into cloud-init:master.
diff --git a/cloudinit/cloud.py b/cloudinit/cloud.py
index 6d12c43..7ae98e1 100644
--- a/cloudinit/cloud.py
+++ b/cloudinit/cloud.py
@@ -47,7 +47,7 @@ class Cloud(object):
 
     @property
     def cfg(self):
-        # Ensure that not indirectly modified
+        # Ensure that cfg is not indirectly modified
         return copy.deepcopy(self._cfg)
 
     def run(self, name, functor, args, freq=None, clear_on_fail=False):
@@ -61,7 +61,7 @@ class Cloud(object):
             return None
         return fn
 
-    # The rest of thes are just useful proxies
+    # The rest of these are just useful proxies
     def get_userdata(self, apply_filter=True):
         return self.datasource.get_userdata(apply_filter)
 
diff --git a/cloudinit/cmd/main.py b/cloudinit/cmd/main.py
index d6ba90f..c0edee1 100644
--- a/cloudinit/cmd/main.py
+++ b/cloudinit/cmd/main.py
@@ -315,7 +315,7 @@ def main_init(name, args):
                 existing = "trust"
 
         init.purge_cache()
-        # Delete the non-net file as well
+        # Delete the no-net file as well
         util.del_file(os.path.join(path_helper.get_cpath("data"), "no-net"))
 
     # Stage 5
@@ -339,7 +339,7 @@ def main_init(name, args):
                               " Likely bad things to come!"))
         if not args.force:
             init.apply_network_config(bring_up=not args.local)
-            LOG.debug("[%s] Exiting without datasource in local mode", mode)
+            LOG.debug("[%s] Exiting without datasource", mode)
             if mode == sources.DSMODE_LOCAL:
                 return (None, [])
             else:
diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py
index ab0b077..fde054e 100755
--- a/cloudinit/distros/__init__.py
+++ b/cloudinit/distros/__init__.py
@@ -157,7 +157,7 @@ class Distro(object):
                     distro)
         header = '\n'.join([
             "# Converted from network_config for distro %s" % distro,
-            "# Implmentation of _write_network_config is needed."
+            "# Implementation of _write_network_config is needed."
         ])
         ns = network_state.parse_net_config_data(netconfig)
         contents = eni.network_state_to_eni(
diff --git a/cloudinit/reporting/__init__.py b/cloudinit/reporting/__init__.py
index 1ed2b48..e047767 100644
--- a/cloudinit/reporting/__init__.py
+++ b/cloudinit/reporting/__init__.py
@@ -18,7 +18,7 @@ DEFAULT_CONFIG = {
 
 
 def update_configuration(config):
-    """Update the instanciated_handler_registry.
+    """Update the instantiated_handler_registry.
 
     :param config:
         The dictionary containing changes to apply.  If a key is given
diff --git a/cloudinit/reporting/events.py b/cloudinit/reporting/events.py
index e5dfab3..b47339e 100644
--- a/cloudinit/reporting/events.py
+++ b/cloudinit/reporting/events.py
@@ -29,7 +29,6 @@ class _nameset(set):
 
 status = _nameset(("SUCCESS", "WARN", "FAIL"))
 
-
 class ReportingEvent(object):
     """Encapsulation of event formatting."""
 
diff --git a/cloudinit/reporting/handlers.py b/cloudinit/reporting/handlers.py
index 4066076..df745e3 100644
--- a/cloudinit/reporting/handlers.py
+++ b/cloudinit/reporting/handlers.py
@@ -1,16 +1,30 @@
 # This file is part of cloud-init. See LICENSE file for license information.
 
 import abc
+import fcntl
+import hashlib
 import json
+import os
 import six
+if six.PY2:
+    import Queue as queue
+else:
+    import queue
+import re
+import struct
+import threading
+import time
 
 from cloudinit import log as logging
 from cloudinit.registry import DictRegistry
 from cloudinit import (url_helper, util)
-
+from datetime import datetime
 
 LOG = logging.getLogger(__name__)
 
+class ReportException(Exception):
+    def __init__(self, message):
+        super().__init__(message)
 
 @six.add_metaclass(abc.ABCMeta)
 class ReportingHandler(object):
@@ -85,9 +99,173 @@ class WebHookHandler(ReportingHandler):
             LOG.warning("failed posting event: %s", event.as_string())
 
 
+class HyperVKvpReportingHandler(ReportingHandler):
+    """
+    Reports events to a Hyper-V host using Key-Value-Pair exchange protocol
+    and can be used to obtain high level diagnostic information from the host.
+
+    To use this facility, the KVP user-space daemon (hv_kvp_daemon) has to be
+    running. It reads the kvp_file when the host requests the guest to
+    enumerate the KVP's.
+
+    This reporter collates all events for a module (origin|name) in a single
+    json string in the dictionary.
+
+    For more information, see
+    https://technet.microsoft.com/en-us/library/dn798287.aspx#Linux%20guests
+    """
+    HV_KVP_EXCHANGE_MAX_VALUE_SIZE = 2048
+    HV_KVP_EXCHANGE_MAX_KEY_SIZE = 512
+    HV_KVP_RECORD_SIZE = (HV_KVP_EXCHANGE_MAX_KEY_SIZE +
+                        HV_KVP_EXCHANGE_MAX_VALUE_SIZE)
+    EVENT_PREFIX = 'CLOUD_INIT'
+    def __init__(self,
+                 kvp_file='/var/lib/hyperv/.kvp_pool_1',
+                 event_types=None):
+        super(HyperVKvpReportingHandler, self).__init__()
+        self._kvp_file = kvp_file
+        self._event_types = event_types
+        self.running = False
+        self.queue_lock = threading.Lock()
+        self.running_lock = threading.Lock()
+        self.q = queue.Queue(100)
+        self.kvp_file = None
+        self.incarnation_no = self._get_incarnation_no()
+        # use the current time stamp as the incarnation number.
+        self.event_key_prefix = u"{0}|{1}".format(self.EVENT_PREFIX, self.incarnation_no)
+        self._current_offset = 0
+
+    def _get_incarnation_no(self):
+        try:
+            uptime_str = util.uptime()
+            return int(time.time() - float(uptime_str))
+        except ValueError:
+            LOG.warn("uptime not in correct format.")
+            return 0
+
+    def _open_kvp_file(self):
+        f = open(self._kvp_file, 'rb+') 
+        return f
+
+    def _iterate_kvps(self, offset=0):
+        """
+        iterate the kvp file from the current offset.
+        """
+        try:
+            with self._open_kvp_file() as f:
+                self.kvp_file = f
+                fcntl.flock(f, fcntl.LOCK_EX)
+                f.seek(offset)
+                record_data = f.read(self.HV_KVP_RECORD_SIZE)
+                while len(record_data) == self.HV_KVP_RECORD_SIZE:
+                    self._current_offset += self.HV_KVP_RECORD_SIZE
+                    kvp_item = self._decode_kvp_item(record_data)
+                    yield kvp_item
+                    record_data = f.read(self.HV_KVP_RECORD_SIZE)
+        finally:
+            self.kvp_file = None
+
+    def _event_key(self, event):
+        """
+        the event key format is:
+        CLOUD_INIT|<incarnation number>|<event_type>|<event_name>
+        """
+        return u"{0}|{1}|{2}".format(self.event_key_prefix, event.event_type, event.name)
+
+    def _encode_kvp_item(self, key, value):
+        data = (struct.pack("%ds%ds" %
+                    (self.HV_KVP_EXCHANGE_MAX_KEY_SIZE,
+                    self.HV_KVP_EXCHANGE_MAX_VALUE_SIZE),
+                    key.encode('utf-8'), value.encode('utf-8')))
+        return data
+
+    def _decode_kvp_item(self, record_data):
+        if len(record_data) != self.HV_KVP_RECORD_SIZE:
+            raise ReportException("record_data len not correct.")
+        k = (record_data[0:self.HV_KVP_EXCHANGE_MAX_KEY_SIZE].decode('utf-8')
+                        .strip('\x00'))
+        v = (record_data[self.HV_KVP_EXCHANGE_MAX_KEY_SIZE:self.HV_KVP_RECORD_SIZE].decode('utf-8')
+                 .strip('\x00'))
+
+        return {'key':k , 'value': v}
+    
+    def _update_kvp_item(self, record_data):
+        if self.kvp_file == None:
+            raise ReportException("kvp file not opened.")
+        self.kvp_file.seek(-self.HV_KVP_RECORD_SIZE, 1)
+        self.kvp_file.write(record_data)
+
+    def _encode_event(self, event):
+        key = self._event_key(event)
+        # TODO trunc the description so the data would be in a correct json format.
+        data = {
+                "type": event.event_type,
+                "name": event.name,
+                "ts": (datetime.utcfromtimestamp(event.timestamp)
+                            .isoformat() + 'Z'),
+                "msg": event.description
+                }
+        if hasattr(event, 'result'):
+            data['result'] = event.result
+        value = json.dumps(data, separators=(',', ':'))
+        data = self._encode_kvp_item(key, value)
+        return data
+
+    def _append_event(self, encoded_event):
+        with self._open_kvp_file() as f:
+            fcntl.flock(f, fcntl.LOCK_EX)
+            # seek to end of the file
+            f.seek(0, 2)
+            f.write(encoded_event)
+            f.flush()
+            self._current_offset = f.tell()
+
+    def _publish_event_routine(self):
+        while True:
+            try:
+                # acquire the lock.
+                event = self.q.get_nowait()
+                need_append = True
+                encoded_event = self._encode_event(event)
+                for kvp in self._iterate_kvps(self._current_offset):
+                    match = re.match(r"^{0}\|(\d+)\|.+".format(self.EVENT_PREFIX),kvp['key'])
+                    if match:
+                        match_groups = match.groups(0)
+                        if int(match_groups[0]) < self.incarnation_no:
+                            need_append = False
+                            self._update_kvp_item(encoded_event)
+                            break
+                if need_append:
+                    self._append_event(encoded_event)
+                self.q.task_done()
+            except queue.Empty:
+                with self.queue_lock:
+                    # double check the queue is empty
+                    if self.q.empty():
+                        self.running = False
+                        break
+
+    def trigger_publish_event(self):
+        if not self.running:
+            with self.running_lock:
+                if not self.running:
+                    self.running = True
+                    thread = threading.Thread(target = self._publish_event_routine)
+                    thread.start()
+
+    # since the saving to the kvp pool can be a time costing task if the kvp pool already
+    # contains a chunk of data, so defer it to another thread.
+    def publish_event(self, event):
+        if (not self._event_types or event.event_type in self._event_types):
+            with self.queue_lock:
+                self.q.put(event)
+            self.trigger_publish_event()
+
+
 available_handlers = DictRegistry()
 available_handlers.register_item('log', LogHandler)
 available_handlers.register_item('print', PrintHandler)
 available_handlers.register_item('webhook', WebHookHandler)
+available_handlers.register_item('hyperv', HyperVKvpReportingHandler)
 
 # vi: ts=4 expandtab
diff --git a/cloudinit/stages.py b/cloudinit/stages.py
index c132b57..8874d40 100644
--- a/cloudinit/stages.py
+++ b/cloudinit/stages.py
@@ -510,7 +510,7 @@ class Init(object):
                 # The default frequency if handlers don't have one
                 'frequency': frequency,
                 # This will be used when new handlers are found
-                # to help write there contents to files with numbered
+                # to help write their contents to files with numbered
                 # names...
                 'handlercount': 0,
                 'excluded': excluded,
diff --git a/cloudinit/util.py b/cloudinit/util.py
index 5068096..29fffb8 100644
--- a/cloudinit/util.py
+++ b/cloudinit/util.py
@@ -2031,6 +2031,24 @@ def subp(args, data=None, rcs=None, env=None, capture=True,
                               stderr=stderr, stdin=stdin,
                               env=env, shell=shell)
         (out, err) = sp.communicate(data)
+
+        LOG.debug("out=%s", out)
+        LOG.debug("err=%s", err)
+        LOG.debug("rc=%d", sp.returncode)
+
+        # Just ensure blank instead of none.
+        if not out and capture:
+            out = b''
+        if not err and capture:
+            err = b''
+        if decode:
+            def ldecode(data, m='utf-8'):
+                if not isinstance(data, bytes):
+                    return data
+                return data.decode(m, decode)
+
+            out = ldecode(out)
+            err = ldecode(err)
     except OSError as e:
         if status_cb:
             status_cb('ERROR: End run command: invalid command provided\n')
diff --git a/tests/unittests/test_reporting_hyperv.py b/tests/unittests/test_reporting_hyperv.py
new file mode 100644
index 0000000..6801bfd
--- /dev/null
+++ b/tests/unittests/test_reporting_hyperv.py
@@ -0,0 +1,112 @@
+# Copyright 2015 Canonical Ltd.
+#
+# This file is part of cloud-init. See LICENSE file for license information.
+
+from cloudinit.reporting import events
+from cloudinit.reporting import handlers
+
+import mock
+import os
+import tempfile
+import time
+import uuid
+
+from cloudinit.tests.helpers import TestCase
+
+class TestKvpEncoding(TestCase):
+    def test_encode_decode(self):
+        kvp = {'key':'key1','value':'value1'}
+        kvp_reporting = handlers.HyperVKvpReportingHandler()
+        data = kvp_reporting._encode_kvp_item(kvp['key'], kvp['value'])
+        self.assertEqual(len(data), kvp_reporting.HV_KVP_RECORD_SIZE)
+        decoded_kvp = kvp_reporting._decode_kvp_item(data)
+        self.assertEqual(kvp, decoded_kvp)
+
+class TextKvpReporter(TestCase):
+    def test_event_type_can_be_filtered(self):
+        tmp_file = tempfile.NamedTemporaryFile(delete=False)
+        tmp_file.close()
+
+        reporter = handlers.HyperVKvpReportingHandler(
+            kvp_file=tmp_file.name,
+            event_types=['foo', 'bar'])
+
+        reporter.publish_event(
+            events.ReportingEvent('foo', 'name', 'description'))
+        reporter.publish_event(
+            events.ReportingEvent('some_other', 'name', 'description3'))
+        reporter.q.join()
+        
+        kvps = list(reporter._iterate_kvps())
+        self.assertEqual(1, len(kvps))
+
+        reporter.publish_event(
+            events.ReportingEvent('bar', 'name', 'description2'))
+        reporter.q.join()
+        kvps = list(reporter._iterate_kvps())
+        self.assertEqual(2, len(kvps))
+    
+        self.assertIn('foo', kvps[0]['key'])
+        self.assertIn('bar', kvps[1]['key'])
+        self.assertNotIn('some_other', kvps[0]['key'])
+        self.assertNotIn('some_other', kvps[1]['key'])
+
+        os.remove(tmp_file.name)
+
+    def test_events_are_over_written(self):
+        tmp_file = tempfile.NamedTemporaryFile(delete=False)
+        tmp_file.close()
+        reporter = handlers.HyperVKvpReportingHandler(kvp_file=tmp_file.name)
+
+        self.assertEqual(0, len(list(reporter._iterate_kvps())))
+
+        reporter.publish_event(
+            events.ReportingEvent('foo', 'name1', 'description'))
+        reporter.publish_event(
+            events.ReportingEvent('foo', 'name2', 'description'))
+        reporter.q.join()
+        self.assertEqual(2, len(list(reporter._iterate_kvps())))
+
+        reporter2 = handlers.HyperVKvpReportingHandler(kvp_file=tmp_file.name)
+        reporter2.incarnation_no = reporter.incarnation_no + 1
+        reporter2.publish_event(
+            events.ReportingEvent('foo', 'name3', 'description'))
+        reporter2.q.join()
+
+        self.assertEqual(2, len(list(reporter2._iterate_kvps())))
+
+        os.remove(tmp_file.name)
+    
+    def test_events_with_higher_incarnation_not_over_written(self):
+        tmp_file = tempfile.NamedTemporaryFile(delete=False)
+        tmp_file.close()
+        reporter = handlers.HyperVKvpReportingHandler(kvp_file=tmp_file.name)
+
+        self.assertEqual(0, len(list(reporter._iterate_kvps())))
+
+        reporter.publish_event(
+            events.ReportingEvent('foo', 'name1', 'description'))
+        reporter.publish_event(
+            events.ReportingEvent('foo', 'name2', 'description'))
+        reporter.q.join()
+        self.assertEqual(2, len(list(reporter._iterate_kvps())))
+
+        reporter3 = handlers.HyperVKvpReportingHandler(kvp_file=tmp_file.name)
+        reporter3.incarnation_no = reporter.incarnation_no - 1
+        reporter3.publish_event(
+            events.ReportingEvent('foo', 'name3', 'description'))
+        reporter3.q.join()
+        self.assertEqual(3, len(list(reporter3._iterate_kvps())))
+
+        os.remove(tmp_file.name)
+
+    def test_finish_event_result_is_logged(self):
+        tmp_file = tempfile.NamedTemporaryFile(delete=False)
+        tmp_file.close()
+        reporter = handlers.HyperVKvpReportingHandler(kvp_file=tmp_file.name)
+        reporter.publish_event(
+            events.FinishReportingEvent('name2', 'description1',
+                                        result=events.status.FAIL))
+        reporter.q.join()
+        self.assertIn('FAIL', list(reporter._iterate_kvps())[0]['value'])
+        os.remove(tmp_file.name)
\ No newline at end of file
diff --git a/tools/read-dependencies b/tools/read-dependencies
index b4656e6..84d5a16 100755
--- a/tools/read-dependencies
+++ b/tools/read-dependencies
@@ -223,6 +223,7 @@ def main(distro):
     renames = deps_from_json.get('renames', {})
     translated_pip_names = translate_pip_to_system_pkg(
         pip_pkg_names, renames, args.python_version)
+
     all_deps = []
     if args.distro:
         all_deps.extend(
@@ -233,6 +234,7 @@ def main(distro):
             all_deps = translated_pip_names
         else:
             all_deps = pip_pkg_names
+    
     if args.install:
         pkg_install(all_deps, args.distro, args.test_distro, args.dry_run)
     else:

Follow ups