diff --git a/SOURCES/ci-azure-net-generate_fallback_nic-emits-network-v2-con.patch b/SOURCES/ci-azure-net-generate_fallback_nic-emits-network-v2-con.patch new file mode 100644 index 0000000..44293d6 --- /dev/null +++ b/SOURCES/ci-azure-net-generate_fallback_nic-emits-network-v2-con.patch @@ -0,0 +1,374 @@ +From b7b814bc0f4e7b63b50106d292e91f2c9555ad87 Mon Sep 17 00:00:00 2001 +From: Eduardo Otubo +Date: Mon, 4 May 2020 12:40:00 +0200 +Subject: [PATCH 3/6] azure/net: generate_fallback_nic emits network v2 config + instead of v1 + +RH-Author: Eduardo Otubo +Message-id: <20200327152826.13343-4-otubo@redhat.com> +Patchwork-id: 94460 +O-Subject: [RHEL-8.1.z/RHEL-8.2.z cloud-init PATCHv2 3/6] azure/net: generate_fallback_nic emits network v2 config instead of v1 +Bugzilla: 1811753 +RH-Acked-by: Cathy Avery +RH-Acked-by: Vitaly Kuznetsov + +commit 7f674256c1426ffc419fd6b13e66a58754d94939 +Author: Chad Smith +Date: Tue Aug 13 20:13:05 2019 +0000 + + azure/net: generate_fallback_nic emits network v2 config instead of v1 + + The function generate_fallback_config is used by Azure by default when + not consuming IMDS configuration data. This function is also used by any + datasource which does not implement it's own network config. This simple + fallback configuration sets up dhcp on the most likely NIC. It will now + emit network v2 instead of network v1. + + This is a step toward moving all components talking in v2 and allows us + to avoid costly conversions between v1 and v2 for newer distributions + which rely on netplan. + +Signed-off-by: Eduardo Otubo +Signed-off-by: Miroslav Rezanina +--- + cloudinit/net/__init__.py | 31 +++++--------- + cloudinit/net/network_state.py | 12 ++++-- + cloudinit/net/tests/test_init.py | 19 +++++---- + cloudinit/sources/DataSourceAzure.py | 7 +++- + tests/unittests/test_datasource/test_azure.py | 59 ++++++++++++++++++++++++++- + tests/unittests/test_net.py | 41 +++++++++++++++++-- + 6 files changed, 130 insertions(+), 39 deletions(-) + +diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py +index 41659b1..b26a190 100644 +--- a/cloudinit/net/__init__.py ++++ b/cloudinit/net/__init__.py +@@ -268,32 +268,23 @@ def find_fallback_nic(blacklist_drivers=None): + + + def generate_fallback_config(blacklist_drivers=None, config_driver=None): +- """Determine which attached net dev is most likely to have a connection and +- generate network state to run dhcp on that interface""" +- ++ """Generate network cfg v2 for dhcp on the NIC most likely connected.""" + if not config_driver: + config_driver = False + + target_name = find_fallback_nic(blacklist_drivers=blacklist_drivers) +- if target_name: +- target_mac = read_sys_net_safe(target_name, 'address') +- nconf = {'config': [], 'version': 1} +- cfg = {'type': 'physical', 'name': target_name, +- 'mac_address': target_mac, 'subnets': [{'type': 'dhcp'}]} +- # inject the device driver name, dev_id into config if enabled and +- # device has a valid device driver value +- if config_driver: +- driver = device_driver(target_name) +- if driver: +- cfg['params'] = { +- 'driver': driver, +- 'device_id': device_devid(target_name), +- } +- nconf['config'].append(cfg) +- return nconf +- else: ++ if not target_name: + # can't read any interfaces addresses (or there are none); give up + return None ++ target_mac = read_sys_net_safe(target_name, 'address') ++ cfg = {'dhcp4': True, 'set-name': target_name, ++ 'match': {'macaddress': target_mac.lower()}} ++ if config_driver: ++ driver = device_driver(target_name) ++ if driver: ++ cfg['match']['driver'] = driver ++ nconf = {'ethernets': {target_name: cfg}, 'version': 2} ++ return nconf + + + def apply_network_config_names(netcfg, strict_present=True, strict_busy=True): +diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py +index 4d19f56..71d3c0f 100644 +--- a/cloudinit/net/network_state.py ++++ b/cloudinit/net/network_state.py +@@ -596,6 +596,7 @@ class NetworkStateInterpreter(object): + eno1: + match: + macaddress: 00:11:22:33:44:55 ++ driver: hv_netsvc + wakeonlan: true + dhcp4: true + dhcp6: false +@@ -631,15 +632,18 @@ class NetworkStateInterpreter(object): + 'type': 'physical', + 'name': cfg.get('set-name', eth), + } +- mac_address = cfg.get('match', {}).get('macaddress', None) ++ match = cfg.get('match', {}) ++ mac_address = match.get('macaddress', None) + if not mac_address: + LOG.debug('NetworkState Version2: missing "macaddress" info ' + 'in config entry: %s: %s', eth, str(cfg)) +- phy_cmd.update({'mac_address': mac_address}) +- ++ phy_cmd['mac_address'] = mac_address ++ driver = match.get('driver', None) ++ if driver: ++ phy_cmd['params'] = {'driver': driver} + for key in ['mtu', 'match', 'wakeonlan']: + if key in cfg: +- phy_cmd.update({key: cfg.get(key)}) ++ phy_cmd[key] = cfg[key] + + subnets = self._v2_to_v1_ipcfg(cfg) + if len(subnets) > 0: +diff --git a/cloudinit/net/tests/test_init.py b/cloudinit/net/tests/test_init.py +index 5519867..5349176 100644 +--- a/cloudinit/net/tests/test_init.py ++++ b/cloudinit/net/tests/test_init.py +@@ -218,9 +218,9 @@ class TestGenerateFallbackConfig(CiTestCase): + mac = 'aa:bb:cc:aa:bb:cc' + write_file(os.path.join(self.sysdir, 'eth1', 'address'), mac) + expected = { +- 'config': [{'type': 'physical', 'mac_address': mac, +- 'name': 'eth1', 'subnets': [{'type': 'dhcp'}]}], +- 'version': 1} ++ 'ethernets': {'eth1': {'match': {'macaddress': mac}, ++ 'dhcp4': True, 'set-name': 'eth1'}}, ++ 'version': 2} + self.assertEqual(expected, net.generate_fallback_config()) + + def test_generate_fallback_finds_dormant_eth_with_mac(self): +@@ -229,9 +229,9 @@ class TestGenerateFallbackConfig(CiTestCase): + mac = 'aa:bb:cc:aa:bb:cc' + write_file(os.path.join(self.sysdir, 'eth0', 'address'), mac) + expected = { +- 'config': [{'type': 'physical', 'mac_address': mac, +- 'name': 'eth0', 'subnets': [{'type': 'dhcp'}]}], +- 'version': 1} ++ 'ethernets': {'eth0': {'match': {'macaddress': mac}, 'dhcp4': True, ++ 'set-name': 'eth0'}}, ++ 'version': 2} + self.assertEqual(expected, net.generate_fallback_config()) + + def test_generate_fallback_finds_eth_by_operstate(self): +@@ -239,9 +239,10 @@ class TestGenerateFallbackConfig(CiTestCase): + mac = 'aa:bb:cc:aa:bb:cc' + write_file(os.path.join(self.sysdir, 'eth0', 'address'), mac) + expected = { +- 'config': [{'type': 'physical', 'mac_address': mac, +- 'name': 'eth0', 'subnets': [{'type': 'dhcp'}]}], +- 'version': 1} ++ 'ethernets': { ++ 'eth0': {'dhcp4': True, 'match': {'macaddress': mac}, ++ 'set-name': 'eth0'}}, ++ 'version': 2} + valid_operstates = ['dormant', 'down', 'lowerlayerdown', 'unknown'] + for state in valid_operstates: + write_file(os.path.join(self.sysdir, 'eth0', 'operstate'), state) +diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py +index 66bbe5e..8cb7e5c 100755 +--- a/cloudinit/sources/DataSourceAzure.py ++++ b/cloudinit/sources/DataSourceAzure.py +@@ -1242,7 +1242,7 @@ def parse_network_config(imds_metadata): + privateIpv4 = addr4['privateIpAddress'] + if privateIpv4: + if dev_config.get('dhcp4', False): +- # Append static address config for nic > 1 ++ # Append static address config for ip > 1 + netPrefix = intf['ipv4']['subnet'][0].get( + 'prefix', '24') + if not dev_config.get('addresses'): +@@ -1252,6 +1252,11 @@ def parse_network_config(imds_metadata): + ip=privateIpv4, prefix=netPrefix)) + else: + dev_config['dhcp4'] = 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['dhcp4-overrides'] = { ++ 'route-metric': (idx + 1) * 100} + for addr6 in intf['ipv6']['ipAddress']: + privateIpv6 = addr6['privateIpAddress'] + if privateIpv6: +diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py +index 1fb0565..f2ff967 100644 +--- a/tests/unittests/test_datasource/test_azure.py ++++ b/tests/unittests/test_datasource/test_azure.py +@@ -13,6 +13,7 @@ from cloudinit.tests.helpers import ( + HttprettyTestCase, CiTestCase, populate_dir, mock, wrap_and_call, + ExitStack, PY26, SkipTest, resourceLocation) + ++import copy + import crypt + import httpretty + import json +@@ -111,6 +112,26 @@ NETWORK_METADATA = { + } + } + ++SECONDARY_INTERFACE = { ++ "macAddress": "220D3A047598", ++ "ipv6": { ++ "ipAddress": [] ++ }, ++ "ipv4": { ++ "subnet": [ ++ { ++ "prefix": "24", ++ "address": "10.0.1.0" ++ } ++ ], ++ "ipAddress": [ ++ { ++ "privateIpAddress": "10.0.1.5", ++ } ++ ] ++ } ++} ++ + MOCKPATH = 'cloudinit.sources.DataSourceAzure.' + + +@@ -632,8 +653,43 @@ fdescfs /dev/fd fdescfs rw 0 0 + 'ethernets': { + 'eth0': {'set-name': 'eth0', + 'match': {'macaddress': '00:0d:3a:04:75:98'}, +- 'dhcp4': True}}, ++ 'dhcp4': True, ++ 'dhcp4-overrides': {'route-metric': 100}}}, ++ 'version': 2} ++ dsrc = self._get_ds(data) ++ dsrc.get_data() ++ self.assertEqual(expected_network_config, dsrc.network_config) ++ ++ def test_network_config_set_from_imds_route_metric_for_secondary_nic(self): ++ """Datasource.network_config adds route-metric to secondary nics.""" ++ sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}} ++ odata = {} ++ data = {'ovfcontent': construct_valid_ovf_env(data=odata), ++ 'sys_cfg': sys_cfg} ++ expected_network_config = { ++ 'ethernets': { ++ 'eth0': {'set-name': 'eth0', ++ 'match': {'macaddress': '00:0d:3a:04:75:98'}, ++ 'dhcp4': True, ++ 'dhcp4-overrides': {'route-metric': 100}}, ++ 'eth1': {'set-name': 'eth1', ++ 'match': {'macaddress': '22:0d:3a:04:75:98'}, ++ 'dhcp4': True, ++ 'dhcp4-overrides': {'route-metric': 200}}, ++ 'eth2': {'set-name': 'eth2', ++ 'match': {'macaddress': '33:0d:3a:04:75:98'}, ++ '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.m_get_metadata_from_imds.return_value = imds_data + dsrc = self._get_ds(data) + dsrc.get_data() + self.assertEqual(expected_network_config, dsrc.network_config) +@@ -936,6 +992,7 @@ fdescfs /dev/fd fdescfs rw 0 0 + expected_cfg = { + 'ethernets': { + 'eth0': {'dhcp4': True, ++ 'dhcp4-overrides': {'route-metric': 100}, + '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 a975678..206de56 100644 +--- a/tests/unittests/test_net.py ++++ b/tests/unittests/test_net.py +@@ -1651,7 +1651,7 @@ DEFAULT_DEV_ATTRS = { + "carrier": False, + "dormant": False, + "operstate": "down", +- "address": "07-1C-C6-75-A4-BE", ++ "address": "07-1c-c6-75-a4-be", + "device/driver": None, + "device/device": None, + "name_assign_type": "4", +@@ -1702,6 +1702,39 @@ class TestGenerateFallbackConfig(CiTestCase): + @mock.patch("cloudinit.net.sys_dev_path") + @mock.patch("cloudinit.net.read_sys_net") + @mock.patch("cloudinit.net.get_devicelist") ++ def test_device_driver_v2(self, mock_get_devicelist, mock_read_sys_net, ++ mock_sys_dev_path): ++ """Network configuration for generate_fallback_config is version 2.""" ++ devices = { ++ 'eth0': { ++ 'bridge': False, 'carrier': False, 'dormant': False, ++ 'operstate': 'down', 'address': '00:11:22:33:44:55', ++ 'device/driver': 'hv_netsvc', 'device/device': '0x3', ++ 'name_assign_type': '4'}, ++ 'eth1': { ++ 'bridge': False, 'carrier': False, 'dormant': False, ++ 'operstate': 'down', 'address': '00:11:22:33:44:55', ++ 'device/driver': 'mlx4_core', 'device/device': '0x7', ++ 'name_assign_type': '4'}, ++ ++ } ++ ++ tmp_dir = self.tmp_dir() ++ _setup_test(tmp_dir, mock_get_devicelist, ++ mock_read_sys_net, mock_sys_dev_path, ++ dev_attrs=devices) ++ ++ network_cfg = net.generate_fallback_config(config_driver=True) ++ expected = { ++ 'ethernets': {'eth0': {'dhcp4': True, 'set-name': 'eth0', ++ 'match': {'macaddress': '00:11:22:33:44:55', ++ 'driver': 'hv_netsvc'}}}, ++ 'version': 2} ++ self.assertEqual(expected, network_cfg) ++ ++ @mock.patch("cloudinit.net.sys_dev_path") ++ @mock.patch("cloudinit.net.read_sys_net") ++ @mock.patch("cloudinit.net.get_devicelist") + def test_device_driver(self, mock_get_devicelist, mock_read_sys_net, + mock_sys_dev_path): + devices = { +@@ -1981,7 +2014,7 @@ class TestRhelSysConfigRendering(CiTestCase): + # + BOOTPROTO=dhcp + DEVICE=eth1000 +-HWADDR=07-1C-C6-75-A4-BE ++HWADDR=07-1c-c6-75-a4-be + ONBOOT=yes + TYPE=Ethernet + USERCTL=no +@@ -2354,7 +2387,7 @@ class TestOpenSuseSysConfigRendering(CiTestCase): + # + BOOTPROTO=dhcp + DEVICE=eth1000 +-HWADDR=07-1C-C6-75-A4-BE ++HWADDR=07-1c-c6-75-a4-be + NM_CONTROLLED=no + ONBOOT=yes + TYPE=Ethernet +@@ -2728,13 +2761,13 @@ class TestNetplanNetRendering(CiTestCase): + + expected = """ + network: +- version: 2 + ethernets: + eth1000: + dhcp4: true + match: + macaddress: 07-1c-c6-75-a4-be + set-name: eth1000 ++ version: 2 + """ + self.assertEqual(expected.lstrip(), contents.lstrip()) + self.assertEqual(1, mock_clean_default.call_count) +-- +1.8.3.1 + diff --git a/SOURCES/ci-azure-support-matching-dhcp-route-metrics-for-dual-s.patch b/SOURCES/ci-azure-support-matching-dhcp-route-metrics-for-dual-s.patch new file mode 100644 index 0000000..f6114eb --- /dev/null +++ b/SOURCES/ci-azure-support-matching-dhcp-route-metrics-for-dual-s.patch @@ -0,0 +1,354 @@ +From 1b1a179bab885c19a4793c4a6cc2b48a541b9a84 Mon Sep 17 00:00:00 2001 +From: Eduardo Otubo +Date: Mon, 4 May 2020 12:40:04 +0200 +Subject: [PATCH 4/6] azure: support matching dhcp route-metrics for dual-stack + ipv4 ipv6 + +RH-Author: Eduardo Otubo +Message-id: <20200327152826.13343-5-otubo@redhat.com> +Patchwork-id: 94455 +O-Subject: [RHEL-8.1.z/RHEL-8.2.z cloud-init PATCHv2 4/6] azure: support matching dhcp route-metrics for dual-stack ipv4 ipv6 +Bugzilla: 1811753 +RH-Acked-by: Cathy Avery +RH-Acked-by: Vitaly Kuznetsov + +commit 02f07b666adc62d70c4f1a98c2ae80cb6629fa9a +Author: Chad Smith +Date: Mon Nov 4 22:11:37 2019 +0000 + + 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 + +Signed-off-by: Eduardo Otubo +Signed-off-by: Miroslav Rezanina +--- + cloudinit/net/network_state.py | 17 ++++- + cloudinit/net/sysconfig.py | 6 +- + cloudinit/sources/DataSourceAzure.py | 10 ++- + tests/unittests/test_datasource/test_azure.py | 101 ++++++++++++++++++++++++++ + tests/unittests/test_net.py | 54 ++++++++++++++ + 5 files changed, 178 insertions(+), 10 deletions(-) + +diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py +index 71d3c0f..571eb57 100644 +--- a/cloudinit/net/network_state.py ++++ b/cloudinit/net/network_state.py +@@ -21,8 +21,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 = { +@@ -734,12 +735,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 5c1b4eb..a4c7660 100644 +--- a/cloudinit/net/sysconfig.py ++++ b/cloudinit/net/sysconfig.py +@@ -373,6 +373,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 +@@ -401,9 +404,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 8cb7e5c..e4d0708 100755 +--- a/cloudinit/sources/DataSourceAzure.py ++++ b/cloudinit/sources/DataSourceAzure.py +@@ -1237,7 +1237,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: +@@ -1255,12 +1256,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 f2ff967..8da3233 100644 +--- a/tests/unittests/test_datasource/test_azure.py ++++ b/tests/unittests/test_datasource/test_azure.py +@@ -135,6 +135,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 +@@ -653,6 +749,7 @@ fdescfs /dev/fd fdescfs rw 0 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} +@@ -670,14 +767,17 @@ fdescfs /dev/fd fdescfs rw 0 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} +@@ -993,6 +1093,7 @@ fdescfs /dev/fd fdescfs rw 0 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 206de56..df5658d 100644 +--- a/tests/unittests/test_net.py ++++ b/tests/unittests/test_net.py +@@ -2304,6 +2304,36 @@ USERCTL=no + self._compare_files_to_expected(entry['expected_sysconfig'], found) + self._assert_headers(found) + ++ 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): + +@@ -2725,6 +2755,30 @@ iface eth0 inet dhcp + config = sysconfig.ConfigObj(nm_cfg) + self.assertIn('ifcfg-rh', config['main']['plugins']) + ++ 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): + +-- +1.8.3.1 + diff --git a/SOURCES/ci-net-IPv6-accept_ra-slaac-stateless-51.patch b/SOURCES/ci-net-IPv6-accept_ra-slaac-stateless-51.patch new file mode 100644 index 0000000..dd2aa5b --- /dev/null +++ b/SOURCES/ci-net-IPv6-accept_ra-slaac-stateless-51.patch @@ -0,0 +1,667 @@ +From f14ed869e5784b1d5a3dfbabc1484eb266e8c3ab Mon Sep 17 00:00:00 2001 +From: Eduardo Otubo +Date: Mon, 4 May 2020 12:40:13 +0200 +Subject: [PATCH 6/6] net: IPv6, accept_ra, slaac, stateless (#51) + +RH-Author: Eduardo Otubo +Message-id: <20200327152826.13343-7-otubo@redhat.com> +Patchwork-id: 94458 +O-Subject: [RHEL-8.1.z/RHEL-8.2.z cloud-init PATCHv2 6/6] net: IPv6, accept_ra, slaac, stateless (#51) +Bugzilla: 1811753 +RH-Acked-by: Cathy Avery +RH-Acked-by: Vitaly Kuznetsov + +commit 62bbc262c3c7f633eac1d09ec78c055eef05166a +Author: Harald +Date: Wed Nov 20 18:55:27 2019 +0100 + + net: IPv6, accept_ra, slaac, stateless (#51) + + Router advertisements are required for the default route + to be set up, thus accept_ra should be enabled for + dhcpv6-stateful. + + sysconf: IPV6_FORCE_ACCEPT_RA controls accept_ra sysctl. + eni: mode static and mode dhcp 'accept_ra' controls sysctl. + + Add 'accept-ra: true|false' parameter to config v1 and + v2. When True: accept_ra is set to '1'. When False: + accept_ra is set to '0'. When not defined in config the + value is left to the operating system default. + + This change also extend the IPv6 support to distinguish + between slaac and dhcpv6-stateless. SLAAC is autoconfig + without any options from DHCP, while stateless auto-configures + the address and the uses DHCP for other options. + + LP: #1806014 + LP: #1808647 + +Signed-off-by: Eduardo Otubo +Signed-off-by: Miroslav Rezanina +--- + cloudinit/net/eni.py | 15 ++ + cloudinit/net/netplan.py | 9 +- + cloudinit/net/network_state.py | 21 +- + cloudinit/net/sysconfig.py | 34 ++- + cloudinit/sources/helpers/openstack.py | 21 +- + .../unittests/test_datasource/test_configdrive.py | 3 +- + tests/unittests/test_net.py | 249 +++++++++++++++++++++ + 7 files changed, 334 insertions(+), 18 deletions(-) + +diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py +index 896a39b..c70435a 100644 +--- a/cloudinit/net/eni.py ++++ b/cloudinit/net/eni.py +@@ -393,6 +393,7 @@ class Renderer(renderer.Renderer): + def _render_iface(self, iface, render_hwaddress=False): + sections = [] + subnets = iface.get('subnets', {}) ++ accept_ra = iface.pop('accept-ra', None) + if subnets: + for index, subnet in enumerate(subnets): + ipv4_subnet_mtu = None +@@ -409,9 +410,23 @@ class Renderer(renderer.Renderer): + subnet['type'] == 'ipv6_dhcpv6-stateful'): + # Configure network settings using DHCP or DHCPv6 + iface['mode'] = 'dhcp' ++ if accept_ra is not None: ++ # Accept router advertisements (0=off, 1=on) ++ iface['accept_ra'] = '1' if accept_ra else '0' + elif subnet['type'] == 'ipv6_dhcpv6-stateless': + # Configure network settings using SLAAC from RAs + iface['mode'] = 'auto' ++ # Use stateless DHCPv6 (0=off, 1=on) ++ iface['dhcp'] = '1' ++ elif subnet['type'] == 'ipv6_slaac': ++ # Configure network settings using SLAAC from RAs ++ iface['mode'] = 'auto' ++ # Use stateless DHCPv6 (0=off, 1=on) ++ iface['dhcp'] = '0' ++ elif subnet_is_ipv6(subnet) and subnet['type'] == 'static': ++ if accept_ra is not None: ++ # Accept router advertisements (0=off, 1=on) ++ iface['accept_ra'] = '1' if accept_ra else '0' + + # do not emit multiple 'auto $IFACE' lines as older (precise) + # ifupdown complains +diff --git a/cloudinit/net/netplan.py b/cloudinit/net/netplan.py +index 21517fd..78ec38c 100644 +--- a/cloudinit/net/netplan.py ++++ b/cloudinit/net/netplan.py +@@ -4,7 +4,7 @@ import copy + import os + + from . import renderer +-from .network_state import subnet_is_ipv6, NET_CONFIG_TO_V2 ++from .network_state import subnet_is_ipv6, NET_CONFIG_TO_V2, IPV6_DYNAMIC_TYPES + + from cloudinit import log as logging + from cloudinit import util +@@ -51,7 +51,8 @@ def _extract_addresses(config, entry, ifname): + 'mtu': 1480, + 'netmask': 64, + 'type': 'static'}], +- 'type: physical' ++ 'type: physical', ++ 'accept-ra': 'true' + } + + An entry dictionary looks like: +@@ -92,6 +93,8 @@ def _extract_addresses(config, entry, ifname): + if sn_type == 'dhcp': + sn_type += '4' + entry.update({sn_type: True}) ++ elif sn_type in IPV6_DYNAMIC_TYPES: ++ entry.update({'dhcp6': True}) + elif sn_type in ['static']: + addr = "%s" % subnet.get('address') + if 'prefix' in subnet: +@@ -144,6 +147,8 @@ def _extract_addresses(config, entry, ifname): + ns = entry.get('nameservers', {}) + ns.update({'search': searchdomains}) + entry.update({'nameservers': ns}) ++ if 'accept-ra' in config and config['accept-ra'] is not None: ++ entry.update({'accept-ra': util.is_true(config.get('accept-ra'))}) + + + def _extract_bond_slaves_by_name(interfaces, entry, bond_master): +diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py +index 571eb57..82cfa42 100644 +--- a/cloudinit/net/network_state.py ++++ b/cloudinit/net/network_state.py +@@ -17,13 +17,17 @@ from cloudinit import util + LOG = logging.getLogger(__name__) + + NETWORK_STATE_VERSION = 1 ++IPV6_DYNAMIC_TYPES = ['dhcp6', ++ 'ipv6_slaac', ++ 'ipv6_dhcpv6-stateless', ++ 'ipv6_dhcpv6-stateful'] + NETWORK_STATE_REQUIRED_KEYS = { + 1: ['version', 'config', 'network_state'], + } + NETWORK_V2_KEY_FILTER = [ + 'addresses', 'dhcp4', 'dhcp4-overrides', 'dhcp6', 'dhcp6-overrides', + 'gateway4', 'gateway6', 'interfaces', 'match', 'mtu', 'nameservers', +- 'renderer', 'set-name', 'wakeonlan' ++ 'renderer', 'set-name', 'wakeonlan', 'accept-ra' + ] + + NET_CONFIG_TO_V2 = { +@@ -341,7 +345,8 @@ class NetworkStateInterpreter(object): + 'name': 'eth0', + 'subnets': [ + {'type': 'dhcp4'} +- ] ++ ], ++ 'accept-ra': 'true' + } + ''' + +@@ -361,6 +366,9 @@ class NetworkStateInterpreter(object): + self.use_ipv6 = True + break + ++ accept_ra = command.get('accept-ra', None) ++ if accept_ra is not None: ++ accept_ra = util.is_true(accept_ra) + iface.update({ + 'name': command.get('name'), + 'type': command.get('type'), +@@ -371,6 +379,7 @@ class NetworkStateInterpreter(object): + 'address': None, + 'gateway': None, + 'subnets': subnets, ++ 'accept-ra': accept_ra + }) + self._network_state['interfaces'].update({command.get('name'): iface}) + self.dump_network_state() +@@ -614,6 +623,7 @@ class NetworkStateInterpreter(object): + driver: ixgbe + set-name: lom1 + dhcp6: true ++ accept-ra: true + switchports: + match: + name: enp2* +@@ -642,7 +652,7 @@ class NetworkStateInterpreter(object): + driver = match.get('driver', None) + if driver: + phy_cmd['params'] = {'driver': driver} +- for key in ['mtu', 'match', 'wakeonlan']: ++ for key in ['mtu', 'match', 'wakeonlan', 'accept-ra']: + if key in cfg: + phy_cmd[key] = cfg[key] + +@@ -915,8 +925,9 @@ def is_ipv6_addr(address): + + def subnet_is_ipv6(subnet): + """Common helper for checking network_state subnets for ipv6.""" +- # 'static6' or 'dhcp6' +- if subnet['type'].endswith('6'): ++ # 'static6', 'dhcp6', 'ipv6_dhcpv6-stateful', 'ipv6_dhcpv6-stateless' or ++ # 'ipv6_slaac' ++ if subnet['type'].endswith('6') or subnet['type'] in IPV6_DYNAMIC_TYPES: + # This is a request for DHCPv6. + return True + elif subnet['type'] == 'static' and is_ipv6_addr(subnet.get('address')): +diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py +index 8b11dbb..13c0a65 100644 +--- a/cloudinit/net/sysconfig.py ++++ b/cloudinit/net/sysconfig.py +@@ -14,7 +14,7 @@ from configobj import ConfigObj + + from . import renderer + from .network_state import ( +- is_ipv6_addr, net_prefix_to_ipv4_mask, subnet_is_ipv6) ++ is_ipv6_addr, net_prefix_to_ipv4_mask, subnet_is_ipv6, IPV6_DYNAMIC_TYPES) + + LOG = logging.getLogger(__name__) + NM_CFG_FILE = "/etc/NetworkManager/NetworkManager.conf" +@@ -319,6 +319,9 @@ class Renderer(renderer.Renderer): + continue + iface_cfg[new_key] = old_value + ++ if iface['accept-ra'] is not None: ++ iface_cfg['IPV6_FORCE_ACCEPT_RA'] = iface['accept-ra'] ++ + @classmethod + def _render_subnets(cls, iface_cfg, subnets, has_default_route): + # setting base values +@@ -335,6 +338,15 @@ class Renderer(renderer.Renderer): + iface_cfg['DHCPV6C'] = True + elif subnet_type == 'ipv6_dhcpv6-stateless': + iface_cfg['IPV6INIT'] = True ++ # Configure network settings using SLAAC from RAs and optional ++ # info from dhcp server using DHCPv6 ++ iface_cfg['IPV6_AUTOCONF'] = True ++ iface_cfg['DHCPV6C'] = True ++ # Use Information-request to get only stateless configuration ++ # parameters (i.e., without address). ++ iface_cfg['DHCPV6C_OPTIONS'] = '-S' ++ elif subnet_type == 'ipv6_slaac': ++ iface_cfg['IPV6INIT'] = True + # Configure network settings using SLAAC from RAs + iface_cfg['IPV6_AUTOCONF'] = True + elif subnet_type in ['dhcp4', 'dhcp']: +@@ -381,10 +393,15 @@ class Renderer(renderer.Renderer): + # metric may apply to both dhcp and static config + if 'metric' in subnet: + iface_cfg['METRIC'] = subnet['metric'] ++ # TODO(hjensas): Including dhcp6 here is likely incorrect. DHCPv6 ++ # does not ever provide a default gateway, the default gateway ++ # come from RA's. (https://github.com/openSUSE/wicked/issues/570) + if subnet_type in ['dhcp', 'dhcp4', 'dhcp6']: + if has_default_route and iface_cfg['BOOTPROTO'] != 'none': + iface_cfg['DHCLIENT_SET_DEFAULT_ROUTE'] = False + continue ++ elif subnet_type in IPV6_DYNAMIC_TYPES: ++ continue + elif subnet_type == 'static': + if subnet_is_ipv6(subnet): + ipv6_index = ipv6_index + 1 +@@ -424,10 +441,14 @@ class Renderer(renderer.Renderer): + @classmethod + def _render_subnet_routes(cls, iface_cfg, route_cfg, subnets): + for _, subnet in enumerate(subnets, start=len(iface_cfg.children)): ++ subnet_type = subnet.get('type') + for route in subnet.get('routes', []): + is_ipv6 = subnet.get('ipv6') or is_ipv6_addr(route['gateway']) + +- if _is_default_route(route): ++ # Any dynamic configuration method, slaac, dhcpv6-stateful/ ++ # stateless should get router information from router RA's. ++ if (_is_default_route(route) and subnet_type not in ++ IPV6_DYNAMIC_TYPES): + if ( + (subnet.get('ipv4') and + route_cfg.has_set_default_ipv4) or +@@ -446,10 +467,17 @@ class Renderer(renderer.Renderer): + # TODO(harlowja): add validation that no other iface has + # also provided the default route? + iface_cfg['DEFROUTE'] = True ++ # TODO(hjensas): Including dhcp6 here is likely incorrect. ++ # DHCPv6 does not ever provide a default gateway, the ++ # default gateway come from RA's. ++ # (https://github.com/openSUSE/wicked/issues/570) + if iface_cfg['BOOTPROTO'] in ('dhcp', 'dhcp4', 'dhcp6'): ++ # NOTE(hjensas): DHCLIENT_SET_DEFAULT_ROUTE is SuSE ++ # only. RHEL, CentOS, Fedora does not implement this ++ # option. + iface_cfg['DHCLIENT_SET_DEFAULT_ROUTE'] = True + if 'gateway' in route: +- if is_ipv6 or is_ipv6_addr(route['gateway']): ++ if is_ipv6: + iface_cfg['IPV6_DEFAULTGW'] = route['gateway'] + route_cfg.has_set_default_ipv6 = True + else: +diff --git a/cloudinit/sources/helpers/openstack.py b/cloudinit/sources/helpers/openstack.py +index 9f2fd2d..77fcdd3 100644 +--- a/cloudinit/sources/helpers/openstack.py ++++ b/cloudinit/sources/helpers/openstack.py +@@ -584,17 +584,24 @@ def convert_net_json(network_json=None, known_macs=None): + if n['link'] == link['id']]: + subnet = dict((k, v) for k, v in network.items() + if k in valid_keys['subnet']) +- if 'dhcp' in network['type']: +- t = (network['type'] if network['type'].startswith('ipv6') +- else 'dhcp4') +- subnet.update({ +- 'type': t, +- }) +- else: ++ ++ if network['type'] == 'ipv4_dhcp': ++ subnet.update({'type': 'dhcp4'}) ++ elif network['type'] == 'ipv6_dhcp': ++ subnet.update({'type': 'dhcp6'}) ++ elif network['type'] in ['ipv6_slaac', 'ipv6_dhcpv6-stateless', ++ 'ipv6_dhcpv6-stateful']: ++ subnet.update({'type': network['type']}) ++ elif network['type'] in ['ipv4', 'ipv6']: + subnet.update({ + 'type': 'static', + 'address': network.get('ip_address'), + }) ++ ++ # Enable accept_ra for stateful and legacy ipv6_dhcp types ++ if network['type'] in ['ipv6_dhcpv6-stateful', 'ipv6_dhcp']: ++ cfg.update({'accept-ra': True}) ++ + if network['type'] == 'ipv4': + subnet['ipv4'] = True + if network['type'] == 'ipv6': +diff --git a/tests/unittests/test_datasource/test_configdrive.py b/tests/unittests/test_datasource/test_configdrive.py +index ed4e9d5..bd4c310 100644 +--- a/tests/unittests/test_datasource/test_configdrive.py ++++ b/tests/unittests/test_datasource/test_configdrive.py +@@ -536,7 +536,8 @@ class TestNetJson(CiTestCase): + 'mtu': None, + 'name': 'enp0s2', + 'subnets': [{'type': 'ipv6_dhcpv6-stateful'}], +- 'type': 'physical'} ++ 'type': 'physical', ++ 'accept-ra': True} + ], + } + conv_data = openstack.convert_net_json(in_data, known_macs=KNOWN_MACS) +diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py +index 70d13f3..21a3f0e 100644 +--- a/tests/unittests/test_net.py ++++ b/tests/unittests/test_net.py +@@ -712,6 +712,143 @@ NETWORK_CONFIGS = { + """), + }, + }, ++ 'dhcpv6_accept_ra': { ++ 'expected_eni': textwrap.dedent("""\ ++ auto lo ++ iface lo inet loopback ++ ++ auto iface0 ++ iface iface0 inet6 dhcp ++ accept_ra 1 ++ """).rstrip(' '), ++ 'expected_netplan': textwrap.dedent(""" ++ network: ++ version: 2 ++ ethernets: ++ iface0: ++ accept-ra: true ++ dhcp6: true ++ """).rstrip(' '), ++ 'yaml_v1': textwrap.dedent("""\ ++ version: 1 ++ config: ++ - type: 'physical' ++ name: 'iface0' ++ subnets: ++ - {'type': 'dhcp6'} ++ accept-ra: true ++ """).rstrip(' '), ++ 'yaml_v2': textwrap.dedent("""\ ++ version: 2 ++ ethernets: ++ iface0: ++ dhcp6: true ++ accept-ra: true ++ """).rstrip(' '), ++ 'expected_sysconfig': { ++ 'ifcfg-iface0': textwrap.dedent("""\ ++ BOOTPROTO=none ++ DEVICE=iface0 ++ DHCPV6C=yes ++ IPV6INIT=yes ++ IPV6_FORCE_ACCEPT_RA=yes ++ DEVICE=iface0 ++ NM_CONTROLLED=no ++ ONBOOT=yes ++ STARTMODE=auto ++ TYPE=Ethernet ++ USERCTL=no ++ """), ++ }, ++ }, ++ 'dhcpv6_reject_ra': { ++ 'expected_eni': textwrap.dedent("""\ ++ auto lo ++ iface lo inet loopback ++ ++ auto iface0 ++ iface iface0 inet6 dhcp ++ accept_ra 0 ++ """).rstrip(' '), ++ 'expected_netplan': textwrap.dedent(""" ++ network: ++ version: 2 ++ ethernets: ++ iface0: ++ accept-ra: false ++ dhcp6: true ++ """).rstrip(' '), ++ 'yaml_v1': textwrap.dedent("""\ ++ version: 1 ++ config: ++ - type: 'physical' ++ name: 'iface0' ++ subnets: ++ - {'type': 'dhcp6'} ++ accept-ra: false ++ """).rstrip(' '), ++ 'yaml_v2': textwrap.dedent("""\ ++ version: 2 ++ ethernets: ++ iface0: ++ dhcp6: true ++ accept-ra: false ++ """).rstrip(' '), ++ 'expected_sysconfig': { ++ 'ifcfg-iface0': textwrap.dedent("""\ ++ BOOTPROTO=none ++ DEVICE=iface0 ++ DHCPV6C=yes ++ IPV6INIT=yes ++ IPV6_FORCE_ACCEPT_RA=no ++ DEVICE=iface0 ++ NM_CONTROLLED=no ++ ONBOOT=yes ++ STARTMODE=auto ++ TYPE=Ethernet ++ USERCTL=no ++ """), ++ }, ++ }, ++ 'ipv6_slaac': { ++ 'expected_eni': textwrap.dedent("""\ ++ auto lo ++ iface lo inet loopback ++ ++ auto iface0 ++ iface iface0 inet6 auto ++ dhcp 0 ++ """).rstrip(' '), ++ 'expected_netplan': textwrap.dedent(""" ++ network: ++ version: 2 ++ ethernets: ++ iface0: ++ dhcp6: true ++ """).rstrip(' '), ++ 'yaml': textwrap.dedent("""\ ++ version: 1 ++ config: ++ - type: 'physical' ++ name: 'iface0' ++ subnets: ++ - {'type': 'ipv6_slaac'} ++ """).rstrip(' '), ++ 'expected_sysconfig': { ++ 'ifcfg-iface0': textwrap.dedent("""\ ++ BOOTPROTO=none ++ DEVICE=iface0 ++ IPV6_AUTOCONF=yes ++ IPV6INIT=yes ++ DEVICE=iface0 ++ NM_CONTROLLED=no ++ ONBOOT=yes ++ STARTMODE=auto ++ TYPE=Ethernet ++ USERCTL=no ++ """), ++ }, ++ }, + 'dhcpv6_stateless': { + 'expected_eni': textwrap.dedent("""\ + auto lo +@@ -719,6 +856,7 @@ NETWORK_CONFIGS = { + + auto iface0 + iface iface0 inet6 auto ++ dhcp 1 + """).rstrip(' '), + 'expected_netplan': textwrap.dedent(""" + network: +@@ -739,6 +877,8 @@ NETWORK_CONFIGS = { + 'ifcfg-iface0': textwrap.dedent("""\ + BOOTPROTO=none + DEVICE=iface0 ++ DHCPV6C=yes ++ DHCPV6C_OPTIONS=-S + IPV6_AUTOCONF=yes + IPV6INIT=yes + DEVICE=iface0 +@@ -763,6 +903,7 @@ NETWORK_CONFIGS = { + version: 2 + ethernets: + iface0: ++ accept-ra: true + dhcp6: true + """).rstrip(' '), + 'yaml': textwrap.dedent("""\ +@@ -772,6 +913,7 @@ NETWORK_CONFIGS = { + name: 'iface0' + subnets: + - {'type': 'ipv6_dhcpv6-stateful'} ++ accept-ra: true + """).rstrip(' '), + 'expected_sysconfig': { + 'ifcfg-iface0': textwrap.dedent("""\ +@@ -779,6 +921,7 @@ NETWORK_CONFIGS = { + DEVICE=iface0 + DHCPV6C=yes + IPV6INIT=yes ++ IPV6_FORCE_ACCEPT_RA=yes + DEVICE=iface0 + NM_CONTROLLED=no + ONBOOT=yes +@@ -2376,6 +2519,33 @@ USERCTL=no + } + } + ++ def test_dhcpv6_accept_ra_config_v1(self): ++ entry = NETWORK_CONFIGS['dhcpv6_accept_ra'] ++ found = self._render_and_read(network_config=yaml.load( ++ entry['yaml_v1'])) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ self._assert_headers(found) ++ ++ def test_dhcpv6_accept_ra_config_v2(self): ++ entry = NETWORK_CONFIGS['dhcpv6_accept_ra'] ++ found = self._render_and_read(network_config=yaml.load( ++ entry['yaml_v2'])) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ self._assert_headers(found) ++ ++ def test_dhcpv6_reject_ra_config_v1(self): ++ entry = NETWORK_CONFIGS['dhcpv6_reject_ra'] ++ found = self._render_and_read(network_config=yaml.load( ++ entry['yaml_v1'])) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ self._assert_headers(found) ++ ++ def test_dhcpv6_reject_ra_config_v2(self): ++ entry = NETWORK_CONFIGS['dhcpv6_reject_ra'] ++ found = self._render_and_read(network_config=yaml.load( ++ entry['yaml_v2'])) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ self._assert_headers(found) + + def test_dhcpv6_stateless_config(self): + entry = NETWORK_CONFIGS['dhcpv6_stateless'] +@@ -3267,6 +3437,46 @@ class TestNetplanRoundTrip(CiTestCase): + entry['expected_netplan'].splitlines(), + files['/etc/netplan/50-cloud-init.yaml'].splitlines()) + ++ def testsimple_render_dhcpv6_accept_ra(self): ++ entry = NETWORK_CONFIGS['dhcpv6_accept_ra'] ++ files = self._render_and_read(network_config=yaml.load( ++ entry['yaml_v1'])) ++ self.assertEqual( ++ entry['expected_netplan'].splitlines(), ++ files['/etc/netplan/50-cloud-init.yaml'].splitlines()) ++ ++ def testsimple_render_dhcpv6_reject_ra(self): ++ entry = NETWORK_CONFIGS['dhcpv6_reject_ra'] ++ files = self._render_and_read(network_config=yaml.load( ++ entry['yaml_v1'])) ++ self.assertEqual( ++ entry['expected_netplan'].splitlines(), ++ files['/etc/netplan/50-cloud-init.yaml'].splitlines()) ++ ++ def testsimple_render_ipv6_slaac(self): ++ entry = NETWORK_CONFIGS['ipv6_slaac'] ++ files = self._render_and_read(network_config=yaml.load( ++ entry['yaml'])) ++ self.assertEqual( ++ entry['expected_netplan'].splitlines(), ++ files['/etc/netplan/50-cloud-init.yaml'].splitlines()) ++ ++ def testsimple_render_dhcpv6_stateless(self): ++ entry = NETWORK_CONFIGS['dhcpv6_stateless'] ++ files = self._render_and_read(network_config=yaml.load( ++ entry['yaml'])) ++ self.assertEqual( ++ entry['expected_netplan'].splitlines(), ++ files['/etc/netplan/50-cloud-init.yaml'].splitlines()) ++ ++ def testsimple_render_dhcpv6_stateful(self): ++ entry = NETWORK_CONFIGS['dhcpv6_stateful'] ++ files = self._render_and_read(network_config=yaml.load( ++ entry['yaml'])) ++ self.assertEqual( ++ entry['expected_netplan'].splitlines(), ++ files['/etc/netplan/50-cloud-init.yaml'].splitlines()) ++ + def testsimple_render_all(self): + entry = NETWORK_CONFIGS['all'] + files = self._render_and_read(network_config=yaml.load(entry['yaml'])) +@@ -3350,6 +3560,45 @@ class TestEniRoundTrip(CiTestCase): + entry['expected_eni'].splitlines(), + files['/etc/network/interfaces'].splitlines()) + ++ def testsimple_render_dhcpv6_stateless(self): ++ entry = NETWORK_CONFIGS['dhcpv6_stateless'] ++ files = self._render_and_read(network_config=yaml.load( ++ entry['yaml'])) ++ files = self._render_and_read(network_config=yaml.load(entry['yaml'])) ++ self.assertEqual( ++ entry['expected_eni'].splitlines(), ++ files['/etc/network/interfaces'].splitlines()) ++ ++ def testsimple_render_ipv6_slaac(self): ++ entry = NETWORK_CONFIGS['ipv6_slaac'] ++ files = self._render_and_read(network_config=yaml.load(entry['yaml'])) ++ self.assertEqual( ++ entry['expected_eni'].splitlines(), ++ files['/etc/network/interfaces'].splitlines()) ++ ++ def testsimple_render_dhcpv6_stateful(self): ++ entry = NETWORK_CONFIGS['dhcpv6_stateless'] ++ files = self._render_and_read(network_config=yaml.load(entry['yaml'])) ++ self.assertEqual( ++ entry['expected_eni'].splitlines(), ++ files['/etc/network/interfaces'].splitlines()) ++ ++ def testsimple_render_dhcpv6_accept_ra(self): ++ entry = NETWORK_CONFIGS['dhcpv6_accept_ra'] ++ files = self._render_and_read(network_config=yaml.load( ++ entry['yaml_v1'])) ++ self.assertEqual( ++ entry['expected_eni'].splitlines(), ++ files['/etc/network/interfaces'].splitlines()) ++ ++ def testsimple_render_dhcpv6_reject_ra(self): ++ entry = NETWORK_CONFIGS['dhcpv6_reject_ra'] ++ files = self._render_and_read(network_config=yaml.load( ++ entry['yaml_v1'])) ++ self.assertEqual( ++ entry['expected_eni'].splitlines(), ++ files['/etc/network/interfaces'].splitlines()) ++ + def testsimple_render_manual(self): + """Test rendering of 'manual' for 'type' and 'control'. + +-- +1.8.3.1 + diff --git a/SOURCES/ci-net-append-type-dhcp-46-only-if-dhcp-46-is-True-in-v.patch b/SOURCES/ci-net-append-type-dhcp-46-only-if-dhcp-46-is-True-in-v.patch new file mode 100644 index 0000000..c92ed41 --- /dev/null +++ b/SOURCES/ci-net-append-type-dhcp-46-only-if-dhcp-46-is-True-in-v.patch @@ -0,0 +1,139 @@ +From 26a150ec6af91ae3ed5053069aa0c08d7064800f Mon Sep 17 00:00:00 2001 +From: Eduardo Otubo +Date: Mon, 4 May 2020 12:39:52 +0200 +Subject: [PATCH 1/6] net: append type:dhcp[46] only if dhcp[46] is True in v2 + netconfig + +RH-Author: Eduardo Otubo +Message-id: <20200327152826.13343-2-otubo@redhat.com> +Patchwork-id: 94459 +O-Subject: [RHEL-8.1.z/RHEL-8.2.z cloud-init PATCHv2 1/6] net: append type:dhcp[46] only if dhcp[46] is True in v2 netconfig +Bugzilla: 1811753 +RH-Acked-by: Cathy Avery +RH-Acked-by: Vitaly Kuznetsov + +commit bd35300ba36bd63686715fa9661516a518781f6d +Author: Kurt Stieger +Date: Mon Mar 4 15:54:25 2019 +0000 + + net: append type:dhcp[46] only if dhcp[46] is True in v2 netconfig + + When providing netplan configuration to cloud-init, the internal + network state would enable DHCP if the 'dhcp' key was present in + the source config. In netplan, dhcp[46] is a boolean and the + value of the boolean should control whether DHCP is enabled rather + than the presence of the key. This issue leaded to inconsistant + sysconfig/network-scripts on fedora. 'BOOTPROTO' was always 'dhcp', + even if the address config was static. + + After this change a dhcp subnet is added only if the 'dhcp' setting + in source cfg dict is True. + + LP: #1818032 + +Signed-off-by: Eduardo Otubo +Signed-off-by: Miroslav Rezanina +--- + cloudinit/net/network_state.py | 4 +-- + tests/unittests/test_net.py | 62 ++++++++++++++++++++++++++++++++++++++++++ + 2 files changed, 64 insertions(+), 2 deletions(-) + +diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py +index f76e508..539b76d 100644 +--- a/cloudinit/net/network_state.py ++++ b/cloudinit/net/network_state.py +@@ -706,9 +706,9 @@ class NetworkStateInterpreter(object): + """Common ipconfig extraction from v2 to v1 subnets array.""" + + subnets = [] +- if 'dhcp4' in cfg: ++ if cfg.get('dhcp4'): + subnets.append({'type': 'dhcp4'}) +- if 'dhcp6' in cfg: ++ if cfg.get('dhcp6'): + self.use_ipv6 = True + subnets.append({'type': 'dhcp6'}) + +diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py +index 012c43b..4224301 100644 +--- a/tests/unittests/test_net.py ++++ b/tests/unittests/test_net.py +@@ -103,6 +103,24 @@ STATIC_EXPECTED_1 = { + 'address': '10.0.0.2'}], + } + ++NETPLAN_DHCP_FALSE = """ ++version: 2 ++ethernets: ++ ens3: ++ match: ++ macaddress: 52:54:00:ab:cd:ef ++ dhcp4: false ++ dhcp6: false ++ addresses: ++ - 192.168.42.100/24 ++ - 2001:db8::100/32 ++ gateway4: 192.168.42.1 ++ gateway6: 2001:db8::1 ++ nameservers: ++ search: [example.com] ++ addresses: [192.168.42.53, 1.1.1.1] ++""" ++ + # Examples (and expected outputs for various renderers). + OS_SAMPLES = [ + { +@@ -2146,6 +2164,50 @@ USERCTL=no + self._compare_files_to_expected(entry[self.expected_name], found) + self._assert_headers(found) + ++ def test_netplan_dhcp_false_disable_dhcp_in_state(self): ++ """netplan config with dhcp[46]: False should not add dhcp in state""" ++ net_config = yaml.load(NETPLAN_DHCP_FALSE) ++ ns = network_state.parse_net_config_data(net_config, ++ skip_broken=False) ++ ++ dhcp_found = [snet for iface in ns.iter_interfaces() ++ for snet in iface['subnets'] if 'dhcp' in snet['type']] ++ ++ self.assertEqual([], dhcp_found) ++ ++ def test_netplan_dhcp_false_no_dhcp_in_sysconfig(self): ++ """netplan cfg with dhcp[46]: False should not have bootproto=dhcp""" ++ ++ entry = { ++ 'yaml': NETPLAN_DHCP_FALSE, ++ 'expected_sysconfig': { ++ 'ifcfg-ens3': textwrap.dedent("""\ ++ BOOTPROTO=none ++ DEFROUTE=yes ++ DEVICE=ens3 ++ DNS1=192.168.42.53 ++ DNS2=1.1.1.1 ++ DOMAIN=example.com ++ GATEWAY=192.168.42.1 ++ HWADDR=52:54:00:ab:cd:ef ++ IPADDR=192.168.42.100 ++ IPV6ADDR=2001:db8::100/32 ++ IPV6INIT=yes ++ IPV6_DEFAULTGW=2001:db8::1 ++ NETMASK=255.255.255.0 ++ NM_CONTROLLED=no ++ ONBOOT=yes ++ STARTMODE=auto ++ TYPE=Ethernet ++ USERCTL=no ++ """), ++ } ++ } ++ ++ found = self._render_and_read(network_config=yaml.load(entry['yaml'])) ++ self._compare_files_to_expected(entry['expected_sysconfig'], found) ++ self._assert_headers(found) ++ + + class TestOpenSuseSysConfigRendering(CiTestCase): + +-- +1.8.3.1 + diff --git a/SOURCES/ci-net-handle-openstack-dhcpv6-stateless-configuration.patch b/SOURCES/ci-net-handle-openstack-dhcpv6-stateless-configuration.patch new file mode 100644 index 0000000..899127e --- /dev/null +++ b/SOURCES/ci-net-handle-openstack-dhcpv6-stateless-configuration.patch @@ -0,0 +1,270 @@ +From 28e77dc7d641233b5793a159befc225fd3a8726b Mon Sep 17 00:00:00 2001 +From: Eduardo Otubo +Date: Mon, 4 May 2020 12:40:08 +0200 +Subject: [PATCH 5/6] net: handle openstack dhcpv6-stateless configuration +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +RH-Author: Eduardo Otubo +Message-id: <20200327152826.13343-6-otubo@redhat.com> +Patchwork-id: 94461 +O-Subject: [RHEL-8.1.z/RHEL-8.2.z cloud-init PATCHv2 5/6] net: handle openstack dhcpv6-stateless configuration +Bugzilla: 1811753 +RH-Acked-by: Cathy Avery +RH-Acked-by: Vitaly Kuznetsov + +commit fac98983187c0984aa79c569c4b76cab90fd6f47 +Author: Harald Jensås +Date: Wed Oct 16 15:30:28 2019 +0000 + + net: handle openstack dhcpv6-stateless configuration + + Openstack subnets can be configured to use SLAAC by setting + ipv6_address_mode=dhcpv6-stateless. When this is the case + the sysconfig interface configuration should use + IPV6_AUTOCONF=yes and not set DHCPV6C=yes. + + This change sets the subnets type property to the full + network['type'] from openstack metadata. + + cloudinit/net/sysconfig.py and cloudinit/net/eni.py + are updated to support new subnet types: + - 'ipv6_dhcpv6-stateless' => IPV6_AUTOCONF=yes + - 'ipv6_dhcpv6-stateful' => DHCPV6C=yes + + Type 'dhcp6' in sysconfig is kept for backward compatibility + with any implementations that set subnet_type == 'dhcp6'. + + LP: #1847517 + +Signed-off-by: Eduardo Otubo +Signed-off-by: Miroslav Rezanina +--- + cloudinit/net/eni.py | 7 +- + cloudinit/net/sysconfig.py | 7 +- + cloudinit/sources/helpers/openstack.py | 3 +- + .../unittests/test_datasource/test_configdrive.py | 39 ++++++++++ + tests/unittests/test_net.py | 87 +++++++++++++++++++++- + 5 files changed, 139 insertions(+), 4 deletions(-) + +diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py +index 6423632..896a39b 100644 +--- a/cloudinit/net/eni.py ++++ b/cloudinit/net/eni.py +@@ -405,8 +405,13 @@ class Renderer(renderer.Renderer): + else: + ipv4_subnet_mtu = subnet.get('mtu') + iface['inet'] = subnet_inet +- if subnet['type'].startswith('dhcp'): ++ if (subnet['type'] == 'dhcp4' or subnet['type'] == 'dhcp6' or ++ subnet['type'] == 'ipv6_dhcpv6-stateful'): ++ # Configure network settings using DHCP or DHCPv6 + iface['mode'] = 'dhcp' ++ elif subnet['type'] == 'ipv6_dhcpv6-stateless': ++ # Configure network settings using SLAAC from RAs ++ iface['mode'] = 'auto' + + # do not emit multiple 'auto $IFACE' lines as older (precise) + # ifupdown complains +diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py +index a4c7660..8b11dbb 100644 +--- a/cloudinit/net/sysconfig.py ++++ b/cloudinit/net/sysconfig.py +@@ -328,10 +328,15 @@ class Renderer(renderer.Renderer): + for i, subnet in enumerate(subnets, start=len(iface_cfg.children)): + mtu_key = 'MTU' + subnet_type = subnet.get('type') +- if subnet_type == 'dhcp6': ++ if subnet_type == 'dhcp6' or subnet_type == 'ipv6_dhcpv6-stateful': + # TODO need to set BOOTPROTO to dhcp6 on SUSE + iface_cfg['IPV6INIT'] = True ++ # Configure network settings using DHCPv6 + iface_cfg['DHCPV6C'] = True ++ elif subnet_type == 'ipv6_dhcpv6-stateless': ++ iface_cfg['IPV6INIT'] = True ++ # Configure network settings using SLAAC from RAs ++ iface_cfg['IPV6_AUTOCONF'] = True + elif subnet_type in ['dhcp4', 'dhcp']: + iface_cfg['BOOTPROTO'] = 'dhcp' + elif subnet_type == 'static': +diff --git a/cloudinit/sources/helpers/openstack.py b/cloudinit/sources/helpers/openstack.py +index 9c29cea..9f2fd2d 100644 +--- a/cloudinit/sources/helpers/openstack.py ++++ b/cloudinit/sources/helpers/openstack.py +@@ -585,7 +585,8 @@ def convert_net_json(network_json=None, known_macs=None): + subnet = dict((k, v) for k, v in network.items() + if k in valid_keys['subnet']) + if 'dhcp' in network['type']: +- t = 'dhcp6' if network['type'].startswith('ipv6') else 'dhcp4' ++ t = (network['type'] if network['type'].startswith('ipv6') ++ else 'dhcp4') + subnet.update({ + 'type': t, + }) +diff --git a/tests/unittests/test_datasource/test_configdrive.py b/tests/unittests/test_datasource/test_configdrive.py +index dcdabea..ed4e9d5 100644 +--- a/tests/unittests/test_datasource/test_configdrive.py ++++ b/tests/unittests/test_datasource/test_configdrive.py +@@ -503,6 +503,45 @@ class TestNetJson(CiTestCase): + known_macs=KNOWN_MACS) + self.assertEqual(myds.network_config, network_config) + ++ def test_network_config_conversion_dhcp6(self): ++ """Test some ipv6 input network json and check the expected ++ conversions.""" ++ in_data = { ++ 'links': [ ++ {'vif_id': '2ecc7709-b3f7-4448-9580-e1ec32d75bbd', ++ 'ethernet_mac_address': 'fa:16:3e:69:b0:58', ++ 'type': 'ovs', 'mtu': None, 'id': 'tap2ecc7709-b3'}, ++ {'vif_id': '2f88d109-5b57-40e6-af32-2472df09dc33', ++ 'ethernet_mac_address': 'fa:16:3e:d4:57:ad', ++ 'type': 'ovs', 'mtu': None, 'id': 'tap2f88d109-5b'}, ++ ], ++ 'networks': [ ++ {'link': 'tap2ecc7709-b3', 'type': 'ipv6_dhcpv6-stateless', ++ 'network_id': '6d6357ac-0f70-4afa-8bd7-c274cc4ea235', ++ 'id': 'network0'}, ++ {'link': 'tap2f88d109-5b', 'type': 'ipv6_dhcpv6-stateful', ++ 'network_id': 'd227a9b3-6960-4d94-8976-ee5788b44f54', ++ 'id': 'network1'}, ++ ] ++ } ++ out_data = { ++ 'version': 1, ++ 'config': [ ++ {'mac_address': 'fa:16:3e:69:b0:58', ++ 'mtu': None, ++ 'name': 'enp0s1', ++ 'subnets': [{'type': 'ipv6_dhcpv6-stateless'}], ++ 'type': 'physical'}, ++ {'mac_address': 'fa:16:3e:d4:57:ad', ++ 'mtu': None, ++ 'name': 'enp0s2', ++ 'subnets': [{'type': 'ipv6_dhcpv6-stateful'}], ++ 'type': 'physical'} ++ ], ++ } ++ conv_data = openstack.convert_net_json(in_data, known_macs=KNOWN_MACS) ++ self.assertEqual(out_data, conv_data) ++ + def test_network_config_conversions(self): + """Tests a bunch of input network json and checks the + expected conversions.""" +diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py +index df5658d..70d13f3 100644 +--- a/tests/unittests/test_net.py ++++ b/tests/unittests/test_net.py +@@ -712,6 +712,82 @@ NETWORK_CONFIGS = { + """), + }, + }, ++ 'dhcpv6_stateless': { ++ 'expected_eni': textwrap.dedent("""\ ++ auto lo ++ iface lo inet loopback ++ ++ auto iface0 ++ iface iface0 inet6 auto ++ """).rstrip(' '), ++ 'expected_netplan': textwrap.dedent(""" ++ network: ++ version: 2 ++ ethernets: ++ iface0: ++ dhcp6: true ++ """).rstrip(' '), ++ 'yaml': textwrap.dedent("""\ ++ version: 1 ++ config: ++ - type: 'physical' ++ name: 'iface0' ++ subnets: ++ - {'type': 'ipv6_dhcpv6-stateless'} ++ """).rstrip(' '), ++ 'expected_sysconfig': { ++ 'ifcfg-iface0': textwrap.dedent("""\ ++ BOOTPROTO=none ++ DEVICE=iface0 ++ IPV6_AUTOCONF=yes ++ IPV6INIT=yes ++ DEVICE=iface0 ++ NM_CONTROLLED=no ++ ONBOOT=yes ++ STARTMODE=auto ++ TYPE=Ethernet ++ USERCTL=no ++ """), ++ }, ++ }, ++ 'dhcpv6_stateful': { ++ 'expected_eni': textwrap.dedent("""\ ++ auto lo ++ iface lo inet loopback ++ ++ auto iface0 ++ iface iface0 inet6 dhcp ++ """).rstrip(' '), ++ 'expected_netplan': textwrap.dedent(""" ++ network: ++ version: 2 ++ ethernets: ++ iface0: ++ dhcp6: true ++ """).rstrip(' '), ++ 'yaml': textwrap.dedent("""\ ++ version: 1 ++ config: ++ - type: 'physical' ++ name: 'iface0' ++ subnets: ++ - {'type': 'ipv6_dhcpv6-stateful'} ++ """).rstrip(' '), ++ 'expected_sysconfig': { ++ 'ifcfg-iface0': textwrap.dedent("""\ ++ BOOTPROTO=none ++ DEVICE=iface0 ++ DHCPV6C=yes ++ IPV6INIT=yes ++ DEVICE=iface0 ++ NM_CONTROLLED=no ++ ONBOOT=yes ++ STARTMODE=auto ++ TYPE=Ethernet ++ USERCTL=no ++ """), ++ }, ++ }, + 'all': { + 'expected_eni': ("""\ + auto lo +@@ -2300,8 +2376,11 @@ USERCTL=no + } + } + ++ ++ def test_dhcpv6_stateless_config(self): ++ entry = NETWORK_CONFIGS['dhcpv6_stateless'] + found = self._render_and_read(network_config=yaml.load(entry['yaml'])) +- self._compare_files_to_expected(entry['expected_sysconfig'], found) ++ self._compare_files_to_expected(entry[self.expected_name], found) + self._assert_headers(found) + + def test_from_v2_route_metric(self): +@@ -2334,6 +2413,12 @@ USERCTL=no + self._compare_files_to_expected( + expected, self._render_and_read(network_config=v2data)) + ++ def test_dhcpv6_stateful_config(self): ++ entry = NETWORK_CONFIGS['dhcpv6_stateful'] ++ found = self._render_and_read(network_config=yaml.load(entry['yaml'])) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ self._assert_headers(found) ++ + + class TestOpenSuseSysConfigRendering(CiTestCase): + +-- +1.8.3.1 + diff --git a/SOURCES/ci-net-sysconfig-Handle-default-route-setup-for-dhcp-co.patch b/SOURCES/ci-net-sysconfig-Handle-default-route-setup-for-dhcp-co.patch new file mode 100644 index 0000000..31f7b35 --- /dev/null +++ b/SOURCES/ci-net-sysconfig-Handle-default-route-setup-for-dhcp-co.patch @@ -0,0 +1,311 @@ +From a22a059e36ec56d0d6d7e2a63ccff56d6c19f9d6 Mon Sep 17 00:00:00 2001 +From: Eduardo Otubo +Date: Mon, 4 May 2020 12:39:55 +0200 +Subject: [PATCH 2/6] net/sysconfig: Handle default route setup for dhcp + configured NICs + +RH-Author: Eduardo Otubo +Message-id: <20200327152826.13343-3-otubo@redhat.com> +Patchwork-id: 94457 +O-Subject: [RHEL-8.1.z/RHEL-8.2.z cloud-init PATCHv2 2/6] net/sysconfig: Handle default route setup for dhcp configured NICs +Bugzilla: 1811753 +RH-Acked-by: Cathy Avery +RH-Acked-by: Vitaly Kuznetsov + +commit 3acaacc92be1b7d7bad099c323d6e923664a8afa +Author: Robert Schweikert +Date: Tue Mar 12 21:08:22 2019 +0000 + + net/sysconfig: Handle default route setup for dhcp configured NICs + + When the network configuration has a default route configured and + another network device that is configured with dhcp, SUSE sysconfig + output should not accept the default route provided by the dhcp + server. + + LP: #1812117 + +Signed-off-by: Eduardo Otubo +Signed-off-by: Miroslav Rezanina +--- + cloudinit/net/network_state.py | 41 +++++++++++++++++++++------ + cloudinit/net/sysconfig.py | 31 +++++++++++++++------ + tests/unittests/test_net.py | 63 ++++++++++++++++++++++++++++++++++++++++++ + 3 files changed, 118 insertions(+), 17 deletions(-) + +diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py +index 539b76d..4d19f56 100644 +--- a/cloudinit/net/network_state.py ++++ b/cloudinit/net/network_state.py +@@ -148,6 +148,7 @@ class NetworkState(object): + self._network_state = copy.deepcopy(network_state) + self._version = version + self.use_ipv6 = network_state.get('use_ipv6', False) ++ self._has_default_route = None + + @property + def config(self): +@@ -157,14 +158,6 @@ class NetworkState(object): + def version(self): + return self._version + +- def iter_routes(self, filter_func=None): +- for route in self._network_state.get('routes', []): +- if filter_func is not None: +- if filter_func(route): +- yield route +- else: +- yield route +- + @property + def dns_nameservers(self): + try: +@@ -179,6 +172,12 @@ class NetworkState(object): + except KeyError: + return [] + ++ @property ++ def has_default_route(self): ++ if self._has_default_route is None: ++ self._has_default_route = self._maybe_has_default_route() ++ return self._has_default_route ++ + def iter_interfaces(self, filter_func=None): + ifaces = self._network_state.get('interfaces', {}) + for iface in six.itervalues(ifaces): +@@ -188,6 +187,32 @@ class NetworkState(object): + if filter_func(iface): + yield iface + ++ def iter_routes(self, filter_func=None): ++ for route in self._network_state.get('routes', []): ++ if filter_func is not None: ++ if filter_func(route): ++ yield route ++ else: ++ yield route ++ ++ def _maybe_has_default_route(self): ++ for route in self.iter_routes(): ++ if self._is_default_route(route): ++ return True ++ for iface in self.iter_interfaces(): ++ for subnet in iface.get('subnets', []): ++ for route in subnet.get('routes', []): ++ if self._is_default_route(route): ++ return True ++ return False ++ ++ def _is_default_route(self, route): ++ default_nets = ('::', '0.0.0.0') ++ return ( ++ route.get('prefix') == 0 ++ and route.get('network') in default_nets ++ ) ++ + + @six.add_metaclass(CommandHandlerMeta) + class NetworkStateInterpreter(object): +diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py +index 52bb848..5c1b4eb 100644 +--- a/cloudinit/net/sysconfig.py ++++ b/cloudinit/net/sysconfig.py +@@ -320,7 +320,7 @@ class Renderer(renderer.Renderer): + iface_cfg[new_key] = old_value + + @classmethod +- def _render_subnets(cls, iface_cfg, subnets): ++ def _render_subnets(cls, iface_cfg, subnets, has_default_route): + # setting base values + iface_cfg['BOOTPROTO'] = 'none' + +@@ -329,6 +329,7 @@ class Renderer(renderer.Renderer): + mtu_key = 'MTU' + subnet_type = subnet.get('type') + if subnet_type == 'dhcp6': ++ # TODO need to set BOOTPROTO to dhcp6 on SUSE + iface_cfg['IPV6INIT'] = True + iface_cfg['DHCPV6C'] = True + elif subnet_type in ['dhcp4', 'dhcp']: +@@ -372,9 +373,9 @@ class Renderer(renderer.Renderer): + ipv6_index = -1 + for i, subnet in enumerate(subnets, start=len(iface_cfg.children)): + subnet_type = subnet.get('type') +- if subnet_type == 'dhcp6': +- continue +- elif subnet_type in ['dhcp4', 'dhcp']: ++ if subnet_type in ['dhcp', 'dhcp4', 'dhcp6']: ++ if has_default_route and iface_cfg['BOOTPROTO'] != 'none': ++ iface_cfg['DHCLIENT_SET_DEFAULT_ROUTE'] = False + continue + elif subnet_type == 'static': + if subnet_is_ipv6(subnet): +@@ -440,6 +441,8 @@ class Renderer(renderer.Renderer): + # TODO(harlowja): add validation that no other iface has + # also provided the default route? + iface_cfg['DEFROUTE'] = True ++ if iface_cfg['BOOTPROTO'] in ('dhcp', 'dhcp4', 'dhcp6'): ++ iface_cfg['DHCLIENT_SET_DEFAULT_ROUTE'] = True + if 'gateway' in route: + if is_ipv6 or is_ipv6_addr(route['gateway']): + iface_cfg['IPV6_DEFAULTGW'] = route['gateway'] +@@ -490,7 +493,9 @@ class Renderer(renderer.Renderer): + iface_cfg = iface_contents[iface_name] + route_cfg = iface_cfg.routes + +- cls._render_subnets(iface_cfg, iface_subnets) ++ cls._render_subnets( ++ iface_cfg, iface_subnets, network_state.has_default_route ++ ) + cls._render_subnet_routes(iface_cfg, route_cfg, iface_subnets) + + @classmethod +@@ -515,7 +520,9 @@ class Renderer(renderer.Renderer): + + iface_subnets = iface.get("subnets", []) + route_cfg = iface_cfg.routes +- cls._render_subnets(iface_cfg, iface_subnets) ++ cls._render_subnets( ++ iface_cfg, iface_subnets, network_state.has_default_route ++ ) + cls._render_subnet_routes(iface_cfg, route_cfg, iface_subnets) + + # iter_interfaces on network-state is not sorted to produce +@@ -544,7 +551,9 @@ class Renderer(renderer.Renderer): + + iface_subnets = iface.get("subnets", []) + route_cfg = iface_cfg.routes +- cls._render_subnets(iface_cfg, iface_subnets) ++ cls._render_subnets( ++ iface_cfg, iface_subnets, network_state.has_default_route ++ ) + cls._render_subnet_routes(iface_cfg, route_cfg, iface_subnets) + + @staticmethod +@@ -603,7 +612,9 @@ class Renderer(renderer.Renderer): + + iface_subnets = iface.get("subnets", []) + route_cfg = iface_cfg.routes +- cls._render_subnets(iface_cfg, iface_subnets) ++ cls._render_subnets( ++ iface_cfg, iface_subnets, network_state.has_default_route ++ ) + cls._render_subnet_routes(iface_cfg, route_cfg, iface_subnets) + + @classmethod +@@ -615,7 +626,9 @@ class Renderer(renderer.Renderer): + iface_cfg.kind = 'infiniband' + iface_subnets = iface.get("subnets", []) + route_cfg = iface_cfg.routes +- cls._render_subnets(iface_cfg, iface_subnets) ++ cls._render_subnets( ++ iface_cfg, iface_subnets, network_state.has_default_route ++ ) + cls._render_subnet_routes(iface_cfg, route_cfg, iface_subnets) + + @classmethod +diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py +index 4224301..a975678 100644 +--- a/tests/unittests/test_net.py ++++ b/tests/unittests/test_net.py +@@ -546,6 +546,7 @@ NETWORK_CONFIGS = { + BOOTPROTO=dhcp + DEFROUTE=yes + DEVICE=eth99 ++ DHCLIENT_SET_DEFAULT_ROUTE=yes + DNS1=8.8.8.8 + DNS2=8.8.4.4 + DOMAIN="barley.maas sach.maas" +@@ -913,6 +914,7 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true + 'ifcfg-bond0.200': textwrap.dedent("""\ + BOOTPROTO=dhcp + DEVICE=bond0.200 ++ DHCLIENT_SET_DEFAULT_ROUTE=no + ONBOOT=yes + PHYSDEV=bond0 + TYPE=Ethernet +@@ -996,6 +998,7 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true + 'ifcfg-eth5': textwrap.dedent("""\ + BOOTPROTO=dhcp + DEVICE=eth5 ++ DHCLIENT_SET_DEFAULT_ROUTE=no + HWADDR=98:bb:9f:2c:e8:8a + ONBOOT=no + TYPE=Ethernet +@@ -1624,6 +1627,23 @@ CONFIG_V1_SIMPLE_SUBNET = { + 'type': 'static'}], + 'type': 'physical'}]} + ++CONFIG_V1_MULTI_IFACE = { ++ 'version': 1, ++ 'config': [{'type': 'physical', ++ 'mtu': 1500, ++ 'subnets': [{'type': 'static', ++ 'netmask': '255.255.240.0', ++ 'routes': [{'netmask': '0.0.0.0', ++ 'network': '0.0.0.0', ++ 'gateway': '51.68.80.1'}], ++ 'address': '51.68.89.122', ++ 'ipv4': True}], ++ 'mac_address': 'fa:16:3e:25:b4:59', ++ 'name': 'eth0'}, ++ {'type': 'physical', ++ 'mtu': 9000, ++ 'subnets': [{'type': 'dhcp4'}], ++ 'mac_address': 'fa:16:3e:b1:ca:29', 'name': 'eth1'}]} + + DEFAULT_DEV_ATTRS = { + 'eth1000': { +@@ -2088,6 +2108,49 @@ USERCTL=no + """ + self.assertEqual(expected, found[nspath + 'ifcfg-interface0']) + ++ def test_network_config_v1_multi_iface_samples(self): ++ ns = network_state.parse_net_config_data(CONFIG_V1_MULTI_IFACE) ++ render_dir = self.tmp_path("render") ++ os.makedirs(render_dir) ++ renderer = self._get_renderer() ++ renderer.render_network_state(ns, target=render_dir) ++ found = dir2dict(render_dir) ++ nspath = '/etc/sysconfig/network-scripts/' ++ self.assertNotIn(nspath + 'ifcfg-lo', found.keys()) ++ expected_i1 = """\ ++# Created by cloud-init on instance boot automatically, do not edit. ++# ++BOOTPROTO=none ++DEFROUTE=yes ++DEVICE=eth0 ++GATEWAY=51.68.80.1 ++HWADDR=fa:16:3e:25:b4:59 ++IPADDR=51.68.89.122 ++MTU=1500 ++NETMASK=255.255.240.0 ++NM_CONTROLLED=no ++ONBOOT=yes ++STARTMODE=auto ++TYPE=Ethernet ++USERCTL=no ++""" ++ self.assertEqual(expected_i1, found[nspath + 'ifcfg-eth0']) ++ expected_i2 = """\ ++# Created by cloud-init on instance boot automatically, do not edit. ++# ++BOOTPROTO=dhcp ++DEVICE=eth1 ++DHCLIENT_SET_DEFAULT_ROUTE=no ++HWADDR=fa:16:3e:b1:ca:29 ++MTU=9000 ++NM_CONTROLLED=no ++ONBOOT=yes ++STARTMODE=auto ++TYPE=Ethernet ++USERCTL=no ++""" ++ self.assertEqual(expected_i2, found[nspath + 'ifcfg-eth1']) ++ + def test_config_with_explicit_loopback(self): + ns = network_state.parse_net_config_data(CONFIG_V1_EXPLICIT_LOOPBACK) + render_dir = self.tmp_path("render") +-- +1.8.3.1 + diff --git a/SPECS/cloud-init.spec b/SPECS/cloud-init.spec index 2bb30ac..02943a0 100644 --- a/SPECS/cloud-init.spec +++ b/SPECS/cloud-init.spec @@ -6,7 +6,7 @@ Name: cloud-init Version: 18.5 -Release: 12%{?dist}.1 +Release: 12%{?dist}.2 Summary: Cloud instance init scripts Group: System Environment/Base @@ -59,6 +59,18 @@ Patch26: ci-net-add-is_master-check-for-filtering-device-list.patch Patch27: ci-Remove-race-condition-between-cloud-init-and-Network.patch # For bz#1826262 - [RHEL8.2] Race condition of starting cloud-init and NetworkManager [rhel-8.2.0.z] Patch28: ci-Make-cloud-init.service-execute-after-network-is-up.patch +# For bz#1811753 - [RHEL8] DHCP Stateful - Openstack DHCPv6 instance get no default gateway and is unable to send IPv6 traffic (Network unreachable errors). [rhel-8.2.0.z] +Patch29: ci-net-append-type-dhcp-46-only-if-dhcp-46-is-True-in-v.patch +# For bz#1811753 - [RHEL8] DHCP Stateful - Openstack DHCPv6 instance get no default gateway and is unable to send IPv6 traffic (Network unreachable errors). [rhel-8.2.0.z] +Patch30: ci-net-sysconfig-Handle-default-route-setup-for-dhcp-co.patch +# For bz#1811753 - [RHEL8] DHCP Stateful - Openstack DHCPv6 instance get no default gateway and is unable to send IPv6 traffic (Network unreachable errors). [rhel-8.2.0.z] +Patch31: ci-azure-net-generate_fallback_nic-emits-network-v2-con.patch +# For bz#1811753 - [RHEL8] DHCP Stateful - Openstack DHCPv6 instance get no default gateway and is unable to send IPv6 traffic (Network unreachable errors). [rhel-8.2.0.z] +Patch32: ci-azure-support-matching-dhcp-route-metrics-for-dual-s.patch +# For bz#1811753 - [RHEL8] DHCP Stateful - Openstack DHCPv6 instance get no default gateway and is unable to send IPv6 traffic (Network unreachable errors). [rhel-8.2.0.z] +Patch33: ci-net-handle-openstack-dhcpv6-stateless-configuration.patch +# For bz#1811753 - [RHEL8] DHCP Stateful - Openstack DHCPv6 instance get no default gateway and is unable to send IPv6 traffic (Network unreachable errors). [rhel-8.2.0.z] +Patch34: ci-net-IPv6-accept_ra-slaac-stateless-51.patch BuildArch: noarch @@ -241,6 +253,16 @@ fi %config(noreplace) %{_sysconfdir}/rsyslog.d/21-cloudinit.conf %changelog +* Mon May 04 2020 Miroslav Rezanina - 18.5-12.el8_2.2 +- ci-net-append-type-dhcp-46-only-if-dhcp-46-is-True-in-v.patch [bz#1811753] +- ci-net-sysconfig-Handle-default-route-setup-for-dhcp-co.patch [bz#1811753] +- ci-azure-net-generate_fallback_nic-emits-network-v2-con.patch [bz#1811753] +- ci-azure-support-matching-dhcp-route-metrics-for-dual-s.patch [bz#1811753] +- ci-net-handle-openstack-dhcpv6-stateless-configuration.patch [bz#1811753] +- ci-net-IPv6-accept_ra-slaac-stateless-51.patch [bz#1811753] +- Resolves: bz#1811753 + ([RHEL8] DHCP Stateful - Openstack DHCPv6 instance get no default gateway and is unable to send IPv6 traffic (Network unreachable errors). [rhel-8.2.0.z]) + * Tue Apr 21 2020 Miroslav Rezanina - 18.5-12.el8_2.1 - ci-Make-cloud-init.service-execute-after-network-is-up.patch [bz#1826262] - Resolves: bz#1826262