Merge ~minagalic/cloud-init:feax/ephemeral_connectivity into cloud-init:master
- Git
- lp:~minagalic/cloud-init
- feax/ephemeral_connectivity
- Merge into master
Status: | Merged |
---|---|
Approved by: | Chad Smith |
Approved revision: | f8466558de72accfcb9aca923a147fb3bedb2a4e |
Merged at revision: | ef0611a51a98a273cfa37b0daeb3e9d151888b88 |
Proposed branch: | ~minagalic/cloud-init:feax/ephemeral_connectivity |
Merge into: | cloud-init:master |
Diff against target: |
270 lines (+134/-7) 4 files modified
cloudinit/net/__init__.py (+34/-2) cloudinit/net/dhcp.py (+14/-3) cloudinit/net/tests/test_dhcp.py (+35/-1) cloudinit/net/tests/test_init.py (+51/-1) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Server Team CI bot | continuous-integration | Approve | |
Mina Galić (community) | Needs Information | ||
Chad Smith | Pending | ||
Review via email: mp+358876@code.launchpad.net |
Commit message
net: Ephemeral*Network: add connectivity check via URL
We add a new Optional parameter: connectivity_url
This is used in __enter__ to verify if a connection already exists.
If it does exist, no operations are performed.
Description of the change
Server Team CI bot (server-team-bot) wrote : | # |
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:0bb25f3a74a
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https:/
https:/
Executed test runs:
SUCCESS: Checkout
FAILED: Unit & Style Tests
Click here to trigger a rebuild:
https:/
- f3c4223... by =?utf-8?q?Igor_Gali=C4=87?= <email address hidden>
-
EphemeralDHCPv4: add connectivity_url parameter
..and pass it on to EphemeralIpv4Ne
twork
Mina Galić (minagalic) wrote : | # |
I'm gonna need some help here
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:119058aadd5
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https:/
https:/
Executed test runs:
SUCCESS: Checkout
FAILED: Unit & Style Tests
Click here to trigger a rebuild:
https:/
- 68046f6... by =?utf-8?q?Igor_Gali=C4=87?= <email address hidden>
-
Thanks to tests, discovered it's better to test connectivity ourselves
in __enter__(), *before* calling self.obtain_lease()
Otherwise we're performing too much discovery before trying to actually test
connectivity by instantiating EphemeralIPv4Network The question left now, is whether we should still pass connectivity_url to
EphemeralIPv4Network. Chances are, nothing has changed in the last sub-second,
and if it hasn't, then we're taking another 5 second penalty here.
Mina Galić (minagalic) wrote : | # |
Looks pretty okay so far, except for…
- 0291cf3... by =?utf-8?q?Igor_Gali=C4=87?= <email address hidden>
-
Remove private _connectivity_
check() we're using a public module function now
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:5dcdff256a4
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https:/
https:/
Executed test runs:
SUCCESS: Checkout
FAILED: Unit & Style Tests
Click here to trigger a rebuild:
https:/
Mina Galić (minagalic) wrote : | # |
should we pass connectivity_url from EphemeralDhcp4N
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:a3b483d25a8
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https:/
https:/
Executed test runs:
SUCCESS: Checkout
FAILED: Unit & Style Tests
Click here to trigger a rebuild:
https:/
Chad Smith (chad.smith) wrote : | # |
Here is a patch with some unit test cleanup and unit test adds for the new function you added.
I also adding a bit of checking on the url param passed to has_url_
Unit test adds and functionality changes below
http://
Chad Smith (chad.smith) wrote : | # |
The integration tests also voted failure on your branch because you didn't import UrlError in net/__init__.py. running tox locally on your system would have shown this if you have unit tests on the new function that was added.
- f846655... by Chad Smith
-
unit test cleanup; add unit test for has_url_
connectivity Also added a bit of checking on the url param passed to has_url_
connectivity
function since we don't want callsites to stuff in the wrong type of content.
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:7cba05e77a3
https:/
Executed test runs:
SUCCESS: Checkout
FAILED: Unit & Style Tests
Click here to trigger a rebuild:
https:/
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:2605bf0efab
https:/
Executed test runs:
SUCCESS: Checkout
FAILED: Unit & Style Tests
Click here to trigger a rebuild:
https:/
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:f8466558de7
https:/
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:/
Preview Diff
1 | diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py |
2 | index ad98a59..3642fb1 100644 |
3 | --- a/cloudinit/net/__init__.py |
4 | +++ b/cloudinit/net/__init__.py |
5 | @@ -12,6 +12,7 @@ import re |
6 | |
7 | from cloudinit.net.network_state import mask_to_net_prefix |
8 | from cloudinit import util |
9 | +from cloudinit.url_helper import UrlError, readurl |
10 | |
11 | LOG = logging.getLogger(__name__) |
12 | SYS_CLASS_NET = "/sys/class/net/" |
13 | @@ -647,16 +648,36 @@ def get_ib_hwaddrs_by_interface(): |
14 | return ret |
15 | |
16 | |
17 | +def has_url_connectivity(url): |
18 | + """Return true when the instance has access to the provided URL |
19 | + |
20 | + Logs a warning if url is not the expected format. |
21 | + """ |
22 | + if not any([url.startswith('http://'), url.startswith('https://')]): |
23 | + LOG.warning( |
24 | + "Ignoring connectivity check. Expected URL beginning with http*://" |
25 | + " received '%s'", url) |
26 | + return False |
27 | + try: |
28 | + readurl(url, timeout=5) |
29 | + except UrlError: |
30 | + return False |
31 | + return True |
32 | + |
33 | + |
34 | class EphemeralIPv4Network(object): |
35 | """Context manager which sets up temporary static network configuration. |
36 | |
37 | - No operations are performed if the provided interface is already connected. |
38 | + No operations are performed if the provided interface already has the |
39 | + specified configuration. |
40 | + This can be verified with the connectivity_url. |
41 | If unconnected, bring up the interface with valid ip, prefix and broadcast. |
42 | If router is provided setup a default route for that interface. Upon |
43 | context exit, clean up the interface leaving no configuration behind. |
44 | """ |
45 | |
46 | - def __init__(self, interface, ip, prefix_or_mask, broadcast, router=None): |
47 | + def __init__(self, interface, ip, prefix_or_mask, broadcast, router=None, |
48 | + connectivity_url=None): |
49 | """Setup context manager and validate call signature. |
50 | |
51 | @param interface: Name of the network interface to bring up. |
52 | @@ -665,6 +686,8 @@ class EphemeralIPv4Network(object): |
53 | prefix. |
54 | @param broadcast: Broadcast address for the IPv4 network. |
55 | @param router: Optionally the default gateway IP. |
56 | + @param connectivity_url: Optionally, a URL to verify if a usable |
57 | + connection already exists. |
58 | """ |
59 | if not all([interface, ip, prefix_or_mask, broadcast]): |
60 | raise ValueError( |
61 | @@ -675,6 +698,8 @@ class EphemeralIPv4Network(object): |
62 | except ValueError as e: |
63 | raise ValueError( |
64 | 'Cannot setup network: {0}'.format(e)) |
65 | + |
66 | + self.connectivity_url = connectivity_url |
67 | self.interface = interface |
68 | self.ip = ip |
69 | self.broadcast = broadcast |
70 | @@ -683,6 +708,13 @@ class EphemeralIPv4Network(object): |
71 | |
72 | def __enter__(self): |
73 | """Perform ephemeral network setup if interface is not connected.""" |
74 | + if self.connectivity_url: |
75 | + if has_url_connectivity(self.connectivity_url): |
76 | + LOG.debug( |
77 | + 'Skip ephemeral network setup, instance has connectivity' |
78 | + ' to %s', self.connectivity_url) |
79 | + return |
80 | + |
81 | self._bringup_device() |
82 | if self.router: |
83 | self._bringup_router() |
84 | diff --git a/cloudinit/net/dhcp.py b/cloudinit/net/dhcp.py |
85 | index bdc5799..0db991d 100644 |
86 | --- a/cloudinit/net/dhcp.py |
87 | +++ b/cloudinit/net/dhcp.py |
88 | @@ -11,7 +11,8 @@ import re |
89 | import signal |
90 | |
91 | from cloudinit.net import ( |
92 | - EphemeralIPv4Network, find_fallback_nic, get_devicelist) |
93 | + EphemeralIPv4Network, find_fallback_nic, get_devicelist, |
94 | + has_url_connectivity) |
95 | from cloudinit.net.network_state import mask_and_ipv4_to_bcast_addr as bcip |
96 | from cloudinit import temp_utils |
97 | from cloudinit import util |
98 | @@ -37,13 +38,21 @@ class NoDHCPLeaseError(Exception): |
99 | |
100 | |
101 | class EphemeralDHCPv4(object): |
102 | - def __init__(self, iface=None): |
103 | + def __init__(self, iface=None, connectivity_url=None): |
104 | self.iface = iface |
105 | self._ephipv4 = None |
106 | self.lease = None |
107 | + self.connectivity_url = connectivity_url |
108 | |
109 | def __enter__(self): |
110 | - """Setup sandboxed dhcp context.""" |
111 | + """Setup sandboxed dhcp context, unless connectivity_url can already be |
112 | + reached.""" |
113 | + if self.connectivity_url: |
114 | + if has_url_connectivity(self.connectivity_url): |
115 | + LOG.debug( |
116 | + 'Skip ephemeral DHCP setup, instance has connectivity' |
117 | + ' to %s', self.connectivity_url) |
118 | + return |
119 | return self.obtain_lease() |
120 | |
121 | def __exit__(self, excp_type, excp_value, excp_traceback): |
122 | @@ -86,6 +95,8 @@ class EphemeralDHCPv4(object): |
123 | kwargs = dict([(k, self.lease.get(v)) for k, v in nmap.items()]) |
124 | if not kwargs['broadcast']: |
125 | kwargs['broadcast'] = bcip(kwargs['prefix_or_mask'], kwargs['ip']) |
126 | + if self.connectivity_url: |
127 | + kwargs['connectivity_url'] = self.connectivity_url |
128 | ephipv4 = EphemeralIPv4Network(**kwargs) |
129 | ephipv4.__enter__() |
130 | self._ephipv4 = ephipv4 |
131 | diff --git a/cloudinit/net/tests/test_dhcp.py b/cloudinit/net/tests/test_dhcp.py |
132 | index db25b6f..cd3e732 100644 |
133 | --- a/cloudinit/net/tests/test_dhcp.py |
134 | +++ b/cloudinit/net/tests/test_dhcp.py |
135 | @@ -1,15 +1,17 @@ |
136 | # This file is part of cloud-init. See LICENSE file for license information. |
137 | |
138 | +import httpretty |
139 | import os |
140 | import signal |
141 | from textwrap import dedent |
142 | |
143 | +import cloudinit.net as net |
144 | from cloudinit.net.dhcp import ( |
145 | InvalidDHCPLeaseFileError, maybe_perform_dhcp_discovery, |
146 | parse_dhcp_lease_file, dhcp_discovery, networkd_load_leases) |
147 | from cloudinit.util import ensure_file, write_file |
148 | from cloudinit.tests.helpers import ( |
149 | - CiTestCase, mock, populate_dir, wrap_and_call) |
150 | + CiTestCase, HttprettyTestCase, mock, populate_dir, wrap_and_call) |
151 | |
152 | |
153 | class TestParseDHCPLeasesFile(CiTestCase): |
154 | @@ -321,3 +323,35 @@ class TestSystemdParseLeases(CiTestCase): |
155 | '9': self.lxd_lease}) |
156 | self.assertEqual({'1': self.azure_parsed, '9': self.lxd_parsed}, |
157 | networkd_load_leases(self.lease_d)) |
158 | + |
159 | + |
160 | +class TestEphemeralDhcpNoNetworkSetup(HttprettyTestCase): |
161 | + |
162 | + @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery') |
163 | + def test_ephemeral_dhcp_no_network_if_url_connectivity(self, m_dhcp): |
164 | + """No EphemeralDhcp4 network setup when connectivity_url succeeds.""" |
165 | + url = 'http://example.org/index.html' |
166 | + |
167 | + httpretty.register_uri(httpretty.GET, url) |
168 | + with net.dhcp.EphemeralDHCPv4(connectivity_url=url) as lease: |
169 | + self.assertIsNone(lease) |
170 | + # Ensure that no teardown happens: |
171 | + m_dhcp.assert_not_called() |
172 | + |
173 | + @mock.patch('cloudinit.net.dhcp.util.subp') |
174 | + @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery') |
175 | + def test_ephemeral_dhcp_setup_network_if_url_connectivity( |
176 | + self, m_dhcp, m_subp): |
177 | + """No EphemeralDhcp4 network setup when connectivity_url succeeds.""" |
178 | + url = 'http://example.org/index.html' |
179 | + fake_lease = { |
180 | + 'interface': 'eth9', 'fixed-address': '192.168.2.2', |
181 | + 'subnet-mask': '255.255.0.0'} |
182 | + m_dhcp.return_value = [fake_lease] |
183 | + m_subp.return_value = ('', '') |
184 | + |
185 | + httpretty.register_uri(httpretty.GET, url, body={}, status=404) |
186 | + with net.dhcp.EphemeralDHCPv4(connectivity_url=url) as lease: |
187 | + self.assertEqual(fake_lease, lease) |
188 | + # Ensure that dhcp discovery occurs |
189 | + m_dhcp.called_once_with() |
190 | diff --git a/cloudinit/net/tests/test_init.py b/cloudinit/net/tests/test_init.py |
191 | index 58e0a59..f55c31e 100644 |
192 | --- a/cloudinit/net/tests/test_init.py |
193 | +++ b/cloudinit/net/tests/test_init.py |
194 | @@ -2,14 +2,16 @@ |
195 | |
196 | import copy |
197 | import errno |
198 | +import httpretty |
199 | import mock |
200 | import os |
201 | +import requests |
202 | import textwrap |
203 | import yaml |
204 | |
205 | import cloudinit.net as net |
206 | from cloudinit.util import ensure_file, write_file, ProcessExecutionError |
207 | -from cloudinit.tests.helpers import CiTestCase |
208 | +from cloudinit.tests.helpers import CiTestCase, HttprettyTestCase |
209 | |
210 | |
211 | class TestSysDevPath(CiTestCase): |
212 | @@ -458,6 +460,22 @@ class TestEphemeralIPV4Network(CiTestCase): |
213 | self.assertEqual(expected_setup_calls, m_subp.call_args_list) |
214 | m_subp.assert_has_calls(expected_teardown_calls) |
215 | |
216 | + @mock.patch('cloudinit.net.readurl') |
217 | + def test_ephemeral_ipv4_no_network_if_url_connectivity( |
218 | + self, m_readurl, m_subp): |
219 | + """No network setup is performed if we can successfully connect to |
220 | + connectivity_url.""" |
221 | + params = { |
222 | + 'interface': 'eth0', 'ip': '192.168.2.2', |
223 | + 'prefix_or_mask': '255.255.255.0', 'broadcast': '192.168.2.255', |
224 | + 'connectivity_url': 'http://example.org/index.html'} |
225 | + |
226 | + with net.EphemeralIPv4Network(**params): |
227 | + self.assertEqual([mock.call('http://example.org/index.html', |
228 | + timeout=5)], m_readurl.call_args_list) |
229 | + # Ensure that no teardown happens: |
230 | + m_subp.assert_has_calls([]) |
231 | + |
232 | def test_ephemeral_ipv4_network_noop_when_configured(self, m_subp): |
233 | """EphemeralIPv4Network handles exception when address is setup. |
234 | |
235 | @@ -619,3 +637,35 @@ class TestApplyNetworkCfgNames(CiTestCase): |
236 | def test_apply_v2_renames_raises_runtime_error_on_unknown_version(self): |
237 | with self.assertRaises(RuntimeError): |
238 | net.apply_network_config_names(yaml.load("version: 3")) |
239 | + |
240 | + |
241 | +class TestHasURLConnectivity(HttprettyTestCase): |
242 | + |
243 | + def setUp(self): |
244 | + super(TestHasURLConnectivity, self).setUp() |
245 | + self.url = 'http://fake/' |
246 | + self.kwargs = {'allow_redirects': True, 'timeout': 5.0} |
247 | + |
248 | + @mock.patch('cloudinit.net.readurl') |
249 | + def test_url_timeout_on_connectivity_check(self, m_readurl): |
250 | + """A timeout of 5 seconds is provided when reading a url.""" |
251 | + self.assertTrue( |
252 | + net.has_url_connectivity(self.url), 'Expected True on url connect') |
253 | + |
254 | + def test_true_on_url_connectivity_success(self): |
255 | + httpretty.register_uri(httpretty.GET, self.url) |
256 | + self.assertTrue( |
257 | + net.has_url_connectivity(self.url), 'Expected True on url connect') |
258 | + |
259 | + @mock.patch('requests.Session.request') |
260 | + def test_true_on_url_connectivity_timeout(self, m_request): |
261 | + """A timeout raised accessing the url will return False.""" |
262 | + m_request.side_effect = requests.Timeout('Fake Connection Timeout') |
263 | + self.assertFalse( |
264 | + net.has_url_connectivity(self.url), |
265 | + 'Expected False on url timeout') |
266 | + |
267 | + def test_true_on_url_connectivity_failure(self): |
268 | + httpretty.register_uri(httpretty.GET, self.url, body={}, status=404) |
269 | + self.assertFalse( |
270 | + net.has_url_connectivity(self.url), 'Expected False on url fail') |
FAILED: Continuous integration, rev:3f711fd9cbd e2e4fb62a02d019 e21770dd3e242c /code.launchpad .net/~i. galic/cloud- init/+git/ cloud-init/ +merge/ 358876/ +edit-commit- message
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https:/
https:/ /jenkins. ubuntu. com/server/ job/cloud- init-ci/ 446/
Executed test runs:
SUCCESS: Checkout
FAILED: Unit & Style Tests
Click here to trigger a rebuild: /jenkins. ubuntu. com/server/ job/cloud- init-ci/ 446/rebuild
https:/