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
diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
index ad98a59..3642fb1 100644
--- a/cloudinit/net/__init__.py
+++ b/cloudinit/net/__init__.py
@@ -12,6 +12,7 @@ import re
1212
13from cloudinit.net.network_state import mask_to_net_prefix13from cloudinit.net.network_state import mask_to_net_prefix
14from cloudinit import util14from cloudinit import util
15from cloudinit.url_helper import UrlError, readurl
1516
16LOG = logging.getLogger(__name__)17LOG = logging.getLogger(__name__)
17SYS_CLASS_NET = "/sys/class/net/"18SYS_CLASS_NET = "/sys/class/net/"
@@ -647,16 +648,36 @@ def get_ib_hwaddrs_by_interface():
647 return ret648 return ret
648649
649650
651def has_url_connectivity(url):
652 """Return true when the instance has access to the provided URL
653
654 Logs a warning if url is not the expected format.
655 """
656 if not any([url.startswith('http://'), url.startswith('https://')]):
657 LOG.warning(
658 "Ignoring connectivity check. Expected URL beginning with http*://"
659 " received '%s'", url)
660 return False
661 try:
662 readurl(url, timeout=5)
663 except UrlError:
664 return False
665 return True
666
667
650class EphemeralIPv4Network(object):668class EphemeralIPv4Network(object):
651 """Context manager which sets up temporary static network configuration.669 """Context manager which sets up temporary static network configuration.
652670
653 No operations are performed if the provided interface is already connected.671 No operations are performed if the provided interface already has the
672 specified configuration.
673 This can be verified with the connectivity_url.
654 If unconnected, bring up the interface with valid ip, prefix and broadcast.674 If unconnected, bring up the interface with valid ip, prefix and broadcast.
655 If router is provided setup a default route for that interface. Upon675 If router is provided setup a default route for that interface. Upon
656 context exit, clean up the interface leaving no configuration behind.676 context exit, clean up the interface leaving no configuration behind.
657 """677 """
658678
659 def __init__(self, interface, ip, prefix_or_mask, broadcast, router=None):679 def __init__(self, interface, ip, prefix_or_mask, broadcast, router=None,
680 connectivity_url=None):
660 """Setup context manager and validate call signature.681 """Setup context manager and validate call signature.
661682
662 @param interface: Name of the network interface to bring up.683 @param interface: Name of the network interface to bring up.
@@ -665,6 +686,8 @@ class EphemeralIPv4Network(object):
665 prefix.686 prefix.
666 @param broadcast: Broadcast address for the IPv4 network.687 @param broadcast: Broadcast address for the IPv4 network.
667 @param router: Optionally the default gateway IP.688 @param router: Optionally the default gateway IP.
689 @param connectivity_url: Optionally, a URL to verify if a usable
690 connection already exists.
668 """691 """
669 if not all([interface, ip, prefix_or_mask, broadcast]):692 if not all([interface, ip, prefix_or_mask, broadcast]):
670 raise ValueError(693 raise ValueError(
@@ -675,6 +698,8 @@ class EphemeralIPv4Network(object):
675 except ValueError as e:698 except ValueError as e:
676 raise ValueError(699 raise ValueError(
677 'Cannot setup network: {0}'.format(e))700 'Cannot setup network: {0}'.format(e))
701
702 self.connectivity_url = connectivity_url
678 self.interface = interface703 self.interface = interface
679 self.ip = ip704 self.ip = ip
680 self.broadcast = broadcast705 self.broadcast = broadcast
@@ -683,6 +708,13 @@ class EphemeralIPv4Network(object):
683708
684 def __enter__(self):709 def __enter__(self):
685 """Perform ephemeral network setup if interface is not connected."""710 """Perform ephemeral network setup if interface is not connected."""
711 if self.connectivity_url:
712 if has_url_connectivity(self.connectivity_url):
713 LOG.debug(
714 'Skip ephemeral network setup, instance has connectivity'
715 ' to %s', self.connectivity_url)
716 return
717
686 self._bringup_device()718 self._bringup_device()
687 if self.router:719 if self.router:
688 self._bringup_router()720 self._bringup_router()
diff --git a/cloudinit/net/dhcp.py b/cloudinit/net/dhcp.py
index bdc5799..0db991d 100644
--- a/cloudinit/net/dhcp.py
+++ b/cloudinit/net/dhcp.py
@@ -11,7 +11,8 @@ import re
11import signal11import signal
1212
13from cloudinit.net import (13from cloudinit.net import (
14 EphemeralIPv4Network, find_fallback_nic, get_devicelist)14 EphemeralIPv4Network, find_fallback_nic, get_devicelist,
15 has_url_connectivity)
15from cloudinit.net.network_state import mask_and_ipv4_to_bcast_addr as bcip16from cloudinit.net.network_state import mask_and_ipv4_to_bcast_addr as bcip
16from cloudinit import temp_utils17from cloudinit import temp_utils
17from cloudinit import util18from cloudinit import util
@@ -37,13 +38,21 @@ class NoDHCPLeaseError(Exception):
3738
3839
39class EphemeralDHCPv4(object):40class EphemeralDHCPv4(object):
40 def __init__(self, iface=None):41 def __init__(self, iface=None, connectivity_url=None):
41 self.iface = iface42 self.iface = iface
42 self._ephipv4 = None43 self._ephipv4 = None
43 self.lease = None44 self.lease = None
45 self.connectivity_url = connectivity_url
4446
45 def __enter__(self):47 def __enter__(self):
46 """Setup sandboxed dhcp context."""48 """Setup sandboxed dhcp context, unless connectivity_url can already be
49 reached."""
50 if self.connectivity_url:
51 if has_url_connectivity(self.connectivity_url):
52 LOG.debug(
53 'Skip ephemeral DHCP setup, instance has connectivity'
54 ' to %s', self.connectivity_url)
55 return
47 return self.obtain_lease()56 return self.obtain_lease()
4857
49 def __exit__(self, excp_type, excp_value, excp_traceback):58 def __exit__(self, excp_type, excp_value, excp_traceback):
@@ -86,6 +95,8 @@ class EphemeralDHCPv4(object):
86 kwargs = dict([(k, self.lease.get(v)) for k, v in nmap.items()])95 kwargs = dict([(k, self.lease.get(v)) for k, v in nmap.items()])
87 if not kwargs['broadcast']:96 if not kwargs['broadcast']:
88 kwargs['broadcast'] = bcip(kwargs['prefix_or_mask'], kwargs['ip'])97 kwargs['broadcast'] = bcip(kwargs['prefix_or_mask'], kwargs['ip'])
98 if self.connectivity_url:
99 kwargs['connectivity_url'] = self.connectivity_url
89 ephipv4 = EphemeralIPv4Network(**kwargs)100 ephipv4 = EphemeralIPv4Network(**kwargs)
90 ephipv4.__enter__()101 ephipv4.__enter__()
91 self._ephipv4 = ephipv4102 self._ephipv4 = ephipv4
diff --git a/cloudinit/net/tests/test_dhcp.py b/cloudinit/net/tests/test_dhcp.py
index db25b6f..cd3e732 100644
--- a/cloudinit/net/tests/test_dhcp.py
+++ b/cloudinit/net/tests/test_dhcp.py
@@ -1,15 +1,17 @@
1# This file is part of cloud-init. See LICENSE file for license information.1# This file is part of cloud-init. See LICENSE file for license information.
22
3import httpretty
3import os4import os
4import signal5import signal
5from textwrap import dedent6from textwrap import dedent
67
8import cloudinit.net as net
7from cloudinit.net.dhcp import (9from cloudinit.net.dhcp import (
8 InvalidDHCPLeaseFileError, maybe_perform_dhcp_discovery,10 InvalidDHCPLeaseFileError, maybe_perform_dhcp_discovery,
9 parse_dhcp_lease_file, dhcp_discovery, networkd_load_leases)11 parse_dhcp_lease_file, dhcp_discovery, networkd_load_leases)
10from cloudinit.util import ensure_file, write_file12from cloudinit.util import ensure_file, write_file
11from cloudinit.tests.helpers import (13from cloudinit.tests.helpers import (
12 CiTestCase, mock, populate_dir, wrap_and_call)14 CiTestCase, HttprettyTestCase, mock, populate_dir, wrap_and_call)
1315
1416
15class TestParseDHCPLeasesFile(CiTestCase):17class TestParseDHCPLeasesFile(CiTestCase):
@@ -321,3 +323,35 @@ class TestSystemdParseLeases(CiTestCase):
321 '9': self.lxd_lease})323 '9': self.lxd_lease})
322 self.assertEqual({'1': self.azure_parsed, '9': self.lxd_parsed},324 self.assertEqual({'1': self.azure_parsed, '9': self.lxd_parsed},
323 networkd_load_leases(self.lease_d))325 networkd_load_leases(self.lease_d))
326
327
328class TestEphemeralDhcpNoNetworkSetup(HttprettyTestCase):
329
330 @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery')
331 def test_ephemeral_dhcp_no_network_if_url_connectivity(self, m_dhcp):
332 """No EphemeralDhcp4 network setup when connectivity_url succeeds."""
333 url = 'http://example.org/index.html'
334
335 httpretty.register_uri(httpretty.GET, url)
336 with net.dhcp.EphemeralDHCPv4(connectivity_url=url) as lease:
337 self.assertIsNone(lease)
338 # Ensure that no teardown happens:
339 m_dhcp.assert_not_called()
340
341 @mock.patch('cloudinit.net.dhcp.util.subp')
342 @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery')
343 def test_ephemeral_dhcp_setup_network_if_url_connectivity(
344 self, m_dhcp, m_subp):
345 """No EphemeralDhcp4 network setup when connectivity_url succeeds."""
346 url = 'http://example.org/index.html'
347 fake_lease = {
348 'interface': 'eth9', 'fixed-address': '192.168.2.2',
349 'subnet-mask': '255.255.0.0'}
350 m_dhcp.return_value = [fake_lease]
351 m_subp.return_value = ('', '')
352
353 httpretty.register_uri(httpretty.GET, url, body={}, status=404)
354 with net.dhcp.EphemeralDHCPv4(connectivity_url=url) as lease:
355 self.assertEqual(fake_lease, lease)
356 # Ensure that dhcp discovery occurs
357 m_dhcp.called_once_with()
diff --git a/cloudinit/net/tests/test_init.py b/cloudinit/net/tests/test_init.py
index 58e0a59..f55c31e 100644
--- a/cloudinit/net/tests/test_init.py
+++ b/cloudinit/net/tests/test_init.py
@@ -2,14 +2,16 @@
22
3import copy3import copy
4import errno4import errno
5import httpretty
5import mock6import mock
6import os7import os
8import requests
7import textwrap9import textwrap
8import yaml10import yaml
911
10import cloudinit.net as net12import cloudinit.net as net
11from cloudinit.util import ensure_file, write_file, ProcessExecutionError13from cloudinit.util import ensure_file, write_file, ProcessExecutionError
12from cloudinit.tests.helpers import CiTestCase14from cloudinit.tests.helpers import CiTestCase, HttprettyTestCase
1315
1416
15class TestSysDevPath(CiTestCase):17class TestSysDevPath(CiTestCase):
@@ -458,6 +460,22 @@ class TestEphemeralIPV4Network(CiTestCase):
458 self.assertEqual(expected_setup_calls, m_subp.call_args_list)460 self.assertEqual(expected_setup_calls, m_subp.call_args_list)
459 m_subp.assert_has_calls(expected_teardown_calls)461 m_subp.assert_has_calls(expected_teardown_calls)
460462
463 @mock.patch('cloudinit.net.readurl')
464 def test_ephemeral_ipv4_no_network_if_url_connectivity(
465 self, m_readurl, m_subp):
466 """No network setup is performed if we can successfully connect to
467 connectivity_url."""
468 params = {
469 'interface': 'eth0', 'ip': '192.168.2.2',
470 'prefix_or_mask': '255.255.255.0', 'broadcast': '192.168.2.255',
471 'connectivity_url': 'http://example.org/index.html'}
472
473 with net.EphemeralIPv4Network(**params):
474 self.assertEqual([mock.call('http://example.org/index.html',
475 timeout=5)], m_readurl.call_args_list)
476 # Ensure that no teardown happens:
477 m_subp.assert_has_calls([])
478
461 def test_ephemeral_ipv4_network_noop_when_configured(self, m_subp):479 def test_ephemeral_ipv4_network_noop_when_configured(self, m_subp):
462 """EphemeralIPv4Network handles exception when address is setup.480 """EphemeralIPv4Network handles exception when address is setup.
463481
@@ -619,3 +637,35 @@ class TestApplyNetworkCfgNames(CiTestCase):
619 def test_apply_v2_renames_raises_runtime_error_on_unknown_version(self):637 def test_apply_v2_renames_raises_runtime_error_on_unknown_version(self):
620 with self.assertRaises(RuntimeError):638 with self.assertRaises(RuntimeError):
621 net.apply_network_config_names(yaml.load("version: 3"))639 net.apply_network_config_names(yaml.load("version: 3"))
640
641
642class TestHasURLConnectivity(HttprettyTestCase):
643
644 def setUp(self):
645 super(TestHasURLConnectivity, self).setUp()
646 self.url = 'http://fake/'
647 self.kwargs = {'allow_redirects': True, 'timeout': 5.0}
648
649 @mock.patch('cloudinit.net.readurl')
650 def test_url_timeout_on_connectivity_check(self, m_readurl):
651 """A timeout of 5 seconds is provided when reading a url."""
652 self.assertTrue(
653 net.has_url_connectivity(self.url), 'Expected True on url connect')
654
655 def test_true_on_url_connectivity_success(self):
656 httpretty.register_uri(httpretty.GET, self.url)
657 self.assertTrue(
658 net.has_url_connectivity(self.url), 'Expected True on url connect')
659
660 @mock.patch('requests.Session.request')
661 def test_true_on_url_connectivity_timeout(self, m_request):
662 """A timeout raised accessing the url will return False."""
663 m_request.side_effect = requests.Timeout('Fake Connection Timeout')
664 self.assertFalse(
665 net.has_url_connectivity(self.url),
666 'Expected False on url timeout')
667
668 def test_true_on_url_connectivity_failure(self):
669 httpretty.register_uri(httpretty.GET, self.url, body={}, status=404)
670 self.assertFalse(
671 net.has_url_connectivity(self.url), 'Expected False on url fail')

Subscribers

People subscribed via source and target branches