summaryrefslogtreecommitdiff
path: root/cloudinit/net
diff options
context:
space:
mode:
Diffstat (limited to 'cloudinit/net')
-rw-r--r--cloudinit/net/__init__.py42
-rw-r--r--cloudinit/net/dhcp.py57
-rw-r--r--cloudinit/net/eni.py29
-rw-r--r--cloudinit/net/netplan.py6
-rw-r--r--cloudinit/net/sysconfig.py25
-rw-r--r--cloudinit/net/tests/test_dhcp.py36
-rw-r--r--cloudinit/net/tests/test_init.py52
7 files changed, 208 insertions, 39 deletions
diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
index f83d3681..3642fb1f 100644
--- a/cloudinit/net/__init__.py
+++ b/cloudinit/net/__init__.py
@@ -12,6 +12,7 @@ import re
from cloudinit.net.network_state import mask_to_net_prefix
from cloudinit import util
+from cloudinit.url_helper import UrlError, readurl
LOG = logging.getLogger(__name__)
SYS_CLASS_NET = "/sys/class/net/"
@@ -612,7 +613,8 @@ def get_interfaces():
Bridges and any devices that have a 'stolen' mac are excluded."""
ret = []
devs = get_devicelist()
- empty_mac = '00:00:00:00:00:00'
+ # 16 somewhat arbitrarily chosen. Normally a mac is 6 '00:' tokens.
+ zero_mac = ':'.join(('00',) * 16)
for name in devs:
if not interface_has_own_mac(name):
continue
@@ -624,7 +626,8 @@ def get_interfaces():
# some devices may not have a mac (tun0)
if not mac:
continue
- if mac == empty_mac and name != 'lo':
+ # skip nics that have no mac (00:00....)
+ if name != 'lo' and mac == zero_mac[:len(mac)]:
continue
ret.append((name, mac, device_driver(name), device_devid(name)))
return ret
@@ -645,16 +648,36 @@ def get_ib_hwaddrs_by_interface():
return ret
+def has_url_connectivity(url):
+ """Return true when the instance has access to the provided URL
+
+ Logs a warning if url is not the expected format.
+ """
+ if not any([url.startswith('http://'), url.startswith('https://')]):
+ LOG.warning(
+ "Ignoring connectivity check. Expected URL beginning with http*://"
+ " received '%s'", url)
+ return False
+ try:
+ readurl(url, timeout=5)
+ except UrlError:
+ return False
+ return True
+
+
class EphemeralIPv4Network(object):
"""Context manager which sets up temporary static network configuration.
- No operations are performed if the provided interface is already connected.
+ No operations are performed if the provided interface already has the
+ specified configuration.
+ This can be verified with the connectivity_url.
If unconnected, bring up the interface with valid ip, prefix and broadcast.
If router is provided setup a default route for that interface. Upon
context exit, clean up the interface leaving no configuration behind.
"""
- def __init__(self, interface, ip, prefix_or_mask, broadcast, router=None):
+ def __init__(self, interface, ip, prefix_or_mask, broadcast, router=None,
+ connectivity_url=None):
"""Setup context manager and validate call signature.
@param interface: Name of the network interface to bring up.
@@ -663,6 +686,8 @@ class EphemeralIPv4Network(object):
prefix.
@param broadcast: Broadcast address for the IPv4 network.
@param router: Optionally the default gateway IP.
+ @param connectivity_url: Optionally, a URL to verify if a usable
+ connection already exists.
"""
if not all([interface, ip, prefix_or_mask, broadcast]):
raise ValueError(
@@ -673,6 +698,8 @@ class EphemeralIPv4Network(object):
except ValueError as e:
raise ValueError(
'Cannot setup network: {0}'.format(e))
+
+ self.connectivity_url = connectivity_url
self.interface = interface
self.ip = ip
self.broadcast = broadcast
@@ -681,6 +708,13 @@ class EphemeralIPv4Network(object):
def __enter__(self):
"""Perform ephemeral network setup if interface is not connected."""
+ if self.connectivity_url:
+ if has_url_connectivity(self.connectivity_url):
+ LOG.debug(
+ 'Skip ephemeral network setup, instance has connectivity'
+ ' to %s', self.connectivity_url)
+ return
+
self._bringup_device()
if self.router:
self._bringup_router()
diff --git a/cloudinit/net/dhcp.py b/cloudinit/net/dhcp.py
index 12cf5097..0db991db 100644
--- a/cloudinit/net/dhcp.py
+++ b/cloudinit/net/dhcp.py
@@ -11,7 +11,8 @@ import re
import signal
from cloudinit.net import (
- EphemeralIPv4Network, find_fallback_nic, get_devicelist)
+ EphemeralIPv4Network, find_fallback_nic, get_devicelist,
+ has_url_connectivity)
from cloudinit.net.network_state import mask_and_ipv4_to_bcast_addr as bcip
from cloudinit import temp_utils
from cloudinit import util
@@ -37,37 +38,69 @@ class NoDHCPLeaseError(Exception):
class EphemeralDHCPv4(object):
- def __init__(self, iface=None):
+ def __init__(self, iface=None, connectivity_url=None):
self.iface = iface
self._ephipv4 = None
+ self.lease = None
+ self.connectivity_url = connectivity_url
def __enter__(self):
+ """Setup sandboxed dhcp context, unless connectivity_url can already be
+ reached."""
+ if self.connectivity_url:
+ if has_url_connectivity(self.connectivity_url):
+ LOG.debug(
+ 'Skip ephemeral DHCP setup, instance has connectivity'
+ ' to %s', self.connectivity_url)
+ return
+ return self.obtain_lease()
+
+ def __exit__(self, excp_type, excp_value, excp_traceback):
+ """Teardown sandboxed dhcp context."""
+ self.clean_network()
+
+ def clean_network(self):
+ """Exit _ephipv4 context to teardown of ip configuration performed."""
+ if self.lease:
+ self.lease = None
+ if not self._ephipv4:
+ return
+ self._ephipv4.__exit__(None, None, None)
+
+ def obtain_lease(self):
+ """Perform dhcp discovery in a sandboxed environment if possible.
+
+ @return: A dict representing dhcp options on the most recent lease
+ obtained from the dhclient discovery if run, otherwise an error
+ is raised.
+
+ @raises: NoDHCPLeaseError if no leases could be obtained.
+ """
+ if self.lease:
+ return self.lease
try:
leases = maybe_perform_dhcp_discovery(self.iface)
except InvalidDHCPLeaseFileError:
raise NoDHCPLeaseError()
if not leases:
raise NoDHCPLeaseError()
- lease = leases[-1]
+ self.lease = leases[-1]
LOG.debug("Received dhcp lease on %s for %s/%s",
- lease['interface'], lease['fixed-address'],
- lease['subnet-mask'])
+ self.lease['interface'], self.lease['fixed-address'],
+ self.lease['subnet-mask'])
nmap = {'interface': 'interface', 'ip': 'fixed-address',
'prefix_or_mask': 'subnet-mask',
'broadcast': 'broadcast-address',
'router': 'routers'}
- kwargs = dict([(k, lease.get(v)) for k, v in nmap.items()])
+ kwargs = dict([(k, self.lease.get(v)) for k, v in nmap.items()])
if not kwargs['broadcast']:
kwargs['broadcast'] = bcip(kwargs['prefix_or_mask'], kwargs['ip'])
+ if self.connectivity_url:
+ kwargs['connectivity_url'] = self.connectivity_url
ephipv4 = EphemeralIPv4Network(**kwargs)
ephipv4.__enter__()
self._ephipv4 = ephipv4
- return lease
-
- def __exit__(self, excp_type, excp_value, excp_traceback):
- if not self._ephipv4:
- return
- self._ephipv4.__exit__(excp_type, excp_value, excp_traceback)
+ return self.lease
def maybe_perform_dhcp_discovery(nic=None):
diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py
index c6f631a9..64236320 100644
--- a/cloudinit/net/eni.py
+++ b/cloudinit/net/eni.py
@@ -371,22 +371,23 @@ class Renderer(renderer.Renderer):
'gateway': 'gw',
'metric': 'metric',
}
+
+ default_gw = ''
if route['network'] == '0.0.0.0' and route['netmask'] == '0.0.0.0':
- default_gw = " default gw %s" % route['gateway']
- content.append(up + default_gw + or_true)
- content.append(down + default_gw + or_true)
+ default_gw = ' default'
elif route['network'] == '::' and route['prefix'] == 0:
- # ipv6!
- default_gw = " -A inet6 default gw %s" % route['gateway']
- content.append(up + default_gw + or_true)
- content.append(down + default_gw + or_true)
- else:
- route_line = ""
- for k in ['network', 'netmask', 'gateway', 'metric']:
- if k in route:
- route_line += " %s %s" % (mapping[k], route[k])
- content.append(up + route_line + or_true)
- content.append(down + route_line + or_true)
+ default_gw = ' -A inet6 default'
+
+ route_line = ''
+ for k in ['network', 'netmask', 'gateway', 'metric']:
+ if default_gw and k in ['network', 'netmask']:
+ continue
+ if k == 'gateway':
+ route_line += '%s %s %s' % (default_gw, mapping[k], route[k])
+ elif k in route:
+ route_line += ' %s %s' % (mapping[k], route[k])
+ content.append(up + route_line + or_true)
+ content.append(down + route_line + or_true)
return content
def _render_iface(self, iface, render_hwaddress=False):
diff --git a/cloudinit/net/netplan.py b/cloudinit/net/netplan.py
index bc1087f9..21517fda 100644
--- a/cloudinit/net/netplan.py
+++ b/cloudinit/net/netplan.py
@@ -114,13 +114,13 @@ def _extract_addresses(config, entry, ifname):
for route in subnet.get('routes', []):
to_net = "%s/%s" % (route.get('network'),
route.get('prefix'))
- route = {
+ new_route = {
'via': route.get('gateway'),
'to': to_net,
}
if 'metric' in route:
- route.update({'metric': route.get('metric', 100)})
- routes.append(route)
+ new_route.update({'metric': route.get('metric', 100)})
+ routes.append(new_route)
addresses.append(addr)
diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
index 9c16d3a7..17293e1d 100644
--- a/cloudinit/net/sysconfig.py
+++ b/cloudinit/net/sysconfig.py
@@ -156,13 +156,23 @@ class Route(ConfigMap):
_quote_value(gateway_value)))
buf.write("%s=%s\n" % ('NETMASK' + str(reindex),
_quote_value(netmask_value)))
+ metric_key = 'METRIC' + index
+ if metric_key in self._conf:
+ metric_value = str(self._conf['METRIC' + index])
+ buf.write("%s=%s\n" % ('METRIC' + str(reindex),
+ _quote_value(metric_value)))
elif proto == "ipv6" and self.is_ipv6_route(address_value):
netmask_value = str(self._conf['NETMASK' + index])
gateway_value = str(self._conf['GATEWAY' + index])
- buf.write("%s/%s via %s dev %s\n" % (address_value,
- netmask_value,
- gateway_value,
- self._route_name))
+ metric_value = (
+ 'metric ' + str(self._conf['METRIC' + index])
+ if 'METRIC' + index in self._conf else '')
+ buf.write(
+ "%s/%s via %s %s dev %s\n" % (address_value,
+ netmask_value,
+ gateway_value,
+ metric_value,
+ self._route_name))
return buf.getvalue()
@@ -370,6 +380,9 @@ class Renderer(renderer.Renderer):
else:
iface_cfg['GATEWAY'] = subnet['gateway']
+ if 'metric' in subnet:
+ iface_cfg['METRIC'] = subnet['metric']
+
if 'dns_search' in subnet:
iface_cfg['DOMAIN'] = ' '.join(subnet['dns_search'])
@@ -414,15 +427,19 @@ class Renderer(renderer.Renderer):
else:
iface_cfg['GATEWAY'] = route['gateway']
route_cfg.has_set_default_ipv4 = True
+ if 'metric' in route:
+ iface_cfg['METRIC'] = route['metric']
else:
gw_key = 'GATEWAY%s' % route_cfg.last_idx
nm_key = 'NETMASK%s' % route_cfg.last_idx
addr_key = 'ADDRESS%s' % route_cfg.last_idx
+ metric_key = 'METRIC%s' % route_cfg.last_idx
route_cfg.last_idx += 1
# add default routes only to ifcfg files, not
# to route-* or route6-*
for (old_key, new_key) in [('gateway', gw_key),
+ ('metric', metric_key),
('netmask', nm_key),
('network', addr_key)]:
if old_key in route:
diff --git a/cloudinit/net/tests/test_dhcp.py b/cloudinit/net/tests/test_dhcp.py
index db25b6f2..cd3e7328 100644
--- a/cloudinit/net/tests/test_dhcp.py
+++ b/cloudinit/net/tests/test_dhcp.py
@@ -1,15 +1,17 @@
# This file is part of cloud-init. See LICENSE file for license information.
+import httpretty
import os
import signal
from textwrap import dedent
+import cloudinit.net as net
from cloudinit.net.dhcp import (
InvalidDHCPLeaseFileError, maybe_perform_dhcp_discovery,
parse_dhcp_lease_file, dhcp_discovery, networkd_load_leases)
from cloudinit.util import ensure_file, write_file
from cloudinit.tests.helpers import (
- CiTestCase, mock, populate_dir, wrap_and_call)
+ CiTestCase, HttprettyTestCase, mock, populate_dir, wrap_and_call)
class TestParseDHCPLeasesFile(CiTestCase):
@@ -321,3 +323,35 @@ class TestSystemdParseLeases(CiTestCase):
'9': self.lxd_lease})
self.assertEqual({'1': self.azure_parsed, '9': self.lxd_parsed},
networkd_load_leases(self.lease_d))
+
+
+class TestEphemeralDhcpNoNetworkSetup(HttprettyTestCase):
+
+ @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery')
+ def test_ephemeral_dhcp_no_network_if_url_connectivity(self, m_dhcp):
+ """No EphemeralDhcp4 network setup when connectivity_url succeeds."""
+ url = 'http://example.org/index.html'
+
+ httpretty.register_uri(httpretty.GET, url)
+ with net.dhcp.EphemeralDHCPv4(connectivity_url=url) as lease:
+ self.assertIsNone(lease)
+ # Ensure that no teardown happens:
+ m_dhcp.assert_not_called()
+
+ @mock.patch('cloudinit.net.dhcp.util.subp')
+ @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery')
+ def test_ephemeral_dhcp_setup_network_if_url_connectivity(
+ self, m_dhcp, m_subp):
+ """No EphemeralDhcp4 network setup when connectivity_url succeeds."""
+ url = 'http://example.org/index.html'
+ fake_lease = {
+ 'interface': 'eth9', 'fixed-address': '192.168.2.2',
+ 'subnet-mask': '255.255.0.0'}
+ m_dhcp.return_value = [fake_lease]
+ m_subp.return_value = ('', '')
+
+ httpretty.register_uri(httpretty.GET, url, body={}, status=404)
+ with net.dhcp.EphemeralDHCPv4(connectivity_url=url) as lease:
+ self.assertEqual(fake_lease, lease)
+ # Ensure that dhcp discovery occurs
+ m_dhcp.called_once_with()
diff --git a/cloudinit/net/tests/test_init.py b/cloudinit/net/tests/test_init.py
index 58e0a591..f55c31e8 100644
--- a/cloudinit/net/tests/test_init.py
+++ b/cloudinit/net/tests/test_init.py
@@ -2,14 +2,16 @@
import copy
import errno
+import httpretty
import mock
import os
+import requests
import textwrap
import yaml
import cloudinit.net as net
from cloudinit.util import ensure_file, write_file, ProcessExecutionError
-from cloudinit.tests.helpers import CiTestCase
+from cloudinit.tests.helpers import CiTestCase, HttprettyTestCase
class TestSysDevPath(CiTestCase):
@@ -458,6 +460,22 @@ class TestEphemeralIPV4Network(CiTestCase):
self.assertEqual(expected_setup_calls, m_subp.call_args_list)
m_subp.assert_has_calls(expected_teardown_calls)
+ @mock.patch('cloudinit.net.readurl')
+ def test_ephemeral_ipv4_no_network_if_url_connectivity(
+ self, m_readurl, m_subp):
+ """No network setup is performed if we can successfully connect to
+ connectivity_url."""
+ params = {
+ 'interface': 'eth0', 'ip': '192.168.2.2',
+ 'prefix_or_mask': '255.255.255.0', 'broadcast': '192.168.2.255',
+ 'connectivity_url': 'http://example.org/index.html'}
+
+ with net.EphemeralIPv4Network(**params):
+ self.assertEqual([mock.call('http://example.org/index.html',
+ timeout=5)], m_readurl.call_args_list)
+ # Ensure that no teardown happens:
+ m_subp.assert_has_calls([])
+
def test_ephemeral_ipv4_network_noop_when_configured(self, m_subp):
"""EphemeralIPv4Network handles exception when address is setup.
@@ -619,3 +637,35 @@ class TestApplyNetworkCfgNames(CiTestCase):
def test_apply_v2_renames_raises_runtime_error_on_unknown_version(self):
with self.assertRaises(RuntimeError):
net.apply_network_config_names(yaml.load("version: 3"))
+
+
+class TestHasURLConnectivity(HttprettyTestCase):
+
+ def setUp(self):
+ super(TestHasURLConnectivity, self).setUp()
+ self.url = 'http://fake/'
+ self.kwargs = {'allow_redirects': True, 'timeout': 5.0}
+
+ @mock.patch('cloudinit.net.readurl')
+ def test_url_timeout_on_connectivity_check(self, m_readurl):
+ """A timeout of 5 seconds is provided when reading a url."""
+ self.assertTrue(
+ net.has_url_connectivity(self.url), 'Expected True on url connect')
+
+ def test_true_on_url_connectivity_success(self):
+ httpretty.register_uri(httpretty.GET, self.url)
+ self.assertTrue(
+ net.has_url_connectivity(self.url), 'Expected True on url connect')
+
+ @mock.patch('requests.Session.request')
+ def test_true_on_url_connectivity_timeout(self, m_request):
+ """A timeout raised accessing the url will return False."""
+ m_request.side_effect = requests.Timeout('Fake Connection Timeout')
+ self.assertFalse(
+ net.has_url_connectivity(self.url),
+ 'Expected False on url timeout')
+
+ def test_true_on_url_connectivity_failure(self):
+ httpretty.register_uri(httpretty.GET, self.url, body={}, status=404)
+ self.assertFalse(
+ net.has_url_connectivity(self.url), 'Expected False on url fail')