Merge ~smoser/cloud-init:bug/1718029-fix-dhcp-parsing-from-networkd into cloud-init:master
- Git
- lp:~smoser/cloud-init
- bug/1718029-fix-dhcp-parsing-from-networkd
- Merge into master
Status: | Merged |
---|---|
Approved by: | Chad Smith |
Approved revision: | d4d3ff41214b5daa80e785dae8a44792bc9dc43e |
Merged at revision: | 9d2a87dc386b7aed1a8243d599676e78ed358749 |
Proposed branch: | ~smoser/cloud-init:bug/1718029-fix-dhcp-parsing-from-networkd |
Merge into: | cloud-init:master |
Diff against target: |
629 lines (+282/-64) 6 files modified
cloudinit/net/dhcp.py (+42/-0) cloudinit/net/tests/test_dhcp.py (+111/-2) cloudinit/sources/DataSourceCloudStack.py (+13/-4) cloudinit/sources/helpers/azure.py (+14/-6) tests/unittests/test_datasource/test_azure_helper.py (+95/-48) tests/unittests/test_datasource/test_cloudstack.py (+7/-4) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Chad Smith | Approve | ||
Dimitri John Ledkov (community) | Approve | ||
Server Team CI bot | continuous-integration | Approve | |
Review via email: mp+331664@code.launchpad.net |
Commit message
Azure, CloudStack: Support reading dhcp options from systemd-networkd
Systems that used systemd-networkd's dhcp client would not be able
to get information on the Azure endpoint (placed in Option 245) or the
CloudStack server (in 'server_address').
The change here supports reading these files in /run/systemd/
The files declare that "This is private data. Do not parse.", but
at this point we do not have another option.
LP: #1718029
Description of the change
Scott Moser (smoser) wrote : | # |
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:dbbc9e0eab5
https:/
Executed test runs:
SUCCESS: Checkout
SUCCESS: Unit & Style Tests
FAILED: Ubuntu LTS: Build
Click here to trigger a rebuild:
https:/
Dimitri John Ledkov (xnox) wrote : | # |
os.listdir is not safe to call, if directory does not exist.
Dimitri John Ledkov (xnox) wrote : | # |
Also needs a test for no leases dir, or use glob.iglob in the implementation.
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:a6e48aaa195
https:/
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:/
Chad Smith (chad.smith) : | # |
Chad Smith (chad.smith) : | # |
Chad Smith (chad.smith) : | # |
Scott Moser (smoser) wrote : | # |
ok. feedbaack addressed.
also renamed the functions to 'networkd' rather than 'systemd'.
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:fe360f58def
https:/
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:/
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:d4d3ff41214
https:/
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:/
Dimitri John Ledkov (xnox) : | # |
Chad Smith (chad.smith) wrote : | # |
tested on Azure Artful with and without ifupdown. It worked in both cases either by parsing the /run/cloud-
Preview Diff
1 | diff --git a/cloudinit/net/dhcp.py b/cloudinit/net/dhcp.py |
2 | index 0535063..0cba703 100644 |
3 | --- a/cloudinit/net/dhcp.py |
4 | +++ b/cloudinit/net/dhcp.py |
5 | @@ -4,6 +4,7 @@ |
6 | # |
7 | # This file is part of cloud-init. See LICENSE file for license information. |
8 | |
9 | +import configobj |
10 | import logging |
11 | import os |
12 | import re |
13 | @@ -11,9 +12,12 @@ import re |
14 | from cloudinit.net import find_fallback_nic, get_devicelist |
15 | from cloudinit import temp_utils |
16 | from cloudinit import util |
17 | +from six import StringIO |
18 | |
19 | LOG = logging.getLogger(__name__) |
20 | |
21 | +NETWORKD_LEASES_DIR = '/run/systemd/netif/leases' |
22 | + |
23 | |
24 | class InvalidDHCPLeaseFileError(Exception): |
25 | """Raised when parsing an empty or invalid dhcp.leases file. |
26 | @@ -118,4 +122,42 @@ def dhcp_discovery(dhclient_cmd_path, interface, cleandir): |
27 | return parse_dhcp_lease_file(lease_file) |
28 | |
29 | |
30 | +def networkd_parse_lease(content): |
31 | + """Parse a systemd lease file content as in /run/systemd/netif/leases/ |
32 | + |
33 | + Parse this (almost) ini style file even though it says: |
34 | + # This is private data. Do not parse. |
35 | + |
36 | + Simply return a dictionary of key/values.""" |
37 | + |
38 | + return dict(configobj.ConfigObj(StringIO(content), list_values=False)) |
39 | + |
40 | + |
41 | +def networkd_load_leases(leases_d=None): |
42 | + """Return a dictionary of dictionaries representing each lease |
43 | + found in lease_d.i |
44 | + |
45 | + The top level key will be the filename, which is typically the ifindex.""" |
46 | + |
47 | + if leases_d is None: |
48 | + leases_d = NETWORKD_LEASES_DIR |
49 | + |
50 | + ret = {} |
51 | + if not os.path.isdir(leases_d): |
52 | + return ret |
53 | + for lfile in os.listdir(leases_d): |
54 | + ret[lfile] = networkd_parse_lease( |
55 | + util.load_file(os.path.join(leases_d, lfile))) |
56 | + return ret |
57 | + |
58 | + |
59 | +def networkd_get_option_from_leases(keyname, leases_d=None): |
60 | + if leases_d is None: |
61 | + leases_d = NETWORKD_LEASES_DIR |
62 | + leases = networkd_load_leases(leases_d=leases_d) |
63 | + for ifindex, data in sorted(leases.items()): |
64 | + if data.get(keyname): |
65 | + return data[keyname] |
66 | + return None |
67 | + |
68 | # vi: ts=4 expandtab |
69 | diff --git a/cloudinit/net/tests/test_dhcp.py b/cloudinit/net/tests/test_dhcp.py |
70 | index a38edae..1c1f504 100644 |
71 | --- a/cloudinit/net/tests/test_dhcp.py |
72 | +++ b/cloudinit/net/tests/test_dhcp.py |
73 | @@ -6,9 +6,9 @@ from textwrap import dedent |
74 | |
75 | from cloudinit.net.dhcp import ( |
76 | InvalidDHCPLeaseFileError, maybe_perform_dhcp_discovery, |
77 | - parse_dhcp_lease_file, dhcp_discovery) |
78 | + parse_dhcp_lease_file, dhcp_discovery, networkd_load_leases) |
79 | from cloudinit.util import ensure_file, write_file |
80 | -from cloudinit.tests.helpers import CiTestCase, wrap_and_call |
81 | +from cloudinit.tests.helpers import CiTestCase, wrap_and_call, populate_dir |
82 | |
83 | |
84 | class TestParseDHCPLeasesFile(CiTestCase): |
85 | @@ -149,3 +149,112 @@ class TestDHCPDiscoveryClean(CiTestCase): |
86 | [os.path.join(tmpdir, 'dhclient'), '-1', '-v', '-lf', |
87 | lease_file, '-pf', os.path.join(tmpdir, 'dhclient.pid'), |
88 | 'eth9', '-sf', '/bin/true'], capture=True)]) |
89 | + |
90 | + |
91 | +class TestSystemdParseLeases(CiTestCase): |
92 | + |
93 | + lxd_lease = dedent("""\ |
94 | + # This is private data. Do not parse. |
95 | + ADDRESS=10.75.205.242 |
96 | + NETMASK=255.255.255.0 |
97 | + ROUTER=10.75.205.1 |
98 | + SERVER_ADDRESS=10.75.205.1 |
99 | + NEXT_SERVER=10.75.205.1 |
100 | + BROADCAST=10.75.205.255 |
101 | + T1=1580 |
102 | + T2=2930 |
103 | + LIFETIME=3600 |
104 | + DNS=10.75.205.1 |
105 | + DOMAINNAME=lxd |
106 | + HOSTNAME=a1 |
107 | + CLIENTID=ffe617693400020000ab110c65a6a0866931c2 |
108 | + """) |
109 | + |
110 | + lxd_parsed = { |
111 | + 'ADDRESS': '10.75.205.242', |
112 | + 'NETMASK': '255.255.255.0', |
113 | + 'ROUTER': '10.75.205.1', |
114 | + 'SERVER_ADDRESS': '10.75.205.1', |
115 | + 'NEXT_SERVER': '10.75.205.1', |
116 | + 'BROADCAST': '10.75.205.255', |
117 | + 'T1': '1580', |
118 | + 'T2': '2930', |
119 | + 'LIFETIME': '3600', |
120 | + 'DNS': '10.75.205.1', |
121 | + 'DOMAINNAME': 'lxd', |
122 | + 'HOSTNAME': 'a1', |
123 | + 'CLIENTID': 'ffe617693400020000ab110c65a6a0866931c2', |
124 | + } |
125 | + |
126 | + azure_lease = dedent("""\ |
127 | + # This is private data. Do not parse. |
128 | + ADDRESS=10.132.0.5 |
129 | + NETMASK=255.255.255.255 |
130 | + ROUTER=10.132.0.1 |
131 | + SERVER_ADDRESS=169.254.169.254 |
132 | + NEXT_SERVER=10.132.0.1 |
133 | + MTU=1460 |
134 | + T1=43200 |
135 | + T2=75600 |
136 | + LIFETIME=86400 |
137 | + DNS=169.254.169.254 |
138 | + NTP=169.254.169.254 |
139 | + DOMAINNAME=c.ubuntu-foundations.internal |
140 | + DOMAIN_SEARCH_LIST=c.ubuntu-foundations.internal google.internal |
141 | + HOSTNAME=tribaal-test-171002-1349.c.ubuntu-foundations.internal |
142 | + ROUTES=10.132.0.1/32,0.0.0.0 0.0.0.0/0,10.132.0.1 |
143 | + CLIENTID=ff405663a200020000ab11332859494d7a8b4c |
144 | + OPTION_245=624c3620 |
145 | + """) |
146 | + |
147 | + azure_parsed = { |
148 | + 'ADDRESS': '10.132.0.5', |
149 | + 'NETMASK': '255.255.255.255', |
150 | + 'ROUTER': '10.132.0.1', |
151 | + 'SERVER_ADDRESS': '169.254.169.254', |
152 | + 'NEXT_SERVER': '10.132.0.1', |
153 | + 'MTU': '1460', |
154 | + 'T1': '43200', |
155 | + 'T2': '75600', |
156 | + 'LIFETIME': '86400', |
157 | + 'DNS': '169.254.169.254', |
158 | + 'NTP': '169.254.169.254', |
159 | + 'DOMAINNAME': 'c.ubuntu-foundations.internal', |
160 | + 'DOMAIN_SEARCH_LIST': 'c.ubuntu-foundations.internal google.internal', |
161 | + 'HOSTNAME': 'tribaal-test-171002-1349.c.ubuntu-foundations.internal', |
162 | + 'ROUTES': '10.132.0.1/32,0.0.0.0 0.0.0.0/0,10.132.0.1', |
163 | + 'CLIENTID': 'ff405663a200020000ab11332859494d7a8b4c', |
164 | + 'OPTION_245': '624c3620'} |
165 | + |
166 | + def setUp(self): |
167 | + super(TestSystemdParseLeases, self).setUp() |
168 | + self.lease_d = self.tmp_dir() |
169 | + |
170 | + def test_no_leases_returns_empty_dict(self): |
171 | + """A leases dir with no lease files should return empty dictionary.""" |
172 | + self.assertEqual({}, networkd_load_leases(self.lease_d)) |
173 | + |
174 | + def test_no_leases_dir_returns_empty_dict(self): |
175 | + """A non-existing leases dir should return empty dict.""" |
176 | + enodir = os.path.join(self.lease_d, 'does-not-exist') |
177 | + self.assertEqual({}, networkd_load_leases(enodir)) |
178 | + |
179 | + def test_single_leases_file(self): |
180 | + """A leases dir with one leases file.""" |
181 | + populate_dir(self.lease_d, {'2': self.lxd_lease}) |
182 | + self.assertEqual( |
183 | + {'2': self.lxd_parsed}, networkd_load_leases(self.lease_d)) |
184 | + |
185 | + def test_single_azure_leases_file(self): |
186 | + """On Azure, option 245 should be present, verify it specifically.""" |
187 | + populate_dir(self.lease_d, {'1': self.azure_lease}) |
188 | + self.assertEqual( |
189 | + {'1': self.azure_parsed}, networkd_load_leases(self.lease_d)) |
190 | + |
191 | + def test_multiple_files(self): |
192 | + """Multiple leases files on azure with one found return that value.""" |
193 | + self.maxDiff = None |
194 | + populate_dir(self.lease_d, {'1': self.azure_lease, |
195 | + '9': self.lxd_lease}) |
196 | + self.assertEqual({'1': self.azure_parsed, '9': self.lxd_parsed}, |
197 | + networkd_load_leases(self.lease_d)) |
198 | diff --git a/cloudinit/sources/DataSourceCloudStack.py b/cloudinit/sources/DataSourceCloudStack.py |
199 | index 7e0f9bb..9dc473f 100644 |
200 | --- a/cloudinit/sources/DataSourceCloudStack.py |
201 | +++ b/cloudinit/sources/DataSourceCloudStack.py |
202 | @@ -19,6 +19,7 @@ import time |
203 | |
204 | from cloudinit import ec2_utils as ec2 |
205 | from cloudinit import log as logging |
206 | +from cloudinit.net import dhcp |
207 | from cloudinit import sources |
208 | from cloudinit import url_helper as uhelp |
209 | from cloudinit import util |
210 | @@ -224,20 +225,28 @@ def get_vr_address(): |
211 | # Get the address of the virtual router via dhcp leases |
212 | # If no virtual router is detected, fallback on default gateway. |
213 | # See http://docs.cloudstack.apache.org/projects/cloudstack-administration/en/4.8/virtual_machines/user-data.html # noqa |
214 | + |
215 | + # Try networkd first... |
216 | + latest_address = dhcp.networkd_get_option_from_leases('SERVER_ADDRESS') |
217 | + if latest_address: |
218 | + LOG.debug("Found SERVER_ADDRESS '%s' via networkd_leases", |
219 | + latest_address) |
220 | + return latest_address |
221 | + |
222 | + # Try dhcp lease files next... |
223 | lease_file = get_latest_lease() |
224 | if not lease_file: |
225 | LOG.debug("No lease file found, using default gateway") |
226 | return get_default_gateway() |
227 | |
228 | - latest_address = None |
229 | with open(lease_file, "r") as fd: |
230 | for line in fd: |
231 | if "dhcp-server-identifier" in line: |
232 | words = line.strip(" ;\r\n").split(" ") |
233 | if len(words) > 2: |
234 | - dhcp = words[2] |
235 | - LOG.debug("Found DHCP identifier %s", dhcp) |
236 | - latest_address = dhcp |
237 | + dhcptok = words[2] |
238 | + LOG.debug("Found DHCP identifier %s", dhcptok) |
239 | + latest_address = dhcptok |
240 | if not latest_address: |
241 | # No virtual router found, fallback on default gateway |
242 | LOG.debug("No DHCP found, using default gateway") |
243 | diff --git a/cloudinit/sources/helpers/azure.py b/cloudinit/sources/helpers/azure.py |
244 | index 28ed0ae..959b1bd 100644 |
245 | --- a/cloudinit/sources/helpers/azure.py |
246 | +++ b/cloudinit/sources/helpers/azure.py |
247 | @@ -8,6 +8,7 @@ import socket |
248 | import struct |
249 | import time |
250 | |
251 | +from cloudinit.net import dhcp |
252 | from cloudinit import stages |
253 | from cloudinit import temp_utils |
254 | from contextlib import contextmanager |
255 | @@ -15,7 +16,6 @@ from xml.etree import ElementTree |
256 | |
257 | from cloudinit import util |
258 | |
259 | - |
260 | LOG = logging.getLogger(__name__) |
261 | |
262 | |
263 | @@ -239,6 +239,11 @@ class WALinuxAgentShim(object): |
264 | return socket.inet_ntoa(packed_bytes) |
265 | |
266 | @staticmethod |
267 | + def _networkd_get_value_from_leases(leases_d=None): |
268 | + return dhcp.networkd_get_option_from_leases( |
269 | + 'OPTION_245', leases_d=leases_d) |
270 | + |
271 | + @staticmethod |
272 | def _get_value_from_leases_file(fallback_lease_file): |
273 | leases = [] |
274 | content = util.load_file(fallback_lease_file) |
275 | @@ -287,12 +292,15 @@ class WALinuxAgentShim(object): |
276 | |
277 | @staticmethod |
278 | def find_endpoint(fallback_lease_file=None): |
279 | - LOG.debug('Finding Azure endpoint...') |
280 | value = None |
281 | - # Option-245 stored in /run/cloud-init/dhclient.hooks/<ifc>.json |
282 | - # a dhclient exit hook that calls cloud-init-dhclient-hook |
283 | - dhcp_options = WALinuxAgentShim._load_dhclient_json() |
284 | - value = WALinuxAgentShim._get_value_from_dhcpoptions(dhcp_options) |
285 | + LOG.debug('Finding Azure endpoint from networkd...') |
286 | + value = WALinuxAgentShim._networkd_get_value_from_leases() |
287 | + if value is None: |
288 | + # Option-245 stored in /run/cloud-init/dhclient.hooks/<ifc>.json |
289 | + # a dhclient exit hook that calls cloud-init-dhclient-hook |
290 | + LOG.debug('Finding Azure endpoint from hook json...') |
291 | + dhcp_options = WALinuxAgentShim._load_dhclient_json() |
292 | + value = WALinuxAgentShim._get_value_from_dhcpoptions(dhcp_options) |
293 | if value is None: |
294 | # Fallback and check the leases file if unsuccessful |
295 | LOG.debug("Unable to find endpoint in dhclient logs. " |
296 | diff --git a/tests/unittests/test_datasource/test_azure_helper.py b/tests/unittests/test_datasource/test_azure_helper.py |
297 | index 44b99ec..b42b073 100644 |
298 | --- a/tests/unittests/test_datasource/test_azure_helper.py |
299 | +++ b/tests/unittests/test_datasource/test_azure_helper.py |
300 | @@ -1,10 +1,12 @@ |
301 | # This file is part of cloud-init. See LICENSE file for license information. |
302 | |
303 | import os |
304 | +from textwrap import dedent |
305 | |
306 | from cloudinit.sources.helpers import azure as azure_helper |
307 | -from cloudinit.tests.helpers import ExitStack, mock, TestCase |
308 | +from cloudinit.tests.helpers import CiTestCase, ExitStack, mock, populate_dir |
309 | |
310 | +from cloudinit.sources.helpers.azure import WALinuxAgentShim as wa_shim |
311 | |
312 | GOAL_STATE_TEMPLATE = """\ |
313 | <?xml version="1.0" encoding="utf-8"?> |
314 | @@ -45,7 +47,7 @@ GOAL_STATE_TEMPLATE = """\ |
315 | """ |
316 | |
317 | |
318 | -class TestFindEndpoint(TestCase): |
319 | +class TestFindEndpoint(CiTestCase): |
320 | |
321 | def setUp(self): |
322 | super(TestFindEndpoint, self).setUp() |
323 | @@ -56,18 +58,19 @@ class TestFindEndpoint(TestCase): |
324 | mock.patch.object(azure_helper.util, 'load_file')) |
325 | |
326 | self.dhcp_options = patches.enter_context( |
327 | - mock.patch.object(azure_helper.WALinuxAgentShim, |
328 | - '_load_dhclient_json')) |
329 | + mock.patch.object(wa_shim, '_load_dhclient_json')) |
330 | + |
331 | + self.networkd_leases = patches.enter_context( |
332 | + mock.patch.object(wa_shim, '_networkd_get_value_from_leases')) |
333 | + self.networkd_leases.return_value = None |
334 | |
335 | def test_missing_file(self): |
336 | - self.assertRaises(ValueError, |
337 | - azure_helper.WALinuxAgentShim.find_endpoint) |
338 | + self.assertRaises(ValueError, wa_shim.find_endpoint) |
339 | |
340 | def test_missing_special_azure_line(self): |
341 | self.load_file.return_value = '' |
342 | self.dhcp_options.return_value = {'eth0': {'key': 'value'}} |
343 | - self.assertRaises(ValueError, |
344 | - azure_helper.WALinuxAgentShim.find_endpoint) |
345 | + self.assertRaises(ValueError, wa_shim.find_endpoint) |
346 | |
347 | @staticmethod |
348 | def _build_lease_content(encoded_address): |
349 | @@ -80,8 +83,7 @@ class TestFindEndpoint(TestCase): |
350 | |
351 | def test_from_dhcp_client(self): |
352 | self.dhcp_options.return_value = {"eth0": {"unknown_245": "5:4:3:2"}} |
353 | - self.assertEqual('5.4.3.2', |
354 | - azure_helper.WALinuxAgentShim.find_endpoint(None)) |
355 | + self.assertEqual('5.4.3.2', wa_shim.find_endpoint(None)) |
356 | |
357 | def test_latest_lease_used(self): |
358 | encoded_addresses = ['5:4:3:2', '4:3:2:1'] |
359 | @@ -89,53 +91,38 @@ class TestFindEndpoint(TestCase): |
360 | for encoded_address in encoded_addresses]) |
361 | self.load_file.return_value = file_content |
362 | self.assertEqual(encoded_addresses[-1].replace(':', '.'), |
363 | - azure_helper.WALinuxAgentShim.find_endpoint("foobar")) |
364 | + wa_shim.find_endpoint("foobar")) |
365 | |
366 | |
367 | -class TestExtractIpAddressFromLeaseValue(TestCase): |
368 | +class TestExtractIpAddressFromLeaseValue(CiTestCase): |
369 | |
370 | def test_hex_string(self): |
371 | ip_address, encoded_address = '98.76.54.32', '62:4c:36:20' |
372 | self.assertEqual( |
373 | - ip_address, |
374 | - azure_helper.WALinuxAgentShim.get_ip_from_lease_value( |
375 | - encoded_address |
376 | - )) |
377 | + ip_address, wa_shim.get_ip_from_lease_value(encoded_address)) |
378 | |
379 | def test_hex_string_with_single_character_part(self): |
380 | ip_address, encoded_address = '4.3.2.1', '4:3:2:1' |
381 | self.assertEqual( |
382 | - ip_address, |
383 | - azure_helper.WALinuxAgentShim.get_ip_from_lease_value( |
384 | - encoded_address |
385 | - )) |
386 | + ip_address, wa_shim.get_ip_from_lease_value(encoded_address)) |
387 | |
388 | def test_packed_string(self): |
389 | ip_address, encoded_address = '98.76.54.32', 'bL6 ' |
390 | self.assertEqual( |
391 | - ip_address, |
392 | - azure_helper.WALinuxAgentShim.get_ip_from_lease_value( |
393 | - encoded_address |
394 | - )) |
395 | + ip_address, wa_shim.get_ip_from_lease_value(encoded_address)) |
396 | |
397 | def test_packed_string_with_escaped_quote(self): |
398 | ip_address, encoded_address = '100.72.34.108', 'dH\\"l' |
399 | self.assertEqual( |
400 | - ip_address, |
401 | - azure_helper.WALinuxAgentShim.get_ip_from_lease_value( |
402 | - encoded_address |
403 | - )) |
404 | + ip_address, wa_shim.get_ip_from_lease_value(encoded_address)) |
405 | |
406 | def test_packed_string_containing_a_colon(self): |
407 | ip_address, encoded_address = '100.72.58.108', 'dH:l' |
408 | self.assertEqual( |
409 | - ip_address, |
410 | - azure_helper.WALinuxAgentShim.get_ip_from_lease_value( |
411 | - encoded_address |
412 | - )) |
413 | + ip_address, wa_shim.get_ip_from_lease_value(encoded_address)) |
414 | |
415 | |
416 | -class TestGoalStateParsing(TestCase): |
417 | +class TestGoalStateParsing(CiTestCase): |
418 | |
419 | default_parameters = { |
420 | 'incarnation': 1, |
421 | @@ -195,7 +182,7 @@ class TestGoalStateParsing(TestCase): |
422 | self.assertIsNone(certificates_xml) |
423 | |
424 | |
425 | -class TestAzureEndpointHttpClient(TestCase): |
426 | +class TestAzureEndpointHttpClient(CiTestCase): |
427 | |
428 | regular_headers = { |
429 | 'x-ms-agent-name': 'WALinuxAgent', |
430 | @@ -258,7 +245,7 @@ class TestAzureEndpointHttpClient(TestCase): |
431 | self.read_file_or_url.call_args) |
432 | |
433 | |
434 | -class TestOpenSSLManager(TestCase): |
435 | +class TestOpenSSLManager(CiTestCase): |
436 | |
437 | def setUp(self): |
438 | super(TestOpenSSLManager, self).setUp() |
439 | @@ -300,7 +287,7 @@ class TestOpenSSLManager(TestCase): |
440 | self.assertEqual([mock.call(manager.tmpdir)], del_dir.call_args_list) |
441 | |
442 | |
443 | -class TestWALinuxAgentShim(TestCase): |
444 | +class TestWALinuxAgentShim(CiTestCase): |
445 | |
446 | def setUp(self): |
447 | super(TestWALinuxAgentShim, self).setUp() |
448 | @@ -310,8 +297,7 @@ class TestWALinuxAgentShim(TestCase): |
449 | self.AzureEndpointHttpClient = patches.enter_context( |
450 | mock.patch.object(azure_helper, 'AzureEndpointHttpClient')) |
451 | self.find_endpoint = patches.enter_context( |
452 | - mock.patch.object( |
453 | - azure_helper.WALinuxAgentShim, 'find_endpoint')) |
454 | + mock.patch.object(wa_shim, 'find_endpoint')) |
455 | self.GoalState = patches.enter_context( |
456 | mock.patch.object(azure_helper, 'GoalState')) |
457 | self.OpenSSLManager = patches.enter_context( |
458 | @@ -320,7 +306,7 @@ class TestWALinuxAgentShim(TestCase): |
459 | mock.patch.object(azure_helper.time, 'sleep', mock.MagicMock())) |
460 | |
461 | def test_http_client_uses_certificate(self): |
462 | - shim = azure_helper.WALinuxAgentShim() |
463 | + shim = wa_shim() |
464 | shim.register_with_azure_and_fetch_data() |
465 | self.assertEqual( |
466 | [mock.call(self.OpenSSLManager.return_value.certificate)], |
467 | @@ -328,7 +314,7 @@ class TestWALinuxAgentShim(TestCase): |
468 | |
469 | def test_correct_url_used_for_goalstate(self): |
470 | self.find_endpoint.return_value = 'test_endpoint' |
471 | - shim = azure_helper.WALinuxAgentShim() |
472 | + shim = wa_shim() |
473 | shim.register_with_azure_and_fetch_data() |
474 | get = self.AzureEndpointHttpClient.return_value.get |
475 | self.assertEqual( |
476 | @@ -340,7 +326,7 @@ class TestWALinuxAgentShim(TestCase): |
477 | self.GoalState.call_args_list) |
478 | |
479 | def test_certificates_used_to_determine_public_keys(self): |
480 | - shim = azure_helper.WALinuxAgentShim() |
481 | + shim = wa_shim() |
482 | data = shim.register_with_azure_and_fetch_data() |
483 | self.assertEqual( |
484 | [mock.call(self.GoalState.return_value.certificates_xml)], |
485 | @@ -351,13 +337,13 @@ class TestWALinuxAgentShim(TestCase): |
486 | |
487 | def test_absent_certificates_produces_empty_public_keys(self): |
488 | self.GoalState.return_value.certificates_xml = None |
489 | - shim = azure_helper.WALinuxAgentShim() |
490 | + shim = wa_shim() |
491 | data = shim.register_with_azure_and_fetch_data() |
492 | self.assertEqual([], data['public-keys']) |
493 | |
494 | def test_correct_url_used_for_report_ready(self): |
495 | self.find_endpoint.return_value = 'test_endpoint' |
496 | - shim = azure_helper.WALinuxAgentShim() |
497 | + shim = wa_shim() |
498 | shim.register_with_azure_and_fetch_data() |
499 | expected_url = 'http://test_endpoint/machine?comp=health' |
500 | self.assertEqual( |
501 | @@ -368,7 +354,7 @@ class TestWALinuxAgentShim(TestCase): |
502 | self.GoalState.return_value.incarnation = 'TestIncarnation' |
503 | self.GoalState.return_value.container_id = 'TestContainerId' |
504 | self.GoalState.return_value.instance_id = 'TestInstanceId' |
505 | - shim = azure_helper.WALinuxAgentShim() |
506 | + shim = wa_shim() |
507 | shim.register_with_azure_and_fetch_data() |
508 | posted_document = ( |
509 | self.AzureEndpointHttpClient.return_value.post.call_args[1]['data'] |
510 | @@ -378,11 +364,11 @@ class TestWALinuxAgentShim(TestCase): |
511 | self.assertIn('TestInstanceId', posted_document) |
512 | |
513 | def test_clean_up_can_be_called_at_any_time(self): |
514 | - shim = azure_helper.WALinuxAgentShim() |
515 | + shim = wa_shim() |
516 | shim.clean_up() |
517 | |
518 | def test_clean_up_will_clean_up_openssl_manager_if_instantiated(self): |
519 | - shim = azure_helper.WALinuxAgentShim() |
520 | + shim = wa_shim() |
521 | shim.register_with_azure_and_fetch_data() |
522 | shim.clean_up() |
523 | self.assertEqual( |
524 | @@ -393,12 +379,12 @@ class TestWALinuxAgentShim(TestCase): |
525 | pass |
526 | self.AzureEndpointHttpClient.return_value.get.side_effect = ( |
527 | SentinelException) |
528 | - shim = azure_helper.WALinuxAgentShim() |
529 | + shim = wa_shim() |
530 | self.assertRaises(SentinelException, |
531 | shim.register_with_azure_and_fetch_data) |
532 | |
533 | |
534 | -class TestGetMetadataFromFabric(TestCase): |
535 | +class TestGetMetadataFromFabric(CiTestCase): |
536 | |
537 | @mock.patch.object(azure_helper, 'WALinuxAgentShim') |
538 | def test_data_from_shim_returned(self, shim): |
539 | @@ -422,4 +408,65 @@ class TestGetMetadataFromFabric(TestCase): |
540 | azure_helper.get_metadata_from_fabric) |
541 | self.assertEqual(1, shim.return_value.clean_up.call_count) |
542 | |
543 | + |
544 | +class TestExtractIpAddressFromNetworkd(CiTestCase): |
545 | + |
546 | + azure_lease = dedent("""\ |
547 | + # This is private data. Do not parse. |
548 | + ADDRESS=10.132.0.5 |
549 | + NETMASK=255.255.255.255 |
550 | + ROUTER=10.132.0.1 |
551 | + SERVER_ADDRESS=169.254.169.254 |
552 | + NEXT_SERVER=10.132.0.1 |
553 | + MTU=1460 |
554 | + T1=43200 |
555 | + T2=75600 |
556 | + LIFETIME=86400 |
557 | + DNS=169.254.169.254 |
558 | + NTP=169.254.169.254 |
559 | + DOMAINNAME=c.ubuntu-foundations.internal |
560 | + DOMAIN_SEARCH_LIST=c.ubuntu-foundations.internal google.internal |
561 | + HOSTNAME=tribaal-test-171002-1349.c.ubuntu-foundations.internal |
562 | + ROUTES=10.132.0.1/32,0.0.0.0 0.0.0.0/0,10.132.0.1 |
563 | + CLIENTID=ff405663a200020000ab11332859494d7a8b4c |
564 | + OPTION_245=624c3620 |
565 | + """) |
566 | + |
567 | + def setUp(self): |
568 | + super(TestExtractIpAddressFromNetworkd, self).setUp() |
569 | + self.lease_d = self.tmp_dir() |
570 | + |
571 | + def test_no_valid_leases_is_none(self): |
572 | + """No valid leases should return None.""" |
573 | + self.assertIsNone( |
574 | + wa_shim._networkd_get_value_from_leases(self.lease_d)) |
575 | + |
576 | + def test_option_245_is_found_in_single(self): |
577 | + """A single valid lease with 245 option should return it.""" |
578 | + populate_dir(self.lease_d, {'9': self.azure_lease}) |
579 | + self.assertEqual( |
580 | + '624c3620', wa_shim._networkd_get_value_from_leases(self.lease_d)) |
581 | + |
582 | + def test_option_245_not_found_returns_None(self): |
583 | + """A valid lease, but no option 245 should return None.""" |
584 | + populate_dir( |
585 | + self.lease_d, |
586 | + {'9': self.azure_lease.replace("OPTION_245", "OPTION_999")}) |
587 | + self.assertIsNone( |
588 | + wa_shim._networkd_get_value_from_leases(self.lease_d)) |
589 | + |
590 | + def test_multiple_returns_first(self): |
591 | + """Somewhat arbitrarily return the first address when multiple. |
592 | + |
593 | + Most important at the moment is that this is consistent behavior |
594 | + rather than changing randomly as in order of a dictionary.""" |
595 | + myval = "624c3601" |
596 | + populate_dir( |
597 | + self.lease_d, |
598 | + {'9': self.azure_lease, |
599 | + '2': self.azure_lease.replace("624c3620", myval)}) |
600 | + self.assertEqual( |
601 | + myval, wa_shim._networkd_get_value_from_leases(self.lease_d)) |
602 | + |
603 | + |
604 | # vi: ts=4 expandtab |
605 | diff --git a/tests/unittests/test_datasource/test_cloudstack.py b/tests/unittests/test_datasource/test_cloudstack.py |
606 | index 8e98e1b..96144b6 100644 |
607 | --- a/tests/unittests/test_datasource/test_cloudstack.py |
608 | +++ b/tests/unittests/test_datasource/test_cloudstack.py |
609 | @@ -23,13 +23,16 @@ class TestCloudStackPasswordFetching(CiTestCase): |
610 | default_gw = "192.201.20.0" |
611 | get_latest_lease = mock.MagicMock(return_value=None) |
612 | self.patches.enter_context(mock.patch( |
613 | - 'cloudinit.sources.DataSourceCloudStack.get_latest_lease', |
614 | - get_latest_lease)) |
615 | + mod_name + '.get_latest_lease', get_latest_lease)) |
616 | |
617 | get_default_gw = mock.MagicMock(return_value=default_gw) |
618 | self.patches.enter_context(mock.patch( |
619 | - 'cloudinit.sources.DataSourceCloudStack.get_default_gateway', |
620 | - get_default_gw)) |
621 | + mod_name + '.get_default_gateway', get_default_gw)) |
622 | + |
623 | + get_networkd_server_address = mock.MagicMock(return_value=None) |
624 | + self.patches.enter_context(mock.patch( |
625 | + mod_name + '.dhcp.networkd_get_option_from_leases', |
626 | + get_networkd_server_address)) |
627 | |
628 | def _set_password_server_response(self, response_string): |
629 | subp = mock.MagicMock(return_value=(response_string, '')) |
This re-works Dimitri's branch a bit (https:/ /code.launchpad .net/~xnox/ cloud-init/ +git/cloud- init/+merge/ 331642)