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
1diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
2index e758006..624c9b4 100644
3--- a/cloudinit/net/__init__.py
4+++ b/cloudinit/net/__init__.py
5@@ -679,7 +679,7 @@ class EphemeralIPv4Network(object):
6 """
7
8 def __init__(self, interface, ip, prefix_or_mask, broadcast, router=None,
9- connectivity_url=None):
10+ connectivity_url=None, static_routes=None):
11 """Setup context manager and validate call signature.
12
13 @param interface: Name of the network interface to bring up.
14@@ -690,6 +690,7 @@ class EphemeralIPv4Network(object):
15 @param router: Optionally the default gateway IP.
16 @param connectivity_url: Optionally, a URL to verify if a usable
17 connection already exists.
18+ @param static_routes: Optionally a list of static routes from DHCP
19 """
20 if not all([interface, ip, prefix_or_mask, broadcast]):
21 raise ValueError(
22@@ -706,6 +707,7 @@ class EphemeralIPv4Network(object):
23 self.ip = ip
24 self.broadcast = broadcast
25 self.router = router
26+ self.static_routes = static_routes
27 self.cleanup_cmds = [] # List of commands to run to cleanup state.
28
29 def __enter__(self):
30@@ -718,7 +720,21 @@ class EphemeralIPv4Network(object):
31 return
32
33 self._bringup_device()
34- if self.router:
35+
36+ # rfc3442 requires us to ignore the router config *if* classless static
37+ # routes are provided.
38+ #
39+ # https://tools.ietf.org/html/rfc3442
40+ #
41+ # If the DHCP server returns both a Classless Static Routes option and
42+ # a Router option, the DHCP client MUST ignore the Router option.
43+ #
44+ # Similarly, if the DHCP server returns both a Classless Static Routes
45+ # option and a Static Routes option, the DHCP client MUST ignore the
46+ # Static Routes option.
47+ if self.static_routes:
48+ self._bringup_static_routes()
49+ elif self.router:
50 self._bringup_router()
51
52 def __exit__(self, excp_type, excp_value, excp_traceback):
53@@ -762,6 +778,20 @@ class EphemeralIPv4Network(object):
54 ['ip', '-family', 'inet', 'addr', 'del', cidr, 'dev',
55 self.interface])
56
57+ def _bringup_static_routes(self):
58+ # static_routes = [("169.254.169.254/32", "130.56.248.255"),
59+ # ("0.0.0.0/0", "130.56.240.1")]
60+ for net_address, gateway in self.static_routes:
61+ via_arg = []
62+ if gateway != "0.0.0.0/0":
63+ via_arg = ['via', gateway]
64+ util.subp(
65+ ['ip', '-4', 'route', 'add', net_address] + via_arg +
66+ ['dev', self.interface], capture=True)
67+ self.cleanup_cmds.insert(
68+ 0, ['ip', '-4', 'route', 'del', net_address] + via_arg +
69+ ['dev', self.interface])
70+
71 def _bringup_router(self):
72 """Perform the ip commands to fully setup the router if needed."""
73 # Check if a default route exists and exit if it does
74diff --git a/cloudinit/net/dhcp.py b/cloudinit/net/dhcp.py
75index c98a97c..1737991 100644
76--- a/cloudinit/net/dhcp.py
77+++ b/cloudinit/net/dhcp.py
78@@ -92,10 +92,14 @@ class EphemeralDHCPv4(object):
79 nmap = {'interface': 'interface', 'ip': 'fixed-address',
80 'prefix_or_mask': 'subnet-mask',
81 'broadcast': 'broadcast-address',
82+ 'static_routes': 'rfc3442-classless-static-routes',
83 'router': 'routers'}
84 kwargs = dict([(k, self.lease.get(v)) for k, v in nmap.items()])
85 if not kwargs['broadcast']:
86 kwargs['broadcast'] = bcip(kwargs['prefix_or_mask'], kwargs['ip'])
87+ if kwargs['static_routes']:
88+ kwargs['static_routes'] = (
89+ parse_static_routes(kwargs['static_routes']))
90 if self.connectivity_url:
91 kwargs['connectivity_url'] = self.connectivity_url
92 ephipv4 = EphemeralIPv4Network(**kwargs)
93@@ -272,4 +276,90 @@ def networkd_get_option_from_leases(keyname, leases_d=None):
94 return data[keyname]
95 return None
96
97+
98+def parse_static_routes(rfc3442):
99+ """ parse rfc3442 format and return a list containing tuple of strings.
100+
101+ The tuple is composed of the network_address (including net length) and
102+ gateway for a parsed static route.
103+
104+ @param rfc3442: string in rfc3442 format
105+ @returns: list of tuple(str, str) for all valid parsed routes until the
106+ first parsing error.
107+
108+ E.g.
109+ sr = parse_state_routes("32,169,254,169,254,130,56,248,255,0,130,56,240,1")
110+ sr = [
111+ ("169.254.169.254/32", "130.56.248.255"), ("0.0.0.0/0", "130.56.240.1")
112+ ]
113+
114+ Python version of isc-dhclient's hooks:
115+ /etc/dhcp/dhclient-exit-hooks.d/rfc3442-classless-routes
116+ """
117+ # raw strings from dhcp lease may end in semi-colon
118+ rfc3442 = rfc3442.rstrip(";")
119+ tokens = rfc3442.split(',')
120+ static_routes = []
121+
122+ def _trunc_error(cidr, required, remain):
123+ msg = ("RFC3442 string malformed. Current route has CIDR of %s "
124+ "and requires %s significant octets, but only %s remain. "
125+ "Verify DHCP rfc3442-classless-static-routes value: %s"
126+ % (cidr, required, remain, rfc3442))
127+ LOG.error(msg)
128+
129+ current_idx = 0
130+ for idx, tok in enumerate(tokens):
131+ if idx < current_idx:
132+ continue
133+ net_length = int(tok)
134+ if net_length in range(25, 33):
135+ req_toks = 9
136+ if len(tokens[idx:]) < req_toks:
137+ _trunc_error(net_length, req_toks, len(tokens[idx:]))
138+ return static_routes
139+ net_address = ".".join(tokens[idx+1:idx+5])
140+ gateway = ".".join(tokens[idx+5:idx+req_toks])
141+ current_idx = idx + req_toks
142+ elif net_length in range(17, 25):
143+ req_toks = 8
144+ if len(tokens[idx:]) < req_toks:
145+ _trunc_error(net_length, req_toks, len(tokens[idx:]))
146+ return static_routes
147+ net_address = ".".join(tokens[idx+1:idx+4] + ["0"])
148+ gateway = ".".join(tokens[idx+4:idx+req_toks])
149+ current_idx = idx + req_toks
150+ elif net_length in range(9, 17):
151+ req_toks = 7
152+ if len(tokens[idx:]) < req_toks:
153+ _trunc_error(net_length, req_toks, len(tokens[idx:]))
154+ return static_routes
155+ net_address = ".".join(tokens[idx+1:idx+3] + ["0", "0"])
156+ gateway = ".".join(tokens[idx+3:idx+req_toks])
157+ current_idx = idx + req_toks
158+ elif net_length in range(1, 9):
159+ req_toks = 6
160+ if len(tokens[idx:]) < req_toks:
161+ _trunc_error(net_length, req_toks, len(tokens[idx:]))
162+ return static_routes
163+ net_address = ".".join(tokens[idx+1:idx+2] + ["0", "0", "0"])
164+ gateway = ".".join(tokens[idx+2:idx+req_toks])
165+ current_idx = idx + req_toks
166+ elif net_length == 0:
167+ req_toks = 5
168+ if len(tokens[idx:]) < req_toks:
169+ _trunc_error(net_length, req_toks, len(tokens[idx:]))
170+ return static_routes
171+ net_address = "0.0.0.0"
172+ gateway = ".".join(tokens[idx+1:idx+req_toks])
173+ current_idx = idx + req_toks
174+ else:
175+ LOG.error('Parsed invalid net length "%s". Verify DHCP '
176+ 'rfc3442-classless-static-routes value.', net_length)
177+ return static_routes
178+
179+ static_routes.append(("%s/%s" % (net_address, net_length), gateway))
180+
181+ return static_routes
182+
183 # vi: ts=4 expandtab
184diff --git a/cloudinit/net/tests/test_dhcp.py b/cloudinit/net/tests/test_dhcp.py
185index 5139024..91f503c 100644
186--- a/cloudinit/net/tests/test_dhcp.py
187+++ b/cloudinit/net/tests/test_dhcp.py
188@@ -8,7 +8,8 @@ from textwrap import dedent
189 import cloudinit.net as net
190 from cloudinit.net.dhcp import (
191 InvalidDHCPLeaseFileError, maybe_perform_dhcp_discovery,
192- parse_dhcp_lease_file, dhcp_discovery, networkd_load_leases)
193+ parse_dhcp_lease_file, dhcp_discovery, networkd_load_leases,
194+ parse_static_routes)
195 from cloudinit.util import ensure_file, write_file
196 from cloudinit.tests.helpers import (
197 CiTestCase, HttprettyTestCase, mock, populate_dir, wrap_and_call)
198@@ -64,6 +65,123 @@ class TestParseDHCPLeasesFile(CiTestCase):
199 self.assertItemsEqual(expected, parse_dhcp_lease_file(lease_file))
200
201
202+class TestDHCPRFC3442(CiTestCase):
203+
204+ def test_parse_lease_finds_rfc3442_classless_static_routes(self):
205+ """parse_dhcp_lease_file returns rfc3442-classless-static-routes."""
206+ lease_file = self.tmp_path('leases')
207+ content = dedent("""
208+ lease {
209+ interface "wlp3s0";
210+ fixed-address 192.168.2.74;
211+ option subnet-mask 255.255.255.0;
212+ option routers 192.168.2.1;
213+ option rfc3442-classless-static-routes 0,130,56,240,1;
214+ renew 4 2017/07/27 18:02:30;
215+ expire 5 2017/07/28 07:08:15;
216+ }
217+ """)
218+ expected = [
219+ {'interface': 'wlp3s0', 'fixed-address': '192.168.2.74',
220+ 'subnet-mask': '255.255.255.0', 'routers': '192.168.2.1',
221+ 'rfc3442-classless-static-routes': '0,130,56,240,1',
222+ 'renew': '4 2017/07/27 18:02:30',
223+ 'expire': '5 2017/07/28 07:08:15'}]
224+ write_file(lease_file, content)
225+ self.assertItemsEqual(expected, parse_dhcp_lease_file(lease_file))
226+
227+ @mock.patch('cloudinit.net.dhcp.EphemeralIPv4Network')
228+ @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery')
229+ def test_obtain_lease_parses_static_routes(self, m_maybe, m_ipv4):
230+ """EphemeralDHPCv4 parses rfc3442 routes for EphemeralIPv4Network"""
231+ lease = [
232+ {'interface': 'wlp3s0', 'fixed-address': '192.168.2.74',
233+ 'subnet-mask': '255.255.255.0', 'routers': '192.168.2.1',
234+ 'rfc3442-classless-static-routes': '0,130,56,240,1',
235+ 'renew': '4 2017/07/27 18:02:30',
236+ 'expire': '5 2017/07/28 07:08:15'}]
237+ m_maybe.return_value = lease
238+ eph = net.dhcp.EphemeralDHCPv4()
239+ eph.obtain_lease()
240+ expected_kwargs = {
241+ 'interface': 'wlp3s0',
242+ 'ip': '192.168.2.74',
243+ 'prefix_or_mask': '255.255.255.0',
244+ 'broadcast': '192.168.2.255',
245+ 'static_routes': [('0.0.0.0/0', '130.56.240.1')],
246+ 'router': '192.168.2.1'}
247+ m_ipv4.assert_called_with(**expected_kwargs)
248+
249+
250+class TestDHCPParseStaticRoutes(CiTestCase):
251+
252+ with_logs = True
253+
254+ def parse_static_routes_empty_string(self):
255+ self.assertEqual([], parse_static_routes(""))
256+
257+ def test_parse_static_routes_invalid_input_returns_empty_list(self):
258+ rfc3442 = "32,169,254,169,254,130,56,248"
259+ self.assertEqual([], parse_static_routes(rfc3442))
260+
261+ def test_parse_static_routes_bogus_width_returns_empty_list(self):
262+ rfc3442 = "33,169,254,169,254,130,56,248"
263+ self.assertEqual([], parse_static_routes(rfc3442))
264+
265+ def test_parse_static_routes_single_ip(self):
266+ rfc3442 = "32,169,254,169,254,130,56,248,255"
267+ self.assertEqual([('169.254.169.254/32', '130.56.248.255')],
268+ parse_static_routes(rfc3442))
269+
270+ def test_parse_static_routes_single_ip_handles_trailing_semicolon(self):
271+ rfc3442 = "32,169,254,169,254,130,56,248,255;"
272+ self.assertEqual([('169.254.169.254/32', '130.56.248.255')],
273+ parse_static_routes(rfc3442))
274+
275+ def test_parse_static_routes_default_route(self):
276+ rfc3442 = "0,130,56,240,1"
277+ self.assertEqual([('0.0.0.0/0', '130.56.240.1')],
278+ parse_static_routes(rfc3442))
279+
280+ def test_parse_static_routes_class_c_b_a(self):
281+ class_c = "24,192,168,74,192,168,0,4"
282+ class_b = "16,172,16,172,16,0,4"
283+ class_a = "8,10,10,0,0,4"
284+ rfc3442 = ",".join([class_c, class_b, class_a])
285+ self.assertEqual(sorted([
286+ ("192.168.74.0/24", "192.168.0.4"),
287+ ("172.16.0.0/16", "172.16.0.4"),
288+ ("10.0.0.0/8", "10.0.0.4")
289+ ]), sorted(parse_static_routes(rfc3442)))
290+
291+ def test_parse_static_routes_logs_error_truncated(self):
292+ bad_rfc3442 = {
293+ "class_c": "24,169,254,169,10",
294+ "class_b": "16,172,16,10",
295+ "class_a": "8,10,10",
296+ "gateway": "0,0",
297+ "netlen": "33,0",
298+ }
299+ for rfc3442 in bad_rfc3442.values():
300+ self.assertEqual([], parse_static_routes(rfc3442))
301+
302+ logs = self.logs.getvalue()
303+ self.assertEqual(len(bad_rfc3442.keys()), len(logs.splitlines()))
304+
305+ def test_parse_static_routes_returns_valid_routes_until_parse_err(self):
306+ class_c = "24,192,168,74,192,168,0,4"
307+ class_b = "16,172,16,172,16,0,4"
308+ class_a_error = "8,10,10,0,0"
309+ rfc3442 = ",".join([class_c, class_b, class_a_error])
310+ self.assertEqual(sorted([
311+ ("192.168.74.0/24", "192.168.0.4"),
312+ ("172.16.0.0/16", "172.16.0.4"),
313+ ]), sorted(parse_static_routes(rfc3442)))
314+
315+ logs = self.logs.getvalue()
316+ self.assertIn(rfc3442, logs.splitlines()[0])
317+
318+
319 class TestDHCPDiscoveryClean(CiTestCase):
320 with_logs = True
321
322diff --git a/cloudinit/net/tests/test_init.py b/cloudinit/net/tests/test_init.py
323index 6d2affe..d393e6a 100644
324--- a/cloudinit/net/tests/test_init.py
325+++ b/cloudinit/net/tests/test_init.py
326@@ -549,6 +549,45 @@ class TestEphemeralIPV4Network(CiTestCase):
327 self.assertEqual(expected_setup_calls, m_subp.call_args_list)
328 m_subp.assert_has_calls(expected_teardown_calls)
329
330+ def test_ephemeral_ipv4_network_with_rfc3442_static_routes(self, m_subp):
331+ params = {
332+ 'interface': 'eth0', 'ip': '192.168.2.2',
333+ 'prefix_or_mask': '255.255.255.0', 'broadcast': '192.168.2.255',
334+ 'static_routes': [('169.254.169.254/32', '192.168.2.1'),
335+ ('0.0.0.0/0', '192.168.2.1')],
336+ 'router': '192.168.2.1'}
337+ expected_setup_calls = [
338+ mock.call(
339+ ['ip', '-family', 'inet', 'addr', 'add', '192.168.2.2/24',
340+ 'broadcast', '192.168.2.255', 'dev', 'eth0'],
341+ capture=True, update_env={'LANG': 'C'}),
342+ mock.call(
343+ ['ip', '-family', 'inet', 'link', 'set', 'dev', 'eth0', 'up'],
344+ capture=True),
345+ mock.call(
346+ ['ip', '-4', 'route', 'add', '169.254.169.254/32',
347+ 'via', '192.168.2.1', 'dev', 'eth0'], capture=True),
348+ mock.call(
349+ ['ip', '-4', 'route', 'add', '0.0.0.0/0',
350+ 'via', '192.168.2.1', 'dev', 'eth0'], capture=True)]
351+ expected_teardown_calls = [
352+ mock.call(
353+ ['ip', '-4', 'route', 'del', '0.0.0.0/0',
354+ 'via', '192.168.2.1', 'dev', 'eth0'], capture=True),
355+ mock.call(
356+ ['ip', '-4', 'route', 'del', '169.254.169.254/32',
357+ 'via', '192.168.2.1', 'dev', 'eth0'], capture=True),
358+ mock.call(
359+ ['ip', '-family', 'inet', 'link', 'set', 'dev',
360+ 'eth0', 'down'], capture=True),
361+ mock.call(
362+ ['ip', '-family', 'inet', 'addr', 'del',
363+ '192.168.2.2/24', 'dev', 'eth0'], capture=True)
364+ ]
365+ with net.EphemeralIPv4Network(**params):
366+ self.assertEqual(expected_setup_calls, m_subp.call_args_list)
367+ m_subp.assert_has_calls(expected_setup_calls + expected_teardown_calls)
368+
369
370 class TestApplyNetworkCfgNames(CiTestCase):
371 V1_CONFIG = textwrap.dedent("""\
372diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py
373index f27ef21..2de2aea 100644
374--- a/tests/unittests/test_datasource/test_azure.py
375+++ b/tests/unittests/test_datasource/test_azure.py
376@@ -1807,7 +1807,8 @@ class TestAzureDataSourcePreprovisioning(CiTestCase):
377 self.assertEqual(m_dhcp.call_count, 2)
378 m_net.assert_any_call(
379 broadcast='192.168.2.255', interface='eth9', ip='192.168.2.9',
380- prefix_or_mask='255.255.255.0', router='192.168.2.1')
381+ prefix_or_mask='255.255.255.0', router='192.168.2.1',
382+ static_routes=None)
383 self.assertEqual(m_net.call_count, 2)
384
385 def test__reprovision_calls__poll_imds(self, fake_resp,
386@@ -1845,7 +1846,8 @@ class TestAzureDataSourcePreprovisioning(CiTestCase):
387 self.assertEqual(m_dhcp.call_count, 2)
388 m_net.assert_any_call(
389 broadcast='192.168.2.255', interface='eth9', ip='192.168.2.9',
390- prefix_or_mask='255.255.255.0', router='192.168.2.1')
391+ prefix_or_mask='255.255.255.0', router='192.168.2.1',
392+ static_routes=None)
393 self.assertEqual(m_net.call_count, 2)
394
395
396diff --git a/tests/unittests/test_datasource/test_ec2.py b/tests/unittests/test_datasource/test_ec2.py
397index 20d59bf..1ec8e00 100644
398--- a/tests/unittests/test_datasource/test_ec2.py
399+++ b/tests/unittests/test_datasource/test_ec2.py
400@@ -538,7 +538,8 @@ class TestEc2(test_helpers.HttprettyTestCase):
401 m_dhcp.assert_called_once_with('eth9')
402 m_net.assert_called_once_with(
403 broadcast='192.168.2.255', interface='eth9', ip='192.168.2.9',
404- prefix_or_mask='255.255.255.0', router='192.168.2.1')
405+ prefix_or_mask='255.255.255.0', router='192.168.2.1',
406+ static_routes=None)
407 self.assertIn('Crawl of metadata service took', self.logs.getvalue())
408
409

Subscribers

People subscribed via source and target branches