Merge ~chad.smith/cloud-init:ec2-dhcp-local-ipv4 into cloud-init:master

Proposed by Chad Smith on 2017-10-27
Status: Merged
Merged at revision: eb292c18c3d83b9f7e5d1fd81b0e8aefaab0cc2d
Proposed branch: ~chad.smith/cloud-init:ec2-dhcp-local-ipv4
Merge into: cloud-init:master
Diff against target: 276 lines (+149/-11)
2 files modified
cloudinit/sources/DataSourceEc2.py (+19/-5)
tests/unittests/test_datasource/test_ec2.py (+130/-6)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve on 2017-10-31
Scott Moser 2017-10-27 Approve on 2017-10-31
Review via email: mp+332954@code.launchpad.net

Commit Message

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

Description of the Change

EC2: Use the fallback nic for network configuration. Limit config to 1 nic

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

To post a comment you must log in.

PASSED: Continuous integration, rev:dacb86f7cc6976c1154c5a2171500e226cc04a8e
https://jenkins.ubuntu.com/server/job/cloud-init-ci/442/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/442/rebuild

review: Approve (continuous-integration)
Scott Moser (smoser) wrote :

Thanks Chad.

review: Approve
Ryan Harper (raharper) wrote :

I'm a bit confused here w.r.t testing for the presense of public or private ips and resulting in configuring DHCPv4.

I'm looking in the Amazon EC2 docs to see (and maybe it's worth looking into an Amazon Linux instance here) but; if we're being provided with a specific IP (or list of ips) then instead of configuring DHCPv4 shouldn't we just configuring statically?

Also, it seems that we should at a minimum provide a fallback on "dhcp on 'eth0'" equivalent that we had prior to the IPv6 check.

b6e0d54... by Chad Smith on 2017-10-30

WIP: limit EC2.network_config to just the fallback nic

FAILED: Continuous integration, rev:b6e0d54c171ad3a801ef0aab2e8f30bd5a8f0daf
https://jenkins.ubuntu.com/server/job/cloud-init-ci/446/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/446/rebuild

review: Needs Fixing (continuous-integration)
Scott Moser (smoser) wrote :

one minor comment.

f5b4601... by Chad Smith on 2017-10-30

add multiple nic definitions to ec2 datasource tests. More unit tests for fallback_nic parameter to conver_ec2_metadata_network_config

b2064bf... by Chad Smith on 2017-10-30

flakes

Ryan Harper (raharper) wrote :

I would like to see a bit of re-wording on the commit.

The primary change is to use the fallback-nic and always configure DHCP4 on it;
that was the change in behavior that really needs to be resolved.

Secondarily, under VPC which only use private-ips, we need to check if there are
interfaces there.

PASSED: Continuous integration, rev:b2064bf6eeb2a96d50051ed4e0203e3c7dcb5a44
https://jenkins.ubuntu.com/server/job/cloud-init-ci/447/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/447/rebuild

review: Approve (continuous-integration)
Chad Smith (chad.smith) wrote :

Just tested on cloud-init with the following:

git clone <email address hidden>:cloud-init/qa-scripts.git

git clone https://github.com/smoser/talk-simplestreams.git
export PATH=$PATH:qa-scripts/scripts:talk-simplestreams/bin

./scripts/launch-ec2 --keep-alive --deb-file ~/src/server/cloud-init/cloud-init/cloud-init_17.1-30-gb2064bf6-1~bddeb_all.deb

ssh -i ~/Downloads/ubuntu.pem ubuntu@<ec2_instance>

ubuntu@ip-172-31-28-42:~$ dpkg-query --show cloud-init
cloud-init 17.1-30-gb2064bf6-1~bddeb
ubuntu@ip-172-31-28-42:~$ cat /run/cloud-init/result.json
{
 "v1": {
  "datasource": "DataSourceEc2Local",
  "errors": []
 }
}

Chad Smith (chad.smith) wrote :

ubuntu@ip-172-31-28-42:~$ cat /etc/network/interfaces.d/50-cloud-init.cfg
# This file is generated from information provided by
# the datasource. Changes to it will not persist across an instance.
# To disable cloud-init's network configuration capabilities, write a file
# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
# network: {config: disabled}
auto lo
iface lo inet loopback

auto eth0
iface eth0 inet dhcp

ac118ac... by Chad Smith on 2017-10-31

address review comments. Call maybe_perform_dhcp_discovery with the fallback_nic, drop accidentaly whitespace

Scott Moser (smoser) wrote :

I adjusted the commit message a bit, but pending a c-i bot approving, then go ahead and merge.

thanks.

review: Approve

PASSED: Continuous integration, rev:ac118ac287b0113b16b9243403348f15437c9b1c
https://jenkins.ubuntu.com/server/job/cloud-init-ci/449/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/449/rebuild

review: Approve (continuous-integration)
Chad Smith (chad.smith) wrote :

Validated latest on EC2
ubuntu@ip-172-31-21-76:~$ dpkg-query --show cloud-init
cloud-init 17.1-27-geb292c18-1~bddeb
ubuntu@ip-172-31-21-76:~$ cat /run/cloud-init/result.json
{
 "v1": {
  "datasource": "DataSourceEc2Local",
  "errors": []
 }
}
ubuntu@ip-172-31-21-76:~$ grep Trace /var/log/cloud-init.log

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/cloudinit/sources/DataSourceEc2.py b/cloudinit/sources/DataSourceEc2.py
2index 41367a8..0ef2217 100644
3--- a/cloudinit/sources/DataSourceEc2.py
4+++ b/cloudinit/sources/DataSourceEc2.py
5@@ -64,6 +64,9 @@ class DataSourceEc2(sources.DataSource):
6 # Whether we want to get network configuration from the metadata service.
7 get_network_metadata = False
8
9+ # Track the discovered fallback nic for use in configuration generation.
10+ fallback_nic = None
11+
12 def __init__(self, sys_cfg, distro, paths):
13 sources.DataSource.__init__(self, sys_cfg, distro, paths)
14 self.metadata_address = None
15@@ -89,16 +92,18 @@ class DataSourceEc2(sources.DataSource):
16 elif self.cloud_platform == Platforms.NO_EC2_METADATA:
17 return False
18
19+ self.fallback_nic = net.find_fallback_nic()
20 if self.get_network_metadata: # Setup networking in init-local stage.
21 if util.is_FreeBSD():
22 LOG.debug("FreeBSD doesn't support running dhclient with -sf")
23 return False
24- dhcp_leases = dhcp.maybe_perform_dhcp_discovery()
25+ dhcp_leases = dhcp.maybe_perform_dhcp_discovery(self.fallback_nic)
26 if not dhcp_leases:
27 # DataSourceEc2Local failed in init-local stage. DataSourceEc2
28 # will still run in init-network stage.
29 return False
30 dhcp_opts = dhcp_leases[-1]
31+ self.fallback_nic = dhcp_opts.get('interface')
32 net_params = {'interface': dhcp_opts.get('interface'),
33 'ip': dhcp_opts.get('fixed-address'),
34 'prefix_or_mask': dhcp_opts.get('subnet-mask'),
35@@ -297,8 +302,13 @@ class DataSourceEc2(sources.DataSource):
36
37 result = None
38 net_md = self.metadata.get('network')
39+ # Limit network configuration to only the primary/fallback nic
40+ macs_to_nics = {
41+ net.get_interface_mac(self.fallback_nic): self.fallback_nic}
42 if isinstance(net_md, dict):
43- result = convert_ec2_metadata_network_config(net_md)
44+ result = convert_ec2_metadata_network_config(
45+ net_md, macs_to_nics=macs_to_nics,
46+ fallback_nic=self.fallback_nic)
47 else:
48 LOG.warning("unexpected metadata 'network' key not valid: %s",
49 net_md)
50@@ -458,15 +468,18 @@ def _collect_platform_data():
51 return data
52
53
54-def convert_ec2_metadata_network_config(network_md, macs_to_nics=None):
55+def convert_ec2_metadata_network_config(network_md, macs_to_nics=None,
56+ fallback_nic=None):
57 """Convert ec2 metadata to network config version 1 data dict.
58
59 @param: network_md: 'network' portion of EC2 metadata.
60 generally formed as {"interfaces": {"macs": {}} where
61 'macs' is a dictionary with mac address as key and contents like:
62 {"device-number": "0", "interface-id": "...", "local-ipv4s": ...}
63- @param: macs_to_name: Optional dict mac addresses and the nic name. If
64+ @param: macs_to_nics: Optional dict of mac addresses and nic names. If
65 not provided, get_interfaces_by_mac is called to get it from the OS.
66+ @param: fallback_nic: Optionally provide the primary nic interface name.
67+ This nic will be guaranteed to minimally have a dhcp4 configuration.
68
69 @return A dict of network config version 1 based on the metadata and macs.
70 """
71@@ -480,7 +493,8 @@ def convert_ec2_metadata_network_config(network_md, macs_to_nics=None):
72 continue # Not a physical nic represented in metadata
73 nic_cfg = {'type': 'physical', 'name': nic_name, 'subnets': []}
74 nic_cfg['mac_address'] = mac
75- if nic_metadata.get('public-ipv4s'):
76+ if (nic_name == fallback_nic or nic_metadata.get('public-ipv4s') or
77+ nic_metadata.get('local-ipv4s')):
78 nic_cfg['subnets'].append({'type': 'dhcp4'})
79 if nic_metadata.get('ipv6s'):
80 nic_cfg['subnets'].append({'type': 'dhcp6'})
81diff --git a/tests/unittests/test_datasource/test_ec2.py b/tests/unittests/test_datasource/test_ec2.py
82index a7301db..6af699a 100644
83--- a/tests/unittests/test_datasource/test_ec2.py
84+++ b/tests/unittests/test_datasource/test_ec2.py
85@@ -51,6 +51,29 @@ DEFAULT_METADATA = {
86 "vpc-ipv4-cidr-block": "172.31.0.0/16",
87 "vpc-ipv4-cidr-blocks": "172.31.0.0/16",
88 "vpc-ipv6-cidr-blocks": "2600:1f16:aeb:b200::/56"
89+ },
90+ "06:17:04:d7:26:0A": {
91+ "device-number": "1", # Only IPv4 local config
92+ "interface-id": "eni-e44ef49f",
93+ "ipv4-associations": {"": "172.3.3.16"},
94+ "ipv6s": "", # No IPv6 config
95+ "local-hostname": ("ip-172-3-3-16.us-east-2."
96+ "compute.internal"),
97+ "local-ipv4s": "172.3.3.16",
98+ "mac": "06:17:04:d7:26:0A",
99+ "owner-id": "950047163771",
100+ "public-hostname": ("ec2-172-3-3-16.us-east-2."
101+ "compute.amazonaws.com"),
102+ "public-ipv4s": "", # No public ipv4 config
103+ "security-group-ids": "sg-5a61d333",
104+ "security-groups": "wide-open",
105+ "subnet-id": "subnet-20b8565b",
106+ "subnet-ipv4-cidr-block": "172.31.16.0/20",
107+ "subnet-ipv6-cidr-blocks": "",
108+ "vpc-id": "vpc-87e72bee",
109+ "vpc-ipv4-cidr-block": "172.31.0.0/16",
110+ "vpc-ipv4-cidr-blocks": "172.31.0.0/16",
111+ "vpc-ipv6-cidr-blocks": ""
112 }
113 }
114 }
115@@ -209,12 +232,20 @@ class TestEc2(test_helpers.HttprettyTestCase):
116
117 @httpretty.activate
118 def test_network_config_property_returns_version_1_network_data(self):
119- """network_config property returns network version 1 for metadata."""
120+ """network_config property returns network version 1 for metadata.
121+
122+ Only one device is configured even when multiple exist in metadata.
123+ """
124 ds = self._setup_ds(
125 platform_data=self.valid_platform_data,
126 sys_cfg={'datasource': {'Ec2': {'strict_id': True}}},
127 md=DEFAULT_METADATA)
128- ds.get_data()
129+ find_fallback_path = (
130+ 'cloudinit.sources.DataSourceEc2.net.find_fallback_nic')
131+ with mock.patch(find_fallback_path) as m_find_fallback:
132+ m_find_fallback.return_value = 'eth9'
133+ ds.get_data()
134+
135 mac1 = '06:17:04:d7:26:09' # Defined in DEFAULT_METADATA
136 expected = {'version': 1, 'config': [
137 {'mac_address': '06:17:04:d7:26:09', 'name': 'eth9',
138@@ -222,9 +253,48 @@ class TestEc2(test_helpers.HttprettyTestCase):
139 'type': 'physical'}]}
140 patch_path = (
141 'cloudinit.sources.DataSourceEc2.net.get_interfaces_by_mac')
142+ get_interface_mac_path = (
143+ 'cloudinit.sources.DataSourceEc2.net.get_interface_mac')
144+ with mock.patch(patch_path) as m_get_interfaces_by_mac:
145+ with mock.patch(find_fallback_path) as m_find_fallback:
146+ with mock.patch(get_interface_mac_path) as m_get_mac:
147+ m_get_interfaces_by_mac.return_value = {mac1: 'eth9'}
148+ m_find_fallback.return_value = 'eth9'
149+ m_get_mac.return_value = mac1
150+ self.assertEqual(expected, ds.network_config)
151+
152+ @httpretty.activate
153+ def test_network_config_property_set_dhcp4_on_private_ipv4(self):
154+ """network_config property configures dhcp4 on private ipv4 nics.
155+
156+ Only one device is configured even when multiple exist in metadata.
157+ """
158+ ds = self._setup_ds(
159+ platform_data=self.valid_platform_data,
160+ sys_cfg={'datasource': {'Ec2': {'strict_id': True}}},
161+ md=DEFAULT_METADATA)
162+ find_fallback_path = (
163+ 'cloudinit.sources.DataSourceEc2.net.find_fallback_nic')
164+ with mock.patch(find_fallback_path) as m_find_fallback:
165+ m_find_fallback.return_value = 'eth9'
166+ ds.get_data()
167+
168+ mac1 = '06:17:04:d7:26:0A' # IPv4 only in DEFAULT_METADATA
169+ expected = {'version': 1, 'config': [
170+ {'mac_address': '06:17:04:d7:26:0A', 'name': 'eth9',
171+ 'subnets': [{'type': 'dhcp4'}],
172+ 'type': 'physical'}]}
173+ patch_path = (
174+ 'cloudinit.sources.DataSourceEc2.net.get_interfaces_by_mac')
175+ get_interface_mac_path = (
176+ 'cloudinit.sources.DataSourceEc2.net.get_interface_mac')
177 with mock.patch(patch_path) as m_get_interfaces_by_mac:
178- m_get_interfaces_by_mac.return_value = {mac1: 'eth9'}
179- self.assertEqual(expected, ds.network_config)
180+ with mock.patch(find_fallback_path) as m_find_fallback:
181+ with mock.patch(get_interface_mac_path) as m_get_mac:
182+ m_get_interfaces_by_mac.return_value = {mac1: 'eth9'}
183+ m_find_fallback.return_value = 'eth9'
184+ m_get_mac.return_value = mac1
185+ self.assertEqual(expected, ds.network_config)
186
187 def test_network_config_property_is_cached_in_datasource(self):
188 """network_config property is cached in DataSourceEc2."""
189@@ -321,9 +391,11 @@ class TestEc2(test_helpers.HttprettyTestCase):
190
191 @httpretty.activate
192 @mock.patch('cloudinit.net.EphemeralIPv4Network')
193+ @mock.patch('cloudinit.net.find_fallback_nic')
194 @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery')
195 @mock.patch('cloudinit.sources.DataSourceEc2.util.is_FreeBSD')
196- def test_ec2_local_performs_dhcp_on_non_bsd(self, m_is_bsd, m_dhcp, m_net):
197+ def test_ec2_local_performs_dhcp_on_non_bsd(self, m_is_bsd, m_dhcp,
198+ m_fallback_nic, m_net):
199 """Ec2Local returns True for valid platform data on non-BSD with dhcp.
200
201 DataSourceEc2Local will setup initial IPv4 network via dhcp discovery.
202@@ -331,6 +403,7 @@ class TestEc2(test_helpers.HttprettyTestCase):
203 When the platform data is valid, return True.
204 """
205
206+ m_fallback_nic.return_value = 'eth9'
207 m_is_bsd.return_value = False
208 m_dhcp.return_value = [{
209 'interface': 'eth9', 'fixed-address': '192.168.2.9',
210@@ -344,7 +417,7 @@ class TestEc2(test_helpers.HttprettyTestCase):
211
212 ret = ds.get_data()
213 self.assertTrue(ret)
214- m_dhcp.assert_called_once_with()
215+ m_dhcp.assert_called_once_with('eth9')
216 m_net.assert_called_once_with(
217 broadcast='192.168.2.255', interface='eth9', ip='192.168.2.9',
218 prefix_or_mask='255.255.255.0', router='192.168.2.1')
219@@ -389,6 +462,57 @@ class TestConvertEc2MetadataNetworkConfig(test_helpers.CiTestCase):
220 ec2.convert_ec2_metadata_network_config(
221 network_metadata_ipv6, macs_to_nics))
222
223+ def test_convert_ec2_metadata_network_config_handles_local_dhcp4(self):
224+ """Config dhcp4 when there are no public addresses in public-ipv4s."""
225+ macs_to_nics = {self.mac1: 'eth9'}
226+ network_metadata_ipv6 = copy.deepcopy(self.network_metadata)
227+ nic1_metadata = (
228+ network_metadata_ipv6['interfaces']['macs'][self.mac1])
229+ nic1_metadata['local-ipv4s'] = '172.3.3.15'
230+ nic1_metadata.pop('public-ipv4s')
231+ expected = {'version': 1, 'config': [
232+ {'mac_address': self.mac1, 'type': 'physical',
233+ 'name': 'eth9', 'subnets': [{'type': 'dhcp4'}]}]}
234+ self.assertEqual(
235+ expected,
236+ ec2.convert_ec2_metadata_network_config(
237+ network_metadata_ipv6, macs_to_nics))
238+
239+ def test_convert_ec2_metadata_network_config_handles_absent_dhcp4(self):
240+ """Config dhcp4 on fallback_nic when there are no ipv4 addresses."""
241+ macs_to_nics = {self.mac1: 'eth9'}
242+ network_metadata_ipv6 = copy.deepcopy(self.network_metadata)
243+ nic1_metadata = (
244+ network_metadata_ipv6['interfaces']['macs'][self.mac1])
245+ nic1_metadata['public-ipv4s'] = ''
246+
247+ # When no ipv4 or ipv6 content but fallback_nic set, set dhcp4 config.
248+ expected = {'version': 1, 'config': [
249+ {'mac_address': self.mac1, 'type': 'physical',
250+ 'name': 'eth9', 'subnets': [{'type': 'dhcp4'}]}]}
251+ self.assertEqual(
252+ expected,
253+ ec2.convert_ec2_metadata_network_config(
254+ network_metadata_ipv6, macs_to_nics, fallback_nic='eth9'))
255+
256+ def test_convert_ec2_metadata_network_config_handles_local_v4_and_v6(self):
257+ """When dhcp6 is public and dhcp4 is set to local enable both."""
258+ macs_to_nics = {self.mac1: 'eth9'}
259+ network_metadata_both = copy.deepcopy(self.network_metadata)
260+ nic1_metadata = (
261+ network_metadata_both['interfaces']['macs'][self.mac1])
262+ nic1_metadata['ipv6s'] = '2620:0:1009:fd00:e442:c88d:c04d:dc85/64'
263+ nic1_metadata.pop('public-ipv4s')
264+ nic1_metadata['local-ipv4s'] = '10.0.0.42' # Local ipv4 only on vpc
265+ expected = {'version': 1, 'config': [
266+ {'mac_address': self.mac1, 'type': 'physical',
267+ 'name': 'eth9',
268+ 'subnets': [{'type': 'dhcp4'}, {'type': 'dhcp6'}]}]}
269+ self.assertEqual(
270+ expected,
271+ ec2.convert_ec2_metadata_network_config(
272+ network_metadata_both, macs_to_nics))
273+
274 def test_convert_ec2_metadata_network_config_handles_dhcp4_and_dhcp6(self):
275 """Config both dhcp4 and dhcp6 when both vpc-ipv6 and ipv4 exists."""
276 macs_to_nics = {self.mac1: 'eth9'}

Subscribers

People subscribed via source and target branches