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