Merge ~raharper/cloud-init:fix/ephemeral-dhcp-static-routes into cloud-init:master

Proposed by Ryan Harper
Status: Merged
Approved by: Dan Watkins
Approved revision: 24e1632715344f3cdb615f153e2fae30dce0f4bc
Merge reported by: Server Team CI bot
Merged at revision: not available
Proposed branch: ~raharper/cloud-init:fix/ephemeral-dhcp-static-routes
Merge into: cloud-init:master
Diff against target: 407 lines (+286/-6)
6 files modified
cloudinit/net/__init__.py (+32/-2)
cloudinit/net/dhcp.py (+90/-0)
cloudinit/net/tests/test_dhcp.py (+119/-1)
cloudinit/net/tests/test_init.py (+39/-0)
tests/unittests/test_datasource/test_azure.py (+4/-2)
tests/unittests/test_datasource/test_ec2.py (+2/-1)
Reviewer Review Type Date Requested Status
Dan Watkins Approve
Server Team CI bot continuous-integration Approve
Review via email: mp+368553@code.launchpad.net

Commit message

net: add rfc3442 (classless static routes) to EphemeralDHCP

The EphemeralDHCP context manager did not parse or handle
rfc3442 classless static routes which prevented reading
datasource metadata in some clouds. This branch adds support
for extracting the field from the leases output, parsing the
format and then adding the required iproute2 ip commands to
apply (and teardown) the static routes.

LP: #1821102

To post a comment you must log in.
Revision history for this message
Dan Watkins (oddbloke) wrote :

Wow, what an annoying format to have to parse. Overall, I think this looks good; I have a couple of inline comments. I'd also like to see us expand the unit testing of the parsing code so that we're at least covering every branch in there (and ideally doing at least one test with a bunch combined, to make sure that our skip lengths are accurate).

review: Needs Fixing
Revision history for this message
Ryan Harper (raharper) wrote :

+1 on expanded testing of the rfc3442 parsing.

Revision history for this message
Ryan Harper (raharper) :
Revision history for this message
Dan Watkins (oddbloke) :
20187bf... by Ryan Harper

address feedback on parsing rfc3442

dhcp.py/parse_static_routes:
 - Fix comments to match code and correct values
 - Always return a list type
 - Reduce parsing of the result but returning a list of tuples
   containing the network address and the gateway.
 - fix unittests

7ff9772... by Ryan Harper

RFC3442: add additional unittests and error logging

Adding additional unittests for parsing of RFC3442 format.
  - Fix missing net_length for class a, b, c routes
  - Removed hard-coded net_length for default routes
  - Log an error with details on what failed during parsing

b5a4708... by Ryan Harper

rfc3442: update docstring to indicate inclusion of net length

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

FAILED: Continuous integration, rev:b5a47081cfd934b5a1789b84ff506b2d0ca1a03e
https://jenkins.ubuntu.com/server/job/cloud-init-ci/735/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

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

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

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

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

FAILED: Continuous integration, rev:b5a47081cfd934b5a1789b84ff506b2d0ca1a03e
https://jenkins.ubuntu.com/server/job/cloud-init-ci/739/
Executed test runs:
    IN_PROGRESS: Declarative: Post Actions

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

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

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

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

review: Approve (continuous-integration)
Revision history for this message
Dan Watkins (oddbloke) wrote :

Incomplete review at EOD.

Revision history for this message
Scott Moser (smoser) wrote :

You've done a good job here. I didn't read the implementation completely, but the tests are good.

Revision history for this message
Ryan Harper (raharper) wrote :

Thanks, I'll update

3a9ec1a... by Ryan Harper

Log and error if netlength is invalid

Revision history for this message
Ryan Harper (raharper) wrote :

@Scott,

I left the comment where it is so to explain when cloud-init will take one path or the other.

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

FAILED: Continuous integration, rev:3a9ec1a0ef6cb3452f7b1bb8b70c5c8275971932
https://jenkins.ubuntu.com/server/job/cloud-init-ci/773/
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/773/rebuild

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

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

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

review: Approve (continuous-integration)
Revision history for this message
Dan Watkins (oddbloke) wrote :

Looking really good overall; a couple more inline questions, but this is v. close.

Revision history for this message
Ryan Harper (raharper) wrote :

Thanks, I've replied inline, looking for your thoughts before I refactor.

Revision history for this message
Dan Watkins (oddbloke) :
Revision history for this message
Dan Watkins (oddbloke) :
24e1632... by Ryan Harper

parse_static_routes: on parsing error return parsed static routes like dhclient

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

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

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

review: Approve (continuous-integration)
Revision history for this message
Dan Watkins (oddbloke) wrote :

Thanks for working through all this, Ryan, looks good to me!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
index e758006..624c9b4 100644
--- a/cloudinit/net/__init__.py
+++ b/cloudinit/net/__init__.py
@@ -679,7 +679,7 @@ class EphemeralIPv4Network(object):
679 """679 """
680680
681 def __init__(self, interface, ip, prefix_or_mask, broadcast, router=None,681 def __init__(self, interface, ip, prefix_or_mask, broadcast, router=None,
682 connectivity_url=None):682 connectivity_url=None, static_routes=None):
683 """Setup context manager and validate call signature.683 """Setup context manager and validate call signature.
684684
685 @param interface: Name of the network interface to bring up.685 @param interface: Name of the network interface to bring up.
@@ -690,6 +690,7 @@ class EphemeralIPv4Network(object):
690 @param router: Optionally the default gateway IP.690 @param router: Optionally the default gateway IP.
691 @param connectivity_url: Optionally, a URL to verify if a usable691 @param connectivity_url: Optionally, a URL to verify if a usable
692 connection already exists.692 connection already exists.
693 @param static_routes: Optionally a list of static routes from DHCP
693 """694 """
694 if not all([interface, ip, prefix_or_mask, broadcast]):695 if not all([interface, ip, prefix_or_mask, broadcast]):
695 raise ValueError(696 raise ValueError(
@@ -706,6 +707,7 @@ class EphemeralIPv4Network(object):
706 self.ip = ip707 self.ip = ip
707 self.broadcast = broadcast708 self.broadcast = broadcast
708 self.router = router709 self.router = router
710 self.static_routes = static_routes
709 self.cleanup_cmds = [] # List of commands to run to cleanup state.711 self.cleanup_cmds = [] # List of commands to run to cleanup state.
710712
711 def __enter__(self):713 def __enter__(self):
@@ -718,7 +720,21 @@ class EphemeralIPv4Network(object):
718 return720 return
719721
720 self._bringup_device()722 self._bringup_device()
721 if self.router:723
724 # rfc3442 requires us to ignore the router config *if* classless static
725 # routes are provided.
726 #
727 # https://tools.ietf.org/html/rfc3442
728 #
729 # If the DHCP server returns both a Classless Static Routes option and
730 # a Router option, the DHCP client MUST ignore the Router option.
731 #
732 # Similarly, if the DHCP server returns both a Classless Static Routes
733 # option and a Static Routes option, the DHCP client MUST ignore the
734 # Static Routes option.
735 if self.static_routes:
736 self._bringup_static_routes()
737 elif self.router:
722 self._bringup_router()738 self._bringup_router()
723739
724 def __exit__(self, excp_type, excp_value, excp_traceback):740 def __exit__(self, excp_type, excp_value, excp_traceback):
@@ -762,6 +778,20 @@ class EphemeralIPv4Network(object):
762 ['ip', '-family', 'inet', 'addr', 'del', cidr, 'dev',778 ['ip', '-family', 'inet', 'addr', 'del', cidr, 'dev',
763 self.interface])779 self.interface])
764780
781 def _bringup_static_routes(self):
782 # static_routes = [("169.254.169.254/32", "130.56.248.255"),
783 # ("0.0.0.0/0", "130.56.240.1")]
784 for net_address, gateway in self.static_routes:
785 via_arg = []
786 if gateway != "0.0.0.0/0":
787 via_arg = ['via', gateway]
788 util.subp(
789 ['ip', '-4', 'route', 'add', net_address] + via_arg +
790 ['dev', self.interface], capture=True)
791 self.cleanup_cmds.insert(
792 0, ['ip', '-4', 'route', 'del', net_address] + via_arg +
793 ['dev', self.interface])
794
765 def _bringup_router(self):795 def _bringup_router(self):
766 """Perform the ip commands to fully setup the router if needed."""796 """Perform the ip commands to fully setup the router if needed."""
767 # Check if a default route exists and exit if it does797 # Check if a default route exists and exit if it does
diff --git a/cloudinit/net/dhcp.py b/cloudinit/net/dhcp.py
index c98a97c..1737991 100644
--- a/cloudinit/net/dhcp.py
+++ b/cloudinit/net/dhcp.py
@@ -92,10 +92,14 @@ class EphemeralDHCPv4(object):
92 nmap = {'interface': 'interface', 'ip': 'fixed-address',92 nmap = {'interface': 'interface', 'ip': 'fixed-address',
93 'prefix_or_mask': 'subnet-mask',93 'prefix_or_mask': 'subnet-mask',
94 'broadcast': 'broadcast-address',94 'broadcast': 'broadcast-address',
95 'static_routes': 'rfc3442-classless-static-routes',
95 'router': 'routers'}96 'router': 'routers'}
96 kwargs = dict([(k, self.lease.get(v)) for k, v in nmap.items()])97 kwargs = dict([(k, self.lease.get(v)) for k, v in nmap.items()])
97 if not kwargs['broadcast']:98 if not kwargs['broadcast']:
98 kwargs['broadcast'] = bcip(kwargs['prefix_or_mask'], kwargs['ip'])99 kwargs['broadcast'] = bcip(kwargs['prefix_or_mask'], kwargs['ip'])
100 if kwargs['static_routes']:
101 kwargs['static_routes'] = (
102 parse_static_routes(kwargs['static_routes']))
99 if self.connectivity_url:103 if self.connectivity_url:
100 kwargs['connectivity_url'] = self.connectivity_url104 kwargs['connectivity_url'] = self.connectivity_url
101 ephipv4 = EphemeralIPv4Network(**kwargs)105 ephipv4 = EphemeralIPv4Network(**kwargs)
@@ -272,4 +276,90 @@ def networkd_get_option_from_leases(keyname, leases_d=None):
272 return data[keyname]276 return data[keyname]
273 return None277 return None
274278
279
280def parse_static_routes(rfc3442):
281 """ parse rfc3442 format and return a list containing tuple of strings.
282
283 The tuple is composed of the network_address (including net length) and
284 gateway for a parsed static route.
285
286 @param rfc3442: string in rfc3442 format
287 @returns: list of tuple(str, str) for all valid parsed routes until the
288 first parsing error.
289
290 E.g.
291 sr = parse_state_routes("32,169,254,169,254,130,56,248,255,0,130,56,240,1")
292 sr = [
293 ("169.254.169.254/32", "130.56.248.255"), ("0.0.0.0/0", "130.56.240.1")
294 ]
295
296 Python version of isc-dhclient's hooks:
297 /etc/dhcp/dhclient-exit-hooks.d/rfc3442-classless-routes
298 """
299 # raw strings from dhcp lease may end in semi-colon
300 rfc3442 = rfc3442.rstrip(";")
301 tokens = rfc3442.split(',')
302 static_routes = []
303
304 def _trunc_error(cidr, required, remain):
305 msg = ("RFC3442 string malformed. Current route has CIDR of %s "
306 "and requires %s significant octets, but only %s remain. "
307 "Verify DHCP rfc3442-classless-static-routes value: %s"
308 % (cidr, required, remain, rfc3442))
309 LOG.error(msg)
310
311 current_idx = 0
312 for idx, tok in enumerate(tokens):
313 if idx < current_idx:
314 continue
315 net_length = int(tok)
316 if net_length in range(25, 33):
317 req_toks = 9
318 if len(tokens[idx:]) < req_toks:
319 _trunc_error(net_length, req_toks, len(tokens[idx:]))
320 return static_routes
321 net_address = ".".join(tokens[idx+1:idx+5])
322 gateway = ".".join(tokens[idx+5:idx+req_toks])
323 current_idx = idx + req_toks
324 elif net_length in range(17, 25):
325 req_toks = 8
326 if len(tokens[idx:]) < req_toks:
327 _trunc_error(net_length, req_toks, len(tokens[idx:]))
328 return static_routes
329 net_address = ".".join(tokens[idx+1:idx+4] + ["0"])
330 gateway = ".".join(tokens[idx+4:idx+req_toks])
331 current_idx = idx + req_toks
332 elif net_length in range(9, 17):
333 req_toks = 7
334 if len(tokens[idx:]) < req_toks:
335 _trunc_error(net_length, req_toks, len(tokens[idx:]))
336 return static_routes
337 net_address = ".".join(tokens[idx+1:idx+3] + ["0", "0"])
338 gateway = ".".join(tokens[idx+3:idx+req_toks])
339 current_idx = idx + req_toks
340 elif net_length in range(1, 9):
341 req_toks = 6
342 if len(tokens[idx:]) < req_toks:
343 _trunc_error(net_length, req_toks, len(tokens[idx:]))
344 return static_routes
345 net_address = ".".join(tokens[idx+1:idx+2] + ["0", "0", "0"])
346 gateway = ".".join(tokens[idx+2:idx+req_toks])
347 current_idx = idx + req_toks
348 elif net_length == 0:
349 req_toks = 5
350 if len(tokens[idx:]) < req_toks:
351 _trunc_error(net_length, req_toks, len(tokens[idx:]))
352 return static_routes
353 net_address = "0.0.0.0"
354 gateway = ".".join(tokens[idx+1:idx+req_toks])
355 current_idx = idx + req_toks
356 else:
357 LOG.error('Parsed invalid net length "%s". Verify DHCP '
358 'rfc3442-classless-static-routes value.', net_length)
359 return static_routes
360
361 static_routes.append(("%s/%s" % (net_address, net_length), gateway))
362
363 return static_routes
364
275# vi: ts=4 expandtab365# vi: ts=4 expandtab
diff --git a/cloudinit/net/tests/test_dhcp.py b/cloudinit/net/tests/test_dhcp.py
index 5139024..91f503c 100644
--- a/cloudinit/net/tests/test_dhcp.py
+++ b/cloudinit/net/tests/test_dhcp.py
@@ -8,7 +8,8 @@ from textwrap import dedent
8import cloudinit.net as net8import cloudinit.net as net
9from cloudinit.net.dhcp import (9from cloudinit.net.dhcp import (
10 InvalidDHCPLeaseFileError, maybe_perform_dhcp_discovery,10 InvalidDHCPLeaseFileError, maybe_perform_dhcp_discovery,
11 parse_dhcp_lease_file, dhcp_discovery, networkd_load_leases)11 parse_dhcp_lease_file, dhcp_discovery, networkd_load_leases,
12 parse_static_routes)
12from cloudinit.util import ensure_file, write_file13from cloudinit.util import ensure_file, write_file
13from cloudinit.tests.helpers import (14from cloudinit.tests.helpers import (
14 CiTestCase, HttprettyTestCase, mock, populate_dir, wrap_and_call)15 CiTestCase, HttprettyTestCase, mock, populate_dir, wrap_and_call)
@@ -64,6 +65,123 @@ class TestParseDHCPLeasesFile(CiTestCase):
64 self.assertItemsEqual(expected, parse_dhcp_lease_file(lease_file))65 self.assertItemsEqual(expected, parse_dhcp_lease_file(lease_file))
6566
6667
68class TestDHCPRFC3442(CiTestCase):
69
70 def test_parse_lease_finds_rfc3442_classless_static_routes(self):
71 """parse_dhcp_lease_file returns rfc3442-classless-static-routes."""
72 lease_file = self.tmp_path('leases')
73 content = dedent("""
74 lease {
75 interface "wlp3s0";
76 fixed-address 192.168.2.74;
77 option subnet-mask 255.255.255.0;
78 option routers 192.168.2.1;
79 option rfc3442-classless-static-routes 0,130,56,240,1;
80 renew 4 2017/07/27 18:02:30;
81 expire 5 2017/07/28 07:08:15;
82 }
83 """)
84 expected = [
85 {'interface': 'wlp3s0', 'fixed-address': '192.168.2.74',
86 'subnet-mask': '255.255.255.0', 'routers': '192.168.2.1',
87 'rfc3442-classless-static-routes': '0,130,56,240,1',
88 'renew': '4 2017/07/27 18:02:30',
89 'expire': '5 2017/07/28 07:08:15'}]
90 write_file(lease_file, content)
91 self.assertItemsEqual(expected, parse_dhcp_lease_file(lease_file))
92
93 @mock.patch('cloudinit.net.dhcp.EphemeralIPv4Network')
94 @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery')
95 def test_obtain_lease_parses_static_routes(self, m_maybe, m_ipv4):
96 """EphemeralDHPCv4 parses rfc3442 routes for EphemeralIPv4Network"""
97 lease = [
98 {'interface': 'wlp3s0', 'fixed-address': '192.168.2.74',
99 'subnet-mask': '255.255.255.0', 'routers': '192.168.2.1',
100 'rfc3442-classless-static-routes': '0,130,56,240,1',
101 'renew': '4 2017/07/27 18:02:30',
102 'expire': '5 2017/07/28 07:08:15'}]
103 m_maybe.return_value = lease
104 eph = net.dhcp.EphemeralDHCPv4()
105 eph.obtain_lease()
106 expected_kwargs = {
107 'interface': 'wlp3s0',
108 'ip': '192.168.2.74',
109 'prefix_or_mask': '255.255.255.0',
110 'broadcast': '192.168.2.255',
111 'static_routes': [('0.0.0.0/0', '130.56.240.1')],
112 'router': '192.168.2.1'}
113 m_ipv4.assert_called_with(**expected_kwargs)
114
115
116class TestDHCPParseStaticRoutes(CiTestCase):
117
118 with_logs = True
119
120 def parse_static_routes_empty_string(self):
121 self.assertEqual([], parse_static_routes(""))
122
123 def test_parse_static_routes_invalid_input_returns_empty_list(self):
124 rfc3442 = "32,169,254,169,254,130,56,248"
125 self.assertEqual([], parse_static_routes(rfc3442))
126
127 def test_parse_static_routes_bogus_width_returns_empty_list(self):
128 rfc3442 = "33,169,254,169,254,130,56,248"
129 self.assertEqual([], parse_static_routes(rfc3442))
130
131 def test_parse_static_routes_single_ip(self):
132 rfc3442 = "32,169,254,169,254,130,56,248,255"
133 self.assertEqual([('169.254.169.254/32', '130.56.248.255')],
134 parse_static_routes(rfc3442))
135
136 def test_parse_static_routes_single_ip_handles_trailing_semicolon(self):
137 rfc3442 = "32,169,254,169,254,130,56,248,255;"
138 self.assertEqual([('169.254.169.254/32', '130.56.248.255')],
139 parse_static_routes(rfc3442))
140
141 def test_parse_static_routes_default_route(self):
142 rfc3442 = "0,130,56,240,1"
143 self.assertEqual([('0.0.0.0/0', '130.56.240.1')],
144 parse_static_routes(rfc3442))
145
146 def test_parse_static_routes_class_c_b_a(self):
147 class_c = "24,192,168,74,192,168,0,4"
148 class_b = "16,172,16,172,16,0,4"
149 class_a = "8,10,10,0,0,4"
150 rfc3442 = ",".join([class_c, class_b, class_a])
151 self.assertEqual(sorted([
152 ("192.168.74.0/24", "192.168.0.4"),
153 ("172.16.0.0/16", "172.16.0.4"),
154 ("10.0.0.0/8", "10.0.0.4")
155 ]), sorted(parse_static_routes(rfc3442)))
156
157 def test_parse_static_routes_logs_error_truncated(self):
158 bad_rfc3442 = {
159 "class_c": "24,169,254,169,10",
160 "class_b": "16,172,16,10",
161 "class_a": "8,10,10",
162 "gateway": "0,0",
163 "netlen": "33,0",
164 }
165 for rfc3442 in bad_rfc3442.values():
166 self.assertEqual([], parse_static_routes(rfc3442))
167
168 logs = self.logs.getvalue()
169 self.assertEqual(len(bad_rfc3442.keys()), len(logs.splitlines()))
170
171 def test_parse_static_routes_returns_valid_routes_until_parse_err(self):
172 class_c = "24,192,168,74,192,168,0,4"
173 class_b = "16,172,16,172,16,0,4"
174 class_a_error = "8,10,10,0,0"
175 rfc3442 = ",".join([class_c, class_b, class_a_error])
176 self.assertEqual(sorted([
177 ("192.168.74.0/24", "192.168.0.4"),
178 ("172.16.0.0/16", "172.16.0.4"),
179 ]), sorted(parse_static_routes(rfc3442)))
180
181 logs = self.logs.getvalue()
182 self.assertIn(rfc3442, logs.splitlines()[0])
183
184
67class TestDHCPDiscoveryClean(CiTestCase):185class TestDHCPDiscoveryClean(CiTestCase):
68 with_logs = True186 with_logs = True
69187
diff --git a/cloudinit/net/tests/test_init.py b/cloudinit/net/tests/test_init.py
index 6d2affe..d393e6a 100644
--- a/cloudinit/net/tests/test_init.py
+++ b/cloudinit/net/tests/test_init.py
@@ -549,6 +549,45 @@ class TestEphemeralIPV4Network(CiTestCase):
549 self.assertEqual(expected_setup_calls, m_subp.call_args_list)549 self.assertEqual(expected_setup_calls, m_subp.call_args_list)
550 m_subp.assert_has_calls(expected_teardown_calls)550 m_subp.assert_has_calls(expected_teardown_calls)
551551
552 def test_ephemeral_ipv4_network_with_rfc3442_static_routes(self, m_subp):
553 params = {
554 'interface': 'eth0', 'ip': '192.168.2.2',
555 'prefix_or_mask': '255.255.255.0', 'broadcast': '192.168.2.255',
556 'static_routes': [('169.254.169.254/32', '192.168.2.1'),
557 ('0.0.0.0/0', '192.168.2.1')],
558 'router': '192.168.2.1'}
559 expected_setup_calls = [
560 mock.call(
561 ['ip', '-family', 'inet', 'addr', 'add', '192.168.2.2/24',
562 'broadcast', '192.168.2.255', 'dev', 'eth0'],
563 capture=True, update_env={'LANG': 'C'}),
564 mock.call(
565 ['ip', '-family', 'inet', 'link', 'set', 'dev', 'eth0', 'up'],
566 capture=True),
567 mock.call(
568 ['ip', '-4', 'route', 'add', '169.254.169.254/32',
569 'via', '192.168.2.1', 'dev', 'eth0'], capture=True),
570 mock.call(
571 ['ip', '-4', 'route', 'add', '0.0.0.0/0',
572 'via', '192.168.2.1', 'dev', 'eth0'], capture=True)]
573 expected_teardown_calls = [
574 mock.call(
575 ['ip', '-4', 'route', 'del', '0.0.0.0/0',
576 'via', '192.168.2.1', 'dev', 'eth0'], capture=True),
577 mock.call(
578 ['ip', '-4', 'route', 'del', '169.254.169.254/32',
579 'via', '192.168.2.1', 'dev', 'eth0'], capture=True),
580 mock.call(
581 ['ip', '-family', 'inet', 'link', 'set', 'dev',
582 'eth0', 'down'], capture=True),
583 mock.call(
584 ['ip', '-family', 'inet', 'addr', 'del',
585 '192.168.2.2/24', 'dev', 'eth0'], capture=True)
586 ]
587 with net.EphemeralIPv4Network(**params):
588 self.assertEqual(expected_setup_calls, m_subp.call_args_list)
589 m_subp.assert_has_calls(expected_setup_calls + expected_teardown_calls)
590
552591
553class TestApplyNetworkCfgNames(CiTestCase):592class TestApplyNetworkCfgNames(CiTestCase):
554 V1_CONFIG = textwrap.dedent("""\593 V1_CONFIG = textwrap.dedent("""\
diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py
index f27ef21..2de2aea 100644
--- a/tests/unittests/test_datasource/test_azure.py
+++ b/tests/unittests/test_datasource/test_azure.py
@@ -1807,7 +1807,8 @@ class TestAzureDataSourcePreprovisioning(CiTestCase):
1807 self.assertEqual(m_dhcp.call_count, 2)1807 self.assertEqual(m_dhcp.call_count, 2)
1808 m_net.assert_any_call(1808 m_net.assert_any_call(
1809 broadcast='192.168.2.255', interface='eth9', ip='192.168.2.9',1809 broadcast='192.168.2.255', interface='eth9', ip='192.168.2.9',
1810 prefix_or_mask='255.255.255.0', router='192.168.2.1')1810 prefix_or_mask='255.255.255.0', router='192.168.2.1',
1811 static_routes=None)
1811 self.assertEqual(m_net.call_count, 2)1812 self.assertEqual(m_net.call_count, 2)
18121813
1813 def test__reprovision_calls__poll_imds(self, fake_resp,1814 def test__reprovision_calls__poll_imds(self, fake_resp,
@@ -1845,7 +1846,8 @@ class TestAzureDataSourcePreprovisioning(CiTestCase):
1845 self.assertEqual(m_dhcp.call_count, 2)1846 self.assertEqual(m_dhcp.call_count, 2)
1846 m_net.assert_any_call(1847 m_net.assert_any_call(
1847 broadcast='192.168.2.255', interface='eth9', ip='192.168.2.9',1848 broadcast='192.168.2.255', interface='eth9', ip='192.168.2.9',
1848 prefix_or_mask='255.255.255.0', router='192.168.2.1')1849 prefix_or_mask='255.255.255.0', router='192.168.2.1',
1850 static_routes=None)
1849 self.assertEqual(m_net.call_count, 2)1851 self.assertEqual(m_net.call_count, 2)
18501852
18511853
diff --git a/tests/unittests/test_datasource/test_ec2.py b/tests/unittests/test_datasource/test_ec2.py
index 20d59bf..1ec8e00 100644
--- a/tests/unittests/test_datasource/test_ec2.py
+++ b/tests/unittests/test_datasource/test_ec2.py
@@ -538,7 +538,8 @@ class TestEc2(test_helpers.HttprettyTestCase):
538 m_dhcp.assert_called_once_with('eth9')538 m_dhcp.assert_called_once_with('eth9')
539 m_net.assert_called_once_with(539 m_net.assert_called_once_with(
540 broadcast='192.168.2.255', interface='eth9', ip='192.168.2.9',540 broadcast='192.168.2.255', interface='eth9', ip='192.168.2.9',
541 prefix_or_mask='255.255.255.0', router='192.168.2.1')541 prefix_or_mask='255.255.255.0', router='192.168.2.1',
542 static_routes=None)
542 self.assertIn('Crawl of metadata service took', self.logs.getvalue())543 self.assertIn('Crawl of metadata service took', self.logs.getvalue())
543544
544545

Subscribers

People subscribed via source and target branches