Merge ~raharper/cloud-init:rebased-netconfig-v2-passthrough into cloud-init:master
- Git
- lp:~raharper/cloud-init
- rebased-netconfig-v2-passthrough
- Merge into master
Status: | Merged |
---|---|
Merged at revision: | ef18b8ac4cf7e3dfd98830fbdb298380a192a0fc |
Proposed branch: | ~raharper/cloud-init:rebased-netconfig-v2-passthrough |
Merge into: | cloud-init:master |
Diff against target: |
1811 lines (+1443/-36) 10 files modified
cloudinit/distros/debian.py (+11/-5) cloudinit/net/eni.py (+3/-12) cloudinit/net/netplan.py (+380/-0) cloudinit/net/network_state.py (+299/-9) cloudinit/net/renderers.py (+3/-1) cloudinit/net/sysconfig.py (+15/-5) systemd/cloud-init.service (+1/-0) tests/unittests/test_distros/test_netconfig.py (+347/-4) tests/unittests/test_net.py (+301/-0) tools/net-convert.py (+83/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Server Team CI bot | continuous-integration | Needs Fixing | |
cloud-init Commiters | Pending | ||
Review via email: mp+320291@code.launchpad.net |
Commit message
Description of the change
cloudinit.net: add network config v2 parsing and rendering
Network configuration version 2 format is implemented in a package
called netplan (nplan)[1] which allows consolidated network config
for multiple network controllers.
- Add a new netplan renderer
- Update default policy, placing eni and sysconfig first
This requires explicit policy to enable netplan over eni
on systems which have both (Yakkety, Zesty, UC16)
- Allow any network state (parsed from any format cloud-init supports) to
render to v2 if system supports netplan.
- Move eni's _subnet_is_ipv6 to common code for use by other renderers
- Make sysconfig renderer always emit /etc/syconfig/
- Update cloud-init.service systemd unit to also wait on systemd-
1. https:/
- f006666... by Ryan Harper
-
(squash): eni.py whitespace damage
Server Team CI bot (server-team-bot) wrote : | # |
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:f006666a84e
https:/
Executed test runs:
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
Click here to trigger a rebuild:
https:/
Scott Moser (smoser) wrote : | # |
Ryan, this looks good, thanks.
I'll take a look first thing monday morning on it.
Scott Moser (smoser) wrote : | # |
i have some small comments.
don't worry about fixing the 'handle_' comment, but i do worry about the confusion.
Ryan Harper (raharper) : | # |
- 3ddea40... by Ryan Harper
-
unittests/
test_distro/ test_netconfig: pass 'system_info' dict into Distro class - 2b63ec0... by Ryan Harper
-
distros/debian.py: update comment in _write_network method
Ryan Harper (raharper) wrote : | # |
I'm mostly done fixing up the small issues; one larger discussion to be had on what to do about v2 passthrough and if we get v2 but need to render eni or sysconfig.
- 4ba0163... by Ryan Harper
-
net.sysconfig.py: Use _make_header() when writing sysconfig/network
- 9bcc80e... by Ryan Harper
-
Disable v2 passthrough, instead parse so we can render network config cross distro
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:9bcc80e67b7
https:/
Executed test runs:
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
Click here to trigger a rebuild:
https:/
- 9a10ab5... by Ryan Harper
-
network_state: v2 don't warn/error on missing mac_address in v2 input
- 1c1fc3a... by Ryan Harper
-
netplan: drop textwrap indent module, and reformat some comments/docstrings
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:1c1fc3a8841
https:/
Executed test runs:
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
Click here to trigger a rebuild:
https:/
Preview Diff
1 | diff --git a/cloudinit/distros/debian.py b/cloudinit/distros/debian.py | |||
2 | index 1101f02..3f0f9d5 100644 | |||
3 | --- a/cloudinit/distros/debian.py | |||
4 | +++ b/cloudinit/distros/debian.py | |||
5 | @@ -42,11 +42,16 @@ NETWORK_CONF_FN = "/etc/network/interfaces.d/50-cloud-init.cfg" | |||
6 | 42 | class Distro(distros.Distro): | 42 | class Distro(distros.Distro): |
7 | 43 | hostname_conf_fn = "/etc/hostname" | 43 | hostname_conf_fn = "/etc/hostname" |
8 | 44 | locale_conf_fn = "/etc/default/locale" | 44 | locale_conf_fn = "/etc/default/locale" |
9 | 45 | network_conf_fn = { | ||
10 | 46 | "eni": "/etc/network/interfaces.d/50-cloud-init.cfg", | ||
11 | 47 | "netplan": "/etc/netplan/50-cloud-init.yaml" | ||
12 | 48 | } | ||
13 | 45 | renderer_configs = { | 49 | renderer_configs = { |
18 | 46 | 'eni': { | 50 | "eni": {"eni_path": network_conf_fn["eni"], |
19 | 47 | 'eni_path': NETWORK_CONF_FN, | 51 | "eni_header": ENI_HEADER}, |
20 | 48 | 'eni_header': ENI_HEADER, | 52 | "netplan": {"netplan_path": network_conf_fn["netplan"], |
21 | 49 | } | 53 | "netplan_header": ENI_HEADER, |
22 | 54 | "postcmds": True} | ||
23 | 50 | } | 55 | } |
24 | 51 | 56 | ||
25 | 52 | def __init__(self, name, cfg, paths): | 57 | def __init__(self, name, cfg, paths): |
26 | @@ -75,7 +80,8 @@ class Distro(distros.Distro): | |||
27 | 75 | self.package_command('install', pkgs=pkglist) | 80 | self.package_command('install', pkgs=pkglist) |
28 | 76 | 81 | ||
29 | 77 | def _write_network(self, settings): | 82 | def _write_network(self, settings): |
31 | 78 | util.write_file(NETWORK_CONF_FN, settings) | 83 | # this is a legacy method, it will always write eni |
32 | 84 | util.write_file(self.network_conf_fn["eni"], settings) | ||
33 | 79 | return ['all'] | 85 | return ['all'] |
34 | 80 | 86 | ||
35 | 81 | def _write_network_config(self, netconfig): | 87 | def _write_network_config(self, netconfig): |
36 | diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py | |||
37 | index f471e05..9819d4f 100644 | |||
38 | --- a/cloudinit/net/eni.py | |||
39 | +++ b/cloudinit/net/eni.py | |||
40 | @@ -8,6 +8,7 @@ import re | |||
41 | 8 | from . import ParserError | 8 | from . import ParserError |
42 | 9 | 9 | ||
43 | 10 | from . import renderer | 10 | from . import renderer |
44 | 11 | from .network_state import subnet_is_ipv6 | ||
45 | 11 | 12 | ||
46 | 12 | from cloudinit import util | 13 | from cloudinit import util |
47 | 13 | 14 | ||
48 | @@ -111,16 +112,6 @@ def _iface_start_entry(iface, index, render_hwaddress=False): | |||
49 | 111 | return lines | 112 | return lines |
50 | 112 | 113 | ||
51 | 113 | 114 | ||
52 | 114 | def _subnet_is_ipv6(subnet): | ||
53 | 115 | # 'static6' or 'dhcp6' | ||
54 | 116 | if subnet['type'].endswith('6'): | ||
55 | 117 | # This is a request for DHCPv6. | ||
56 | 118 | return True | ||
57 | 119 | elif subnet['type'] == 'static' and ":" in subnet['address']: | ||
58 | 120 | return True | ||
59 | 121 | return False | ||
60 | 122 | |||
61 | 123 | |||
62 | 124 | def _parse_deb_config_data(ifaces, contents, src_dir, src_path): | 115 | def _parse_deb_config_data(ifaces, contents, src_dir, src_path): |
63 | 125 | """Parses the file contents, placing result into ifaces. | 116 | """Parses the file contents, placing result into ifaces. |
64 | 126 | 117 | ||
65 | @@ -370,7 +361,7 @@ class Renderer(renderer.Renderer): | |||
66 | 370 | iface['mode'] = subnet['type'] | 361 | iface['mode'] = subnet['type'] |
67 | 371 | iface['control'] = subnet.get('control', 'auto') | 362 | iface['control'] = subnet.get('control', 'auto') |
68 | 372 | subnet_inet = 'inet' | 363 | subnet_inet = 'inet' |
70 | 373 | if _subnet_is_ipv6(subnet): | 364 | if subnet_is_ipv6(subnet): |
71 | 374 | subnet_inet += '6' | 365 | subnet_inet += '6' |
72 | 375 | iface['inet'] = subnet_inet | 366 | iface['inet'] = subnet_inet |
73 | 376 | if subnet['type'].startswith('dhcp'): | 367 | if subnet['type'].startswith('dhcp'): |
74 | @@ -486,7 +477,7 @@ class Renderer(renderer.Renderer): | |||
75 | 486 | def network_state_to_eni(network_state, header=None, render_hwaddress=False): | 477 | def network_state_to_eni(network_state, header=None, render_hwaddress=False): |
76 | 487 | # render the provided network state, return a string of equivalent eni | 478 | # render the provided network state, return a string of equivalent eni |
77 | 488 | eni_path = 'etc/network/interfaces' | 479 | eni_path = 'etc/network/interfaces' |
79 | 489 | renderer = Renderer({ | 480 | renderer = Renderer(config={ |
80 | 490 | 'eni_path': eni_path, | 481 | 'eni_path': eni_path, |
81 | 491 | 'eni_header': header, | 482 | 'eni_header': header, |
82 | 492 | 'links_path_prefix': None, | 483 | 'links_path_prefix': None, |
83 | diff --git a/cloudinit/net/netplan.py b/cloudinit/net/netplan.py | |||
84 | 493 | new file mode 100644 | 484 | new file mode 100644 |
85 | index 0000000..7b3a6e8 | |||
86 | --- /dev/null | |||
87 | +++ b/cloudinit/net/netplan.py | |||
88 | @@ -0,0 +1,380 @@ | |||
89 | 1 | # This file is part of cloud-init. See LICENSE file ... | ||
90 | 2 | |||
91 | 3 | import copy | ||
92 | 4 | import os | ||
93 | 5 | |||
94 | 6 | from . import renderer | ||
95 | 7 | from .network_state import subnet_is_ipv6 | ||
96 | 8 | |||
97 | 9 | from cloudinit import util | ||
98 | 10 | from cloudinit.net import SYS_CLASS_NET, get_devicelist | ||
99 | 11 | |||
100 | 12 | |||
101 | 13 | NET_CONFIG_TO_V2 = { | ||
102 | 14 | 'bond': {'bond-ad-select': 'ad-select', | ||
103 | 15 | 'bond-arp-interval': 'arp-interval', | ||
104 | 16 | 'bond-arp-ip-target': 'arp-ip-target', | ||
105 | 17 | 'bond-arp-validate': 'arp-validate', | ||
106 | 18 | 'bond-downdelay': 'down-delay', | ||
107 | 19 | 'bond-fail-over-mac': 'fail-over-mac-policy', | ||
108 | 20 | 'bond-lacp-rate': 'lacp-rate', | ||
109 | 21 | 'bond-miimon': 'mii-monitor-interval', | ||
110 | 22 | 'bond-min-links': 'min-links', | ||
111 | 23 | 'bond-mode': 'mode', | ||
112 | 24 | 'bond-num-grat-arp': 'gratuitious-arp', | ||
113 | 25 | 'bond-primary-reselect': 'primary-reselect-policy', | ||
114 | 26 | 'bond-updelay': 'up-delay', | ||
115 | 27 | 'bond-xmit_hash_policy': 'transmit_hash_policy'}, | ||
116 | 28 | 'bridge': {'bridge_ageing': 'ageing-time', | ||
117 | 29 | 'bridge_bridgeprio': 'priority', | ||
118 | 30 | 'bridge_fd': 'forward-delay', | ||
119 | 31 | 'bridge_gcint': None, | ||
120 | 32 | 'bridge_hello': 'hello-time', | ||
121 | 33 | 'bridge_maxage': 'max-age', | ||
122 | 34 | 'bridge_maxwait': None, | ||
123 | 35 | 'bridge_pathcost': 'path-cost', | ||
124 | 36 | 'bridge_portprio': None, | ||
125 | 37 | 'bridge_waitport': None}} | ||
126 | 38 | |||
127 | 39 | |||
128 | 40 | def indent(text, prefix): | ||
129 | 41 | lines = [] | ||
130 | 42 | for line in text.splitlines(True): | ||
131 | 43 | lines.append(prefix + line) | ||
132 | 44 | return ''.join(lines) | ||
133 | 45 | |||
134 | 46 | |||
135 | 47 | def _get_params_dict_by_match(config, match): | ||
136 | 48 | return dict((key, value) for (key, value) in config.items() | ||
137 | 49 | if key.startswith(match)) | ||
138 | 50 | |||
139 | 51 | |||
140 | 52 | def _extract_addresses(config, entry): | ||
141 | 53 | """This method parse a cloudinit.net.network_state dictionary (config) and | ||
142 | 54 | maps netstate keys/values into a dictionary (entry) to represent | ||
143 | 55 | netplan yaml. | ||
144 | 56 | |||
145 | 57 | An example config dictionary might look like: | ||
146 | 58 | |||
147 | 59 | {'mac_address': '52:54:00:12:34:00', | ||
148 | 60 | 'name': 'interface0', | ||
149 | 61 | 'subnets': [ | ||
150 | 62 | {'address': '192.168.1.2/24', | ||
151 | 63 | 'mtu': 1501, | ||
152 | 64 | 'type': 'static'}, | ||
153 | 65 | {'address': '2001:4800:78ff:1b:be76:4eff:fe06:1000", | ||
154 | 66 | 'mtu': 1480, | ||
155 | 67 | 'netmask': 64, | ||
156 | 68 | 'type': 'static'}], | ||
157 | 69 | 'type: physical' | ||
158 | 70 | } | ||
159 | 71 | |||
160 | 72 | An entry dictionary looks like: | ||
161 | 73 | |||
162 | 74 | {'set-name': 'interface0', | ||
163 | 75 | 'match': {'macaddress': '52:54:00:12:34:00'}, | ||
164 | 76 | 'mtu': 1501} | ||
165 | 77 | |||
166 | 78 | After modification returns | ||
167 | 79 | |||
168 | 80 | {'set-name': 'interface0', | ||
169 | 81 | 'match': {'macaddress': '52:54:00:12:34:00'}, | ||
170 | 82 | 'mtu': 1501, | ||
171 | 83 | 'address': ['192.168.1.2/24', '2001:4800:78ff:1b:be76:4eff:fe06:1000"], | ||
172 | 84 | 'mtu6': 1480} | ||
173 | 85 | |||
174 | 86 | """ | ||
175 | 87 | |||
176 | 88 | def _listify(obj, token=' '): | ||
177 | 89 | "Helper to convert strings to list of strings, handle single string" | ||
178 | 90 | if not obj or type(obj) not in [str]: | ||
179 | 91 | return obj | ||
180 | 92 | if token in obj: | ||
181 | 93 | return obj.split(token) | ||
182 | 94 | else: | ||
183 | 95 | return [obj, ] | ||
184 | 96 | |||
185 | 97 | addresses = [] | ||
186 | 98 | routes = [] | ||
187 | 99 | nameservers = [] | ||
188 | 100 | searchdomains = [] | ||
189 | 101 | subnets = config.get('subnets', []) | ||
190 | 102 | if subnets is None: | ||
191 | 103 | subnets = [] | ||
192 | 104 | for subnet in subnets: | ||
193 | 105 | sn_type = subnet.get('type') | ||
194 | 106 | if sn_type.startswith('dhcp'): | ||
195 | 107 | if sn_type == 'dhcp': | ||
196 | 108 | sn_type += '4' | ||
197 | 109 | entry.update({sn_type: True}) | ||
198 | 110 | elif sn_type in ['static']: | ||
199 | 111 | addr = "%s" % subnet.get('address') | ||
200 | 112 | if 'netmask' in subnet: | ||
201 | 113 | addr += "/%s" % subnet.get('netmask') | ||
202 | 114 | if 'gateway' in subnet and subnet.get('gateway'): | ||
203 | 115 | gateway = subnet.get('gateway') | ||
204 | 116 | if ":" in gateway: | ||
205 | 117 | entry.update({'gateway6': gateway}) | ||
206 | 118 | else: | ||
207 | 119 | entry.update({'gateway4': gateway}) | ||
208 | 120 | if 'dns_nameservers' in subnet: | ||
209 | 121 | nameservers += _listify(subnet.get('dns_nameservers', [])) | ||
210 | 122 | if 'dns_search' in subnet: | ||
211 | 123 | searchdomains += _listify(subnet.get('dns_search', [])) | ||
212 | 124 | if 'mtu' in subnet: | ||
213 | 125 | mtukey = 'mtu' | ||
214 | 126 | if subnet_is_ipv6(subnet): | ||
215 | 127 | mtukey += '6' | ||
216 | 128 | entry.update({mtukey: subnet.get('mtu')}) | ||
217 | 129 | for route in subnet.get('routes', []): | ||
218 | 130 | to_net = "%s/%s" % (route.get('network'), | ||
219 | 131 | route.get('netmask')) | ||
220 | 132 | route = { | ||
221 | 133 | 'via': route.get('gateway'), | ||
222 | 134 | 'to': to_net, | ||
223 | 135 | } | ||
224 | 136 | if 'metric' in route: | ||
225 | 137 | route.update({'metric': route.get('metric', 100)}) | ||
226 | 138 | routes.append(route) | ||
227 | 139 | |||
228 | 140 | addresses.append(addr) | ||
229 | 141 | |||
230 | 142 | if len(addresses) > 0: | ||
231 | 143 | entry.update({'addresses': addresses}) | ||
232 | 144 | if len(routes) > 0: | ||
233 | 145 | entry.update({'routes': routes}) | ||
234 | 146 | if len(nameservers) > 0: | ||
235 | 147 | ns = {'addresses': nameservers} | ||
236 | 148 | entry.update({'nameservers': ns}) | ||
237 | 149 | if len(searchdomains) > 0: | ||
238 | 150 | ns = entry.get('nameservers', {}) | ||
239 | 151 | ns.update({'search': searchdomains}) | ||
240 | 152 | entry.update({'nameservers': ns}) | ||
241 | 153 | |||
242 | 154 | |||
243 | 155 | def _extract_bond_slaves_by_name(interfaces, entry, bond_master): | ||
244 | 156 | bond_slave_names = sorted([name for (name, cfg) in interfaces.items() | ||
245 | 157 | if cfg.get('bond-master', None) == bond_master]) | ||
246 | 158 | if len(bond_slave_names) > 0: | ||
247 | 159 | entry.update({'interfaces': bond_slave_names}) | ||
248 | 160 | |||
249 | 161 | |||
250 | 162 | class Renderer(renderer.Renderer): | ||
251 | 163 | """Renders network information in a /etc/netplan/network.yaml format.""" | ||
252 | 164 | |||
253 | 165 | NETPLAN_GENERATE = ['netplan', 'generate'] | ||
254 | 166 | |||
255 | 167 | def __init__(self, config=None): | ||
256 | 168 | if not config: | ||
257 | 169 | config = {} | ||
258 | 170 | self.netplan_path = config.get('netplan_path', | ||
259 | 171 | 'etc/netplan/50-cloud-init.yaml') | ||
260 | 172 | self.netplan_header = config.get('netplan_header', None) | ||
261 | 173 | self._postcmds = config.get('postcmds', False) | ||
262 | 174 | |||
263 | 175 | def render_network_state(self, target, network_state): | ||
264 | 176 | # check network state for version | ||
265 | 177 | # if v2, then extract network_state.config | ||
266 | 178 | # else render_v2_from_state | ||
267 | 179 | fpnplan = os.path.join(target, self.netplan_path) | ||
268 | 180 | util.ensure_dir(os.path.dirname(fpnplan)) | ||
269 | 181 | header = self.netplan_header if self.netplan_header else "" | ||
270 | 182 | |||
271 | 183 | # render from state | ||
272 | 184 | content = self._render_content(network_state) | ||
273 | 185 | |||
274 | 186 | # ensure we poke udev to run net_setup_link | ||
275 | 187 | if not header.endswith("\n"): | ||
276 | 188 | header += "\n" | ||
277 | 189 | util.write_file(fpnplan, header + content) | ||
278 | 190 | |||
279 | 191 | self._netplan_generate(run=self._postcmds) | ||
280 | 192 | self._net_setup_link(run=self._postcmds) | ||
281 | 193 | |||
282 | 194 | def _netplan_generate(self, run=False): | ||
283 | 195 | if not run: | ||
284 | 196 | print("netplan postcmd disabled") | ||
285 | 197 | return | ||
286 | 198 | util.subp(self.NETPLAN_GENERATE, capture=True) | ||
287 | 199 | |||
288 | 200 | def _net_setup_link(self, run=False): | ||
289 | 201 | """To ensure device link properties are applied, we poke | ||
290 | 202 | udev to re-evaluate networkd .link files and call | ||
291 | 203 | the setup_link udev builtin command | ||
292 | 204 | """ | ||
293 | 205 | if not run: | ||
294 | 206 | print("netsetup postcmd disabled") | ||
295 | 207 | return | ||
296 | 208 | setup_lnk = ['udevadm', 'test-builtin', 'net_setup_link'] | ||
297 | 209 | for cmd in [setup_lnk + [SYS_CLASS_NET + iface] | ||
298 | 210 | for iface in get_devicelist() if | ||
299 | 211 | os.path.islink(SYS_CLASS_NET + iface)]: | ||
300 | 212 | print(cmd) | ||
301 | 213 | util.subp(cmd, capture=True) | ||
302 | 214 | |||
303 | 215 | def _render_content(self, network_state): | ||
304 | 216 | print('rendering v2 for victory!') | ||
305 | 217 | ethernets = {} | ||
306 | 218 | wifis = {} | ||
307 | 219 | bridges = {} | ||
308 | 220 | bonds = {} | ||
309 | 221 | vlans = {} | ||
310 | 222 | content = [] | ||
311 | 223 | |||
312 | 224 | interfaces = network_state._network_state.get('interfaces', []) | ||
313 | 225 | |||
314 | 226 | nameservers = network_state.dns_nameservers | ||
315 | 227 | searchdomains = network_state.dns_searchdomains | ||
316 | 228 | |||
317 | 229 | for config in network_state.iter_interfaces(): | ||
318 | 230 | ifname = config.get('name') | ||
319 | 231 | # filter None entries up front so we can do simple if key in dict | ||
320 | 232 | ifcfg = dict((key, value) for (key, value) in config.items() | ||
321 | 233 | if value) | ||
322 | 234 | |||
323 | 235 | if_type = ifcfg.get('type') | ||
324 | 236 | if if_type == 'physical': | ||
325 | 237 | # required_keys = ['name', 'mac_address'] | ||
326 | 238 | eth = { | ||
327 | 239 | 'set-name': ifname, | ||
328 | 240 | 'match': ifcfg.get('match', None), | ||
329 | 241 | } | ||
330 | 242 | if eth['match'] is None: | ||
331 | 243 | macaddr = ifcfg.get('mac_address', None) | ||
332 | 244 | if macaddr is not None: | ||
333 | 245 | eth['match'] = {'macaddress': macaddr.lower()} | ||
334 | 246 | else: | ||
335 | 247 | del eth['match'] | ||
336 | 248 | del eth['set-name'] | ||
337 | 249 | if 'mtu' in ifcfg: | ||
338 | 250 | eth['mtu'] = ifcfg.get('mtu') | ||
339 | 251 | |||
340 | 252 | _extract_addresses(ifcfg, eth) | ||
341 | 253 | ethernets.update({ifname: eth}) | ||
342 | 254 | |||
343 | 255 | elif if_type == 'bond': | ||
344 | 256 | # required_keys = ['name', 'bond_interfaces'] | ||
345 | 257 | bond = {} | ||
346 | 258 | bond_config = {} | ||
347 | 259 | # extract bond params and drop the bond_ prefix as it's | ||
348 | 260 | # redundent in v2 yaml format | ||
349 | 261 | v2_bond_map = NET_CONFIG_TO_V2.get('bond') | ||
350 | 262 | for match in ['bond_', 'bond-']: | ||
351 | 263 | bond_params = _get_params_dict_by_match(ifcfg, match) | ||
352 | 264 | for (param, value) in bond_params.items(): | ||
353 | 265 | newname = v2_bond_map.get(param) | ||
354 | 266 | if newname is None: | ||
355 | 267 | continue | ||
356 | 268 | bond_config.update({newname: value}) | ||
357 | 269 | |||
358 | 270 | if len(bond_config) > 0: | ||
359 | 271 | bond.update({'parameters': bond_config}) | ||
360 | 272 | slave_interfaces = ifcfg.get('bond-slaves') | ||
361 | 273 | if slave_interfaces == 'none': | ||
362 | 274 | _extract_bond_slaves_by_name(interfaces, bond, ifname) | ||
363 | 275 | _extract_addresses(ifcfg, bond) | ||
364 | 276 | bonds.update({ifname: bond}) | ||
365 | 277 | |||
366 | 278 | elif if_type == 'bridge': | ||
367 | 279 | # required_keys = ['name', 'bridge_ports'] | ||
368 | 280 | ports = sorted(copy.copy(ifcfg.get('bridge_ports'))) | ||
369 | 281 | bridge = { | ||
370 | 282 | 'interfaces': ports, | ||
371 | 283 | } | ||
372 | 284 | # extract bridge params and drop the bridge prefix as it's | ||
373 | 285 | # redundent in v2 yaml format | ||
374 | 286 | match_prefix = 'bridge_' | ||
375 | 287 | params = _get_params_dict_by_match(ifcfg, match_prefix) | ||
376 | 288 | br_config = {} | ||
377 | 289 | |||
378 | 290 | # v2 yaml uses different names for the keys | ||
379 | 291 | # and at least one value format change | ||
380 | 292 | v2_bridge_map = NET_CONFIG_TO_V2.get('bridge') | ||
381 | 293 | for (param, value) in params.items(): | ||
382 | 294 | newname = v2_bridge_map.get(param) | ||
383 | 295 | if newname is None: | ||
384 | 296 | continue | ||
385 | 297 | br_config.update({newname: value}) | ||
386 | 298 | if newname == 'path-cost': | ||
387 | 299 | # <interface> <cost> -> <interface>: int(<cost>) | ||
388 | 300 | newvalue = {} | ||
389 | 301 | for costval in value: | ||
390 | 302 | (port, cost) = costval.split() | ||
391 | 303 | newvalue[port] = int(cost) | ||
392 | 304 | br_config.update({newname: newvalue}) | ||
393 | 305 | if len(br_config) > 0: | ||
394 | 306 | bridge.update({'parameters': br_config}) | ||
395 | 307 | _extract_addresses(ifcfg, bridge) | ||
396 | 308 | bridges.update({ifname: bridge}) | ||
397 | 309 | |||
398 | 310 | elif if_type == 'vlan': | ||
399 | 311 | # required_keys = ['name', 'vlan_id', 'vlan-raw-device'] | ||
400 | 312 | vlan = { | ||
401 | 313 | 'id': ifcfg.get('vlan_id'), | ||
402 | 314 | 'link': ifcfg.get('vlan-raw-device') | ||
403 | 315 | } | ||
404 | 316 | |||
405 | 317 | _extract_addresses(ifcfg, vlan) | ||
406 | 318 | vlans.update({ifname: vlan}) | ||
407 | 319 | |||
408 | 320 | # inject global nameserver values under each physical interface | ||
409 | 321 | if nameservers: | ||
410 | 322 | for _eth, cfg in ethernets.items(): | ||
411 | 323 | nscfg = cfg.get('nameservers', {}) | ||
412 | 324 | addresses = nscfg.get('addresses', []) | ||
413 | 325 | addresses += nameservers | ||
414 | 326 | nscfg.update({'addresses': addresses}) | ||
415 | 327 | cfg.update({'nameservers': nscfg}) | ||
416 | 328 | |||
417 | 329 | if searchdomains: | ||
418 | 330 | for _eth, cfg in ethernets.items(): | ||
419 | 331 | nscfg = cfg.get('nameservers', {}) | ||
420 | 332 | search = nscfg.get('search', []) | ||
421 | 333 | search += searchdomains | ||
422 | 334 | nscfg.update({'search': search}) | ||
423 | 335 | cfg.update({'nameservers': nscfg}) | ||
424 | 336 | |||
425 | 337 | # workaround yaml dictionary key sorting when dumping | ||
426 | 338 | def _render_section(name, section): | ||
427 | 339 | if section: | ||
428 | 340 | dump = util.yaml_dumps({name: section}, | ||
429 | 341 | explicit_start=False, | ||
430 | 342 | explicit_end=False) | ||
431 | 343 | txt = indent(dump, ' ' * 4) | ||
432 | 344 | return [txt] | ||
433 | 345 | return [] | ||
434 | 346 | |||
435 | 347 | content.append("network:\n version: 2\n") | ||
436 | 348 | content += _render_section('ethernets', ethernets) | ||
437 | 349 | content += _render_section('wifis', wifis) | ||
438 | 350 | content += _render_section('bonds', bonds) | ||
439 | 351 | content += _render_section('bridges', bridges) | ||
440 | 352 | content += _render_section('vlans', vlans) | ||
441 | 353 | |||
442 | 354 | return "".join(content) | ||
443 | 355 | |||
444 | 356 | |||
445 | 357 | def available(target=None): | ||
446 | 358 | expected = ['netplan'] | ||
447 | 359 | search = ['/usr/sbin', '/sbin'] | ||
448 | 360 | for p in expected: | ||
449 | 361 | if not util.which(p, search=search, target=target): | ||
450 | 362 | return False | ||
451 | 363 | return True | ||
452 | 364 | |||
453 | 365 | |||
454 | 366 | def network_state_to_netplan(network_state, header=None): | ||
455 | 367 | # render the provided network state, return a string of equivalent eni | ||
456 | 368 | netplan_path = 'etc/network/50-cloud-init.yaml' | ||
457 | 369 | renderer = Renderer({ | ||
458 | 370 | 'netplan_path': netplan_path, | ||
459 | 371 | 'netplan_header': header, | ||
460 | 372 | }) | ||
461 | 373 | if not header: | ||
462 | 374 | header = "" | ||
463 | 375 | if not header.endswith("\n"): | ||
464 | 376 | header += "\n" | ||
465 | 377 | contents = renderer._render_content(network_state) | ||
466 | 378 | return header + contents | ||
467 | 379 | |||
468 | 380 | # vi: ts=4 expandtab | ||
469 | diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py | |||
470 | index 90b2835..c14aae2 100644 | |||
471 | --- a/cloudinit/net/network_state.py | |||
472 | +++ b/cloudinit/net/network_state.py | |||
473 | @@ -1,4 +1,4 @@ | |||
475 | 1 | # Copyright (C) 2013-2014 Canonical Ltd. | 1 | # Copyright (C) 2017 Canonical Ltd. |
476 | 2 | # | 2 | # |
477 | 3 | # Author: Ryan Harper <ryan.harper@canonical.com> | 3 | # Author: Ryan Harper <ryan.harper@canonical.com> |
478 | 4 | # | 4 | # |
479 | @@ -18,6 +18,10 @@ NETWORK_STATE_VERSION = 1 | |||
480 | 18 | NETWORK_STATE_REQUIRED_KEYS = { | 18 | NETWORK_STATE_REQUIRED_KEYS = { |
481 | 19 | 1: ['version', 'config', 'network_state'], | 19 | 1: ['version', 'config', 'network_state'], |
482 | 20 | } | 20 | } |
483 | 21 | NETWORK_V2_KEY_FILTER = [ | ||
484 | 22 | 'addresses', 'dhcp4', 'dhcp6', 'gateway4', 'gateway6', 'interfaces', | ||
485 | 23 | 'match', 'mtu', 'nameservers', 'renderer', 'set-name', 'wakeonlan' | ||
486 | 24 | ] | ||
487 | 21 | 25 | ||
488 | 22 | 26 | ||
489 | 23 | def parse_net_config_data(net_config, skip_broken=True): | 27 | def parse_net_config_data(net_config, skip_broken=True): |
490 | @@ -26,11 +30,18 @@ def parse_net_config_data(net_config, skip_broken=True): | |||
491 | 26 | :param net_config: curtin network config dict | 30 | :param net_config: curtin network config dict |
492 | 27 | """ | 31 | """ |
493 | 28 | state = None | 32 | state = None |
497 | 29 | if 'version' in net_config and 'config' in net_config: | 33 | version = net_config.get('version') |
498 | 30 | nsi = NetworkStateInterpreter(version=net_config.get('version'), | 34 | config = net_config.get('config') |
499 | 31 | config=net_config.get('config')) | 35 | if version == 2: |
500 | 36 | # v2 does not have explicit 'config' key so we | ||
501 | 37 | # pass the whole net-config as-is | ||
502 | 38 | config = net_config | ||
503 | 39 | |||
504 | 40 | if version and config: | ||
505 | 41 | nsi = NetworkStateInterpreter(version=version, config=config) | ||
506 | 32 | nsi.parse_config(skip_broken=skip_broken) | 42 | nsi.parse_config(skip_broken=skip_broken) |
508 | 33 | state = nsi.network_state | 43 | state = nsi.get_network_state() |
509 | 44 | |||
510 | 34 | return state | 45 | return state |
511 | 35 | 46 | ||
512 | 36 | 47 | ||
513 | @@ -106,6 +117,7 @@ class NetworkState(object): | |||
514 | 106 | def __init__(self, network_state, version=NETWORK_STATE_VERSION): | 117 | def __init__(self, network_state, version=NETWORK_STATE_VERSION): |
515 | 107 | self._network_state = copy.deepcopy(network_state) | 118 | self._network_state = copy.deepcopy(network_state) |
516 | 108 | self._version = version | 119 | self._version = version |
517 | 120 | self.use_ipv6 = network_state.get('use_ipv6', False) | ||
518 | 109 | 121 | ||
519 | 110 | @property | 122 | @property |
520 | 111 | def version(self): | 123 | def version(self): |
521 | @@ -152,7 +164,8 @@ class NetworkStateInterpreter(object): | |||
522 | 152 | 'dns': { | 164 | 'dns': { |
523 | 153 | 'nameservers': [], | 165 | 'nameservers': [], |
524 | 154 | 'search': [], | 166 | 'search': [], |
526 | 155 | } | 167 | }, |
527 | 168 | 'use_ipv6': False, | ||
528 | 156 | } | 169 | } |
529 | 157 | 170 | ||
530 | 158 | def __init__(self, version=NETWORK_STATE_VERSION, config=None): | 171 | def __init__(self, version=NETWORK_STATE_VERSION, config=None): |
531 | @@ -165,6 +178,14 @@ class NetworkStateInterpreter(object): | |||
532 | 165 | def network_state(self): | 178 | def network_state(self): |
533 | 166 | return NetworkState(self._network_state, version=self._version) | 179 | return NetworkState(self._network_state, version=self._version) |
534 | 167 | 180 | ||
535 | 181 | @property | ||
536 | 182 | def use_ipv6(self): | ||
537 | 183 | return self._network_state.get('use_ipv6') | ||
538 | 184 | |||
539 | 185 | @use_ipv6.setter | ||
540 | 186 | def use_ipv6(self, val): | ||
541 | 187 | self._network_state.update({'use_ipv6': val}) | ||
542 | 188 | |||
543 | 168 | def dump(self): | 189 | def dump(self): |
544 | 169 | state = { | 190 | state = { |
545 | 170 | 'version': self._version, | 191 | 'version': self._version, |
546 | @@ -192,8 +213,22 @@ class NetworkStateInterpreter(object): | |||
547 | 192 | def dump_network_state(self): | 213 | def dump_network_state(self): |
548 | 193 | return util.yaml_dumps(self._network_state) | 214 | return util.yaml_dumps(self._network_state) |
549 | 194 | 215 | ||
550 | 216 | def as_dict(self): | ||
551 | 217 | return {'version': self.version, 'config': self.config} | ||
552 | 218 | |||
553 | 219 | def get_network_state(self): | ||
554 | 220 | ns = self.network_state | ||
555 | 221 | return ns | ||
556 | 222 | |||
557 | 195 | def parse_config(self, skip_broken=True): | 223 | def parse_config(self, skip_broken=True): |
559 | 196 | # rebuild network state | 224 | if self._version == 1: |
560 | 225 | self.parse_config_v1(skip_broken=skip_broken) | ||
561 | 226 | self._parsed = True | ||
562 | 227 | elif self._version == 2: | ||
563 | 228 | self.parse_config_v2(skip_broken=skip_broken) | ||
564 | 229 | self._parsed = True | ||
565 | 230 | |||
566 | 231 | def parse_config_v1(self, skip_broken=True): | ||
567 | 197 | for command in self._config: | 232 | for command in self._config: |
568 | 198 | command_type = command['type'] | 233 | command_type = command['type'] |
569 | 199 | try: | 234 | try: |
570 | @@ -211,6 +246,26 @@ class NetworkStateInterpreter(object): | |||
571 | 211 | exc_info=True) | 246 | exc_info=True) |
572 | 212 | LOG.debug(self.dump_network_state()) | 247 | LOG.debug(self.dump_network_state()) |
573 | 213 | 248 | ||
574 | 249 | def parse_config_v2(self, skip_broken=True): | ||
575 | 250 | for command_type, command in self._config.items(): | ||
576 | 251 | if command_type == 'version': | ||
577 | 252 | continue | ||
578 | 253 | try: | ||
579 | 254 | handler = self.command_handlers[command_type] | ||
580 | 255 | except KeyError: | ||
581 | 256 | raise RuntimeError("No handler found for" | ||
582 | 257 | " command '%s'" % command_type) | ||
583 | 258 | try: | ||
584 | 259 | handler(self, command) | ||
585 | 260 | self._v2_common(command) | ||
586 | 261 | except InvalidCommand: | ||
587 | 262 | if not skip_broken: | ||
588 | 263 | raise | ||
589 | 264 | else: | ||
590 | 265 | LOG.warn("Skipping invalid command: %s", command, | ||
591 | 266 | exc_info=True) | ||
592 | 267 | LOG.debug(self.dump_network_state()) | ||
593 | 268 | |||
594 | 214 | @ensure_command_keys(['name']) | 269 | @ensure_command_keys(['name']) |
595 | 215 | def handle_loopback(self, command): | 270 | def handle_loopback(self, command): |
596 | 216 | return self.handle_physical(command) | 271 | return self.handle_physical(command) |
597 | @@ -238,11 +293,16 @@ class NetworkStateInterpreter(object): | |||
598 | 238 | if subnets: | 293 | if subnets: |
599 | 239 | for subnet in subnets: | 294 | for subnet in subnets: |
600 | 240 | if subnet['type'] == 'static': | 295 | if subnet['type'] == 'static': |
601 | 296 | if ':' in subnet['address']: | ||
602 | 297 | self.use_ipv6 = True | ||
603 | 241 | if 'netmask' in subnet and ':' in subnet['address']: | 298 | if 'netmask' in subnet and ':' in subnet['address']: |
604 | 242 | subnet['netmask'] = mask2cidr(subnet['netmask']) | 299 | subnet['netmask'] = mask2cidr(subnet['netmask']) |
605 | 243 | for route in subnet.get('routes', []): | 300 | for route in subnet.get('routes', []): |
606 | 244 | if 'netmask' in route: | 301 | if 'netmask' in route: |
607 | 245 | route['netmask'] = mask2cidr(route['netmask']) | 302 | route['netmask'] = mask2cidr(route['netmask']) |
608 | 303 | elif subnet['type'].endswith('6'): | ||
609 | 304 | self.use_ipv6 = True | ||
610 | 305 | |||
611 | 246 | iface.update({ | 306 | iface.update({ |
612 | 247 | 'name': command.get('name'), | 307 | 'name': command.get('name'), |
613 | 248 | 'type': command.get('type'), | 308 | 'type': command.get('type'), |
614 | @@ -327,7 +387,7 @@ class NetworkStateInterpreter(object): | |||
615 | 327 | bond_if.update({param: val}) | 387 | bond_if.update({param: val}) |
616 | 328 | self._network_state['interfaces'].update({ifname: bond_if}) | 388 | self._network_state['interfaces'].update({ifname: bond_if}) |
617 | 329 | 389 | ||
619 | 330 | @ensure_command_keys(['name', 'bridge_interfaces', 'params']) | 390 | @ensure_command_keys(['name', 'bridge_interfaces']) |
620 | 331 | def handle_bridge(self, command): | 391 | def handle_bridge(self, command): |
621 | 332 | ''' | 392 | ''' |
622 | 333 | auto br0 | 393 | auto br0 |
623 | @@ -373,7 +433,7 @@ class NetworkStateInterpreter(object): | |||
624 | 373 | self.handle_physical(command) | 433 | self.handle_physical(command) |
625 | 374 | iface = interfaces.get(command.get('name'), {}) | 434 | iface = interfaces.get(command.get('name'), {}) |
626 | 375 | iface['bridge_ports'] = command['bridge_interfaces'] | 435 | iface['bridge_ports'] = command['bridge_interfaces'] |
628 | 376 | for param, val in command.get('params').items(): | 436 | for param, val in command.get('params', {}).items(): |
629 | 377 | iface.update({param: val}) | 437 | iface.update({param: val}) |
630 | 378 | 438 | ||
631 | 379 | interfaces.update({iface['name']: iface}) | 439 | interfaces.update({iface['name']: iface}) |
632 | @@ -407,6 +467,236 @@ class NetworkStateInterpreter(object): | |||
633 | 407 | } | 467 | } |
634 | 408 | routes.append(route) | 468 | routes.append(route) |
635 | 409 | 469 | ||
636 | 470 | # V2 handlers | ||
637 | 471 | def handle_bonds(self, command): | ||
638 | 472 | ''' | ||
639 | 473 | v2_command = { | ||
640 | 474 | bond0: { | ||
641 | 475 | 'interfaces': ['interface0', 'interface1'], | ||
642 | 476 | 'miimon': 100, | ||
643 | 477 | 'mode': '802.3ad', | ||
644 | 478 | 'xmit_hash_policy': 'layer3+4'}, | ||
645 | 479 | bond1: { | ||
646 | 480 | 'bond-slaves': ['interface2', 'interface7'], | ||
647 | 481 | 'mode': 1 | ||
648 | 482 | } | ||
649 | 483 | } | ||
650 | 484 | |||
651 | 485 | v1_command = { | ||
652 | 486 | 'type': 'bond' | ||
653 | 487 | 'name': 'bond0', | ||
654 | 488 | 'bond_interfaces': [interface0, interface1], | ||
655 | 489 | 'params': { | ||
656 | 490 | 'bond-mode': '802.3ad', | ||
657 | 491 | 'bond_miimon: 100, | ||
658 | 492 | 'bond_xmit_hash_policy': 'layer3+4', | ||
659 | 493 | } | ||
660 | 494 | } | ||
661 | 495 | |||
662 | 496 | ''' | ||
663 | 497 | self._handle_bond_bridge(command, cmd_type='bond') | ||
664 | 498 | |||
665 | 499 | def handle_bridges(self, command): | ||
666 | 500 | |||
667 | 501 | ''' | ||
668 | 502 | v2_command = { | ||
669 | 503 | br0: { | ||
670 | 504 | 'interfaces': ['interface0', 'interface1'], | ||
671 | 505 | 'fd': 0, | ||
672 | 506 | 'stp': 'off', | ||
673 | 507 | 'maxwait': 0, | ||
674 | 508 | } | ||
675 | 509 | } | ||
676 | 510 | |||
677 | 511 | v1_command = { | ||
678 | 512 | 'type': 'bridge' | ||
679 | 513 | 'name': 'br0', | ||
680 | 514 | 'bridge_interfaces': [interface0, interface1], | ||
681 | 515 | 'params': { | ||
682 | 516 | 'bridge_stp': 'off', | ||
683 | 517 | 'bridge_fd: 0, | ||
684 | 518 | 'bridge_maxwait': 0 | ||
685 | 519 | } | ||
686 | 520 | } | ||
687 | 521 | |||
688 | 522 | ''' | ||
689 | 523 | self._handle_bond_bridge(command, cmd_type='bridge') | ||
690 | 524 | |||
691 | 525 | def handle_ethernets(self, command): | ||
692 | 526 | ''' | ||
693 | 527 | ethernets: | ||
694 | 528 | eno1: | ||
695 | 529 | match: | ||
696 | 530 | macaddress: 00:11:22:33:44:55 | ||
697 | 531 | wakeonlan: true | ||
698 | 532 | dhcp4: true | ||
699 | 533 | dhcp6: false | ||
700 | 534 | addresses: | ||
701 | 535 | - 192.168.14.2/24 | ||
702 | 536 | - 2001:1::1/64 | ||
703 | 537 | gateway4: 192.168.14.1 | ||
704 | 538 | gateway6: 2001:1::2 | ||
705 | 539 | nameservers: | ||
706 | 540 | search: [foo.local, bar.local] | ||
707 | 541 | addresses: [8.8.8.8, 8.8.4.4] | ||
708 | 542 | lom: | ||
709 | 543 | match: | ||
710 | 544 | driver: ixgbe | ||
711 | 545 | set-name: lom1 | ||
712 | 546 | dhcp6: true | ||
713 | 547 | switchports: | ||
714 | 548 | match: | ||
715 | 549 | name: enp2* | ||
716 | 550 | mtu: 1280 | ||
717 | 551 | |||
718 | 552 | command = { | ||
719 | 553 | 'type': 'physical', | ||
720 | 554 | 'mac_address': 'c0:d6:9f:2c:e8:80', | ||
721 | 555 | 'name': 'eth0', | ||
722 | 556 | 'subnets': [ | ||
723 | 557 | {'type': 'dhcp4'} | ||
724 | 558 | ] | ||
725 | 559 | } | ||
726 | 560 | ''' | ||
727 | 561 | for eth, cfg in command.items(): | ||
728 | 562 | phy_cmd = { | ||
729 | 563 | 'type': 'physical', | ||
730 | 564 | 'name': cfg.get('set-name', eth), | ||
731 | 565 | } | ||
732 | 566 | |||
733 | 567 | for key in ['mtu', 'match', 'wakeonlan']: | ||
734 | 568 | if key in cfg: | ||
735 | 569 | phy_cmd.update({key: cfg.get(key)}) | ||
736 | 570 | |||
737 | 571 | subnets = self._v2_to_v1_ipcfg(cfg) | ||
738 | 572 | if len(subnets) > 0: | ||
739 | 573 | phy_cmd.update({'subnets': subnets}) | ||
740 | 574 | |||
741 | 575 | LOG.debug('v2(ethernets) -> v1(physical):\n%s', phy_cmd) | ||
742 | 576 | self.handle_physical(phy_cmd) | ||
743 | 577 | |||
744 | 578 | def handle_vlans(self, command): | ||
745 | 579 | ''' | ||
746 | 580 | v2_vlans = { | ||
747 | 581 | 'eth0.123': { | ||
748 | 582 | 'id': 123, | ||
749 | 583 | 'link': 'eth0', | ||
750 | 584 | 'dhcp4': True, | ||
751 | 585 | } | ||
752 | 586 | } | ||
753 | 587 | |||
754 | 588 | v1_command = { | ||
755 | 589 | 'type': 'vlan', | ||
756 | 590 | 'name': 'eth0.123', | ||
757 | 591 | 'vlan_link': 'eth0', | ||
758 | 592 | 'vlan_id': 123, | ||
759 | 593 | 'subnets': [{'type': 'dhcp4'}], | ||
760 | 594 | } | ||
761 | 595 | ''' | ||
762 | 596 | for vlan, cfg in command.items(): | ||
763 | 597 | vlan_cmd = { | ||
764 | 598 | 'type': 'vlan', | ||
765 | 599 | 'name': vlan, | ||
766 | 600 | 'vlan_id': cfg.get('id'), | ||
767 | 601 | 'vlan_link': cfg.get('link'), | ||
768 | 602 | } | ||
769 | 603 | subnets = self._v2_to_v1_ipcfg(cfg) | ||
770 | 604 | if len(subnets) > 0: | ||
771 | 605 | vlan_cmd.update({'subnets': subnets}) | ||
772 | 606 | LOG.debug('v2(vlans) -> v1(vlan):\n%s', vlan_cmd) | ||
773 | 607 | self.handle_vlan(vlan_cmd) | ||
774 | 608 | |||
775 | 609 | def handle_wifis(self, command): | ||
776 | 610 | raise NotImplemented('NetworkState V2: Skipping wifi configuration') | ||
777 | 611 | |||
778 | 612 | def _v2_common(self, cfg): | ||
779 | 613 | LOG.debug('v2_common: handling config:\n%s', cfg) | ||
780 | 614 | if 'nameservers' in cfg: | ||
781 | 615 | search = cfg.get('nameservers').get('search', []) | ||
782 | 616 | dns = cfg.get('nameservers').get('addresses', []) | ||
783 | 617 | name_cmd = {'type': 'nameserver'} | ||
784 | 618 | if len(search) > 0: | ||
785 | 619 | name_cmd.update({'search': search}) | ||
786 | 620 | if len(dns) > 0: | ||
787 | 621 | name_cmd.update({'addresses': dns}) | ||
788 | 622 | LOG.debug('v2(nameserver) -> v1(nameserver):\n%s', name_cmd) | ||
789 | 623 | self.handle_nameserver(name_cmd) | ||
790 | 624 | |||
791 | 625 | def _handle_bond_bridge(self, command, cmd_type=None): | ||
792 | 626 | """Common handler for bond and bridge types""" | ||
793 | 627 | for item_name, item_cfg in command.items(): | ||
794 | 628 | item_params = dict((key, value) for (key, value) in | ||
795 | 629 | item_cfg.items() if key not in | ||
796 | 630 | NETWORK_V2_KEY_FILTER) | ||
797 | 631 | v1_cmd = { | ||
798 | 632 | 'type': cmd_type, | ||
799 | 633 | 'name': item_name, | ||
800 | 634 | cmd_type + '_interfaces': item_cfg.get('interfaces'), | ||
801 | 635 | 'params': item_params, | ||
802 | 636 | } | ||
803 | 637 | subnets = self._v2_to_v1_ipcfg(item_cfg) | ||
804 | 638 | if len(subnets) > 0: | ||
805 | 639 | v1_cmd.update({'subnets': subnets}) | ||
806 | 640 | |||
807 | 641 | LOG.debug('v2(%ss) -> v1(%s):\n%s', cmd_type, cmd_type, v1_cmd) | ||
808 | 642 | self.handle_bridge(v1_cmd) | ||
809 | 643 | |||
810 | 644 | def _v2_to_v1_ipcfg(self, cfg): | ||
811 | 645 | """Common ipconfig extraction from v2 to v1 subnets array.""" | ||
812 | 646 | |||
813 | 647 | subnets = [] | ||
814 | 648 | if 'dhcp4' in cfg: | ||
815 | 649 | subnets.append({'type': 'dhcp4'}) | ||
816 | 650 | if 'dhcp6' in cfg: | ||
817 | 651 | self.use_ipv6 = True | ||
818 | 652 | subnets.append({'type': 'dhcp6'}) | ||
819 | 653 | |||
820 | 654 | gateway4 = None | ||
821 | 655 | gateway6 = None | ||
822 | 656 | for address in cfg.get('addresses', []): | ||
823 | 657 | subnet = { | ||
824 | 658 | 'type': 'static', | ||
825 | 659 | 'address': address, | ||
826 | 660 | } | ||
827 | 661 | |||
828 | 662 | routes = [] | ||
829 | 663 | for route in cfg.get('routes', []): | ||
830 | 664 | route_addr = route.get('to') | ||
831 | 665 | if "/" in route_addr: | ||
832 | 666 | route_addr, route_cidr = route_addr.split("/") | ||
833 | 667 | route_netmask = cidr2mask(route_cidr) | ||
834 | 668 | subnet_route = { | ||
835 | 669 | 'address': route_addr, | ||
836 | 670 | 'netmask': route_netmask, | ||
837 | 671 | 'gateway': route.get('via') | ||
838 | 672 | } | ||
839 | 673 | routes.append(subnet_route) | ||
840 | 674 | if len(routes) > 0: | ||
841 | 675 | subnet.update({'routes': routes}) | ||
842 | 676 | |||
843 | 677 | if ":" in address: | ||
844 | 678 | if 'gateway6' in cfg and gateway6 is None: | ||
845 | 679 | gateway6 = cfg.get('gateway6') | ||
846 | 680 | subnet.update({'gateway': gateway6}) | ||
847 | 681 | else: | ||
848 | 682 | if 'gateway4' in cfg and gateway4 is None: | ||
849 | 683 | gateway4 = cfg.get('gateway4') | ||
850 | 684 | subnet.update({'gateway': gateway4}) | ||
851 | 685 | |||
852 | 686 | subnets.append(subnet) | ||
853 | 687 | return subnets | ||
854 | 688 | |||
855 | 689 | |||
856 | 690 | def subnet_is_ipv6(subnet): | ||
857 | 691 | """ Common helper for checking network_state subnets for ipv6""" | ||
858 | 692 | # 'static6' or 'dhcp6' | ||
859 | 693 | if subnet['type'].endswith('6'): | ||
860 | 694 | # This is a request for DHCPv6. | ||
861 | 695 | return True | ||
862 | 696 | elif subnet['type'] == 'static' and ":" in subnet['address']: | ||
863 | 697 | return True | ||
864 | 698 | return False | ||
865 | 699 | |||
866 | 410 | 700 | ||
867 | 411 | def cidr2mask(cidr): | 701 | def cidr2mask(cidr): |
868 | 412 | mask = [0, 0, 0, 0] | 702 | mask = [0, 0, 0, 0] |
869 | diff --git a/cloudinit/net/renderers.py b/cloudinit/net/renderers.py | |||
870 | index 5ad8455..5117b4a 100644 | |||
871 | --- a/cloudinit/net/renderers.py | |||
872 | +++ b/cloudinit/net/renderers.py | |||
873 | @@ -1,15 +1,17 @@ | |||
874 | 1 | # This file is part of cloud-init. See LICENSE file for license information. | 1 | # This file is part of cloud-init. See LICENSE file for license information. |
875 | 2 | 2 | ||
876 | 3 | from . import eni | 3 | from . import eni |
877 | 4 | from . import netplan | ||
878 | 4 | from . import RendererNotFoundError | 5 | from . import RendererNotFoundError |
879 | 5 | from . import sysconfig | 6 | from . import sysconfig |
880 | 6 | 7 | ||
881 | 7 | NAME_TO_RENDERER = { | 8 | NAME_TO_RENDERER = { |
882 | 8 | "eni": eni, | 9 | "eni": eni, |
883 | 10 | "netplan": netplan, | ||
884 | 9 | "sysconfig": sysconfig, | 11 | "sysconfig": sysconfig, |
885 | 10 | } | 12 | } |
886 | 11 | 13 | ||
888 | 12 | DEFAULT_PRIORITY = ["eni", "sysconfig"] | 14 | DEFAULT_PRIORITY = ["eni", "sysconfig", "netplan"] |
889 | 13 | 15 | ||
890 | 14 | 16 | ||
891 | 15 | def search(priority=None, target=None, first=False): | 17 | def search(priority=None, target=None, first=False): |
892 | diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py | |||
893 | index 117b515..504e4d0 100644 | |||
894 | --- a/cloudinit/net/sysconfig.py | |||
895 | +++ b/cloudinit/net/sysconfig.py | |||
896 | @@ -9,6 +9,7 @@ from cloudinit.distros.parsers import resolv_conf | |||
897 | 9 | from cloudinit import util | 9 | from cloudinit import util |
898 | 10 | 10 | ||
899 | 11 | from . import renderer | 11 | from . import renderer |
900 | 12 | from .network_state import subnet_is_ipv6 | ||
901 | 12 | 13 | ||
902 | 13 | 14 | ||
903 | 14 | def _make_header(sep='#'): | 15 | def _make_header(sep='#'): |
904 | @@ -194,7 +195,7 @@ class Renderer(renderer.Renderer): | |||
905 | 194 | def __init__(self, config=None): | 195 | def __init__(self, config=None): |
906 | 195 | if not config: | 196 | if not config: |
907 | 196 | config = {} | 197 | config = {} |
909 | 197 | self.sysconf_dir = config.get('sysconf_dir', 'etc/sysconfig/') | 198 | self.sysconf_dir = config.get('sysconf_dir', 'etc/sysconfig') |
910 | 198 | self.netrules_path = config.get( | 199 | self.netrules_path = config.get( |
911 | 199 | 'netrules_path', 'etc/udev/rules.d/70-persistent-net.rules') | 200 | 'netrules_path', 'etc/udev/rules.d/70-persistent-net.rules') |
912 | 200 | self.dns_path = config.get('dns_path', 'etc/resolv.conf') | 201 | self.dns_path = config.get('dns_path', 'etc/resolv.conf') |
913 | @@ -220,7 +221,7 @@ class Renderer(renderer.Renderer): | |||
914 | 220 | iface_cfg['BOOTPROTO'] = 'dhcp' | 221 | iface_cfg['BOOTPROTO'] = 'dhcp' |
915 | 221 | elif subnet_type == 'static': | 222 | elif subnet_type == 'static': |
916 | 222 | iface_cfg['BOOTPROTO'] = 'static' | 223 | iface_cfg['BOOTPROTO'] = 'static' |
918 | 223 | if subnet.get('ipv6'): | 224 | if subnet_is_ipv6(subnet): |
919 | 224 | iface_cfg['IPV6ADDR'] = subnet['address'] | 225 | iface_cfg['IPV6ADDR'] = subnet['address'] |
920 | 225 | iface_cfg['IPV6INIT'] = True | 226 | iface_cfg['IPV6INIT'] = True |
921 | 226 | else: | 227 | else: |
922 | @@ -390,19 +391,28 @@ class Renderer(renderer.Renderer): | |||
923 | 390 | return contents | 391 | return contents |
924 | 391 | 392 | ||
925 | 392 | def render_network_state(self, network_state, target=None): | 393 | def render_network_state(self, network_state, target=None): |
926 | 394 | file_mode = 0o644 | ||
927 | 393 | base_sysconf_dir = util.target_path(target, self.sysconf_dir) | 395 | base_sysconf_dir = util.target_path(target, self.sysconf_dir) |
928 | 394 | for path, data in self._render_sysconfig(base_sysconf_dir, | 396 | for path, data in self._render_sysconfig(base_sysconf_dir, |
929 | 395 | network_state).items(): | 397 | network_state).items(): |
931 | 396 | util.write_file(path, data) | 398 | util.write_file(path, data, file_mode) |
932 | 397 | if self.dns_path: | 399 | if self.dns_path: |
933 | 398 | dns_path = util.target_path(target, self.dns_path) | 400 | dns_path = util.target_path(target, self.dns_path) |
934 | 399 | resolv_content = self._render_dns(network_state, | 401 | resolv_content = self._render_dns(network_state, |
935 | 400 | existing_dns_path=dns_path) | 402 | existing_dns_path=dns_path) |
937 | 401 | util.write_file(dns_path, resolv_content) | 403 | util.write_file(dns_path, resolv_content, file_mode) |
938 | 402 | if self.netrules_path: | 404 | if self.netrules_path: |
939 | 403 | netrules_content = self._render_persistent_net(network_state) | 405 | netrules_content = self._render_persistent_net(network_state) |
940 | 404 | netrules_path = util.target_path(target, self.netrules_path) | 406 | netrules_path = util.target_path(target, self.netrules_path) |
942 | 405 | util.write_file(netrules_path, netrules_content) | 407 | util.write_file(netrules_path, netrules_content, file_mode) |
943 | 408 | |||
944 | 409 | # always write /etc/sysconfig/network configuration | ||
945 | 410 | sysconfig_path = util.target_path(target, "etc/sysconfig/network") | ||
946 | 411 | netcfg = [_make_header(), 'NETWORKING=yes'] | ||
947 | 412 | if network_state.use_ipv6: | ||
948 | 413 | netcfg.append('NETWORKING_IPV6=yes') | ||
949 | 414 | netcfg.append('IPV6_AUTOCONF=no') | ||
950 | 415 | util.write_file(sysconfig_path, "\n".join(netcfg) + "\n", file_mode) | ||
951 | 406 | 416 | ||
952 | 407 | 417 | ||
953 | 408 | def available(target=None): | 418 | def available(target=None): |
954 | diff --git a/systemd/cloud-init.service b/systemd/cloud-init.service | |||
955 | index fb3b918..39acc20 100644 | |||
956 | --- a/systemd/cloud-init.service | |||
957 | +++ b/systemd/cloud-init.service | |||
958 | @@ -5,6 +5,7 @@ Wants=cloud-init-local.service | |||
959 | 5 | Wants=sshd-keygen.service | 5 | Wants=sshd-keygen.service |
960 | 6 | Wants=sshd.service | 6 | Wants=sshd.service |
961 | 7 | After=cloud-init-local.service | 7 | After=cloud-init-local.service |
962 | 8 | After=systemd-networkd-wait-online.service | ||
963 | 8 | After=networking.service | 9 | After=networking.service |
964 | 9 | Before=network-online.target | 10 | Before=network-online.target |
965 | 10 | Before=sshd-keygen.service | 11 | Before=sshd-keygen.service |
966 | diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py | |||
967 | index bde3bb5..d187c9d 100644 | |||
968 | --- a/tests/unittests/test_distros/test_netconfig.py | |||
969 | +++ b/tests/unittests/test_distros/test_netconfig.py | |||
970 | @@ -19,6 +19,7 @@ from cloudinit.distros.parsers.sys_conf import SysConf | |||
971 | 19 | from cloudinit import helpers | 19 | from cloudinit import helpers |
972 | 20 | from cloudinit import settings | 20 | from cloudinit import settings |
973 | 21 | from cloudinit import util | 21 | from cloudinit import util |
974 | 22 | from cloudinit.net import eni | ||
975 | 22 | 23 | ||
976 | 23 | 24 | ||
977 | 24 | BASE_NET_CFG = ''' | 25 | BASE_NET_CFG = ''' |
978 | @@ -28,10 +29,10 @@ iface lo inet loopback | |||
979 | 28 | auto eth0 | 29 | auto eth0 |
980 | 29 | iface eth0 inet static | 30 | iface eth0 inet static |
981 | 30 | address 192.168.1.5 | 31 | address 192.168.1.5 |
982 | 31 | netmask 255.255.255.0 | ||
983 | 32 | network 192.168.0.0 | ||
984 | 33 | broadcast 192.168.1.0 | 32 | broadcast 192.168.1.0 |
985 | 34 | gateway 192.168.1.254 | 33 | gateway 192.168.1.254 |
986 | 34 | netmask 255.255.255.0 | ||
987 | 35 | network 192.168.0.0 | ||
988 | 35 | 36 | ||
989 | 36 | auto eth1 | 37 | auto eth1 |
990 | 37 | iface eth1 inet dhcp | 38 | iface eth1 inet dhcp |
991 | @@ -67,6 +68,100 @@ iface eth1 inet6 static | |||
992 | 67 | gateway 2607:f0d0:1002:0011::1 | 68 | gateway 2607:f0d0:1002:0011::1 |
993 | 68 | ''' | 69 | ''' |
994 | 69 | 70 | ||
995 | 71 | V1_NET_CFG = {'config': [{'name': 'eth0', | ||
996 | 72 | |||
997 | 73 | 'subnets': [{'address': '192.168.1.5', | ||
998 | 74 | 'broadcast': '192.168.1.0', | ||
999 | 75 | 'gateway': '192.168.1.254', | ||
1000 | 76 | 'netmask': '255.255.255.0', | ||
1001 | 77 | 'type': 'static'}], | ||
1002 | 78 | 'type': 'physical'}, | ||
1003 | 79 | {'name': 'eth1', | ||
1004 | 80 | 'subnets': [{'control': 'auto', 'type': 'dhcp4'}], | ||
1005 | 81 | 'type': 'physical'}], | ||
1006 | 82 | 'version': 1} | ||
1007 | 83 | |||
1008 | 84 | V1_NET_CFG_OUTPUT = """ | ||
1009 | 85 | # This file is generated from information provided by | ||
1010 | 86 | # the datasource. Changes to it will not persist across an instance. | ||
1011 | 87 | # To disable cloud-init's network configuration capabilities, write a file | ||
1012 | 88 | # /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following: | ||
1013 | 89 | # network: {config: disabled} | ||
1014 | 90 | auto lo | ||
1015 | 91 | iface lo inet loopback | ||
1016 | 92 | |||
1017 | 93 | auto eth0 | ||
1018 | 94 | iface eth0 inet static | ||
1019 | 95 | address 192.168.1.5 | ||
1020 | 96 | broadcast 192.168.1.0 | ||
1021 | 97 | gateway 192.168.1.254 | ||
1022 | 98 | netmask 255.255.255.0 | ||
1023 | 99 | |||
1024 | 100 | auto eth1 | ||
1025 | 101 | iface eth1 inet dhcp | ||
1026 | 102 | """ | ||
1027 | 103 | |||
1028 | 104 | V1_NET_CFG_IPV6 = {'config': [{'name': 'eth0', | ||
1029 | 105 | 'subnets': [{'address': | ||
1030 | 106 | '2607:f0d0:1002:0011::2', | ||
1031 | 107 | 'gateway': | ||
1032 | 108 | '2607:f0d0:1002:0011::1', | ||
1033 | 109 | 'netmask': '64', | ||
1034 | 110 | 'type': 'static'}], | ||
1035 | 111 | 'type': 'physical'}, | ||
1036 | 112 | {'name': 'eth1', | ||
1037 | 113 | 'subnets': [{'control': 'auto', | ||
1038 | 114 | 'type': 'dhcp4'}], | ||
1039 | 115 | 'type': 'physical'}], | ||
1040 | 116 | 'version': 1} | ||
1041 | 117 | |||
1042 | 118 | |||
1043 | 119 | V1_TO_V2_NET_CFG_OUTPUT = """ | ||
1044 | 120 | # This file is generated from information provided by | ||
1045 | 121 | # the datasource. Changes to it will not persist across an instance. | ||
1046 | 122 | # To disable cloud-init's network configuration capabilities, write a file | ||
1047 | 123 | # /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following: | ||
1048 | 124 | # network: {config: disabled} | ||
1049 | 125 | network: | ||
1050 | 126 | version: 2 | ||
1051 | 127 | ethernets: | ||
1052 | 128 | eth0: | ||
1053 | 129 | addresses: | ||
1054 | 130 | - 192.168.1.5/255.255.255.0 | ||
1055 | 131 | gateway4: 192.168.1.254 | ||
1056 | 132 | eth1: | ||
1057 | 133 | dhcp4: true | ||
1058 | 134 | """ | ||
1059 | 135 | |||
1060 | 136 | V2_NET_CFG = { | ||
1061 | 137 | 'ethernets': { | ||
1062 | 138 | 'eth7': { | ||
1063 | 139 | 'addresses': ['192.168.1.5/255.255.255.0'], | ||
1064 | 140 | 'gateway4': '192.168.1.254'}, | ||
1065 | 141 | 'eth9': { | ||
1066 | 142 | 'dhcp4': True} | ||
1067 | 143 | }, | ||
1068 | 144 | 'version': 2 | ||
1069 | 145 | } | ||
1070 | 146 | |||
1071 | 147 | |||
1072 | 148 | V2_TO_V2_NET_CFG_OUTPUT = """ | ||
1073 | 149 | # This file is generated from information provided by | ||
1074 | 150 | # the datasource. Changes to it will not persist across an instance. | ||
1075 | 151 | # To disable cloud-init's network configuration capabilities, write a file | ||
1076 | 152 | # /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following: | ||
1077 | 153 | # network: {config: disabled} | ||
1078 | 154 | network: | ||
1079 | 155 | version: 2 | ||
1080 | 156 | ethernets: | ||
1081 | 157 | eth7: | ||
1082 | 158 | addresses: | ||
1083 | 159 | - 192.168.1.5/255.255.255.0 | ||
1084 | 160 | gateway4: 192.168.1.254 | ||
1085 | 161 | eth9: | ||
1086 | 162 | dhcp4: true | ||
1087 | 163 | """ | ||
1088 | 164 | |||
1089 | 70 | 165 | ||
1090 | 71 | class WriteBuffer(object): | 166 | class WriteBuffer(object): |
1091 | 72 | def __init__(self): | 167 | def __init__(self): |
1092 | @@ -83,12 +178,14 @@ class WriteBuffer(object): | |||
1093 | 83 | 178 | ||
1094 | 84 | class TestNetCfgDistro(TestCase): | 179 | class TestNetCfgDistro(TestCase): |
1095 | 85 | 180 | ||
1097 | 86 | def _get_distro(self, dname): | 181 | def _get_distro(self, dname, renderers=None): |
1098 | 87 | cls = distros.fetch(dname) | 182 | cls = distros.fetch(dname) |
1099 | 88 | cfg = settings.CFG_BUILTIN | 183 | cfg = settings.CFG_BUILTIN |
1100 | 89 | cfg['system_info']['distro'] = dname | 184 | cfg['system_info']['distro'] = dname |
1101 | 185 | if renderers: | ||
1102 | 186 | cfg['system_info']['network'] = {'renderers': renderers} | ||
1103 | 90 | paths = helpers.Paths({}) | 187 | paths = helpers.Paths({}) |
1105 | 91 | return cls(dname, cfg, paths) | 188 | return cls(dname, cfg.get('system_info'), paths) |
1106 | 92 | 189 | ||
1107 | 93 | def test_simple_write_ub(self): | 190 | def test_simple_write_ub(self): |
1108 | 94 | ub_distro = self._get_distro('ubuntu') | 191 | ub_distro = self._get_distro('ubuntu') |
1109 | @@ -116,6 +213,107 @@ class TestNetCfgDistro(TestCase): | |||
1110 | 116 | self.assertEqual(str(write_buf).strip(), BASE_NET_CFG.strip()) | 213 | self.assertEqual(str(write_buf).strip(), BASE_NET_CFG.strip()) |
1111 | 117 | self.assertEqual(write_buf.mode, 0o644) | 214 | self.assertEqual(write_buf.mode, 0o644) |
1112 | 118 | 215 | ||
1113 | 216 | def test_apply_network_config_eni_ub(self): | ||
1114 | 217 | ub_distro = self._get_distro('ubuntu') | ||
1115 | 218 | with ExitStack() as mocks: | ||
1116 | 219 | write_bufs = {} | ||
1117 | 220 | |||
1118 | 221 | def replace_write(filename, content, mode=0o644, omode="wb"): | ||
1119 | 222 | buf = WriteBuffer() | ||
1120 | 223 | buf.mode = mode | ||
1121 | 224 | buf.omode = omode | ||
1122 | 225 | buf.write(content) | ||
1123 | 226 | write_bufs[filename] = buf | ||
1124 | 227 | |||
1125 | 228 | # eni availability checks | ||
1126 | 229 | mocks.enter_context( | ||
1127 | 230 | mock.patch.object(util, 'which', return_value=True)) | ||
1128 | 231 | mocks.enter_context( | ||
1129 | 232 | mock.patch.object(eni, 'available', return_value=True)) | ||
1130 | 233 | mocks.enter_context( | ||
1131 | 234 | mock.patch.object(util, 'ensure_dir')) | ||
1132 | 235 | mocks.enter_context( | ||
1133 | 236 | mock.patch.object(util, 'write_file', replace_write)) | ||
1134 | 237 | mocks.enter_context( | ||
1135 | 238 | mock.patch.object(os.path, 'isfile', return_value=False)) | ||
1136 | 239 | |||
1137 | 240 | ub_distro.apply_network_config(V1_NET_CFG, False) | ||
1138 | 241 | |||
1139 | 242 | self.assertEqual(len(write_bufs), 2) | ||
1140 | 243 | eni_name = '/etc/network/interfaces.d/50-cloud-init.cfg' | ||
1141 | 244 | self.assertIn(eni_name, write_bufs) | ||
1142 | 245 | write_buf = write_bufs[eni_name] | ||
1143 | 246 | self.assertEqual(str(write_buf).strip(), V1_NET_CFG_OUTPUT.strip()) | ||
1144 | 247 | self.assertEqual(write_buf.mode, 0o644) | ||
1145 | 248 | |||
1146 | 249 | def test_apply_network_config_v1_to_netplan_ub(self): | ||
1147 | 250 | renderers = ['netplan'] | ||
1148 | 251 | ub_distro = self._get_distro('ubuntu', renderers=renderers) | ||
1149 | 252 | with ExitStack() as mocks: | ||
1150 | 253 | write_bufs = {} | ||
1151 | 254 | |||
1152 | 255 | def replace_write(filename, content, mode=0o644, omode="wb"): | ||
1153 | 256 | buf = WriteBuffer() | ||
1154 | 257 | buf.mode = mode | ||
1155 | 258 | buf.omode = omode | ||
1156 | 259 | buf.write(content) | ||
1157 | 260 | write_bufs[filename] = buf | ||
1158 | 261 | |||
1159 | 262 | mocks.enter_context( | ||
1160 | 263 | mock.patch.object(util, 'which', return_value=True)) | ||
1161 | 264 | mocks.enter_context( | ||
1162 | 265 | mock.patch.object(util, 'write_file', replace_write)) | ||
1163 | 266 | mocks.enter_context( | ||
1164 | 267 | mock.patch.object(util, 'ensure_dir')) | ||
1165 | 268 | mocks.enter_context( | ||
1166 | 269 | mock.patch.object(util, 'subp', return_value=(0, 0))) | ||
1167 | 270 | mocks.enter_context( | ||
1168 | 271 | mock.patch.object(os.path, 'isfile', return_value=False)) | ||
1169 | 272 | |||
1170 | 273 | ub_distro.apply_network_config(V1_NET_CFG, False) | ||
1171 | 274 | |||
1172 | 275 | self.assertEqual(len(write_bufs), 1) | ||
1173 | 276 | netplan_name = '/etc/netplan/50-cloud-init.yaml' | ||
1174 | 277 | self.assertIn(netplan_name, write_bufs) | ||
1175 | 278 | write_buf = write_bufs[netplan_name] | ||
1176 | 279 | self.assertEqual(str(write_buf).strip(), | ||
1177 | 280 | V1_TO_V2_NET_CFG_OUTPUT.strip()) | ||
1178 | 281 | self.assertEqual(write_buf.mode, 0o644) | ||
1179 | 282 | |||
1180 | 283 | def test_apply_network_config_v2_passthrough_ub(self): | ||
1181 | 284 | renderers = ['netplan'] | ||
1182 | 285 | ub_distro = self._get_distro('ubuntu', renderers=renderers) | ||
1183 | 286 | with ExitStack() as mocks: | ||
1184 | 287 | write_bufs = {} | ||
1185 | 288 | |||
1186 | 289 | def replace_write(filename, content, mode=0o644, omode="wb"): | ||
1187 | 290 | buf = WriteBuffer() | ||
1188 | 291 | buf.mode = mode | ||
1189 | 292 | buf.omode = omode | ||
1190 | 293 | buf.write(content) | ||
1191 | 294 | write_bufs[filename] = buf | ||
1192 | 295 | |||
1193 | 296 | mocks.enter_context( | ||
1194 | 297 | mock.patch.object(util, 'which', return_value=True)) | ||
1195 | 298 | mocks.enter_context( | ||
1196 | 299 | mock.patch.object(util, 'write_file', replace_write)) | ||
1197 | 300 | mocks.enter_context( | ||
1198 | 301 | mock.patch.object(util, 'ensure_dir')) | ||
1199 | 302 | mocks.enter_context( | ||
1200 | 303 | mock.patch.object(util, 'subp', return_value=(0, 0))) | ||
1201 | 304 | mocks.enter_context( | ||
1202 | 305 | mock.patch.object(os.path, 'isfile', return_value=False)) | ||
1203 | 306 | |||
1204 | 307 | ub_distro.apply_network_config(V2_NET_CFG, False) | ||
1205 | 308 | |||
1206 | 309 | self.assertEqual(len(write_bufs), 1) | ||
1207 | 310 | netplan_name = '/etc/netplan/50-cloud-init.yaml' | ||
1208 | 311 | self.assertIn(netplan_name, write_bufs) | ||
1209 | 312 | write_buf = write_bufs[netplan_name] | ||
1210 | 313 | self.assertEqual(str(write_buf).strip(), | ||
1211 | 314 | V2_TO_V2_NET_CFG_OUTPUT.strip()) | ||
1212 | 315 | self.assertEqual(write_buf.mode, 0o644) | ||
1213 | 316 | |||
1214 | 119 | def assertCfgEquals(self, blob1, blob2): | 317 | def assertCfgEquals(self, blob1, blob2): |
1215 | 120 | b1 = dict(SysConf(blob1.strip().splitlines())) | 318 | b1 = dict(SysConf(blob1.strip().splitlines())) |
1216 | 121 | b2 = dict(SysConf(blob2.strip().splitlines())) | 319 | b2 = dict(SysConf(blob2.strip().splitlines())) |
1217 | @@ -195,6 +393,79 @@ NETWORKING=yes | |||
1218 | 195 | self.assertCfgEquals(expected_buf, str(write_buf)) | 393 | self.assertCfgEquals(expected_buf, str(write_buf)) |
1219 | 196 | self.assertEqual(write_buf.mode, 0o644) | 394 | self.assertEqual(write_buf.mode, 0o644) |
1220 | 197 | 395 | ||
1221 | 396 | def test_apply_network_config_rh(self): | ||
1222 | 397 | renderers = ['sysconfig'] | ||
1223 | 398 | rh_distro = self._get_distro('rhel', renderers=renderers) | ||
1224 | 399 | |||
1225 | 400 | write_bufs = {} | ||
1226 | 401 | |||
1227 | 402 | def replace_write(filename, content, mode=0o644, omode="wb"): | ||
1228 | 403 | buf = WriteBuffer() | ||
1229 | 404 | buf.mode = mode | ||
1230 | 405 | buf.omode = omode | ||
1231 | 406 | buf.write(content) | ||
1232 | 407 | write_bufs[filename] = buf | ||
1233 | 408 | |||
1234 | 409 | with ExitStack() as mocks: | ||
1235 | 410 | # sysconfig availability checks | ||
1236 | 411 | mocks.enter_context( | ||
1237 | 412 | mock.patch.object(util, 'which', return_value=True)) | ||
1238 | 413 | mocks.enter_context( | ||
1239 | 414 | mock.patch.object(util, 'write_file', replace_write)) | ||
1240 | 415 | mocks.enter_context( | ||
1241 | 416 | mock.patch.object(util, 'load_file', return_value='')) | ||
1242 | 417 | mocks.enter_context( | ||
1243 | 418 | mock.patch.object(os.path, 'isfile', return_value=True)) | ||
1244 | 419 | |||
1245 | 420 | rh_distro.apply_network_config(V1_NET_CFG, False) | ||
1246 | 421 | |||
1247 | 422 | self.assertEqual(len(write_bufs), 5) | ||
1248 | 423 | |||
1249 | 424 | # eth0 | ||
1250 | 425 | self.assertIn('/etc/sysconfig/network-scripts/ifcfg-eth0', | ||
1251 | 426 | write_bufs) | ||
1252 | 427 | write_buf = write_bufs['/etc/sysconfig/network-scripts/ifcfg-eth0'] | ||
1253 | 428 | expected_buf = ''' | ||
1254 | 429 | # Created by cloud-init on instance boot automatically, do not edit. | ||
1255 | 430 | # | ||
1256 | 431 | BOOTPROTO=static | ||
1257 | 432 | DEVICE=eth0 | ||
1258 | 433 | IPADDR=192.168.1.5 | ||
1259 | 434 | NETMASK=255.255.255.0 | ||
1260 | 435 | NM_CONTROLLED=no | ||
1261 | 436 | ONBOOT=yes | ||
1262 | 437 | TYPE=Ethernet | ||
1263 | 438 | USERCTL=no | ||
1264 | 439 | ''' | ||
1265 | 440 | self.assertCfgEquals(expected_buf, str(write_buf)) | ||
1266 | 441 | self.assertEqual(write_buf.mode, 0o644) | ||
1267 | 442 | |||
1268 | 443 | # eth1 | ||
1269 | 444 | self.assertIn('/etc/sysconfig/network-scripts/ifcfg-eth1', | ||
1270 | 445 | write_bufs) | ||
1271 | 446 | write_buf = write_bufs['/etc/sysconfig/network-scripts/ifcfg-eth1'] | ||
1272 | 447 | expected_buf = ''' | ||
1273 | 448 | # Created by cloud-init on instance boot automatically, do not edit. | ||
1274 | 449 | # | ||
1275 | 450 | BOOTPROTO=dhcp | ||
1276 | 451 | DEVICE=eth1 | ||
1277 | 452 | NM_CONTROLLED=no | ||
1278 | 453 | ONBOOT=yes | ||
1279 | 454 | TYPE=Ethernet | ||
1280 | 455 | USERCTL=no | ||
1281 | 456 | ''' | ||
1282 | 457 | self.assertCfgEquals(expected_buf, str(write_buf)) | ||
1283 | 458 | self.assertEqual(write_buf.mode, 0o644) | ||
1284 | 459 | |||
1285 | 460 | self.assertIn('/etc/sysconfig/network', write_bufs) | ||
1286 | 461 | write_buf = write_bufs['/etc/sysconfig/network'] | ||
1287 | 462 | expected_buf = ''' | ||
1288 | 463 | # Created by cloud-init v. 0.7 | ||
1289 | 464 | NETWORKING=yes | ||
1290 | 465 | ''' | ||
1291 | 466 | self.assertCfgEquals(expected_buf, str(write_buf)) | ||
1292 | 467 | self.assertEqual(write_buf.mode, 0o644) | ||
1293 | 468 | |||
1294 | 198 | def test_write_ipv6_rhel(self): | 469 | def test_write_ipv6_rhel(self): |
1295 | 199 | rh_distro = self._get_distro('rhel') | 470 | rh_distro = self._get_distro('rhel') |
1296 | 200 | 471 | ||
1297 | @@ -274,6 +545,78 @@ IPV6_AUTOCONF=no | |||
1298 | 274 | self.assertCfgEquals(expected_buf, str(write_buf)) | 545 | self.assertCfgEquals(expected_buf, str(write_buf)) |
1299 | 275 | self.assertEqual(write_buf.mode, 0o644) | 546 | self.assertEqual(write_buf.mode, 0o644) |
1300 | 276 | 547 | ||
1301 | 548 | def test_apply_network_config_ipv6_rh(self): | ||
1302 | 549 | renderers = ['sysconfig'] | ||
1303 | 550 | rh_distro = self._get_distro('rhel', renderers=renderers) | ||
1304 | 551 | |||
1305 | 552 | write_bufs = {} | ||
1306 | 553 | |||
1307 | 554 | def replace_write(filename, content, mode=0o644, omode="wb"): | ||
1308 | 555 | buf = WriteBuffer() | ||
1309 | 556 | buf.mode = mode | ||
1310 | 557 | buf.omode = omode | ||
1311 | 558 | buf.write(content) | ||
1312 | 559 | write_bufs[filename] = buf | ||
1313 | 560 | |||
1314 | 561 | with ExitStack() as mocks: | ||
1315 | 562 | mocks.enter_context( | ||
1316 | 563 | mock.patch.object(util, 'which', return_value=True)) | ||
1317 | 564 | mocks.enter_context( | ||
1318 | 565 | mock.patch.object(util, 'write_file', replace_write)) | ||
1319 | 566 | mocks.enter_context( | ||
1320 | 567 | mock.patch.object(util, 'load_file', return_value='')) | ||
1321 | 568 | mocks.enter_context( | ||
1322 | 569 | mock.patch.object(os.path, 'isfile', return_value=True)) | ||
1323 | 570 | |||
1324 | 571 | rh_distro.apply_network_config(V1_NET_CFG_IPV6, False) | ||
1325 | 572 | |||
1326 | 573 | self.assertEqual(len(write_bufs), 5) | ||
1327 | 574 | |||
1328 | 575 | self.assertIn('/etc/sysconfig/network-scripts/ifcfg-eth0', | ||
1329 | 576 | write_bufs) | ||
1330 | 577 | write_buf = write_bufs['/etc/sysconfig/network-scripts/ifcfg-eth0'] | ||
1331 | 578 | expected_buf = ''' | ||
1332 | 579 | # Created by cloud-init on instance boot automatically, do not edit. | ||
1333 | 580 | # | ||
1334 | 581 | BOOTPROTO=static | ||
1335 | 582 | DEVICE=eth0 | ||
1336 | 583 | IPV6ADDR=2607:f0d0:1002:0011::2 | ||
1337 | 584 | IPV6INIT=yes | ||
1338 | 585 | NETMASK=64 | ||
1339 | 586 | NM_CONTROLLED=no | ||
1340 | 587 | ONBOOT=yes | ||
1341 | 588 | TYPE=Ethernet | ||
1342 | 589 | USERCTL=no | ||
1343 | 590 | ''' | ||
1344 | 591 | self.assertCfgEquals(expected_buf, str(write_buf)) | ||
1345 | 592 | self.assertEqual(write_buf.mode, 0o644) | ||
1346 | 593 | self.assertIn('/etc/sysconfig/network-scripts/ifcfg-eth1', | ||
1347 | 594 | write_bufs) | ||
1348 | 595 | write_buf = write_bufs['/etc/sysconfig/network-scripts/ifcfg-eth1'] | ||
1349 | 596 | expected_buf = ''' | ||
1350 | 597 | # Created by cloud-init on instance boot automatically, do not edit. | ||
1351 | 598 | # | ||
1352 | 599 | BOOTPROTO=dhcp | ||
1353 | 600 | DEVICE=eth1 | ||
1354 | 601 | NM_CONTROLLED=no | ||
1355 | 602 | ONBOOT=yes | ||
1356 | 603 | TYPE=Ethernet | ||
1357 | 604 | USERCTL=no | ||
1358 | 605 | ''' | ||
1359 | 606 | self.assertCfgEquals(expected_buf, str(write_buf)) | ||
1360 | 607 | self.assertEqual(write_buf.mode, 0o644) | ||
1361 | 608 | |||
1362 | 609 | self.assertIn('/etc/sysconfig/network', write_bufs) | ||
1363 | 610 | write_buf = write_bufs['/etc/sysconfig/network'] | ||
1364 | 611 | expected_buf = ''' | ||
1365 | 612 | # Created by cloud-init v. 0.7 | ||
1366 | 613 | NETWORKING=yes | ||
1367 | 614 | NETWORKING_IPV6=yes | ||
1368 | 615 | IPV6_AUTOCONF=no | ||
1369 | 616 | ''' | ||
1370 | 617 | self.assertCfgEquals(expected_buf, str(write_buf)) | ||
1371 | 618 | self.assertEqual(write_buf.mode, 0o644) | ||
1372 | 619 | |||
1373 | 277 | def test_simple_write_freebsd(self): | 620 | def test_simple_write_freebsd(self): |
1374 | 278 | fbsd_distro = self._get_distro('freebsd') | 621 | fbsd_distro = self._get_distro('freebsd') |
1375 | 279 | 622 | ||
1376 | diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py | |||
1377 | index 902204a..4f07d80 100644 | |||
1378 | --- a/tests/unittests/test_net.py | |||
1379 | +++ b/tests/unittests/test_net.py | |||
1380 | @@ -3,6 +3,7 @@ | |||
1381 | 3 | from cloudinit import net | 3 | from cloudinit import net |
1382 | 4 | from cloudinit.net import cmdline | 4 | from cloudinit.net import cmdline |
1383 | 5 | from cloudinit.net import eni | 5 | from cloudinit.net import eni |
1384 | 6 | from cloudinit.net import netplan | ||
1385 | 6 | from cloudinit.net import network_state | 7 | from cloudinit.net import network_state |
1386 | 7 | from cloudinit.net import renderers | 8 | from cloudinit.net import renderers |
1387 | 8 | from cloudinit.net import sysconfig | 9 | from cloudinit.net import sysconfig |
1388 | @@ -408,6 +409,41 @@ NETWORK_CONFIGS = { | |||
1389 | 408 | post-up route add default gw 65.61.151.37 || true | 409 | post-up route add default gw 65.61.151.37 || true |
1390 | 409 | pre-down route del default gw 65.61.151.37 || true | 410 | pre-down route del default gw 65.61.151.37 || true |
1391 | 410 | """).rstrip(' '), | 411 | """).rstrip(' '), |
1392 | 412 | 'expected_netplan': textwrap.dedent(""" | ||
1393 | 413 | network: | ||
1394 | 414 | version: 2 | ||
1395 | 415 | ethernets: | ||
1396 | 416 | eth1: | ||
1397 | 417 | match: | ||
1398 | 418 | macaddress: cf:d6:af:48:e8:80 | ||
1399 | 419 | nameservers: | ||
1400 | 420 | addresses: | ||
1401 | 421 | - 1.2.3.4 | ||
1402 | 422 | - 5.6.7.8 | ||
1403 | 423 | search: | ||
1404 | 424 | - wark.maas | ||
1405 | 425 | set-name: eth1 | ||
1406 | 426 | eth99: | ||
1407 | 427 | addresses: | ||
1408 | 428 | - 192.168.21.3/24 | ||
1409 | 429 | dhcp4: true | ||
1410 | 430 | match: | ||
1411 | 431 | macaddress: c0:d6:9f:2c:e8:80 | ||
1412 | 432 | nameservers: | ||
1413 | 433 | addresses: | ||
1414 | 434 | - 8.8.8.8 | ||
1415 | 435 | - 8.8.4.4 | ||
1416 | 436 | - 1.2.3.4 | ||
1417 | 437 | - 5.6.7.8 | ||
1418 | 438 | search: | ||
1419 | 439 | - barley.maas | ||
1420 | 440 | - sach.maas | ||
1421 | 441 | - wark.maas | ||
1422 | 442 | routes: | ||
1423 | 443 | - to: 0.0.0.0/0.0.0.0 | ||
1424 | 444 | via: 65.61.151.37 | ||
1425 | 445 | set-name: eth99 | ||
1426 | 446 | """).rstrip(' '), | ||
1427 | 411 | 'yaml': textwrap.dedent(""" | 447 | 'yaml': textwrap.dedent(""" |
1428 | 412 | version: 1 | 448 | version: 1 |
1429 | 413 | config: | 449 | config: |
1430 | @@ -450,6 +486,14 @@ NETWORK_CONFIGS = { | |||
1431 | 450 | # control-alias iface0 | 486 | # control-alias iface0 |
1432 | 451 | iface iface0 inet6 dhcp | 487 | iface iface0 inet6 dhcp |
1433 | 452 | """).rstrip(' '), | 488 | """).rstrip(' '), |
1434 | 489 | 'expected_netplan': textwrap.dedent(""" | ||
1435 | 490 | network: | ||
1436 | 491 | version: 2 | ||
1437 | 492 | ethernets: | ||
1438 | 493 | iface0: | ||
1439 | 494 | dhcp4: true | ||
1440 | 495 | dhcp6: true | ||
1441 | 496 | """).rstrip(' '), | ||
1442 | 453 | 'yaml': textwrap.dedent("""\ | 497 | 'yaml': textwrap.dedent("""\ |
1443 | 454 | version: 1 | 498 | version: 1 |
1444 | 455 | config: | 499 | config: |
1445 | @@ -524,6 +568,126 @@ iface eth0.101 inet static | |||
1446 | 524 | post-up route add -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true | 568 | post-up route add -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true |
1447 | 525 | pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true | 569 | pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true |
1448 | 526 | """), | 570 | """), |
1449 | 571 | 'expected_netplan': textwrap.dedent(""" | ||
1450 | 572 | network: | ||
1451 | 573 | version: 2 | ||
1452 | 574 | ethernets: | ||
1453 | 575 | eth0: | ||
1454 | 576 | match: | ||
1455 | 577 | macaddress: c0:d6:9f:2c:e8:80 | ||
1456 | 578 | nameservers: | ||
1457 | 579 | addresses: | ||
1458 | 580 | - 8.8.8.8 | ||
1459 | 581 | - 4.4.4.4 | ||
1460 | 582 | - 8.8.4.4 | ||
1461 | 583 | search: | ||
1462 | 584 | - barley.maas | ||
1463 | 585 | - wark.maas | ||
1464 | 586 | - foobar.maas | ||
1465 | 587 | set-name: eth0 | ||
1466 | 588 | eth1: | ||
1467 | 589 | match: | ||
1468 | 590 | macaddress: aa:d6:9f:2c:e8:80 | ||
1469 | 591 | nameservers: | ||
1470 | 592 | addresses: | ||
1471 | 593 | - 8.8.8.8 | ||
1472 | 594 | - 4.4.4.4 | ||
1473 | 595 | - 8.8.4.4 | ||
1474 | 596 | search: | ||
1475 | 597 | - barley.maas | ||
1476 | 598 | - wark.maas | ||
1477 | 599 | - foobar.maas | ||
1478 | 600 | set-name: eth1 | ||
1479 | 601 | eth2: | ||
1480 | 602 | match: | ||
1481 | 603 | macaddress: c0:bb:9f:2c:e8:80 | ||
1482 | 604 | nameservers: | ||
1483 | 605 | addresses: | ||
1484 | 606 | - 8.8.8.8 | ||
1485 | 607 | - 4.4.4.4 | ||
1486 | 608 | - 8.8.4.4 | ||
1487 | 609 | search: | ||
1488 | 610 | - barley.maas | ||
1489 | 611 | - wark.maas | ||
1490 | 612 | - foobar.maas | ||
1491 | 613 | set-name: eth2 | ||
1492 | 614 | eth3: | ||
1493 | 615 | match: | ||
1494 | 616 | macaddress: 66:bb:9f:2c:e8:80 | ||
1495 | 617 | nameservers: | ||
1496 | 618 | addresses: | ||
1497 | 619 | - 8.8.8.8 | ||
1498 | 620 | - 4.4.4.4 | ||
1499 | 621 | - 8.8.4.4 | ||
1500 | 622 | search: | ||
1501 | 623 | - barley.maas | ||
1502 | 624 | - wark.maas | ||
1503 | 625 | - foobar.maas | ||
1504 | 626 | set-name: eth3 | ||
1505 | 627 | eth4: | ||
1506 | 628 | match: | ||
1507 | 629 | macaddress: 98:bb:9f:2c:e8:80 | ||
1508 | 630 | nameservers: | ||
1509 | 631 | addresses: | ||
1510 | 632 | - 8.8.8.8 | ||
1511 | 633 | - 4.4.4.4 | ||
1512 | 634 | - 8.8.4.4 | ||
1513 | 635 | search: | ||
1514 | 636 | - barley.maas | ||
1515 | 637 | - wark.maas | ||
1516 | 638 | - foobar.maas | ||
1517 | 639 | set-name: eth4 | ||
1518 | 640 | eth5: | ||
1519 | 641 | dhcp4: true | ||
1520 | 642 | match: | ||
1521 | 643 | macaddress: 98:bb:9f:2c:e8:8a | ||
1522 | 644 | nameservers: | ||
1523 | 645 | addresses: | ||
1524 | 646 | - 8.8.8.8 | ||
1525 | 647 | - 4.4.4.4 | ||
1526 | 648 | - 8.8.4.4 | ||
1527 | 649 | search: | ||
1528 | 650 | - barley.maas | ||
1529 | 651 | - wark.maas | ||
1530 | 652 | - foobar.maas | ||
1531 | 653 | set-name: eth5 | ||
1532 | 654 | bonds: | ||
1533 | 655 | bond0: | ||
1534 | 656 | dhcp6: true | ||
1535 | 657 | interfaces: | ||
1536 | 658 | - eth1 | ||
1537 | 659 | - eth2 | ||
1538 | 660 | parameters: | ||
1539 | 661 | mode: active-backup | ||
1540 | 662 | bridges: | ||
1541 | 663 | br0: | ||
1542 | 664 | addresses: | ||
1543 | 665 | - 192.168.14.2/24 | ||
1544 | 666 | - 2001:1::1/64 | ||
1545 | 667 | interfaces: | ||
1546 | 668 | - eth3 | ||
1547 | 669 | - eth4 | ||
1548 | 670 | vlans: | ||
1549 | 671 | bond0.200: | ||
1550 | 672 | dhcp4: true | ||
1551 | 673 | id: 200 | ||
1552 | 674 | link: bond0 | ||
1553 | 675 | eth0.101: | ||
1554 | 676 | addresses: | ||
1555 | 677 | - 192.168.0.2/24 | ||
1556 | 678 | - 192.168.2.10/24 | ||
1557 | 679 | gateway4: 192.168.0.1 | ||
1558 | 680 | id: 101 | ||
1559 | 681 | link: eth0 | ||
1560 | 682 | nameservers: | ||
1561 | 683 | addresses: | ||
1562 | 684 | - 192.168.0.10 | ||
1563 | 685 | - 10.23.23.134 | ||
1564 | 686 | search: | ||
1565 | 687 | - barley.maas | ||
1566 | 688 | - sacchromyces.maas | ||
1567 | 689 | - brettanomyces.maas | ||
1568 | 690 | """).rstrip(' '), | ||
1569 | 527 | 'yaml': textwrap.dedent(""" | 691 | 'yaml': textwrap.dedent(""" |
1570 | 528 | version: 1 | 692 | version: 1 |
1571 | 529 | config: | 693 | config: |
1572 | @@ -808,6 +972,99 @@ iface eth0 inet dhcp | |||
1573 | 808 | expected, dir2dict(tmp_dir)['/etc/network/interfaces']) | 972 | expected, dir2dict(tmp_dir)['/etc/network/interfaces']) |
1574 | 809 | 973 | ||
1575 | 810 | 974 | ||
1576 | 975 | class TestNetplanNetRendering(CiTestCase): | ||
1577 | 976 | |||
1578 | 977 | @mock.patch("cloudinit.net.sys_dev_path") | ||
1579 | 978 | @mock.patch("cloudinit.net.read_sys_net") | ||
1580 | 979 | @mock.patch("cloudinit.net.get_devicelist") | ||
1581 | 980 | def test_default_generation(self, mock_get_devicelist, | ||
1582 | 981 | mock_read_sys_net, | ||
1583 | 982 | mock_sys_dev_path): | ||
1584 | 983 | tmp_dir = self.tmp_dir() | ||
1585 | 984 | _setup_test(tmp_dir, mock_get_devicelist, | ||
1586 | 985 | mock_read_sys_net, mock_sys_dev_path) | ||
1587 | 986 | |||
1588 | 987 | network_cfg = net.generate_fallback_config() | ||
1589 | 988 | ns = network_state.parse_net_config_data(network_cfg, | ||
1590 | 989 | skip_broken=False) | ||
1591 | 990 | |||
1592 | 991 | render_dir = os.path.join(tmp_dir, "render") | ||
1593 | 992 | os.makedirs(render_dir) | ||
1594 | 993 | |||
1595 | 994 | render_target = 'netplan.yaml' | ||
1596 | 995 | renderer = netplan.Renderer( | ||
1597 | 996 | {'netplan_path': render_target, 'postcmds': False}) | ||
1598 | 997 | renderer.render_network_state(render_dir, ns) | ||
1599 | 998 | |||
1600 | 999 | self.assertTrue(os.path.exists(os.path.join(render_dir, | ||
1601 | 1000 | render_target))) | ||
1602 | 1001 | with open(os.path.join(render_dir, render_target)) as fh: | ||
1603 | 1002 | contents = fh.read() | ||
1604 | 1003 | print(contents) | ||
1605 | 1004 | |||
1606 | 1005 | expected = """ | ||
1607 | 1006 | network: | ||
1608 | 1007 | version: 2 | ||
1609 | 1008 | ethernets: | ||
1610 | 1009 | eth1000: | ||
1611 | 1010 | dhcp4: true | ||
1612 | 1011 | match: | ||
1613 | 1012 | macaddress: 07-1c-c6-75-a4-be | ||
1614 | 1013 | set-name: eth1000 | ||
1615 | 1014 | """ | ||
1616 | 1015 | self.assertEqual(expected.lstrip(), contents.lstrip()) | ||
1617 | 1016 | |||
1618 | 1017 | |||
1619 | 1018 | class TestNetplanPostcommands(CiTestCase): | ||
1620 | 1019 | mycfg = { | ||
1621 | 1020 | 'config': [{"type": "physical", "name": "eth0", | ||
1622 | 1021 | "mac_address": "c0:d6:9f:2c:e8:80", | ||
1623 | 1022 | "subnets": [{"type": "dhcp"}]}], | ||
1624 | 1023 | 'version': 1} | ||
1625 | 1024 | |||
1626 | 1025 | @mock.patch.object(netplan.Renderer, '_netplan_generate') | ||
1627 | 1026 | @mock.patch.object(netplan.Renderer, '_net_setup_link') | ||
1628 | 1027 | def test_netplan_render_calls_postcmds(self, mock_netplan_generate, | ||
1629 | 1028 | mock_net_setup_link): | ||
1630 | 1029 | tmp_dir = self.tmp_dir() | ||
1631 | 1030 | ns = network_state.parse_net_config_data(self.mycfg, | ||
1632 | 1031 | skip_broken=False) | ||
1633 | 1032 | |||
1634 | 1033 | render_dir = os.path.join(tmp_dir, "render") | ||
1635 | 1034 | os.makedirs(render_dir) | ||
1636 | 1035 | |||
1637 | 1036 | render_target = 'netplan.yaml' | ||
1638 | 1037 | renderer = netplan.Renderer( | ||
1639 | 1038 | {'netplan_path': render_target, 'postcmds': True}) | ||
1640 | 1039 | renderer.render_network_state(render_dir, ns) | ||
1641 | 1040 | |||
1642 | 1041 | mock_netplan_generate.assert_called_with(run=True) | ||
1643 | 1042 | mock_net_setup_link.assert_called_with(run=True) | ||
1644 | 1043 | |||
1645 | 1044 | @mock.patch.object(netplan, "get_devicelist") | ||
1646 | 1045 | @mock.patch('cloudinit.util.subp') | ||
1647 | 1046 | def test_netplan_postcmds(self, mock_subp, mock_devlist): | ||
1648 | 1047 | mock_devlist.side_effect = [['lo']] | ||
1649 | 1048 | tmp_dir = self.tmp_dir() | ||
1650 | 1049 | ns = network_state.parse_net_config_data(self.mycfg, | ||
1651 | 1050 | skip_broken=False) | ||
1652 | 1051 | |||
1653 | 1052 | render_dir = os.path.join(tmp_dir, "render") | ||
1654 | 1053 | os.makedirs(render_dir) | ||
1655 | 1054 | |||
1656 | 1055 | render_target = 'netplan.yaml' | ||
1657 | 1056 | renderer = netplan.Renderer( | ||
1658 | 1057 | {'netplan_path': render_target, 'postcmds': True}) | ||
1659 | 1058 | renderer.render_network_state(render_dir, ns) | ||
1660 | 1059 | |||
1661 | 1060 | expected = [ | ||
1662 | 1061 | mock.call(['netplan', 'generate'], capture=True), | ||
1663 | 1062 | mock.call(['udevadm', 'test-builtin', 'net_setup_link', | ||
1664 | 1063 | '/sys/class/net/lo'], capture=True), | ||
1665 | 1064 | ] | ||
1666 | 1065 | mock_subp.assert_has_calls(expected) | ||
1667 | 1066 | |||
1668 | 1067 | |||
1669 | 811 | class TestEniNetworkStateToEni(CiTestCase): | 1068 | class TestEniNetworkStateToEni(CiTestCase): |
1670 | 812 | mycfg = { | 1069 | mycfg = { |
1671 | 813 | 'config': [{"type": "physical", "name": "eth0", | 1070 | 'config': [{"type": "physical", "name": "eth0", |
1672 | @@ -953,6 +1210,50 @@ class TestCmdlineReadKernelConfig(CiTestCase): | |||
1673 | 953 | self.assertEqual(found['config'], expected) | 1210 | self.assertEqual(found['config'], expected) |
1674 | 954 | 1211 | ||
1675 | 955 | 1212 | ||
1676 | 1213 | class TestNetplanRoundTrip(CiTestCase): | ||
1677 | 1214 | def _render_and_read(self, network_config=None, state=None, | ||
1678 | 1215 | netplan_path=None, dir=None): | ||
1679 | 1216 | if dir is None: | ||
1680 | 1217 | dir = self.tmp_dir() | ||
1681 | 1218 | |||
1682 | 1219 | if network_config: | ||
1683 | 1220 | ns = network_state.parse_net_config_data(network_config) | ||
1684 | 1221 | elif state: | ||
1685 | 1222 | ns = state | ||
1686 | 1223 | else: | ||
1687 | 1224 | raise ValueError("Expected data or state, got neither") | ||
1688 | 1225 | |||
1689 | 1226 | if netplan_path is None: | ||
1690 | 1227 | netplan_path = 'etc/netplan/50-cloud-init.yaml' | ||
1691 | 1228 | |||
1692 | 1229 | renderer = netplan.Renderer( | ||
1693 | 1230 | config={'netplan_path': netplan_path}) | ||
1694 | 1231 | |||
1695 | 1232 | renderer.render_network_state(dir, ns) | ||
1696 | 1233 | return dir2dict(dir) | ||
1697 | 1234 | |||
1698 | 1235 | def testsimple_render_small_netplan(self): | ||
1699 | 1236 | entry = NETWORK_CONFIGS['small'] | ||
1700 | 1237 | files = self._render_and_read(network_config=yaml.load(entry['yaml'])) | ||
1701 | 1238 | self.assertEqual( | ||
1702 | 1239 | entry['expected_netplan'].splitlines(), | ||
1703 | 1240 | files['/etc/netplan/50-cloud-init.yaml'].splitlines()) | ||
1704 | 1241 | |||
1705 | 1242 | def testsimple_render_v4_and_v6(self): | ||
1706 | 1243 | entry = NETWORK_CONFIGS['v4_and_v6'] | ||
1707 | 1244 | files = self._render_and_read(network_config=yaml.load(entry['yaml'])) | ||
1708 | 1245 | self.assertEqual( | ||
1709 | 1246 | entry['expected_netplan'].splitlines(), | ||
1710 | 1247 | files['/etc/netplan/50-cloud-init.yaml'].splitlines()) | ||
1711 | 1248 | |||
1712 | 1249 | def testsimple_render_all(self): | ||
1713 | 1250 | entry = NETWORK_CONFIGS['all'] | ||
1714 | 1251 | files = self._render_and_read(network_config=yaml.load(entry['yaml'])) | ||
1715 | 1252 | self.assertEqual( | ||
1716 | 1253 | entry['expected_netplan'].splitlines(), | ||
1717 | 1254 | files['/etc/netplan/50-cloud-init.yaml'].splitlines()) | ||
1718 | 1255 | |||
1719 | 1256 | |||
1720 | 956 | class TestEniRoundTrip(CiTestCase): | 1257 | class TestEniRoundTrip(CiTestCase): |
1721 | 957 | def _render_and_read(self, network_config=None, state=None, eni_path=None, | 1258 | def _render_and_read(self, network_config=None, state=None, eni_path=None, |
1722 | 958 | links_prefix=None, netrules_path=None, dir=None): | 1259 | links_prefix=None, netrules_path=None, dir=None): |
1723 | diff --git a/tools/net-convert.py b/tools/net-convert.py | |||
1724 | 959 | new file mode 100755 | 1260 | new file mode 100755 |
1725 | index 0000000..1424bb0 | |||
1726 | --- /dev/null | |||
1727 | +++ b/tools/net-convert.py | |||
1728 | @@ -0,0 +1,83 @@ | |||
1729 | 1 | #!/usr/bin/python3 | ||
1730 | 2 | # | ||
1731 | 3 | # This file is part of cloud-init. See LICENSE file ... | ||
1732 | 4 | |||
1733 | 5 | import argparse | ||
1734 | 6 | import json | ||
1735 | 7 | import os | ||
1736 | 8 | import yaml | ||
1737 | 9 | |||
1738 | 10 | from cloudinit.sources.helpers import openstack | ||
1739 | 11 | |||
1740 | 12 | from cloudinit.net import eni | ||
1741 | 13 | from cloudinit.net import network_state | ||
1742 | 14 | from cloudinit.net import netplan | ||
1743 | 15 | from cloudinit.net import sysconfig | ||
1744 | 16 | |||
1745 | 17 | |||
1746 | 18 | def main(): | ||
1747 | 19 | parser = argparse.ArgumentParser() | ||
1748 | 20 | parser.add_argument("--network-data", "-p", type=open, | ||
1749 | 21 | metavar="PATH", required=True) | ||
1750 | 22 | parser.add_argument("--kind", "-k", | ||
1751 | 23 | choices=['eni', 'network_data.json', 'yaml'], | ||
1752 | 24 | required=True) | ||
1753 | 25 | parser.add_argument("-d", "--directory", | ||
1754 | 26 | metavar="PATH", | ||
1755 | 27 | help="directory to place output in", | ||
1756 | 28 | required=True) | ||
1757 | 29 | parser.add_argument("-m", "--mac", | ||
1758 | 30 | metavar="name,mac", | ||
1759 | 31 | action='append', | ||
1760 | 32 | help="interface name to mac mapping") | ||
1761 | 33 | parser.add_argument("--output-kind", "-ok", | ||
1762 | 34 | choices=['eni', 'netplan', 'sysconfig'], | ||
1763 | 35 | required=True) | ||
1764 | 36 | args = parser.parse_args() | ||
1765 | 37 | |||
1766 | 38 | if not os.path.isdir(args.directory): | ||
1767 | 39 | os.makedirs(args.directory) | ||
1768 | 40 | |||
1769 | 41 | if args.mac: | ||
1770 | 42 | known_macs = {} | ||
1771 | 43 | for item in args.mac: | ||
1772 | 44 | iface_name, iface_mac = item.split(",", 1) | ||
1773 | 45 | known_macs[iface_mac] = iface_name | ||
1774 | 46 | else: | ||
1775 | 47 | known_macs = None | ||
1776 | 48 | |||
1777 | 49 | net_data = args.network_data.read() | ||
1778 | 50 | if args.kind == "eni": | ||
1779 | 51 | pre_ns = eni.convert_eni_data(net_data) | ||
1780 | 52 | ns = network_state.parse_net_config_data(pre_ns) | ||
1781 | 53 | elif args.kind == "yaml": | ||
1782 | 54 | pre_ns = yaml.load(net_data) | ||
1783 | 55 | if 'network' in pre_ns: | ||
1784 | 56 | pre_ns = pre_ns.get('network') | ||
1785 | 57 | print("Input YAML") | ||
1786 | 58 | print(yaml.dump(pre_ns, default_flow_style=False, indent=4)) | ||
1787 | 59 | ns = network_state.parse_net_config_data(pre_ns) | ||
1788 | 60 | else: | ||
1789 | 61 | pre_ns = openstack.convert_net_json( | ||
1790 | 62 | json.loads(net_data), known_macs=known_macs) | ||
1791 | 63 | ns = network_state.parse_net_config_data(pre_ns) | ||
1792 | 64 | |||
1793 | 65 | if not ns: | ||
1794 | 66 | raise RuntimeError("No valid network_state object created from" | ||
1795 | 67 | "input data") | ||
1796 | 68 | |||
1797 | 69 | print("\nInternal State") | ||
1798 | 70 | print(yaml.dump(ns, default_flow_style=False, indent=4)) | ||
1799 | 71 | if args.output_kind == "eni": | ||
1800 | 72 | r_cls = eni.Renderer | ||
1801 | 73 | elif args.output_kind == "netplan": | ||
1802 | 74 | r_cls = netplan.Renderer | ||
1803 | 75 | else: | ||
1804 | 76 | r_cls = sysconfig.Renderer | ||
1805 | 77 | |||
1806 | 78 | r = r_cls() | ||
1807 | 79 | r.render_network_state(ns, target=args.directory) | ||
1808 | 80 | |||
1809 | 81 | |||
1810 | 82 | if __name__ == '__main__': | ||
1811 | 83 | main() |
FAILED: Continuous integration, rev:c43f407e2ac 41e3e474dbfd023 fc6325919819a9 /jenkins. ubuntu. com/server/ job/cloud- init-ci/ 139/ /jenkins. ubuntu. com/server/ job/cloud- init-ci/ nodes=metal- amd64/139/ console /jenkins. ubuntu. com/server/ job/cloud- init-ci/ nodes=metal- arm64/139/ console /jenkins. ubuntu. com/server/ job/cloud- init-ci/ nodes=metal- ppc64el/ 139/console /jenkins. ubuntu. com/server/ job/cloud- init-ci/ nodes=metal- s390x/139/ console /jenkins. ubuntu. com/server/ job/cloud- init-ci/ nodes=vm- i386/139/ console
https:/
Executed test runs:
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
Click here to trigger a rebuild: /jenkins. ubuntu. com/server/ job/cloud- init-ci/ 139/rebuild
https:/