Merge ~minagalic/cloud-init:feax/ephemeral_connectivity into cloud-init:master

Proposed by Mina Galić
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)
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.

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

FAILED: Continuous integration, rev:3f711fd9cbde2e4fb62a02d019e21770dd3e242c
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://code.launchpad.net/~i.galic/cloud-init/+git/cloud-init/+merge/358876/+edit-commit-message

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:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/446/rebuild

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

FAILED: Continuous integration, rev:0bb25f3a74af9d2cf21c5e657f0b3325ab79c0e8
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://code.launchpad.net/~i.galic/cloud-init/+git/cloud-init/+merge/358876/+edit-commit-message

https://jenkins.ubuntu.com/server/job/cloud-init-ci/448/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

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

review: Needs Fixing (continuous-integration)
f3c4223... by =?utf-8?q?Igor_Gali=C4=87?= <email address hidden>

EphemeralDHCPv4: add connectivity_url parameter

..and pass it on to EphemeralIpv4Network

Revision history for this message
Mina Galić (minagalic) wrote :

I'm gonna need some help here

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

FAILED: Continuous integration, rev:119058aadd56a91c7889e861b7d7903fde164f3e
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://code.launchpad.net/~i.galic/cloud-init/+git/cloud-init/+merge/358876/+edit-commit-message

https://jenkins.ubuntu.com/server/job/cloud-init-ci/450/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

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

review: Needs Fixing (continuous-integration)
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.

Revision history for this message
Mina Galić (minagalic) wrote :

Looks pretty okay so far, except for…

review: Needs Fixing
0291cf3... by =?utf-8?q?Igor_Gali=C4=87?= <email address hidden>

Remove private _connectivity_check()

we're using a public module function now

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

FAILED: Continuous integration, rev:5dcdff256a4a6b87a5b5cb54ebcadb04864b1fbb
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://code.launchpad.net/~i.galic/cloud-init/+git/cloud-init/+merge/358876/+edit-commit-message

https://jenkins.ubuntu.com/server/job/cloud-init-ci/452/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

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

review: Needs Fixing (continuous-integration)
Revision history for this message
Mina Galić (minagalic) wrote :

should we pass connectivity_url from EphemeralDhcp4Network to EphemeralIPv4Network?

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

FAILED: Continuous integration, rev:a3b483d25a8e0972f53783696a20de8b305978d6
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://code.launchpad.net/~i.galic/cloud-init/+git/cloud-init/+merge/358876/+edit-commit-message

https://jenkins.ubuntu.com/server/job/cloud-init-ci/453/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

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

review: Needs Fixing (continuous-integration)
Revision history for this message
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_connectivity function since we don't want callsites to stuff in the wrong type of content.

Unit test adds and functionality changes below
http://paste.ubuntu.com/p/PQkjG6pHG8/

Revision history for this message
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.

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

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

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

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

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

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

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

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

review: Approve (continuous-integration)

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 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()
84diff --git a/cloudinit/net/dhcp.py b/cloudinit/net/dhcp.py
85index 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
131diff --git a/cloudinit/net/tests/test_dhcp.py b/cloudinit/net/tests/test_dhcp.py
132index 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()
190diff --git a/cloudinit/net/tests/test_init.py b/cloudinit/net/tests/test_init.py
191index 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')

Subscribers

People subscribed via source and target branches