summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChad Smith <chad.smith@canonical.com>2019-11-04 22:11:37 +0000
committerServer Team CI Bot <josh.powers+server-team-bot@canonical.com>2019-11-04 22:11:37 +0000
commit02f07b666adc62d70c4f1a98c2ae80cb6629fa9a (patch)
tree5d5172affdee635aae20e8b935eb06dca7194a19
parent15fa154602f281c9239084d7d20a0999c6b09970 (diff)
downloadvyos-cloud-init-02f07b666adc62d70c4f1a98c2ae80cb6629fa9a.tar.gz
vyos-cloud-init-02f07b666adc62d70c4f1a98c2ae80cb6629fa9a.zip
azure: support matching dhcp route-metrics for dual-stack ipv4 ipv6
Network v2 configuration for Azure will set both dhcp4 and dhcp6 to False by default. When IPv6 privateIpAddresses are present for an interface in Azure's Instance Metadata Service (IMDS), set dhcp6: True and provide a route-metric value that will match the corresponding dhcp4 route-metric. The route-metric value will increase by 100 for each additional interface present to ensure the primary interface has a route to IMDS. Also fix dhcp route-metric rendering for eni and sysconfig distros. LP: #1850308
-rw-r--r--cloudinit/net/network_state.py17
-rw-r--r--cloudinit/net/sysconfig.py6
-rwxr-xr-xcloudinit/sources/DataSourceAzure.py10
-rw-r--r--tests/unittests/test_datasource/test_azure.py101
-rw-r--r--tests/unittests/test_net.py54
5 files changed, 178 insertions, 10 deletions
diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
index ba85c69e..20b7716b 100644
--- a/cloudinit/net/network_state.py
+++ b/cloudinit/net/network_state.py
@@ -22,8 +22,9 @@ NETWORK_STATE_REQUIRED_KEYS = {
1: ['version', 'config', 'network_state'],
}
NETWORK_V2_KEY_FILTER = [
- 'addresses', 'dhcp4', 'dhcp6', 'gateway4', 'gateway6', 'interfaces',
- 'match', 'mtu', 'nameservers', 'renderer', 'set-name', 'wakeonlan'
+ 'addresses', 'dhcp4', 'dhcp4-overrides', 'dhcp6', 'dhcp6-overrides',
+ 'gateway4', 'gateway6', 'interfaces', 'match', 'mtu', 'nameservers',
+ 'renderer', 'set-name', 'wakeonlan'
]
NET_CONFIG_TO_V2 = {
@@ -747,12 +748,20 @@ class NetworkStateInterpreter(object):
def _v2_to_v1_ipcfg(self, cfg):
"""Common ipconfig extraction from v2 to v1 subnets array."""
+ def _add_dhcp_overrides(overrides, subnet):
+ if 'route-metric' in overrides:
+ subnet['metric'] = overrides['route-metric']
+
subnets = []
if cfg.get('dhcp4'):
- subnets.append({'type': 'dhcp4'})
+ subnet = {'type': 'dhcp4'}
+ _add_dhcp_overrides(cfg.get('dhcp4-overrides', {}), subnet)
+ subnets.append(subnet)
if cfg.get('dhcp6'):
+ subnet = {'type': 'dhcp6'}
self.use_ipv6 = True
- subnets.append({'type': 'dhcp6'})
+ _add_dhcp_overrides(cfg.get('dhcp6-overrides', {}), subnet)
+ subnets.append(subnet)
gateway4 = None
gateway6 = None
diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
index 6717d924..fe0c67ca 100644
--- a/cloudinit/net/sysconfig.py
+++ b/cloudinit/net/sysconfig.py
@@ -395,6 +395,9 @@ class Renderer(renderer.Renderer):
ipv6_index = -1
for i, subnet in enumerate(subnets, start=len(iface_cfg.children)):
subnet_type = subnet.get('type')
+ # metric may apply to both dhcp and static config
+ if 'metric' in subnet:
+ iface_cfg['METRIC'] = subnet['metric']
if subnet_type in ['dhcp', 'dhcp4', 'dhcp6']:
if has_default_route and iface_cfg['BOOTPROTO'] != 'none':
iface_cfg['DHCLIENT_SET_DEFAULT_ROUTE'] = False
@@ -426,9 +429,6 @@ 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'])
diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
index cdf49d36..44cca210 100755
--- a/cloudinit/sources/DataSourceAzure.py
+++ b/cloudinit/sources/DataSourceAzure.py
@@ -1322,7 +1322,8 @@ def parse_network_config(imds_metadata):
network_metadata = imds_metadata['network']
for idx, intf in enumerate(network_metadata['interface']):
nicname = 'eth{idx}'.format(idx=idx)
- dev_config = {}
+ dev_config = {'dhcp4': False, 'dhcp6': False}
+ dhcp_override = {'route-metric': (idx + 1) * 100}
for addr4 in intf['ipv4']['ipAddress']:
privateIpv4 = addr4['privateIpAddress']
if privateIpv4:
@@ -1340,12 +1341,15 @@ def parse_network_config(imds_metadata):
# non-primary interfaces should have a higher
# route-metric (cost) so default routes prefer
# primary nic due to lower route-metric value
- dev_config['dhcp4-overrides'] = {
- 'route-metric': (idx + 1) * 100}
+ dev_config['dhcp4-overrides'] = dhcp_override
for addr6 in intf['ipv6']['ipAddress']:
privateIpv6 = addr6['privateIpAddress']
if privateIpv6:
dev_config['dhcp6'] = True
+ # non-primary interfaces should have a higher
+ # route-metric (cost) so default routes prefer
+ # primary nic due to lower route-metric value
+ dev_config['dhcp6-overrides'] = dhcp_override
break
if dev_config:
mac = ':'.join(re.findall(r'..', intf['macAddress']))
diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py
index 80c6f019..d92d7b2f 100644
--- a/tests/unittests/test_datasource/test_azure.py
+++ b/tests/unittests/test_datasource/test_azure.py
@@ -153,6 +153,102 @@ SECONDARY_INTERFACE = {
MOCKPATH = 'cloudinit.sources.DataSourceAzure.'
+class TestParseNetworkConfig(CiTestCase):
+
+ maxDiff = None
+
+ def test_single_ipv4_nic_configuration(self):
+ """parse_network_config emits dhcp on single nic with ipv4"""
+ expected = {'ethernets': {
+ 'eth0': {'dhcp4': True,
+ 'dhcp4-overrides': {'route-metric': 100},
+ 'dhcp6': False,
+ 'match': {'macaddress': '00:0d:3a:04:75:98'},
+ 'set-name': 'eth0'}}, 'version': 2}
+ self.assertEqual(expected, dsaz.parse_network_config(NETWORK_METADATA))
+
+ def test_increases_route_metric_for_non_primary_nics(self):
+ """parse_network_config increases route-metric for each nic"""
+ expected = {'ethernets': {
+ 'eth0': {'dhcp4': True,
+ 'dhcp4-overrides': {'route-metric': 100},
+ 'dhcp6': False,
+ 'match': {'macaddress': '00:0d:3a:04:75:98'},
+ 'set-name': 'eth0'},
+ 'eth1': {'set-name': 'eth1',
+ 'match': {'macaddress': '22:0d:3a:04:75:98'},
+ 'dhcp6': False,
+ 'dhcp4': True,
+ 'dhcp4-overrides': {'route-metric': 200}},
+ 'eth2': {'set-name': 'eth2',
+ 'match': {'macaddress': '33:0d:3a:04:75:98'},
+ 'dhcp6': False,
+ 'dhcp4': True,
+ 'dhcp4-overrides': {'route-metric': 300}}}, 'version': 2}
+ imds_data = copy.deepcopy(NETWORK_METADATA)
+ imds_data['network']['interface'].append(SECONDARY_INTERFACE)
+ third_intf = copy.deepcopy(SECONDARY_INTERFACE)
+ third_intf['macAddress'] = third_intf['macAddress'].replace('22', '33')
+ third_intf['ipv4']['subnet'][0]['address'] = '10.0.2.0'
+ third_intf['ipv4']['ipAddress'][0]['privateIpAddress'] = '10.0.2.6'
+ imds_data['network']['interface'].append(third_intf)
+ self.assertEqual(expected, dsaz.parse_network_config(imds_data))
+
+ def test_ipv4_and_ipv6_route_metrics_match_for_nics(self):
+ """parse_network_config emits matching ipv4 and ipv6 route-metrics."""
+ expected = {'ethernets': {
+ 'eth0': {'dhcp4': True,
+ 'dhcp4-overrides': {'route-metric': 100},
+ 'dhcp6': False,
+ 'match': {'macaddress': '00:0d:3a:04:75:98'},
+ 'set-name': 'eth0'},
+ 'eth1': {'set-name': 'eth1',
+ 'match': {'macaddress': '22:0d:3a:04:75:98'},
+ 'dhcp4': True,
+ 'dhcp6': False,
+ 'dhcp4-overrides': {'route-metric': 200}},
+ 'eth2': {'set-name': 'eth2',
+ 'match': {'macaddress': '33:0d:3a:04:75:98'},
+ 'dhcp4': True,
+ 'dhcp4-overrides': {'route-metric': 300},
+ 'dhcp6': True,
+ 'dhcp6-overrides': {'route-metric': 300}}}, 'version': 2}
+ imds_data = copy.deepcopy(NETWORK_METADATA)
+ imds_data['network']['interface'].append(SECONDARY_INTERFACE)
+ third_intf = copy.deepcopy(SECONDARY_INTERFACE)
+ third_intf['macAddress'] = third_intf['macAddress'].replace('22', '33')
+ third_intf['ipv4']['subnet'][0]['address'] = '10.0.2.0'
+ third_intf['ipv4']['ipAddress'][0]['privateIpAddress'] = '10.0.2.6'
+ third_intf['ipv6'] = {
+ "subnet": [{"prefix": "64", "address": "2001:dead:beef::2"}],
+ "ipAddress": [{"privateIpAddress": "2001:dead:beef::1"}]
+ }
+ imds_data['network']['interface'].append(third_intf)
+ self.assertEqual(expected, dsaz.parse_network_config(imds_data))
+
+ def test_ipv4_secondary_ips_will_be_static_addrs(self):
+ """parse_network_config emits primary ipv4 as dhcp others are static"""
+ expected = {'ethernets': {
+ 'eth0': {'addresses': ['10.0.0.5/24'],
+ 'dhcp4': True,
+ 'dhcp4-overrides': {'route-metric': 100},
+ 'dhcp6': True,
+ 'dhcp6-overrides': {'route-metric': 100},
+ 'match': {'macaddress': '00:0d:3a:04:75:98'},
+ 'set-name': 'eth0'}}, 'version': 2}
+ imds_data = copy.deepcopy(NETWORK_METADATA)
+ nic1 = imds_data['network']['interface'][0]
+ nic1['ipv4']['ipAddress'].append({'privateIpAddress': '10.0.0.5'})
+
+ # Secondary ipv6 addresses currently ignored/unconfigured
+ nic1['ipv6'] = {
+ "subnet": [{"prefix": "10", "address": "2001:dead:beef::16"}],
+ "ipAddress": [{"privateIpAddress": "2001:dead:beef::1"},
+ {"privateIpAddress": "2001:dead:beef::2"}]
+ }
+ self.assertEqual(expected, dsaz.parse_network_config(imds_data))
+
+
class TestGetMetadataFromIMDS(HttprettyTestCase):
with_logs = True
@@ -641,6 +737,7 @@ scbus-1 on xpt0 bus 0
'ethernets': {
'eth0': {'set-name': 'eth0',
'match': {'macaddress': '00:0d:3a:04:75:98'},
+ 'dhcp6': False,
'dhcp4': True,
'dhcp4-overrides': {'route-metric': 100}}},
'version': 2}
@@ -658,14 +755,17 @@ scbus-1 on xpt0 bus 0
'ethernets': {
'eth0': {'set-name': 'eth0',
'match': {'macaddress': '00:0d:3a:04:75:98'},
+ 'dhcp6': False,
'dhcp4': True,
'dhcp4-overrides': {'route-metric': 100}},
'eth1': {'set-name': 'eth1',
'match': {'macaddress': '22:0d:3a:04:75:98'},
+ 'dhcp6': False,
'dhcp4': True,
'dhcp4-overrides': {'route-metric': 200}},
'eth2': {'set-name': 'eth2',
'match': {'macaddress': '33:0d:3a:04:75:98'},
+ 'dhcp6': False,
'dhcp4': True,
'dhcp4-overrides': {'route-metric': 300}}},
'version': 2}
@@ -999,6 +1099,7 @@ scbus-1 on xpt0 bus 0
'ethernets': {
'eth0': {'dhcp4': True,
'dhcp4-overrides': {'route-metric': 100},
+ 'dhcp6': False,
'match': {'macaddress': '00:0d:3a:04:75:98'},
'set-name': 'eth0'}},
'version': 2}
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
index 6f83ad73..35ce55d2 100644
--- a/tests/unittests/test_net.py
+++ b/tests/unittests/test_net.py
@@ -3101,6 +3101,36 @@ USERCTL=no
self._compare_files_to_expected(
expected, self._render_and_read(network_config=v2data))
+ def test_from_v2_route_metric(self):
+ """verify route-metric gets rendered on nic when source is netplan."""
+ overrides = {'route-metric': 100}
+ v2base = {
+ 'version': 2,
+ 'ethernets': {
+ 'eno1': {'dhcp4': True,
+ 'match': {'macaddress': '07-1c-c6-75-a4-be'}}}}
+ expected = {
+ 'ifcfg-eno1': textwrap.dedent("""\
+ BOOTPROTO=dhcp
+ DEVICE=eno1
+ HWADDR=07-1c-c6-75-a4-be
+ METRIC=100
+ NM_CONTROLLED=no
+ ONBOOT=yes
+ STARTMODE=auto
+ TYPE=Ethernet
+ USERCTL=no
+ """),
+ }
+ for dhcp_ver in ('dhcp4', 'dhcp6'):
+ v2data = copy.deepcopy(v2base)
+ if dhcp_ver == 'dhcp6':
+ expected['ifcfg-eno1'] += "IPV6INIT=yes\nDHCPV6C=yes\n"
+ v2data['ethernets']['eno1'].update(
+ {dhcp_ver: True, '{0}-overrides'.format(dhcp_ver): overrides})
+ self._compare_files_to_expected(
+ expected, self._render_and_read(network_config=v2data))
+
class TestOpenSuseSysConfigRendering(CiTestCase):
@@ -3466,6 +3496,30 @@ iface eth0 inet dhcp
self.assertEqual(
expected, dir2dict(tmp_dir)['/etc/network/interfaces'])
+ def test_v2_route_metric_to_eni(self):
+ """Network v2 route-metric overrides are preserved in eni output"""
+ tmp_dir = self.tmp_dir()
+ renderer = eni.Renderer()
+ expected_tmpl = textwrap.dedent("""\
+ auto lo
+ iface lo inet loopback
+
+ auto eth0
+ iface eth0 inet{suffix} dhcp
+ metric 100
+ """)
+ for dhcp_ver in ('dhcp4', 'dhcp6'):
+ suffix = '6' if dhcp_ver == 'dhcp6' else ''
+ dhcp_cfg = {
+ dhcp_ver: True,
+ '{ver}-overrides'.format(ver=dhcp_ver): {'route-metric': 100}}
+ v2_input = {'version': 2, 'ethernets': {'eth0': dhcp_cfg}}
+ ns = network_state.parse_net_config_data(v2_input)
+ renderer.render_network_state(ns, target=tmp_dir)
+ self.assertEqual(
+ expected_tmpl.format(suffix=suffix),
+ dir2dict(tmp_dir)['/etc/network/interfaces'])
+
class TestNetplanNetRendering(CiTestCase):