From d41feb88d2064d356ab47f8652c6cda9cecfea29 Mon Sep 17 00:00:00 2001 From: Lars Kellogg-Stedman Date: Apr 23 2018 02:55:03 +0000 Subject: Enable dhcp on ec2 interfaces with only local ipv4 addresses [RH:1569321] Cherry pick upstream commit eb292c1. https://bugzilla.redhat.com/show_bug.cgi?id=1569321 Signed-off-by: Lars Kellogg-Stedman --- diff --git a/cloud-init-17.1-fix-local-ipv4-only.patch b/cloud-init-17.1-fix-local-ipv4-only.patch new file mode 100644 index 0000000..c60be12 --- /dev/null +++ b/cloud-init-17.1-fix-local-ipv4-only.patch @@ -0,0 +1,298 @@ +From a16fb4e1e1379db61a1ee40513f2ad10c9b38ef9 Mon Sep 17 00:00:00 2001 +From: Chad Smith +Date: Tue, 31 Oct 2017 12:42:15 -0600 +Subject: [PATCH 4/4] EC2: Limit network config to fallback nic, fix local-ipv4 + only instances. + +VPC instances have the option to specific local only IPv4 addresses. Allow +Ec2Datasource to enable dhcp4 on instances even if local-ipv4s is +configured on an instance. + +Also limit network_configuration to only the primary (fallback) nic. + +LP: #1728152 +(cherry picked from commit eb292c18c3d83b9f7e5d1fd81b0e8aefaab0cc2d) +--- + cloudinit/sources/DataSourceEc2.py | 24 ++++- + tests/unittests/test_datasource/test_ec2.py | 136 ++++++++++++++++++++++++++-- + 2 files changed, 149 insertions(+), 11 deletions(-) + +diff --git a/cloudinit/sources/DataSourceEc2.py b/cloudinit/sources/DataSourceEc2.py +index 41367a8b..0ef22174 100644 +--- a/cloudinit/sources/DataSourceEc2.py ++++ b/cloudinit/sources/DataSourceEc2.py +@@ -64,6 +64,9 @@ class DataSourceEc2(sources.DataSource): + # Whether we want to get network configuration from the metadata service. + get_network_metadata = False + ++ # Track the discovered fallback nic for use in configuration generation. ++ fallback_nic = None ++ + def __init__(self, sys_cfg, distro, paths): + sources.DataSource.__init__(self, sys_cfg, distro, paths) + self.metadata_address = None +@@ -89,16 +92,18 @@ class DataSourceEc2(sources.DataSource): + elif self.cloud_platform == Platforms.NO_EC2_METADATA: + return False + ++ self.fallback_nic = net.find_fallback_nic() + if self.get_network_metadata: # Setup networking in init-local stage. + if util.is_FreeBSD(): + LOG.debug("FreeBSD doesn't support running dhclient with -sf") + return False +- dhcp_leases = dhcp.maybe_perform_dhcp_discovery() ++ dhcp_leases = dhcp.maybe_perform_dhcp_discovery(self.fallback_nic) + if not dhcp_leases: + # DataSourceEc2Local failed in init-local stage. DataSourceEc2 + # will still run in init-network stage. + return False + dhcp_opts = dhcp_leases[-1] ++ self.fallback_nic = dhcp_opts.get('interface') + net_params = {'interface': dhcp_opts.get('interface'), + 'ip': dhcp_opts.get('fixed-address'), + 'prefix_or_mask': dhcp_opts.get('subnet-mask'), +@@ -297,8 +302,13 @@ class DataSourceEc2(sources.DataSource): + + result = None + net_md = self.metadata.get('network') ++ # Limit network configuration to only the primary/fallback nic ++ macs_to_nics = { ++ net.get_interface_mac(self.fallback_nic): self.fallback_nic} + if isinstance(net_md, dict): +- result = convert_ec2_metadata_network_config(net_md) ++ result = convert_ec2_metadata_network_config( ++ net_md, macs_to_nics=macs_to_nics, ++ fallback_nic=self.fallback_nic) + else: + LOG.warning("unexpected metadata 'network' key not valid: %s", + net_md) +@@ -458,15 +468,18 @@ def _collect_platform_data(): + return data + + +-def convert_ec2_metadata_network_config(network_md, macs_to_nics=None): ++def convert_ec2_metadata_network_config(network_md, macs_to_nics=None, ++ fallback_nic=None): + """Convert ec2 metadata to network config version 1 data dict. + + @param: network_md: 'network' portion of EC2 metadata. + generally formed as {"interfaces": {"macs": {}} where + 'macs' is a dictionary with mac address as key and contents like: + {"device-number": "0", "interface-id": "...", "local-ipv4s": ...} +- @param: macs_to_name: Optional dict mac addresses and the nic name. If ++ @param: macs_to_nics: Optional dict of mac addresses and nic names. If + not provided, get_interfaces_by_mac is called to get it from the OS. ++ @param: fallback_nic: Optionally provide the primary nic interface name. ++ This nic will be guaranteed to minimally have a dhcp4 configuration. + + @return A dict of network config version 1 based on the metadata and macs. + """ +@@ -480,7 +493,8 @@ def convert_ec2_metadata_network_config(network_md, macs_to_nics=None): + continue # Not a physical nic represented in metadata + nic_cfg = {'type': 'physical', 'name': nic_name, 'subnets': []} + nic_cfg['mac_address'] = mac +- if nic_metadata.get('public-ipv4s'): ++ if (nic_name == fallback_nic or nic_metadata.get('public-ipv4s') or ++ nic_metadata.get('local-ipv4s')): + nic_cfg['subnets'].append({'type': 'dhcp4'}) + if nic_metadata.get('ipv6s'): + nic_cfg['subnets'].append({'type': 'dhcp6'}) +diff --git a/tests/unittests/test_datasource/test_ec2.py b/tests/unittests/test_datasource/test_ec2.py +index a7301dbf..6af699a6 100644 +--- a/tests/unittests/test_datasource/test_ec2.py ++++ b/tests/unittests/test_datasource/test_ec2.py +@@ -51,6 +51,29 @@ DEFAULT_METADATA = { + "vpc-ipv4-cidr-block": "172.31.0.0/16", + "vpc-ipv4-cidr-blocks": "172.31.0.0/16", + "vpc-ipv6-cidr-blocks": "2600:1f16:aeb:b200::/56" ++ }, ++ "06:17:04:d7:26:0A": { ++ "device-number": "1", # Only IPv4 local config ++ "interface-id": "eni-e44ef49f", ++ "ipv4-associations": {"": "172.3.3.16"}, ++ "ipv6s": "", # No IPv6 config ++ "local-hostname": ("ip-172-3-3-16.us-east-2." ++ "compute.internal"), ++ "local-ipv4s": "172.3.3.16", ++ "mac": "06:17:04:d7:26:0A", ++ "owner-id": "950047163771", ++ "public-hostname": ("ec2-172-3-3-16.us-east-2." ++ "compute.amazonaws.com"), ++ "public-ipv4s": "", # No public ipv4 config ++ "security-group-ids": "sg-5a61d333", ++ "security-groups": "wide-open", ++ "subnet-id": "subnet-20b8565b", ++ "subnet-ipv4-cidr-block": "172.31.16.0/20", ++ "subnet-ipv6-cidr-blocks": "", ++ "vpc-id": "vpc-87e72bee", ++ "vpc-ipv4-cidr-block": "172.31.0.0/16", ++ "vpc-ipv4-cidr-blocks": "172.31.0.0/16", ++ "vpc-ipv6-cidr-blocks": "" + } + } + } +@@ -209,12 +232,20 @@ class TestEc2(test_helpers.HttprettyTestCase): + + @httpretty.activate + def test_network_config_property_returns_version_1_network_data(self): +- """network_config property returns network version 1 for metadata.""" ++ """network_config property returns network version 1 for metadata. ++ ++ Only one device is configured even when multiple exist in metadata. ++ """ + ds = self._setup_ds( + platform_data=self.valid_platform_data, + sys_cfg={'datasource': {'Ec2': {'strict_id': True}}}, + md=DEFAULT_METADATA) +- ds.get_data() ++ find_fallback_path = ( ++ 'cloudinit.sources.DataSourceEc2.net.find_fallback_nic') ++ with mock.patch(find_fallback_path) as m_find_fallback: ++ m_find_fallback.return_value = 'eth9' ++ ds.get_data() ++ + mac1 = '06:17:04:d7:26:09' # Defined in DEFAULT_METADATA + expected = {'version': 1, 'config': [ + {'mac_address': '06:17:04:d7:26:09', 'name': 'eth9', +@@ -222,9 +253,48 @@ class TestEc2(test_helpers.HttprettyTestCase): + 'type': 'physical'}]} + patch_path = ( + 'cloudinit.sources.DataSourceEc2.net.get_interfaces_by_mac') ++ get_interface_mac_path = ( ++ 'cloudinit.sources.DataSourceEc2.net.get_interface_mac') ++ with mock.patch(patch_path) as m_get_interfaces_by_mac: ++ with mock.patch(find_fallback_path) as m_find_fallback: ++ with mock.patch(get_interface_mac_path) as m_get_mac: ++ m_get_interfaces_by_mac.return_value = {mac1: 'eth9'} ++ m_find_fallback.return_value = 'eth9' ++ m_get_mac.return_value = mac1 ++ self.assertEqual(expected, ds.network_config) ++ ++ @httpretty.activate ++ def test_network_config_property_set_dhcp4_on_private_ipv4(self): ++ """network_config property configures dhcp4 on private ipv4 nics. ++ ++ Only one device is configured even when multiple exist in metadata. ++ """ ++ ds = self._setup_ds( ++ platform_data=self.valid_platform_data, ++ sys_cfg={'datasource': {'Ec2': {'strict_id': True}}}, ++ md=DEFAULT_METADATA) ++ find_fallback_path = ( ++ 'cloudinit.sources.DataSourceEc2.net.find_fallback_nic') ++ with mock.patch(find_fallback_path) as m_find_fallback: ++ m_find_fallback.return_value = 'eth9' ++ ds.get_data() ++ ++ mac1 = '06:17:04:d7:26:0A' # IPv4 only in DEFAULT_METADATA ++ expected = {'version': 1, 'config': [ ++ {'mac_address': '06:17:04:d7:26:0A', 'name': 'eth9', ++ 'subnets': [{'type': 'dhcp4'}], ++ 'type': 'physical'}]} ++ patch_path = ( ++ 'cloudinit.sources.DataSourceEc2.net.get_interfaces_by_mac') ++ get_interface_mac_path = ( ++ 'cloudinit.sources.DataSourceEc2.net.get_interface_mac') + with mock.patch(patch_path) as m_get_interfaces_by_mac: +- m_get_interfaces_by_mac.return_value = {mac1: 'eth9'} +- self.assertEqual(expected, ds.network_config) ++ with mock.patch(find_fallback_path) as m_find_fallback: ++ with mock.patch(get_interface_mac_path) as m_get_mac: ++ m_get_interfaces_by_mac.return_value = {mac1: 'eth9'} ++ m_find_fallback.return_value = 'eth9' ++ m_get_mac.return_value = mac1 ++ self.assertEqual(expected, ds.network_config) + + def test_network_config_property_is_cached_in_datasource(self): + """network_config property is cached in DataSourceEc2.""" +@@ -321,9 +391,11 @@ class TestEc2(test_helpers.HttprettyTestCase): + + @httpretty.activate + @mock.patch('cloudinit.net.EphemeralIPv4Network') ++ @mock.patch('cloudinit.net.find_fallback_nic') + @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery') + @mock.patch('cloudinit.sources.DataSourceEc2.util.is_FreeBSD') +- def test_ec2_local_performs_dhcp_on_non_bsd(self, m_is_bsd, m_dhcp, m_net): ++ def test_ec2_local_performs_dhcp_on_non_bsd(self, m_is_bsd, m_dhcp, ++ m_fallback_nic, m_net): + """Ec2Local returns True for valid platform data on non-BSD with dhcp. + + DataSourceEc2Local will setup initial IPv4 network via dhcp discovery. +@@ -331,6 +403,7 @@ class TestEc2(test_helpers.HttprettyTestCase): + When the platform data is valid, return True. + """ + ++ m_fallback_nic.return_value = 'eth9' + m_is_bsd.return_value = False + m_dhcp.return_value = [{ + 'interface': 'eth9', 'fixed-address': '192.168.2.9', +@@ -344,7 +417,7 @@ class TestEc2(test_helpers.HttprettyTestCase): + + ret = ds.get_data() + self.assertTrue(ret) +- m_dhcp.assert_called_once_with() ++ m_dhcp.assert_called_once_with('eth9') + m_net.assert_called_once_with( + broadcast='192.168.2.255', interface='eth9', ip='192.168.2.9', + prefix_or_mask='255.255.255.0', router='192.168.2.1') +@@ -389,6 +462,57 @@ class TestConvertEc2MetadataNetworkConfig(test_helpers.CiTestCase): + ec2.convert_ec2_metadata_network_config( + network_metadata_ipv6, macs_to_nics)) + ++ def test_convert_ec2_metadata_network_config_handles_local_dhcp4(self): ++ """Config dhcp4 when there are no public addresses in public-ipv4s.""" ++ macs_to_nics = {self.mac1: 'eth9'} ++ network_metadata_ipv6 = copy.deepcopy(self.network_metadata) ++ nic1_metadata = ( ++ network_metadata_ipv6['interfaces']['macs'][self.mac1]) ++ nic1_metadata['local-ipv4s'] = '172.3.3.15' ++ nic1_metadata.pop('public-ipv4s') ++ expected = {'version': 1, 'config': [ ++ {'mac_address': self.mac1, 'type': 'physical', ++ 'name': 'eth9', 'subnets': [{'type': 'dhcp4'}]}]} ++ self.assertEqual( ++ expected, ++ ec2.convert_ec2_metadata_network_config( ++ network_metadata_ipv6, macs_to_nics)) ++ ++ def test_convert_ec2_metadata_network_config_handles_absent_dhcp4(self): ++ """Config dhcp4 on fallback_nic when there are no ipv4 addresses.""" ++ macs_to_nics = {self.mac1: 'eth9'} ++ network_metadata_ipv6 = copy.deepcopy(self.network_metadata) ++ nic1_metadata = ( ++ network_metadata_ipv6['interfaces']['macs'][self.mac1]) ++ nic1_metadata['public-ipv4s'] = '' ++ ++ # When no ipv4 or ipv6 content but fallback_nic set, set dhcp4 config. ++ expected = {'version': 1, 'config': [ ++ {'mac_address': self.mac1, 'type': 'physical', ++ 'name': 'eth9', 'subnets': [{'type': 'dhcp4'}]}]} ++ self.assertEqual( ++ expected, ++ ec2.convert_ec2_metadata_network_config( ++ network_metadata_ipv6, macs_to_nics, fallback_nic='eth9')) ++ ++ def test_convert_ec2_metadata_network_config_handles_local_v4_and_v6(self): ++ """When dhcp6 is public and dhcp4 is set to local enable both.""" ++ macs_to_nics = {self.mac1: 'eth9'} ++ network_metadata_both = copy.deepcopy(self.network_metadata) ++ nic1_metadata = ( ++ network_metadata_both['interfaces']['macs'][self.mac1]) ++ nic1_metadata['ipv6s'] = '2620:0:1009:fd00:e442:c88d:c04d:dc85/64' ++ nic1_metadata.pop('public-ipv4s') ++ nic1_metadata['local-ipv4s'] = '10.0.0.42' # Local ipv4 only on vpc ++ expected = {'version': 1, 'config': [ ++ {'mac_address': self.mac1, 'type': 'physical', ++ 'name': 'eth9', ++ 'subnets': [{'type': 'dhcp4'}, {'type': 'dhcp6'}]}]} ++ self.assertEqual( ++ expected, ++ ec2.convert_ec2_metadata_network_config( ++ network_metadata_both, macs_to_nics)) ++ + def test_convert_ec2_metadata_network_config_handles_dhcp4_and_dhcp6(self): + """Config both dhcp4 and dhcp6 when both vpc-ipv6 and ipv4 exists.""" + macs_to_nics = {self.mac1: 'eth9'} +-- +2.14.3 + diff --git a/cloud-init.spec b/cloud-init.spec index 68c0548..ef1d81a 100644 --- a/cloud-init.spec +++ b/cloud-init.spec @@ -1,6 +1,6 @@ Name: cloud-init Version: 17.1 -Release: 4%{?dist} +Release: 5%{?dist} Summary: Cloud instance init scripts License: ASL 2.0 or GPLv3 URL: http://launchpad.net/cloud-init @@ -19,6 +19,10 @@ Patch2: cloud-init-17.1-nm-controlled.patch # https://bugzilla.redhat.com/show_bug.cgi?id=1558641 Patch3: cloud-init-17.1-no-override-default-network.patch +# Enable dhcp for interfaces on EC2 instances with only local addresses. +# https://bugzilla.redhat.com/show_bug.cgi?id=1569321 +Patch4: cloud-init-17.1-fix-local-ipv4-only.patch + BuildArch: noarch BuildRequires: pkgconfig(systemd) @@ -151,6 +155,10 @@ nosetests-%{python3_version} tests/unittests/ %changelog +* Sat Apr 21 2018 Lars Kellogg-Stedman - 17.1-5 +- Enable dhcp on EC2 interfaces with only local ipv4 addresses [RH:1569321] + (cherry pick upstream commit eb292c1) + * Mon Mar 26 2018 Patrick Uiterwijk - 17.1-4 - Make sure the patch does not add infinitely many entries