f6265e
From a4b4a11f904d7f70b53c7959e489d7aab72a9fa4 Mon Sep 17 00:00:00 2001
f6265e
From: Eduardo Otubo <otubo@redhat.com>
f6265e
Date: Wed, 15 May 2019 12:15:27 +0200
f6265e
Subject: [PATCH 3/5] Azure: Changes to the Hyper-V KVP Reporter
f6265e
MIME-Version: 1.0
f6265e
Content-Type: text/plain; charset=UTF-8
f6265e
Content-Transfer-Encoding: 8bit
f6265e
f6265e
RH-Author: Eduardo Otubo <otubo@redhat.com>
f6265e
Message-id: <20190515121529.11191-4-otubo@redhat.com>
f6265e
Patchwork-id: 87885
f6265e
O-Subject: [rhel-7 cloud-init PATCHv2 3/5] Azure: Changes to the Hyper-V KVP Reporter
f6265e
Bugzilla: 1687565
f6265e
RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com>
f6265e
RH-Acked-by: Mohammed Gamal <mgamal@redhat.com>
f6265e
f6265e
From: Anh Vo <anhvo@microsoft.com>
f6265e
f6265e
BZ: 1687565
f6265e
BRANCH: rhel7/master-18.5
f6265e
UPSTREAM: 86674f01
f6265e
BREW: 21696239
f6265e
f6265e
commit 86674f013dfcea3c075ab41373ffb475881066f6
f6265e
Author: Anh Vo <anhvo@microsoft.com>
f6265e
Date:   Mon Apr 29 20:22:16 2019 +0000
f6265e
f6265e
    Azure: Changes to the Hyper-V KVP Reporter
f6265e
f6265e
     + Truncate KVP Pool file to prevent stale entries from
f6265e
       being processed by the Hyper-V KVP reporter.
f6265e
     + Drop filtering of KVPs as it is no longer needed.
f6265e
     + Batch appending of existing KVP entries.
f6265e
f6265e
Signed-off-by: Eduardo Otubo <otubo@redhat.com>
f6265e
Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com>
f6265e
---
f6265e
 cloudinit/reporting/handlers.py          | 117 +++++++++++++++----------------
f6265e
 tests/unittests/test_reporting_hyperv.py | 104 +++++++++++++--------------
f6265e
 2 files changed, 106 insertions(+), 115 deletions(-)
f6265e
 mode change 100644 => 100755 cloudinit/reporting/handlers.py
f6265e
 mode change 100644 => 100755 tests/unittests/test_reporting_hyperv.py
f6265e
f6265e
diff --git a/cloudinit/reporting/handlers.py b/cloudinit/reporting/handlers.py
f6265e
old mode 100644
f6265e
new mode 100755
f6265e
index 6d23558..10165ae
f6265e
--- a/cloudinit/reporting/handlers.py
f6265e
+++ b/cloudinit/reporting/handlers.py
f6265e
@@ -5,7 +5,6 @@ import fcntl
f6265e
 import json
f6265e
 import six
f6265e
 import os
f6265e
-import re
f6265e
 import struct
f6265e
 import threading
f6265e
 import time
f6265e
@@ -14,6 +13,7 @@ from cloudinit import log as logging
f6265e
 from cloudinit.registry import DictRegistry
f6265e
 from cloudinit import (url_helper, util)
f6265e
 from datetime import datetime
f6265e
+from six.moves.queue import Empty as QueueEmptyError
f6265e
 
f6265e
 if six.PY2:
f6265e
     from multiprocessing.queues import JoinableQueue as JQueue
f6265e
@@ -129,24 +129,50 @@ class HyperVKvpReportingHandler(ReportingHandler):
f6265e
     DESC_IDX_KEY = 'msg_i'
f6265e
     JSON_SEPARATORS = (',', ':')
f6265e
     KVP_POOL_FILE_GUEST = '/var/lib/hyperv/.kvp_pool_1'
f6265e
+    _already_truncated_pool_file = False
f6265e
 
f6265e
     def __init__(self,
f6265e
                  kvp_file_path=KVP_POOL_FILE_GUEST,
f6265e
                  event_types=None):
f6265e
         super(HyperVKvpReportingHandler, self).__init__()
f6265e
         self._kvp_file_path = kvp_file_path
f6265e
+        HyperVKvpReportingHandler._truncate_guest_pool_file(
f6265e
+            self._kvp_file_path)
f6265e
+
f6265e
         self._event_types = event_types
f6265e
         self.q = JQueue()
f6265e
-        self.kvp_file = None
f6265e
         self.incarnation_no = self._get_incarnation_no()
f6265e
         self.event_key_prefix = u"{0}|{1}".format(self.EVENT_PREFIX,
f6265e
                                                   self.incarnation_no)
f6265e
-        self._current_offset = 0
f6265e
         self.publish_thread = threading.Thread(
f6265e
                 target=self._publish_event_routine)
f6265e
         self.publish_thread.daemon = True
f6265e
         self.publish_thread.start()
f6265e
 
f6265e
+    @classmethod
f6265e
+    def _truncate_guest_pool_file(cls, kvp_file):
f6265e
+        """
f6265e
+        Truncate the pool file if it has not been truncated since boot.
f6265e
+        This should be done exactly once for the file indicated by
f6265e
+        KVP_POOL_FILE_GUEST constant above. This method takes a filename
f6265e
+        so that we can use an arbitrary file during unit testing.
f6265e
+        Since KVP is a best-effort telemetry channel we only attempt to
f6265e
+        truncate the file once and only if the file has not been modified
f6265e
+        since boot. Additional truncation can lead to loss of existing
f6265e
+        KVPs.
f6265e
+        """
f6265e
+        if cls._already_truncated_pool_file:
f6265e
+            return
f6265e
+        boot_time = time.time() - float(util.uptime())
f6265e
+        try:
f6265e
+            if os.path.getmtime(kvp_file) < boot_time:
f6265e
+                with open(kvp_file, "w"):
f6265e
+                    pass
f6265e
+        except (OSError, IOError) as e:
f6265e
+            LOG.warning("failed to truncate kvp pool file, %s", e)
f6265e
+        finally:
f6265e
+            cls._already_truncated_pool_file = True
f6265e
+
f6265e
     def _get_incarnation_no(self):
f6265e
         """
f6265e
         use the time passed as the incarnation number.
f6265e
@@ -162,20 +188,15 @@ class HyperVKvpReportingHandler(ReportingHandler):
f6265e
 
f6265e
     def _iterate_kvps(self, offset):
f6265e
         """iterate the kvp file from the current offset."""
f6265e
-        try:
f6265e
-            with open(self._kvp_file_path, 'rb+') as f:
f6265e
-                self.kvp_file = f
f6265e
-                fcntl.flock(f, fcntl.LOCK_EX)
f6265e
-                f.seek(offset)
f6265e
+        with open(self._kvp_file_path, 'rb') as f:
f6265e
+            fcntl.flock(f, fcntl.LOCK_EX)
f6265e
+            f.seek(offset)
f6265e
+            record_data = f.read(self.HV_KVP_RECORD_SIZE)
f6265e
+            while len(record_data) == self.HV_KVP_RECORD_SIZE:
f6265e
+                kvp_item = self._decode_kvp_item(record_data)
f6265e
+                yield kvp_item
f6265e
                 record_data = f.read(self.HV_KVP_RECORD_SIZE)
f6265e
-                while len(record_data) == self.HV_KVP_RECORD_SIZE:
f6265e
-                    self._current_offset += self.HV_KVP_RECORD_SIZE
f6265e
-                    kvp_item = self._decode_kvp_item(record_data)
f6265e
-                    yield kvp_item
f6265e
-                    record_data = f.read(self.HV_KVP_RECORD_SIZE)
f6265e
-                fcntl.flock(f, fcntl.LOCK_UN)
f6265e
-        finally:
f6265e
-            self.kvp_file = None
f6265e
+            fcntl.flock(f, fcntl.LOCK_UN)
f6265e
 
f6265e
     def _event_key(self, event):
f6265e
         """
f6265e
@@ -207,23 +228,13 @@ class HyperVKvpReportingHandler(ReportingHandler):
f6265e
 
f6265e
         return {'key': k, 'value': v}
f6265e
 
f6265e
-    def _update_kvp_item(self, record_data):
f6265e
-        if self.kvp_file is None:
f6265e
-            raise ReportException(
f6265e
-                "kvp file '{0}' not opened."
f6265e
-                .format(self._kvp_file_path))
f6265e
-        self.kvp_file.seek(-self.HV_KVP_RECORD_SIZE, 1)
f6265e
-        self.kvp_file.write(record_data)
f6265e
-
f6265e
     def _append_kvp_item(self, record_data):
f6265e
-        with open(self._kvp_file_path, 'rb+') as f:
f6265e
+        with open(self._kvp_file_path, 'ab') as f:
f6265e
             fcntl.flock(f, fcntl.LOCK_EX)
f6265e
-            # seek to end of the file
f6265e
-            f.seek(0, 2)
f6265e
-            f.write(record_data)
f6265e
+            for data in record_data:
f6265e
+                f.write(data)
f6265e
             f.flush()
f6265e
             fcntl.flock(f, fcntl.LOCK_UN)
f6265e
-            self._current_offset = f.tell()
f6265e
 
f6265e
     def _break_down(self, key, meta_data, description):
f6265e
         del meta_data[self.MSG_KEY]
f6265e
@@ -279,40 +290,26 @@ class HyperVKvpReportingHandler(ReportingHandler):
f6265e
 
f6265e
     def _publish_event_routine(self):
f6265e
         while True:
f6265e
+            items_from_queue = 0
f6265e
             try:
f6265e
                 event = self.q.get(block=True)
f6265e
-                need_append = True
f6265e
+                items_from_queue += 1
f6265e
+                encoded_data = []
f6265e
+                while event is not None:
f6265e
+                    encoded_data += self._encode_event(event)
f6265e
+                    try:
f6265e
+                        # get all the rest of the events in the queue
f6265e
+                        event = self.q.get(block=False)
f6265e
+                        items_from_queue += 1
f6265e
+                    except QueueEmptyError:
f6265e
+                        event = None
f6265e
                 try:
f6265e
-                    if not os.path.exists(self._kvp_file_path):
f6265e
-                        LOG.warning(
f6265e
-                            "skip writing events %s to %s. file not present.",
f6265e
-                            event.as_string(),
f6265e
-                            self._kvp_file_path)
f6265e
-                    encoded_event = self._encode_event(event)
f6265e
-                    # for each encoded_event
f6265e
-                    for encoded_data in (encoded_event):
f6265e
-                        for kvp in self._iterate_kvps(self._current_offset):
f6265e
-                            match = (
f6265e
-                                re.match(
f6265e
-                                    r"^{0}\|(\d+)\|.+"
f6265e
-                                    .format(self.EVENT_PREFIX),
f6265e
-                                    kvp['key']
f6265e
-                                ))
f6265e
-                            if match:
f6265e
-                                match_groups = match.groups(0)
f6265e
-                                if int(match_groups[0]) < self.incarnation_no:
f6265e
-                                    need_append = False
f6265e
-                                    self._update_kvp_item(encoded_data)
f6265e
-                                    continue
f6265e
-                        if need_append:
f6265e
-                            self._append_kvp_item(encoded_data)
f6265e
-                except IOError as e:
f6265e
-                    LOG.warning(
f6265e
-                        "failed posting event to kvp: %s e:%s",
f6265e
-                        event.as_string(), e)
f6265e
+                    self._append_kvp_item(encoded_data)
f6265e
+                except (OSError, IOError) as e:
f6265e
+                    LOG.warning("failed posting events to kvp, %s", e)
f6265e
                 finally:
f6265e
-                    self.q.task_done()
f6265e
-
f6265e
+                    for _ in range(items_from_queue):
f6265e
+                        self.q.task_done()
f6265e
             # when main process exits, q.get() will through EOFError
f6265e
             # indicating we should exit this thread.
f6265e
             except EOFError:
f6265e
@@ -322,7 +319,7 @@ class HyperVKvpReportingHandler(ReportingHandler):
f6265e
     # if the kvp pool already contains a chunk of data,
f6265e
     # so defer it to another thread.
f6265e
     def publish_event(self, event):
f6265e
-        if (not self._event_types or event.event_type in self._event_types):
f6265e
+        if not self._event_types or event.event_type in self._event_types:
f6265e
             self.q.put(event)
f6265e
 
f6265e
     def flush(self):
f6265e
diff --git a/tests/unittests/test_reporting_hyperv.py b/tests/unittests/test_reporting_hyperv.py
f6265e
old mode 100644
f6265e
new mode 100755
f6265e
index 2e64c6c..d01ed5b
f6265e
--- a/tests/unittests/test_reporting_hyperv.py
f6265e
+++ b/tests/unittests/test_reporting_hyperv.py
f6265e
@@ -1,10 +1,12 @@
f6265e
 # This file is part of cloud-init. See LICENSE file for license information.
f6265e
 
f6265e
 from cloudinit.reporting import events
f6265e
-from cloudinit.reporting import handlers
f6265e
+from cloudinit.reporting.handlers import HyperVKvpReportingHandler
f6265e
 
f6265e
 import json
f6265e
 import os
f6265e
+import struct
f6265e
+import time
f6265e
 
f6265e
 from cloudinit import util
f6265e
 from cloudinit.tests.helpers import CiTestCase
f6265e
@@ -13,7 +15,7 @@ from cloudinit.tests.helpers import CiTestCase
f6265e
 class TestKvpEncoding(CiTestCase):
f6265e
     def test_encode_decode(self):
f6265e
         kvp = {'key': 'key1', 'value': 'value1'}
f6265e
-        kvp_reporting = handlers.HyperVKvpReportingHandler()
f6265e
+        kvp_reporting = HyperVKvpReportingHandler()
f6265e
         data = kvp_reporting._encode_kvp_item(kvp['key'], kvp['value'])
f6265e
         self.assertEqual(len(data), kvp_reporting.HV_KVP_RECORD_SIZE)
f6265e
         decoded_kvp = kvp_reporting._decode_kvp_item(data)
f6265e
@@ -26,57 +28,9 @@ class TextKvpReporter(CiTestCase):
f6265e
         self.tmp_file_path = self.tmp_path('kvp_pool_file')
f6265e
         util.ensure_file(self.tmp_file_path)
f6265e
 
f6265e
-    def test_event_type_can_be_filtered(self):
f6265e
-        reporter = handlers.HyperVKvpReportingHandler(
f6265e
-            kvp_file_path=self.tmp_file_path,
f6265e
-            event_types=['foo', 'bar'])
f6265e
-
f6265e
-        reporter.publish_event(
f6265e
-            events.ReportingEvent('foo', 'name', 'description'))
f6265e
-        reporter.publish_event(
f6265e
-            events.ReportingEvent('some_other', 'name', 'description3'))
f6265e
-        reporter.q.join()
f6265e
-
f6265e
-        kvps = list(reporter._iterate_kvps(0))
f6265e
-        self.assertEqual(1, len(kvps))
f6265e
-
f6265e
-        reporter.publish_event(
f6265e
-            events.ReportingEvent('bar', 'name', 'description2'))
f6265e
-        reporter.q.join()
f6265e
-        kvps = list(reporter._iterate_kvps(0))
f6265e
-        self.assertEqual(2, len(kvps))
f6265e
-
f6265e
-        self.assertIn('foo', kvps[0]['key'])
f6265e
-        self.assertIn('bar', kvps[1]['key'])
f6265e
-        self.assertNotIn('some_other', kvps[0]['key'])
f6265e
-        self.assertNotIn('some_other', kvps[1]['key'])
f6265e
-
f6265e
-    def test_events_are_over_written(self):
f6265e
-        reporter = handlers.HyperVKvpReportingHandler(
f6265e
-            kvp_file_path=self.tmp_file_path)
f6265e
-
f6265e
-        self.assertEqual(0, len(list(reporter._iterate_kvps(0))))
f6265e
-
f6265e
-        reporter.publish_event(
f6265e
-            events.ReportingEvent('foo', 'name1', 'description'))
f6265e
-        reporter.publish_event(
f6265e
-            events.ReportingEvent('foo', 'name2', 'description'))
f6265e
-        reporter.q.join()
f6265e
-        self.assertEqual(2, len(list(reporter._iterate_kvps(0))))
f6265e
-
f6265e
-        reporter2 = handlers.HyperVKvpReportingHandler(
f6265e
-            kvp_file_path=self.tmp_file_path)
f6265e
-        reporter2.incarnation_no = reporter.incarnation_no + 1
f6265e
-        reporter2.publish_event(
f6265e
-            events.ReportingEvent('foo', 'name3', 'description'))
f6265e
-        reporter2.q.join()
f6265e
-
f6265e
-        self.assertEqual(2, len(list(reporter2._iterate_kvps(0))))
f6265e
-
f6265e
     def test_events_with_higher_incarnation_not_over_written(self):
f6265e
-        reporter = handlers.HyperVKvpReportingHandler(
f6265e
+        reporter = HyperVKvpReportingHandler(
f6265e
             kvp_file_path=self.tmp_file_path)
f6265e
-
f6265e
         self.assertEqual(0, len(list(reporter._iterate_kvps(0))))
f6265e
 
f6265e
         reporter.publish_event(
f6265e
@@ -86,7 +40,7 @@ class TextKvpReporter(CiTestCase):
f6265e
         reporter.q.join()
f6265e
         self.assertEqual(2, len(list(reporter._iterate_kvps(0))))
f6265e
 
f6265e
-        reporter3 = handlers.HyperVKvpReportingHandler(
f6265e
+        reporter3 = HyperVKvpReportingHandler(
f6265e
             kvp_file_path=self.tmp_file_path)
f6265e
         reporter3.incarnation_no = reporter.incarnation_no - 1
f6265e
         reporter3.publish_event(
f6265e
@@ -95,7 +49,7 @@ class TextKvpReporter(CiTestCase):
f6265e
         self.assertEqual(3, len(list(reporter3._iterate_kvps(0))))
f6265e
 
f6265e
     def test_finish_event_result_is_logged(self):
f6265e
-        reporter = handlers.HyperVKvpReportingHandler(
f6265e
+        reporter = HyperVKvpReportingHandler(
f6265e
             kvp_file_path=self.tmp_file_path)
f6265e
         reporter.publish_event(
f6265e
             events.FinishReportingEvent('name2', 'description1',
f6265e
@@ -105,7 +59,7 @@ class TextKvpReporter(CiTestCase):
f6265e
 
f6265e
     def test_file_operation_issue(self):
f6265e
         os.remove(self.tmp_file_path)
f6265e
-        reporter = handlers.HyperVKvpReportingHandler(
f6265e
+        reporter = HyperVKvpReportingHandler(
f6265e
             kvp_file_path=self.tmp_file_path)
f6265e
         reporter.publish_event(
f6265e
             events.FinishReportingEvent('name2', 'description1',
f6265e
@@ -113,7 +67,7 @@ class TextKvpReporter(CiTestCase):
f6265e
         reporter.q.join()
f6265e
 
f6265e
     def test_event_very_long(self):
f6265e
-        reporter = handlers.HyperVKvpReportingHandler(
f6265e
+        reporter = HyperVKvpReportingHandler(
f6265e
             kvp_file_path=self.tmp_file_path)
f6265e
         description = 'ab' * reporter.HV_KVP_EXCHANGE_MAX_VALUE_SIZE
f6265e
         long_event = events.FinishReportingEvent(
f6265e
@@ -132,3 +86,43 @@ class TextKvpReporter(CiTestCase):
f6265e
             self.assertEqual(msg_slice['msg_i'], i)
f6265e
             full_description += msg_slice['msg']
f6265e
         self.assertEqual(description, full_description)
f6265e
+
f6265e
+    def test_not_truncate_kvp_file_modified_after_boot(self):
f6265e
+        with open(self.tmp_file_path, "wb+") as f:
f6265e
+            kvp = {'key': 'key1', 'value': 'value1'}
f6265e
+            data = (struct.pack("%ds%ds" % (
f6265e
+                    HyperVKvpReportingHandler.HV_KVP_EXCHANGE_MAX_KEY_SIZE,
f6265e
+                    HyperVKvpReportingHandler.HV_KVP_EXCHANGE_MAX_VALUE_SIZE),
f6265e
+                    kvp['key'].encode('utf-8'), kvp['value'].encode('utf-8')))
f6265e
+            f.write(data)
f6265e
+        cur_time = time.time()
f6265e
+        os.utime(self.tmp_file_path, (cur_time, cur_time))
f6265e
+
f6265e
+        # reset this because the unit test framework
f6265e
+        # has already polluted the class variable
f6265e
+        HyperVKvpReportingHandler._already_truncated_pool_file = False
f6265e
+
f6265e
+        reporter = HyperVKvpReportingHandler(kvp_file_path=self.tmp_file_path)
f6265e
+        kvps = list(reporter._iterate_kvps(0))
f6265e
+        self.assertEqual(1, len(kvps))
f6265e
+
f6265e
+    def test_truncate_stale_kvp_file(self):
f6265e
+        with open(self.tmp_file_path, "wb+") as f:
f6265e
+            kvp = {'key': 'key1', 'value': 'value1'}
f6265e
+            data = (struct.pack("%ds%ds" % (
f6265e
+                HyperVKvpReportingHandler.HV_KVP_EXCHANGE_MAX_KEY_SIZE,
f6265e
+                HyperVKvpReportingHandler.HV_KVP_EXCHANGE_MAX_VALUE_SIZE),
f6265e
+                kvp['key'].encode('utf-8'), kvp['value'].encode('utf-8')))
f6265e
+            f.write(data)
f6265e
+
f6265e
+        # set the time ways back to make it look like
f6265e
+        # we had an old kvp file
f6265e
+        os.utime(self.tmp_file_path, (1000000, 1000000))
f6265e
+
f6265e
+        # reset this because the unit test framework
f6265e
+        # has already polluted the class variable
f6265e
+        HyperVKvpReportingHandler._already_truncated_pool_file = False
f6265e
+
f6265e
+        reporter = HyperVKvpReportingHandler(kvp_file_path=self.tmp_file_path)
f6265e
+        kvps = list(reporter._iterate_kvps(0))
f6265e
+        self.assertEqual(0, len(kvps))
f6265e
-- 
f6265e
1.8.3.1
f6265e