Merge ~paul-meyer/cloud-init:lp1722959 into cloud-init:master

Proposed by Paul Meyer on 2017-12-08
Status: Needs review
Proposed branch: ~paul-meyer/cloud-init:lp1722959
Merge into: cloud-init:master
Diff against target: 226 lines (+192/-1)
2 files modified
cloudinit/reporting/handlers.py (+95/-1)
tests/unittests/test_reporting_hyperv.py (+97/-0)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve on 2017-12-11
Scott Moser 2017-12-08 Pending
Review via email: mp+334989@code.launchpad.net

Commit Message

Add Hyper-V KVP reporter

Add a reporter that 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 kvpFile 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

Description of the Change

Add a reporter that 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 kvpFile 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

To post a comment you must log in.
Scott Moser (smoser) wrote :

So would i expect to find a subsequent merge proposal that would build in configuration of this reporter into Azure datasource?

~paul-meyer/cloud-init:lp1722959 updated on 2017-12-08
e7cca41... by Paul Meyer on 2017-12-08

Add test intent docstrings, import mock from helpers

Paul Meyer (paul-meyer) wrote :

> So would i expect to find a subsequent merge proposal that would build in
> configuration of this reporter into Azure datasource?

Actually, I've been thinking that we may want to move most of that built-in config to files on disk in the images themselves, such that they can be properly overridden by custom images. Also, for this reporter it makes sense to have it on even while cloud-init searches for a data source, that way, we can detect if DSAzure is broken if the VM doesn't come online.

Scott Moser (smoser) wrote :

> > So would i expect to find a subsequent merge proposal that would build in
> > configuration of this reporter into Azure datasource?
>
> Actually, I've been thinking that we may want to move most of that built-in
> config to files on disk in the images themselves, such that they can be
> properly overridden by custom images. Also, for this reporter it makes sense
> to have it on even while cloud-init searches for a data source, that way, we
> can detect if DSAzure is broken if the VM doesn't come online.

Generally speaking we're after the goal that installing cloud-init in an image "does the right thing". That is as opposed to requiring Canonical or anyone building images to "know" that they should put some configuration files down in /etc/cloud/cloud.cfg.d.

I do appreciate that you'd like information to be posted when cloud-init is searching for easier debugging, but such changes wont work or cause issue on other platforms. We like to allow "one image" to work on different platforms.

Paul Meyer (paul-meyer) wrote :

> > > So would i expect to find a subsequent merge proposal that would build in
> > > configuration of this reporter into Azure datasource?
> >
> > Actually, I've been thinking that we may want to move most of that built-in
> > config to files on disk in the images themselves, such that they can be
> > properly overridden by custom images. Also, for this reporter it makes sense
> > to have it on even while cloud-init searches for a data source, that way, we
> > can detect if DSAzure is broken if the VM doesn't come online.
>
> Generally speaking we're after the goal that installing cloud-init in an image
> "does the right thing". That is as opposed to requiring Canonical or anyone
> building images to "know" that they should put some configuration files down
> in /etc/cloud/cloud.cfg.d.
>
> I do appreciate that you'd like information to be posted when cloud-init is
> searching for easier debugging, but such changes wont work or cause issue on
> other platforms. We like to allow "one image" to work on different platforms.

I understand the concern and I agree. We'll do both then. Add this as default configuration from the Azure DS and also make the defaults overridable from /etc/cloud for custom images (lp 1735008).

PASSED: Continuous integration, rev:e7cca41b254f76af884c14c5aa6d0b3387f6546d
https://jenkins.ubuntu.com/server/job/cloud-init-ci/616/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/616/rebuild

review: Approve (continuous-integration)

Unmerged commits

e7cca41... by Paul Meyer on 2017-12-08

Add test intent docstrings, import mock from helpers

1e4cc7d... by Paul Meyer on 2017-05-17

Add Hyper-V KVP reporter

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/cloudinit/reporting/handlers.py b/cloudinit/reporting/handlers.py
2index 4066076..6c89616 100644
3--- a/cloudinit/reporting/handlers.py
4+++ b/cloudinit/reporting/handlers.py
5@@ -1,13 +1,16 @@
6 # This file is part of cloud-init. See LICENSE file for license information.
7
8 import abc
9+import contextlib
10+import fcntl
11 import json
12 import six
13+import struct
14
15 from cloudinit import log as logging
16 from cloudinit.registry import DictRegistry
17 from cloudinit import (url_helper, util)
18-
19+from datetime import datetime
20
21 LOG = logging.getLogger(__name__)
22
23@@ -85,9 +88,100 @@ class WebHookHandler(ReportingHandler):
24 LOG.warning("failed posting event: %s", event.as_string())
25
26
27+HV_KVP_EXCHANGE_MAX_VALUE_SIZE = 2048
28+HV_KVP_EXCHANGE_MAX_KEY_SIZE = 512
29+HV_KVP_RECORD_SIZE = (HV_KVP_EXCHANGE_MAX_KEY_SIZE +
30+ HV_KVP_EXCHANGE_MAX_VALUE_SIZE)
31+
32+
33+class HyperVKvpReportingHandler(ReportingHandler):
34+ """
35+ Reports events to a Hyper-V host using Key-Value-Pair exchange protocol
36+ and can be used to obtain high level diagnostic information from the host.
37+
38+ To use this facility, the KVP user-space daemon (hv_kvp_daemon) has to be
39+ running. It reads the kvpFile when the host requests the guest to
40+ enumerate the KVP's.
41+
42+ This reporter collates all events for a module (origin|name) in a single
43+ json string in the dictionary.
44+
45+ For more information, see
46+ https://technet.microsoft.com/en-us/library/dn798287.aspx#Linux%20guests
47+ """
48+
49+ def __init__(self,
50+ kvpFile='/var/lib/hyperv/.kvp_pool_1',
51+ event_types=None):
52+ super(HyperVKvpReportingHandler, self).__init__()
53+ self._kvpFile = kvpFile
54+ self._event_types = event_types
55+
56+ @staticmethod
57+ def _decodeKVP(data):
58+ kvp = dict()
59+
60+ num = int(len(data) / (HV_KVP_RECORD_SIZE))
61+ for i in range(num):
62+ k = (data[HV_KVP_RECORD_SIZE * i:]
63+ [0:HV_KVP_EXCHANGE_MAX_KEY_SIZE].decode('utf-8')
64+ .strip('\x00'))
65+ v = (data[HV_KVP_RECORD_SIZE * i + HV_KVP_EXCHANGE_MAX_KEY_SIZE:]
66+ [0:HV_KVP_EXCHANGE_MAX_VALUE_SIZE].decode('utf-8')
67+ .strip('\x00'))
68+ kvp[k] = v
69+
70+ return kvp
71+
72+ @staticmethod
73+ def _encodeKVP(kvp):
74+ data = bytes()
75+ for k, v in kvp.items():
76+ data += (struct.pack("%ds%ds" %
77+ (HV_KVP_EXCHANGE_MAX_KEY_SIZE,
78+ HV_KVP_EXCHANGE_MAX_VALUE_SIZE),
79+ k.encode('utf-8'), v.encode('utf-8')))
80+ return data
81+
82+ @contextlib.contextmanager
83+ def _kvps(self):
84+ """Add a key/value pair to the kvp file."""
85+ with open(self._kvpFile, 'rb+') as f:
86+ fcntl.flock(f, fcntl.LOCK_EX) # lock will be release when f closes
87+ kvp = self._decodeKVP(f.read())
88+ yield kvp
89+ data = self._encodeKVP(kvp)
90+ f.seek(0)
91+ f.truncate(0)
92+ f.write(data)
93+
94+ def publish_event(self, event):
95+ if (not self._event_types or event.event_type in self._event_types):
96+ with self._kvps() as kvp:
97+ k = "{0}|{1}".format(event.origin, event.name)
98+ v = kvp.get(k)
99+ if v is not None:
100+ v = json.loads(v)
101+ else:
102+ v = []
103+
104+ data = {
105+ "type": event.event_type,
106+ "ts": (datetime.utcfromtimestamp(event.timestamp)
107+ .isoformat() + 'Z'),
108+ "msg": event.description}
109+
110+ if hasattr(event, 'result'):
111+ data['result'] = event.result
112+
113+ v.append(data)
114+ kvp[k] = json.dumps(v, separators=(',', ':'))
115+
116+
117 available_handlers = DictRegistry()
118 available_handlers.register_item('log', LogHandler)
119 available_handlers.register_item('print', PrintHandler)
120 available_handlers.register_item('webhook', WebHookHandler)
121+available_handlers.register_item('hyperv', HyperVKvpReportingHandler)
122
123 # vi: ts=4 expandtab
124diff --git a/tests/unittests/test_reporting_hyperv.py b/tests/unittests/test_reporting_hyperv.py
125new file mode 100644
126index 0000000..38d5ebe
127--- /dev/null
128+++ b/tests/unittests/test_reporting_hyperv.py
129@@ -0,0 +1,97 @@
130+# Copyright 2015 Canonical Ltd.
131+#
132+# This file is part of cloud-init. See LICENSE file for license information.
133+
134+from cloudinit.reporting import events
135+from cloudinit.reporting import handlers
136+
137+from cloudinit.tests.helpers import TestCase, mock
138+
139+
140+class TextKvpReporter(TestCase):
141+
142+ def test_encode_decode(self):
143+ """Binary encoding of KVP yields expected size and is reversible.
144+
145+ See https://technet.microsoft.com/en-us/library/dn798287
146+ """
147+ kvp = {'key1': 'value1', 'key2': 'value2'}
148+ data = handlers.HyperVKvpReportingHandler._encodeKVP(kvp)
149+ self.assertEqual(len(data), handlers.HV_KVP_RECORD_SIZE * len(kvp))
150+ decodedkvp = handlers.HyperVKvpReportingHandler._decodeKVP(data)
151+ self.assertEqual(kvp, decodedkvp)
152+
153+ def test_event_type_can_be_filtered(self):
154+ """HyperVKvpReportingHandler only reports events specified in constructor.
155+
156+ This can be used to limit what event_types are reported."""
157+ reporter = handlers.HyperVKvpReportingHandler(
158+ event_types=['foo', 'bar'])
159+
160+ kvps = dict()
161+ mock_kvps = mock.Mock()
162+ mock_kvps.__enter__ = mock.Mock(return_value=kvps)
163+ mock_kvps.__exit__ = mock.Mock(return_value=False)
164+
165+ with mock.patch.object(reporter, '_kvps', return_value=mock_kvps):
166+ reporter.publish_event(
167+ events.ReportingEvent('foo', 'name', 'description'))
168+ reporter.publish_event(
169+ events.ReportingEvent('bar', 'name', 'description2'))
170+ reporter.publish_event(
171+ events.ReportingEvent('some_other', 'name', 'description3'))
172+
173+ print("kvps: " + str(kvps))
174+
175+ self.assertEqual(1, len(kvps))
176+ v = list(kvps.values())[0]
177+ self.assertIn('foo', v)
178+ self.assertIn('bar', v)
179+ self.assertNotIn('some_other', v)
180+
181+ def test_events_are_grouped_by_source_and_name(self):
182+ """HyperVKvpReportingHandler collates events by origin|name.
183+
184+ This keeps the number of KVPs relatively small as such works
185+ around a bug in the KVP daemon."""
186+ reporter = handlers.HyperVKvpReportingHandler()
187+
188+ kvps = dict()
189+ mock_kvps = mock.Mock()
190+ mock_kvps.__enter__ = mock.Mock(return_value=kvps)
191+ mock_kvps.__exit__ = mock.Mock(return_value=False)
192+
193+ with mock.patch.object(reporter, '_kvps', return_value=mock_kvps):
194+ self.assertEqual(0, len(kvps))
195+
196+ reporter.publish_event(
197+ events.ReportingEvent('foo', 'name1', 'description'))
198+ reporter.publish_event(
199+ events.ReportingEvent('bar', 'name1', 'description'))
200+ self.assertEqual(1, len(kvps))
201+
202+ reporter.publish_event(
203+ events.ReportingEvent('foo', 'name2', 'description'))
204+ self.assertEqual(2, len(kvps))
205+
206+ reporter.publish_event(
207+ events.ReportingEvent('foo', 'name1', 'description',
208+ origin="origin2"))
209+ self.assertEqual(3, len(kvps))
210+
211+ def test_finish_event_result_is_logged(self):
212+ """The result field is logged when available on the event."""
213+ reporter = handlers.HyperVKvpReportingHandler()
214+
215+ kvps = dict()
216+ mock_kvps = mock.Mock()
217+ mock_kvps.__enter__ = mock.Mock(return_value=kvps)
218+ mock_kvps.__exit__ = mock.Mock(return_value=False)
219+
220+ with mock.patch.object(reporter, '_kvps', return_value=mock_kvps):
221+ reporter.publish_event(
222+ events.FinishReportingEvent('name2', 'description1',
223+ result=events.status.FAIL))
224+ self.assertIn('FAIL', list(kvps.values())[0])
225+
226+# vi: ts=4 expandtab

Subscribers

People subscribed via source and target branches