Merge ~chad.smith/cloud-init:aws-ipv6-dhcp-support into cloud-init:master

Proposed by Chad Smith
Status: Merged
Approved by: Scott Moser
Approved revision: ca7da13aa0501c481742b94bf3943a03c7fee4bb
Merged at revision: 3c45330af2a301f2bf219da556844d01cef6778e
Proposed branch: ~chad.smith/cloud-init:aws-ipv6-dhcp-support
Merge into: cloud-init:master
Diff against target: 182 lines (+135/-0)
2 files modified
cloudinit/sources/DataSourceEc2.py (+38/-0)
tests/unittests/test_datasource/test_ec2.py (+97/-0)
Reviewer Review Type Date Requested Status
Scott Moser Approve
Server Team CI bot continuous-integration Approve
Review via email: mp+329499@code.launchpad.net

Description of the change

ec2: Add IPv6 dhcp support to Ec2DataSource.

DataSourceEc2 now parses the metadata for each nic to determine if
configured for ipv6 and/or ipv4 addresses. In AWS for metadata version
2016-09-02, nics configured for ipv4 or ipv6 addresses will have non-zero
values stored in metadata at network/interfaces/macs/<MAC>/public-ipv4 or ipv6s respectively. Those metadata files are only non-zero when an ipv4 or ipv6 ip is associated to the specific nic. A new DataSourceEc2.network_config property is added which parses the metadata and renders a network version 1 dictionary representing both dhcp4 and dhcp6 configuration for associated nics.

The network configuration returned from the datasource will also 'pin' the
nic name to the name presented on the instance for each nic.

LP: #1639030

To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

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

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

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:83bffab38292026588d55e40586e3b02d3dea7df
https://jenkins.ubuntu.com/server/job/cloud-init-ci/195/
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/195/rebuild

review: Approve (continuous-integration)
ca7da13... by Chad Smith

use public-ipv4s and ipv6s instead of vpc-ipv(4|6)-cidr-blocks

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:ca7da13aa0501c481742b94bf3943a03c7fee4bb
https://jenkins.ubuntu.com/server/job/cloud-init-ci/196/
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/196/rebuild

review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

I just tested this
$ dpkg-query --show cloud-init
cloud-init 0.7.9-250-gca7da13aa-1~bddeb

$ grep 'Applying network config' /var/log/cloud-init.log
2017-08-29 16:19:04,367 - stages.py[INFO]: Applying network configuration from ds bringup=False: {'version': 1, 'config': [{'type': 'physical', 'name': 'eth0', 'subnets': [{'type': 'dhcp4'}, {'type': 'dhcp6'}], 'mac_address': '06:8d:72:65:39:7c'}]}

$ grep 'Crawl' /var/log/cloud-init.log
2017-08-29 16:19:04,239 - util.py[DEBUG]: Crawl of metadata service took 0.232 seconds

looks great.

review: Approve

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 8e5f8ee..07c12bb 100644
3--- a/cloudinit/sources/DataSourceEc2.py
4+++ b/cloudinit/sources/DataSourceEc2.py
5@@ -57,6 +57,8 @@ class DataSourceEc2(sources.DataSource):
6
7 _cloud_platform = None
8
9+ _network_config = None # Used for caching calculated network config v1
10+
11 # Whether we want to get network configuration from the metadata service.
12 get_network_metadata = False
13
14@@ -279,6 +281,15 @@ class DataSourceEc2(sources.DataSource):
15 util.get_cfg_by_path(cfg, STRICT_ID_PATH, STRICT_ID_DEFAULT),
16 cfg)
17
18+ @property
19+ def network_config(self):
20+ """Return a network config dict for rendering ENI or netplan files."""
21+ if self._network_config is None:
22+ if self.metadata is not None:
23+ self._network_config = convert_ec2_metadata_network_config(
24+ self.metadata)
25+ return self._network_config
26+
27 def _crawl_metadata(self):
28 """Crawl metadata service when available.
29
30@@ -423,6 +434,33 @@ def _collect_platform_data():
31 return data
32
33
34+def convert_ec2_metadata_network_config(metadata=None, macs_to_nics=None):
35+ """Convert ec2 metadata to network config version 1 data dict.
36+
37+ @param: metadata: Dictionary of metadata crawled from EC2 metadata url.
38+ @param: macs_to_name: Optional dict mac addresses and the nic name. If
39+ not provided, get_interfaces_by_mac is called to get it from the OS.
40+
41+ @return A dict of network config version 1 based on the metadata and macs.
42+ """
43+ netcfg = {'version': 1, 'config': []}
44+ if not macs_to_nics:
45+ macs_to_nics = net.get_interfaces_by_mac()
46+ macs_metadata = metadata['network']['interfaces']['macs']
47+ for mac, nic_name in macs_to_nics.items():
48+ nic_metadata = macs_metadata.get(mac)
49+ if not nic_metadata:
50+ continue # Not a physical nic represented in metadata
51+ nic_cfg = {'type': 'physical', 'name': nic_name, 'subnets': []}
52+ nic_cfg['mac_address'] = mac
53+ if nic_metadata.get('public-ipv4s'):
54+ nic_cfg['subnets'].append({'type': 'dhcp4'})
55+ if nic_metadata.get('ipv6s'):
56+ nic_cfg['subnets'].append({'type': 'dhcp6'})
57+ netcfg['config'].append(nic_cfg)
58+ return netcfg
59+
60+
61 # Used to match classes to dependencies
62 datasources = [
63 (DataSourceEc2Local, (sources.DEP_FILESYSTEM,)), # Run at init-local
64diff --git a/tests/unittests/test_datasource/test_ec2.py b/tests/unittests/test_datasource/test_ec2.py
65index 33d0261..e1ce644 100644
66--- a/tests/unittests/test_datasource/test_ec2.py
67+++ b/tests/unittests/test_datasource/test_ec2.py
68@@ -1,5 +1,6 @@
69 # This file is part of cloud-init. See LICENSE file for license information.
70
71+import copy
72 import httpretty
73 import mock
74
75@@ -195,6 +196,34 @@ class TestEc2(test_helpers.HttprettyTestCase):
76 return ds
77
78 @httpretty.activate
79+ def test_network_config_property_returns_version_1_network_data(self):
80+ """network_config property returns network version 1 for metadata."""
81+ ds = self._setup_ds(
82+ platform_data=self.valid_platform_data,
83+ sys_cfg={'datasource': {'Ec2': {'strict_id': True}}},
84+ md=DEFAULT_METADATA)
85+ ds.get_data()
86+ mac1 = '06:17:04:d7:26:09' # Defined in DEFAULT_METADATA
87+ expected = {'version': 1, 'config': [
88+ {'mac_address': '06:17:04:d7:26:09', 'name': 'eth9',
89+ 'subnets': [{'type': 'dhcp4'}, {'type': 'dhcp6'}],
90+ 'type': 'physical'}]}
91+ patch_path = (
92+ 'cloudinit.sources.DataSourceEc2.net.get_interfaces_by_mac')
93+ with mock.patch(patch_path) as m_get_interfaces_by_mac:
94+ m_get_interfaces_by_mac.return_value = {mac1: 'eth9'}
95+ self.assertEqual(expected, ds.network_config)
96+
97+ def test_network_config_property_is_cached_in_datasource(self):
98+ """network_config property is cached in DataSourceEc2."""
99+ ds = self._setup_ds(
100+ platform_data=self.valid_platform_data,
101+ sys_cfg={'datasource': {'Ec2': {'strict_id': True}}},
102+ md=DEFAULT_METADATA)
103+ ds._network_config = {'cached': 'data'}
104+ self.assertEqual({'cached': 'data'}, ds.network_config)
105+
106+ @httpretty.activate
107 @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery')
108 def test_valid_platform_with_strict_true(self, m_dhcp):
109 """Valid platform data should return true with strict_id true."""
110@@ -287,4 +316,72 @@ class TestEc2(test_helpers.HttprettyTestCase):
111 self.assertIn('Crawl of metadata service took', self.logs.getvalue())
112
113
114+class TestConvertEc2MetadataNetworkConfig(test_helpers.CiTestCase):
115+
116+ def setUp(self):
117+ super(TestConvertEc2MetadataNetworkConfig, self).setUp()
118+ self.mac1 = '06:17:04:d7:26:09'
119+ self.network_metadata = {
120+ 'network': {'interfaces': {'macs': {
121+ self.mac1: {'public-ipv4s': '172.31.2.16'}}}}}
122+
123+ def test_convert_ec2_metadata_network_config_skips_absent_macs(self):
124+ """Any mac absent from metadata is skipped by network config."""
125+ macs_to_nics = {self.mac1: 'eth9', 'DE:AD:BE:EF:FF:FF': 'vitualnic2'}
126+
127+ # DE:AD:BE:EF:FF:FF represented by OS but not in metadata
128+ expected = {'version': 1, 'config': [
129+ {'mac_address': self.mac1, 'type': 'physical',
130+ 'name': 'eth9', 'subnets': [{'type': 'dhcp4'}]}]}
131+ self.assertEqual(
132+ expected,
133+ ec2.convert_ec2_metadata_network_config(
134+ self.network_metadata, macs_to_nics))
135+
136+ def test_convert_ec2_metadata_network_config_handles_only_dhcp6(self):
137+ """Config dhcp6 when ipv6s is in metadata for a mac."""
138+ macs_to_nics = {self.mac1: 'eth9'}
139+ network_metadata_ipv6 = copy.deepcopy(self.network_metadata)
140+ nic1_metadata = (
141+ network_metadata_ipv6['network']['interfaces']['macs'][self.mac1])
142+ nic1_metadata['ipv6s'] = '2620:0:1009:fd00:e442:c88d:c04d:dc85/64'
143+ nic1_metadata.pop('public-ipv4s')
144+ expected = {'version': 1, 'config': [
145+ {'mac_address': self.mac1, 'type': 'physical',
146+ 'name': 'eth9', 'subnets': [{'type': 'dhcp6'}]}]}
147+ self.assertEqual(
148+ expected,
149+ ec2.convert_ec2_metadata_network_config(
150+ network_metadata_ipv6, macs_to_nics))
151+
152+ def test_convert_ec2_metadata_network_config_handles_dhcp4_and_dhcp6(self):
153+ """Config both dhcp4 and dhcp6 when both vpc-ipv6 and ipv4 exists."""
154+ macs_to_nics = {self.mac1: 'eth9'}
155+ network_metadata_both = copy.deepcopy(self.network_metadata)
156+ nic1_metadata = (
157+ network_metadata_both['network']['interfaces']['macs'][self.mac1])
158+ nic1_metadata['ipv6s'] = '2620:0:1009:fd00:e442:c88d:c04d:dc85/64'
159+ expected = {'version': 1, 'config': [
160+ {'mac_address': self.mac1, 'type': 'physical',
161+ 'name': 'eth9',
162+ 'subnets': [{'type': 'dhcp4'}, {'type': 'dhcp6'}]}]}
163+ self.assertEqual(
164+ expected,
165+ ec2.convert_ec2_metadata_network_config(
166+ network_metadata_both, macs_to_nics))
167+
168+ def test_convert_ec2_metadata_gets_macs_from_get_interfaces_by_mac(self):
169+ """Convert Ec2 Metadata calls get_interfaces_by_mac by default."""
170+ expected = {'version': 1, 'config': [
171+ {'mac_address': self.mac1, 'type': 'physical',
172+ 'name': 'eth9',
173+ 'subnets': [{'type': 'dhcp4'}]}]}
174+ patch_path = (
175+ 'cloudinit.sources.DataSourceEc2.net.get_interfaces_by_mac')
176+ with mock.patch(patch_path) as m_get_interfaces_by_mac:
177+ m_get_interfaces_by_mac.return_value = {self.mac1: 'eth9'}
178+ self.assertEqual(
179+ expected,
180+ ec2.convert_ec2_metadata_network_config(self.network_metadata))
181+
182 # vi: ts=4 expandtab

Subscribers

People subscribed via source and target branches