Merge ~smoser/cloud-init:bug/1718029-fix-dhcp-parsing-from-networkd into cloud-init:master

Proposed by Scott Moser
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)
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/netif/leases.
The files declare that "This is private data. Do not parse.", but
at this point we do not have another option.

LP: #1718029

To post a comment you must log in.
Revision history for this message
Scott Moser (smoser) wrote :
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:dbbc9e0eab5cb492449c4e853623307861002363
https://jenkins.ubuntu.com/server/job/cloud-init-ci/367/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    FAILED: Ubuntu LTS: Build

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

review: Needs Fixing (continuous-integration)
Revision history for this message
Dimitri John Ledkov (xnox) wrote :

os.listdir is not safe to call, if directory does not exist.

review: Needs Fixing
Revision history for this message
Dimitri John Ledkov (xnox) wrote :

Also needs a test for no leases dir, or use glob.iglob in the implementation.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:a6e48aaa195e24ee459a754f780b4cd7aa4776cd
https://jenkins.ubuntu.com/server/job/cloud-init-ci/368/
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/368/rebuild

review: Approve (continuous-integration)
Revision history for this message
Chad Smith (chad.smith) :
Revision history for this message
Chad Smith (chad.smith) :
Revision history for this message
Chad Smith (chad.smith) :
Revision history for this message
Scott Moser (smoser) wrote :

ok. feedbaack addressed.
also renamed the functions to 'networkd' rather than 'systemd'.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:fe360f58defcb511decaadce41ef57efbfcea5ce
https://jenkins.ubuntu.com/server/job/cloud-init-ci/371/
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/371/rebuild

review: Approve (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:d4d3ff41214b5daa80e785dae8a44792bc9dc43e
https://jenkins.ubuntu.com/server/job/cloud-init-ci/372/
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/372/rebuild

review: Approve (continuous-integration)
Revision history for this message
Dimitri John Ledkov (xnox) :
review: Approve
Revision history for this message
Chad Smith (chad.smith) wrote :

tested on Azure Artful with and without ifupdown. It worked in both cases either by parsing the /run/cloud-init/dhclient.hooks/*json or by parsing /run/systemd/netif/leases/2

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/cloudinit/net/dhcp.py b/cloudinit/net/dhcp.py
2index 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
69diff --git a/cloudinit/net/tests/test_dhcp.py b/cloudinit/net/tests/test_dhcp.py
70index 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))
198diff --git a/cloudinit/sources/DataSourceCloudStack.py b/cloudinit/sources/DataSourceCloudStack.py
199index 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")
243diff --git a/cloudinit/sources/helpers/azure.py b/cloudinit/sources/helpers/azure.py
244index 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. "
296diff --git a/tests/unittests/test_datasource/test_azure_helper.py b/tests/unittests/test_datasource/test_azure_helper.py
297index 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
605diff --git a/tests/unittests/test_datasource/test_cloudstack.py b/tests/unittests/test_datasource/test_cloudstack.py
606index 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, ''))

Subscribers

People subscribed via source and target branches