Merge ~raharper/cloud-init:bug-lp-1709180-v2-params into cloud-init:master

Proposed by Ryan Harper on 2017-08-09
Status: Merged
Merged at revision: dc2bd79949492bccdc1d7df0132f98c354d51943
Proposed branch: ~raharper/cloud-init:bug-lp-1709180-v2-params
Merge into: cloud-init:master
Diff against target: 401 lines (+195/-44)
4 files modified
cloudinit/net/netplan.py (+9/-26)
cloudinit/net/network_state.py (+69/-16)
tests/unittests/test_distros/test_netconfig.py (+2/-2)
tests/unittests/test_net.py (+115/-0)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve on 2017-08-15
Chad Smith 2017-08-09 Approve on 2017-08-10
Review via email: mp+328800@code.launchpad.net

Description of the Change

network: add v2 passthrough and fix parsing v2 config with parameters

If the network-config sent to cloud-init is in version: 2 format then
when rendering netplan, we can pass the content through and avoid
consuming network_state elements. This removes the need for trying to
map many v2 features onto network state where other renderers won't be
able to use anyhow (for example match parameters for multi-interface
configuration and wifi configuration support).

Additionally ensure we retain bond/bridge v2 configuration in network
state so when rendering to eni or sysconfig we don't lose the configuration

- Drop the NotImplemented wifi exception, log a warning that it works for
  netplan only
- Adjust unittests to new code path and output
- Fix issue with v2 macaddress values getting dropped
- Add unittests for consuming/validating v2 configurations

LP: #1709180

To post a comment you must log in.

PASSED: Continuous integration, rev:09863394a2119a9e91d34abdfab7e2f14205d053
https://jenkins.ubuntu.com/server/job/cloud-init-ci/131/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: CentOS 6 & 7: Build & Test
    IN_PROGRESS: Declarative: Post Actions

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

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

Approved with minor nits questions inline. +1!

Validated and inspected netplan output with the following:

PYTHONPATH=. ./tools/net-convert.py --network-data=net-config.yaml --kind=yaml --output-kind=netplan --directory=out.d --mac=eth0,52:54:00:12:34:00 --mac=ens4,52:54:00:12:34:02

Prior to this branch netplan output was empty/broken:
 cat out2.d/etc/netplan/50-cloud-init.yaml

network:
    version: 2
    ethernets:
        ens4: {}
        eth0: {}

Chad Smith (chad.smith) :
review: Approve
Ryan Harper (raharper) wrote :

Thanks for the review, will fix the bikeshed name. Not sure how to do a py2/py3 iterator cleanly; list isn't huge though. I'm going to push another update to this branch to address more v2 -> v1 -> v2 issues found and instead allow the netplan renderer to write out the original v2 config.

Ryan Harper (raharper) :

PASSED: Continuous integration, rev:fb634903af397678a636f390e1bc4df45e9148be
https://jenkins.ubuntu.com/server/job/cloud-init-ci/135/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: CentOS 6 & 7: Build & Test
    IN_PROGRESS: Declarative: Post Actions

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

review: Approve (continuous-integration)

PASSED: Continuous integration, rev:c3c19526beec952d865e483de0837fa0a27c1d74
https://jenkins.ubuntu.com/server/job/cloud-init-ci/136/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: CentOS 6 & 7: Build & Test
    IN_PROGRESS: Declarative: Post Actions

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

review: Approve (continuous-integration)

PASSED: Continuous integration, rev:dc2bd79949492bccdc1d7df0132f98c354d51943
https://jenkins.ubuntu.com/server/job/cloud-init-ci/145/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: CentOS 6 & 7: Build & Test
    IN_PROGRESS: Declarative: Post Actions

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

review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/cloudinit/net/netplan.py b/cloudinit/net/netplan.py
2index 9f35b72..3b06fbf 100644
3--- a/cloudinit/net/netplan.py
4+++ b/cloudinit/net/netplan.py
5@@ -4,7 +4,7 @@ import copy
6 import os
7
8 from . import renderer
9-from .network_state import subnet_is_ipv6
10+from .network_state import subnet_is_ipv6, NET_CONFIG_TO_V2
11
12 from cloudinit import log as logging
13 from cloudinit import util
14@@ -27,31 +27,6 @@ network:
15 """
16
17 LOG = logging.getLogger(__name__)
18-NET_CONFIG_TO_V2 = {
19- 'bond': {'bond-ad-select': 'ad-select',
20- 'bond-arp-interval': 'arp-interval',
21- 'bond-arp-ip-target': 'arp-ip-target',
22- 'bond-arp-validate': 'arp-validate',
23- 'bond-downdelay': 'down-delay',
24- 'bond-fail-over-mac': 'fail-over-mac-policy',
25- 'bond-lacp-rate': 'lacp-rate',
26- 'bond-miimon': 'mii-monitor-interval',
27- 'bond-min-links': 'min-links',
28- 'bond-mode': 'mode',
29- 'bond-num-grat-arp': 'gratuitious-arp',
30- 'bond-primary-reselect': 'primary-reselect-policy',
31- 'bond-updelay': 'up-delay',
32- 'bond-xmit-hash-policy': 'transmit-hash-policy'},
33- 'bridge': {'bridge_ageing': 'ageing-time',
34- 'bridge_bridgeprio': 'priority',
35- 'bridge_fd': 'forward-delay',
36- 'bridge_gcint': None,
37- 'bridge_hello': 'hello-time',
38- 'bridge_maxage': 'max-age',
39- 'bridge_maxwait': None,
40- 'bridge_pathcost': 'path-cost',
41- 'bridge_portprio': None,
42- 'bridge_waitport': None}}
43
44
45 def _get_params_dict_by_match(config, match):
46@@ -247,6 +222,14 @@ class Renderer(renderer.Renderer):
47 util.subp(cmd, capture=True)
48
49 def _render_content(self, network_state):
50+
51+ # if content already in netplan format, pass it back
52+ if network_state.version == 2:
53+ LOG.debug('V2 to V2 passthrough')
54+ return util.yaml_dumps({'network': network_state.config},
55+ explicit_start=False,
56+ explicit_end=False)
57+
58 ethernets = {}
59 wifis = {}
60 bridges = {}
61diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
62index 87a7222..6faf01b 100644
63--- a/cloudinit/net/network_state.py
64+++ b/cloudinit/net/network_state.py
65@@ -23,6 +23,33 @@ NETWORK_V2_KEY_FILTER = [
66 'match', 'mtu', 'nameservers', 'renderer', 'set-name', 'wakeonlan'
67 ]
68
69+NET_CONFIG_TO_V2 = {
70+ 'bond': {'bond-ad-select': 'ad-select',
71+ 'bond-arp-interval': 'arp-interval',
72+ 'bond-arp-ip-target': 'arp-ip-target',
73+ 'bond-arp-validate': 'arp-validate',
74+ 'bond-downdelay': 'down-delay',
75+ 'bond-fail-over-mac': 'fail-over-mac-policy',
76+ 'bond-lacp-rate': 'lacp-rate',
77+ 'bond-miimon': 'mii-monitor-interval',
78+ 'bond-min-links': 'min-links',
79+ 'bond-mode': 'mode',
80+ 'bond-num-grat-arp': 'gratuitious-arp',
81+ 'bond-primary': 'primary',
82+ 'bond-primary-reselect': 'primary-reselect-policy',
83+ 'bond-updelay': 'up-delay',
84+ 'bond-xmit-hash-policy': 'transmit-hash-policy'},
85+ 'bridge': {'bridge_ageing': 'ageing-time',
86+ 'bridge_bridgeprio': 'priority',
87+ 'bridge_fd': 'forward-delay',
88+ 'bridge_gcint': None,
89+ 'bridge_hello': 'hello-time',
90+ 'bridge_maxage': 'max-age',
91+ 'bridge_maxwait': None,
92+ 'bridge_pathcost': 'path-cost',
93+ 'bridge_portprio': None,
94+ 'bridge_waitport': None}}
95+
96
97 def parse_net_config_data(net_config, skip_broken=True):
98 """Parses the config, returns NetworkState object
99@@ -120,6 +147,10 @@ class NetworkState(object):
100 self.use_ipv6 = network_state.get('use_ipv6', False)
101
102 @property
103+ def config(self):
104+ return self._network_state['config']
105+
106+ @property
107 def version(self):
108 return self._version
109
110@@ -166,12 +197,14 @@ class NetworkStateInterpreter(object):
111 'search': [],
112 },
113 'use_ipv6': False,
114+ 'config': None,
115 }
116
117 def __init__(self, version=NETWORK_STATE_VERSION, config=None):
118 self._version = version
119 self._config = config
120 self._network_state = copy.deepcopy(self.initial_network_state)
121+ self._network_state['config'] = config
122 self._parsed = False
123
124 @property
125@@ -460,12 +493,15 @@ class NetworkStateInterpreter(object):
126 v2_command = {
127 bond0: {
128 'interfaces': ['interface0', 'interface1'],
129- 'miimon': 100,
130- 'mode': '802.3ad',
131- 'xmit_hash_policy': 'layer3+4'},
132+ 'parameters': {
133+ 'mii-monitor-interval': 100,
134+ 'mode': '802.3ad',
135+ 'xmit_hash_policy': 'layer3+4'}},
136 bond1: {
137 'bond-slaves': ['interface2', 'interface7'],
138- 'mode': 1
139+ 'parameters': {
140+ 'mode': 1,
141+ }
142 }
143 }
144
145@@ -554,6 +590,7 @@ class NetworkStateInterpreter(object):
146 if not mac_address:
147 LOG.debug('NetworkState Version2: missing "macaddress" info '
148 'in config entry: %s: %s', eth, str(cfg))
149+ phy_cmd.update({'mac_address': mac_address})
150
151 for key in ['mtu', 'match', 'wakeonlan']:
152 if key in cfg:
153@@ -598,8 +635,8 @@ class NetworkStateInterpreter(object):
154 self.handle_vlan(vlan_cmd)
155
156 def handle_wifis(self, command):
157- raise NotImplementedError("NetworkState V2: "
158- "Skipping wifi configuration")
159+ LOG.warning('Wifi configuration is only available to distros with'
160+ 'netplan rendering support.')
161
162 def _v2_common(self, cfg):
163 LOG.debug('v2_common: handling config:\n%s', cfg)
164@@ -616,6 +653,11 @@ class NetworkStateInterpreter(object):
165
166 def _handle_bond_bridge(self, command, cmd_type=None):
167 """Common handler for bond and bridge types"""
168+
169+ # inverse mapping for v2 keynames to v1 keynames
170+ v2key_to_v1 = dict((v, k) for k, v in
171+ NET_CONFIG_TO_V2.get(cmd_type).items())
172+
173 for item_name, item_cfg in command.items():
174 item_params = dict((key, value) for (key, value) in
175 item_cfg.items() if key not in
176@@ -624,14 +666,20 @@ class NetworkStateInterpreter(object):
177 'type': cmd_type,
178 'name': item_name,
179 cmd_type + '_interfaces': item_cfg.get('interfaces'),
180- 'params': item_params,
181+ 'params': dict((v2key_to_v1[k], v) for k, v in
182+ item_params.get('parameters', {}).items())
183 }
184 subnets = self._v2_to_v1_ipcfg(item_cfg)
185 if len(subnets) > 0:
186 v1_cmd.update({'subnets': subnets})
187
188- LOG.debug('v2(%ss) -> v1(%s):\n%s', cmd_type, cmd_type, v1_cmd)
189- self.handle_bridge(v1_cmd)
190+ LOG.debug('v2(%s) -> v1(%s):\n%s', cmd_type, cmd_type, v1_cmd)
191+ if cmd_type == "bridge":
192+ self.handle_bridge(v1_cmd)
193+ elif cmd_type == "bond":
194+ self.handle_bond(v1_cmd)
195+ else:
196+ raise ValueError('Unknown command type: %s', cmd_type)
197
198 def _v2_to_v1_ipcfg(self, cfg):
199 """Common ipconfig extraction from v2 to v1 subnets array."""
200@@ -651,12 +699,6 @@ class NetworkStateInterpreter(object):
201 'address': address,
202 }
203
204- routes = []
205- for route in cfg.get('routes', []):
206- routes.append(_normalize_route(
207- {'address': route.get('to'), 'gateway': route.get('via')}))
208- subnet['routes'] = routes
209-
210 if ":" in address:
211 if 'gateway6' in cfg and gateway6 is None:
212 gateway6 = cfg.get('gateway6')
213@@ -667,6 +709,17 @@ class NetworkStateInterpreter(object):
214 subnet.update({'gateway': gateway4})
215
216 subnets.append(subnet)
217+
218+ routes = []
219+ for route in cfg.get('routes', []):
220+ routes.append(_normalize_route(
221+ {'destination': route.get('to'), 'gateway': route.get('via')}))
222+
223+ # v2 routes are bound to the interface, in v1 we add them under
224+ # the first subnet since there isn't an equivalent interface level.
225+ if len(subnets) and len(routes):
226+ subnets[0]['routes'] = routes
227+
228 return subnets
229
230
231@@ -721,7 +774,7 @@ def _normalize_net_keys(network, address_keys=()):
232 elif netmask:
233 prefix = mask_to_net_prefix(netmask)
234 elif 'prefix' in net:
235- prefix = int(prefix)
236+ prefix = int(net['prefix'])
237 else:
238 prefix = 64 if ipv6 else 24
239
240diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py
241index 2f505d9..6d89dba 100644
242--- a/tests/unittests/test_distros/test_netconfig.py
243+++ b/tests/unittests/test_distros/test_netconfig.py
244@@ -135,7 +135,7 @@ network:
245 V2_NET_CFG = {
246 'ethernets': {
247 'eth7': {
248- 'addresses': ['192.168.1.5/255.255.255.0'],
249+ 'addresses': ['192.168.1.5/24'],
250 'gateway4': '192.168.1.254'},
251 'eth9': {
252 'dhcp4': True}
253@@ -151,7 +151,6 @@ V2_TO_V2_NET_CFG_OUTPUT = """
254 # /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
255 # network: {config: disabled}
256 network:
257- version: 2
258 ethernets:
259 eth7:
260 addresses:
261@@ -159,6 +158,7 @@ network:
262 gateway4: 192.168.1.254
263 eth9:
264 dhcp4: true
265+ version: 2
266 """
267
268
269diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
270index 4653be1..f251024 100644
271--- a/tests/unittests/test_net.py
272+++ b/tests/unittests/test_net.py
273@@ -1059,6 +1059,100 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
274 - type: static
275 address: 2001:1::1/92
276 """),
277+ 'expected_netplan': textwrap.dedent("""
278+ network:
279+ version: 2
280+ ethernets:
281+ bond0s0:
282+ match:
283+ macaddress: aa:bb:cc:dd:e8:00
284+ set-name: bond0s0
285+ bond0s1:
286+ match:
287+ macaddress: aa:bb:cc:dd:e8:01
288+ set-name: bond0s1
289+ bonds:
290+ bond0:
291+ addresses:
292+ - 192.168.0.2/24
293+ - 192.168.1.2/24
294+ - 2001:1::1/92
295+ gateway4: 192.168.0.1
296+ interfaces:
297+ - bond0s0
298+ - bond0s1
299+ parameters:
300+ mii-monitor-interval: 100
301+ mode: active-backup
302+ transmit-hash-policy: layer3+4
303+ routes:
304+ - to: 10.1.3.0/24
305+ via: 192.168.0.3
306+ """),
307+ 'yaml-v2': textwrap.dedent("""
308+ version: 2
309+ ethernets:
310+ eth0:
311+ match:
312+ driver: "virtio_net"
313+ macaddress: "aa:bb:cc:dd:e8:00"
314+ vf0:
315+ set-name: vf0
316+ match:
317+ driver: "e1000"
318+ macaddress: "aa:bb:cc:dd:e8:01"
319+ bonds:
320+ bond0:
321+ addresses:
322+ - 192.168.0.2/24
323+ - 192.168.1.2/24
324+ - 2001:1::1/92
325+ gateway4: 192.168.0.1
326+ interfaces:
327+ - eth0
328+ - vf0
329+ parameters:
330+ mii-monitor-interval: 100
331+ mode: active-backup
332+ primary: vf0
333+ transmit-hash-policy: "layer3+4"
334+ routes:
335+ - to: 10.1.3.0/24
336+ via: 192.168.0.3
337+ """),
338+ 'expected_netplan-v2': textwrap.dedent("""
339+ network:
340+ bonds:
341+ bond0:
342+ addresses:
343+ - 192.168.0.2/24
344+ - 192.168.1.2/24
345+ - 2001:1::1/92
346+ gateway4: 192.168.0.1
347+ interfaces:
348+ - eth0
349+ - vf0
350+ parameters:
351+ mii-monitor-interval: 100
352+ mode: active-backup
353+ primary: vf0
354+ transmit-hash-policy: layer3+4
355+ routes:
356+ - to: 10.1.3.0/24
357+ via: 192.168.0.3
358+ ethernets:
359+ eth0:
360+ match:
361+ driver: virtio_net
362+ macaddress: aa:bb:cc:dd:e8:00
363+ vf0:
364+ match:
365+ driver: e1000
366+ macaddress: aa:bb:cc:dd:e8:01
367+ set-name: vf0
368+ version: 2
369+ """),
370+
371 'expected_sysconfig': {
372 'ifcfg-bond0': textwrap.dedent("""\
373 BONDING_MASTER=yes
374@@ -2159,6 +2253,27 @@ class TestNetplanRoundTrip(CiTestCase):
375 renderer.render_network_state(ns, target)
376 return dir2dict(target)
377
378+ def testsimple_render_bond_netplan(self):
379+ entry = NETWORK_CONFIGS['bond']
380+ files = self._render_and_read(network_config=yaml.load(entry['yaml']))
381+ print(entry['expected_netplan'])
382+ print('-- expected ^ | v rendered --')
383+ print(files['/etc/netplan/50-cloud-init.yaml'])
384+ self.assertEqual(
385+ entry['expected_netplan'].splitlines(),
386+ files['/etc/netplan/50-cloud-init.yaml'].splitlines())
387+
388+ def testsimple_render_bond_v2_input_netplan(self):
389+ entry = NETWORK_CONFIGS['bond']
390+ files = self._render_and_read(
391+ network_config=yaml.load(entry['yaml-v2']))
392+ print(entry['expected_netplan-v2'])
393+ print('-- expected ^ | v rendered --')
394+ print(files['/etc/netplan/50-cloud-init.yaml'])
395+ self.assertEqual(
396+ entry['expected_netplan-v2'].splitlines(),
397+ files['/etc/netplan/50-cloud-init.yaml'].splitlines())
398+
399 def testsimple_render_small_netplan(self):
400 entry = NETWORK_CONFIGS['small']
401 files = self._render_and_read(network_config=yaml.load(entry['yaml']))

Subscribers

People subscribed via source and target branches