Merge ~chad.smith/cloud-init:feature/openstack-network-v2-multi-nic into cloud-init:master
- Git
- lp:~chad.smith/cloud-init
- feature/openstack-network-v2-multi-nic
- Merge into master
Proposed by
Chad Smith
Status: | Work in progress |
---|---|
Proposed branch: | ~chad.smith/cloud-init:feature/openstack-network-v2-multi-nic |
Merge into: | cloud-init:master |
Diff against target: |
526 lines (+356/-28) 5 files modified
cloudinit/cmd/devel/net_convert.py (+5/-5) cloudinit/net/network_state.py (+89/-17) cloudinit/net/sysconfig.py (+2/-2) cloudinit/sources/helpers/openstack.py (+257/-1) tests/unittests/test_datasource/test_configdrive.py (+3/-3) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
cloud-init Commiters | Pending | ||
Review via email: mp+372009@code.launchpad.net |
Commit message
Work in progress for initial review:
openstack: return network v2 from parsed network_data.json
TODO: sort promotion of global dns
Description of the change
To post a comment you must log in.
Revision history for this message
Chad Smith (chad.smith) : | # |
Unmerged commits
- 5fac7a8... by Chad Smith
-
wip: generate openstack v2 networking config
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/cloudinit/cmd/devel/net_convert.py b/cloudinit/cmd/devel/net_convert.py |
2 | index 1ad7e0b..7b4ae3f 100755 |
3 | --- a/cloudinit/cmd/devel/net_convert.py |
4 | +++ b/cloudinit/cmd/devel/net_convert.py |
5 | @@ -81,12 +81,8 @@ def handle_args(name, args): |
6 | pre_ns = yaml.load(net_data) |
7 | if 'network' in pre_ns: |
8 | pre_ns = pre_ns.get('network') |
9 | - if args.debug: |
10 | - sys.stderr.write('\n'.join( |
11 | - ["Input YAML", |
12 | - yaml.dump(pre_ns, default_flow_style=False, indent=4), ""])) |
13 | elif args.kind == 'network_data.json': |
14 | - pre_ns = openstack.convert_net_json( |
15 | + pre_ns = openstack.convert_net_json_v2( |
16 | json.loads(net_data), known_macs=known_macs) |
17 | elif args.kind == 'azure-imds': |
18 | pre_ns = azure.parse_network_config(json.loads(net_data)) |
19 | @@ -94,6 +90,10 @@ def handle_args(name, args): |
20 | config = ovf.Config(ovf.ConfigFile(args.network_data.name)) |
21 | pre_ns = ovf.get_network_config_from_conf(config, False) |
22 | |
23 | + if args.debug: |
24 | + sys.stderr.write('\n'.join( |
25 | + ["Input YAML", |
26 | + yaml.dump(pre_ns, default_flow_style=False, indent=4), ""])) |
27 | ns = network_state.parse_net_config_data(pre_ns) |
28 | if not ns: |
29 | raise RuntimeError("No valid network_state object created from" |
30 | diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py |
31 | index c0c415d..a7454f7 100644 |
32 | --- a/cloudinit/net/network_state.py |
33 | +++ b/cloudinit/net/network_state.py |
34 | @@ -4,6 +4,7 @@ |
35 | # |
36 | # This file is part of cloud-init. See LICENSE file for license information. |
37 | |
38 | +from collections import defaultdict |
39 | import copy |
40 | import functools |
41 | import logging |
42 | @@ -159,6 +160,10 @@ class NetworkState(object): |
43 | return self._version |
44 | |
45 | @property |
46 | + def _global_dns_counts(self): |
47 | + return self._network_state['global_dns_counts'] |
48 | + |
49 | + @property |
50 | def dns_nameservers(self): |
51 | try: |
52 | return self._network_state['dns']['nameservers'] |
53 | @@ -234,6 +239,10 @@ class NetworkStateInterpreter(object): |
54 | self._network_state = copy.deepcopy(self.initial_network_state) |
55 | self._network_state['config'] = config |
56 | self._parsed = False |
57 | + # Reference counters to promote to global |
58 | + self._global_dns_refs = { |
59 | + 'nameserver': defaultdict(list), 'search': defaultdict(list)} |
60 | + self._network_state['global_dns_refs'] = self._global_dns_refs |
61 | |
62 | @property |
63 | def network_state(self): |
64 | @@ -318,7 +327,7 @@ class NetworkStateInterpreter(object): |
65 | " command '%s'" % command_type) |
66 | try: |
67 | handler(self, command) |
68 | - self._v2_common(command) |
69 | + self._maybe_promote_v2_common(command) |
70 | except InvalidCommand: |
71 | if not skip_broken: |
72 | raise |
73 | @@ -326,6 +335,41 @@ class NetworkStateInterpreter(object): |
74 | LOG.warning("Skipping invalid command: %s", command, |
75 | exc_info=True) |
76 | LOG.debug(self.dump_network_state()) |
77 | + # Post-process v2 dns promotions if needed |
78 | + # count interfaces with ip, compare unpromoted global dns |
79 | + self._cleanup_v2_common_from_interfaces() |
80 | + |
81 | + def _cleanup_v2_common_from_interfaces(self): |
82 | + """Strip any promoted global dns/search from specific interfaces.""" |
83 | + interfaces = self._network_state.get('interfaces') |
84 | + global_dns = set(self._network_state['dns'].get('nameservers', [])) |
85 | + global_search = set(self._network_state['dns'].get('search', [])) |
86 | + dns_refs = self._global_dns_refs['nameserver'] |
87 | + search_refs = self._global_dns_refs['search'] |
88 | + promoted_dns = global_dns.intersection(dns_refs) |
89 | + promoted_search = global_dns.intersection(search_refs) |
90 | + for intf_name, intf_cfg in interfaces.items(): |
91 | + for subnet in intf_cfg['subnets']: |
92 | + promote_dns = bool(not promoted_dns and len(interfaces) == 1) |
93 | + subnet_dns = subnet.get('dns_nameservers', []) |
94 | + if promote_dns and (subnet_dns or subnet.get('dns_search')): |
95 | + name_cmd = {'type': 'nameserver', |
96 | + 'search': subnet.get('dns_search', []), |
97 | + 'address': subnet_dns} |
98 | + self.handle_nameserver(name_cmd) |
99 | + subnet.pop('dns_search', None) |
100 | + subnet.pop('dns_nameservers', None) |
101 | + continue |
102 | + for dns_ip in subnet_dns: |
103 | + if dns_ip in promoted_dns: |
104 | + subnet['dns_nameservers'].remove(dns_ip) |
105 | + if not subnet['dns_nameservers']: |
106 | + subnet.pop('dns_nameservers') |
107 | + for search in subnet.get('dns_search', []): |
108 | + if search in promoted_search: |
109 | + subnet['dns_search'].remove(search) |
110 | + if not subnet['dns_search']: |
111 | + subnet.pop('dns_search') |
112 | |
113 | @ensure_command_keys(['name']) |
114 | def handle_loopback(self, command): |
115 | @@ -372,7 +416,6 @@ class NetworkStateInterpreter(object): |
116 | 'subnets': subnets, |
117 | }) |
118 | self._network_state['interfaces'].update({command.get('name'): iface}) |
119 | - self.dump_network_state() |
120 | |
121 | @ensure_command_keys(['name', 'vlan_id', 'vlan_link']) |
122 | def handle_vlan(self, command): |
123 | @@ -520,13 +563,15 @@ class NetworkStateInterpreter(object): |
124 | if not type(addrs) == list: |
125 | addrs = [addrs] |
126 | for addr in addrs: |
127 | - dns['nameservers'].append(addr) |
128 | + if addr not in dns['nameservers']: |
129 | + dns['nameservers'].append(addr) |
130 | if 'search' in command: |
131 | paths = command['search'] |
132 | if not isinstance(paths, list): |
133 | paths = [paths] |
134 | for path in paths: |
135 | - dns['search'].append(path) |
136 | + if path not in dns['search']: |
137 | + dns['search'].append(path) |
138 | |
139 | @ensure_command_keys(['destination']) |
140 | def handle_route(self, command): |
141 | @@ -689,18 +734,45 @@ class NetworkStateInterpreter(object): |
142 | LOG.warning('Wifi configuration is only available to distros with' |
143 | 'netplan rendering support.') |
144 | |
145 | - def _v2_common(self, cfg): |
146 | - LOG.debug('v2_common: handling config:\n%s', cfg) |
147 | - if 'nameservers' in cfg: |
148 | - search = cfg.get('nameservers').get('search', []) |
149 | - dns = cfg.get('nameservers').get('addresses', []) |
150 | - name_cmd = {'type': 'nameserver'} |
151 | - if len(search) > 0: |
152 | - name_cmd.update({'search': search}) |
153 | - if len(dns) > 0: |
154 | - name_cmd.update({'addresses': dns}) |
155 | - LOG.debug('v2(nameserver) -> v1(nameserver):\n%s', name_cmd) |
156 | - self.handle_nameserver(name_cmd) |
157 | + def _maybe_promote_v2_common(self, cfg): |
158 | + """Possibly promote v2 common/global services from specific devices. |
159 | + |
160 | + Since network v2 only supports per-interface DNS config settings, there |
161 | + is no 'global' dns service that can be expressed, unless we set |
162 | + the same dns values on every interface. If v2 config has the same |
163 | + dns config on every configured interface, it will be assumed that |
164 | + the common dns setting needs to be written to the distribution's |
165 | + 'global (read /etc/resolv.conf)' dns config. |
166 | + |
167 | + Track reference counts in _global_dns_refs so net/sysconfig renderer |
168 | + can determine whether to use /etc/resolv.conf of not for specific |
169 | + device dns configuration. |
170 | + """ |
171 | + LOG.debug('maybe_promote_v2_common: handling config:\n%s', cfg) |
172 | + for if_name, iface_cfg in cfg.items(): |
173 | + if 'nameservers' in iface_cfg: |
174 | + search = iface_cfg.get('nameservers').get('search', []) |
175 | + if not search: |
176 | + search = [] |
177 | + elif not isinstance(search, list): |
178 | + search = [search] |
179 | + dns = iface_cfg.get('nameservers').get('addresses') |
180 | + if not dns: |
181 | + dns = [] |
182 | + elif not isinstance(dns, list): |
183 | + dns = [dns] |
184 | + name_cmd = {'type': 'nameserver', 'search': [], 'address': []} |
185 | + for sname in search: |
186 | + if self._global_dns_refs['search'][sname]: |
187 | + name_cmd['search'].append[sname] |
188 | + self._global_dns_refs['search'][sname].append(if_name) |
189 | + for dns_ip in dns: |
190 | + if self._global_dns_refs['nameserver'][dns_ip]: |
191 | + name_cmd['address'].append[dns_ip] |
192 | + self._global_dns_refs['nameserver'][dns_ip].append(if_name) |
193 | + if any([name_cmd['search'], name_cmd['address']]): |
194 | + # promote DNS config seen by multiple interfaces |
195 | + self.handle_nameserver(name_cmd) |
196 | |
197 | def _handle_bond_bridge(self, command, cmd_type=None): |
198 | """Common handler for bond and bridge types""" |
199 | @@ -827,7 +899,7 @@ def _normalize_net_keys(network, address_keys=()): |
200 | |
201 | @returns: A dict containing normalized prefix and matching addr_key. |
202 | """ |
203 | - net = dict((k, v) for k, v in network.items() if v) |
204 | + net = dict((k, v) for k, v in network.items() if v is not None) |
205 | addr_key = None |
206 | for key in address_keys: |
207 | if net.get(key): |
208 | diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py |
209 | index be5dede..0fa0508 100644 |
210 | --- a/cloudinit/net/sysconfig.py |
211 | +++ b/cloudinit/net/sysconfig.py |
212 | @@ -444,9 +444,9 @@ class Renderer(renderer.Renderer): |
213 | |
214 | if _is_default_route(route): |
215 | if ( |
216 | - (subnet.get('ipv4') and |
217 | + (not is_ipv6 and |
218 | route_cfg.has_set_default_ipv4) or |
219 | - (subnet.get('ipv6') and |
220 | + (is_ipv6 and |
221 | route_cfg.has_set_default_ipv6) |
222 | ): |
223 | raise ValueError("Duplicate declaration of default " |
224 | diff --git a/cloudinit/sources/helpers/openstack.py b/cloudinit/sources/helpers/openstack.py |
225 | index 8f06911..ceaf868 100644 |
226 | --- a/cloudinit/sources/helpers/openstack.py |
227 | +++ b/cloudinit/sources/helpers/openstack.py |
228 | @@ -17,6 +17,7 @@ import six |
229 | from cloudinit import ec2_utils |
230 | from cloudinit import log as logging |
231 | from cloudinit import net |
232 | +from cloudinit.net import network_state |
233 | from cloudinit import sources |
234 | from cloudinit import url_helper |
235 | from cloudinit import util |
236 | @@ -82,6 +83,36 @@ KNOWN_PHYSICAL_TYPES = ( |
237 | 'vif', |
238 | ) |
239 | |
240 | +LINK_TYPE_TO_NETWORK_V2_KEYS = { |
241 | + 'bond': 'bonds', |
242 | + 'vlan': 'vlans', |
243 | +} |
244 | + |
245 | +NET_V2_PHYSICAL_TYPES = ['ethernets', 'vlans', 'bonds', 'bridges', 'wifi'] |
246 | + |
247 | +NETWORK_DATA_TO_V2 = { |
248 | + 'bond': { |
249 | + 'mtu': 'mtu', |
250 | + 'bond_links': 'interfaces', |
251 | + 'ethernet_mac_address': 'macaddress', |
252 | + }, |
253 | + 'ethernets': { |
254 | + 'mtu': 'mtu', |
255 | + 'ethernet_mac_address': 'match.macaddress', |
256 | + 'id': 'set-name', |
257 | + }, |
258 | + 'vlan': { |
259 | + 'key_rename': '{link}.{id}', # override top level key for object |
260 | + 'mtu': 'mtu', |
261 | + 'vlan_mac_address': 'macaddress', |
262 | + 'vlan_id': 'id', |
263 | + 'vlan_link': 'link', |
264 | + }, |
265 | +} |
266 | + |
267 | +# network_data.json keys for which converted net v2 values should be lowercase |
268 | +LOWERCASE_KEY_VALUE_V2 = ('macaddress',) |
269 | + |
270 | |
271 | class NonReadable(IOError): |
272 | pass |
273 | @@ -496,8 +527,233 @@ class MetadataReader(BaseReader): |
274 | retries=self.retries) |
275 | |
276 | |
277 | -# Convert OpenStack ConfigDrive NetworkData json to network_config yaml |
278 | +def _find_v2_device_type(name, net_v2): |
279 | + """Return the netv2 physical device type containing matching name.""" |
280 | + for device_type in NET_V2_PHYSICAL_TYPES: |
281 | + if name in net_v2.get(device_type, {}): |
282 | + return device_type |
283 | + return None |
284 | + |
285 | + |
286 | +def _convert_network_json_network_to_net_v2(src_json): |
287 | + """Parse a single network item from the networks list in network_data.json |
288 | + |
289 | + @param src_json: One network item from network_data.json 'networks' key. |
290 | + |
291 | + @return: Tuple of <interface_name>, network v2 configuration dict for the |
292 | + src_json. For example: eth0, {'addresses': [...], 'dhcp4': True} |
293 | + """ |
294 | + net_v2 = {'addresses': []} |
295 | + ignored_keys = set() |
296 | + |
297 | + # In Liberty spec https://specs.openstack.org/openstack/nova-specs/ |
298 | + # specs/liberty/implemented/metadata-service-network-info.html |
299 | + if src_json['type'] == 'ipv4_dhcp': |
300 | + net_v2['dhcp4'] = True |
301 | + elif src_json['type'] == 'ipv6_dhcp': |
302 | + net_v2['dhcp6'] = True |
303 | + |
304 | + for service in src_json.get('services', []): |
305 | + if service['type'] != 'dns': |
306 | + ignored_keys.update(['services.type(%s)' % service['type']]) |
307 | + continue |
308 | + if 'nameservers' not in net_v2: |
309 | + net_v2['nameservers'] = {'addresses': [], 'search': []} |
310 | + net_v2['nameservers']['addresses'].append(service['address']) |
311 | + # In Rocky spec https://specs.openstack.org/openstack/nova-specs/specs/ |
312 | + # rocky/approved/multiple-fixed-ips-network-information.html |
313 | + dns_nameservers = src_json.get('dns_nameservers', []) |
314 | + if dns_nameservers: |
315 | + if 'nameservers' not in net_v2: |
316 | + net_v2['nameservers'] = {'addresses': [], 'search': []} |
317 | + net_v2['nameservers']['addresses'] = copy.copy(dns_nameservers) |
318 | + |
319 | + # Parse routes for network, prefix and gateway |
320 | + route_keys = set(['netmask', 'network', 'gateway']) |
321 | + for route in src_json.get('routes', []): |
322 | + ignored_route_keys = (set(route.keys()).difference(route_keys)) |
323 | + ignored_keys.update(['route.%s' % key for key in ignored_route_keys]) |
324 | + route_cfg = { |
325 | + 'to': '{network}/{prefix}'.format( |
326 | + network=route['network'], |
327 | + prefix=net.network_state.mask_to_net_prefix(route['netmask'])), |
328 | + 'via': route['gateway']} |
329 | + if route.get('metric'): |
330 | + route_cfg['metric'] = route.get('metric') |
331 | + if 'routes' not in net_v2: |
332 | + net_v2['routes'] = [] |
333 | + net_v2['routes'].append(route_cfg) |
334 | + |
335 | + # Parse ip addresses on Rocky and Liberty |
336 | + for ip_cfg in src_json.get('ip_addresses', []): |
337 | + if ip_cfg.get('netmask'): |
338 | + prefix = net.network_state.mask_to_net_prefix(ip_cfg['netmask']) |
339 | + cidr_fmt = '{ip}/{prefix}' |
340 | + else: |
341 | + cidr_fmt = '{ip}' |
342 | + prefix = None |
343 | + net_v2['addresses'].append( |
344 | + cidr_fmt.format(ip=ip_cfg['address'], prefix=prefix)) |
345 | + liberty_ip = src_json.get('ip_address') |
346 | + if liberty_ip: |
347 | + if src_json.get('netmask'): |
348 | + prefix = net.network_state.mask_to_net_prefix(src_json['netmask']) |
349 | + cidr_fmt = '{ip}/{prefix}' |
350 | + else: |
351 | + cidr_fmt = '{ip}' |
352 | + prefix = None |
353 | + liberty_cidr = cidr_fmt.format(ip=liberty_ip, prefix=prefix) |
354 | + if liberty_cidr not in net_v2['addresses']: |
355 | + net_v2['addresses'].append(liberty_cidr) |
356 | + if not net_v2['addresses']: |
357 | + net_v2.pop('addresses') |
358 | + if ignored_keys: |
359 | + LOG.debug( |
360 | + 'Ignoring the network_data.json %s config keys %s', |
361 | + src_json['id'], ', '.join(ignored_keys)) |
362 | + return src_json['link'], net_v2 |
363 | + |
364 | + |
365 | +def _convert_network_json_to_net_v2(src_json, var_map): |
366 | + """Return network v2 for an element of OpenStack NetworkData json. |
367 | + |
368 | + @param src_json: Dict of network_data.json for a single src_json object |
369 | + @param var_map: Dict with a variable name map from network_data.json to |
370 | + network v2 |
371 | + |
372 | + @return Tuple of the interface name and the converted network v2 for the |
373 | + src_json object. For example: eth0, {'match': {'macaddress': 'AA:BB'}} |
374 | + """ |
375 | + net_v2 = {} |
376 | + # Map openstack bond keys to network v2 |
377 | + # Copy key values |
378 | + current_keys = set(src_json) |
379 | + for key in current_keys.intersection(set(var_map)): |
380 | + keyparts = var_map[key].split('.') |
381 | + tmp_cfg = net_v2 # allow traversing net_v2 dict |
382 | + while keyparts: |
383 | + keypart = keyparts.pop(0) |
384 | + if keyparts: |
385 | + if keypart not in tmp_cfg: |
386 | + tmp_cfg[keypart] = {} |
387 | + tmp_cfg = tmp_cfg[keypart] |
388 | + elif isinstance(src_json[key], list): |
389 | + tmp_cfg[keypart] = copy.copy(src_json[key]) |
390 | + elif src_json[key]: |
391 | + if keypart in LOWERCASE_KEY_VALUE_V2: |
392 | + tmp_cfg[keypart] = src_json[key].lower() |
393 | + else: |
394 | + tmp_cfg[keypart] = src_json[key] |
395 | + if 'key_rename' in var_map: |
396 | + net_v2['key_rename'] = var_map['key_rename'].format(**net_v2) |
397 | + return src_json['id'], net_v2 |
398 | + |
399 | + |
400 | def convert_net_json(network_json=None, known_macs=None): |
401 | + """Parse OpenStack ConfigDrive NetworkData json, returning network cfg v2. |
402 | + |
403 | + OpenStack network_data.json provides a 3 element dictionary |
404 | + - "links" (links are network devices, physical or virtual) |
405 | + - "networks" (networks are ip network configurations for one or more |
406 | + links) |
407 | + - services (non-ip services, like dns) |
408 | + |
409 | + networks and links are combined via network items referencing specific |
410 | + links via a 'link_id' which maps to a links 'id' field. |
411 | + """ |
412 | + if network_json is None: |
413 | + return None |
414 | + net_config = {'version': 2} |
415 | + for link in network_json.get('links', []): |
416 | + link_type = link['type'] |
417 | + v2_key = LINK_TYPE_TO_NETWORK_V2_KEYS.get(link_type) |
418 | + if not v2_key: |
419 | + v2_key = 'ethernets' |
420 | + if link_type not in KNOWN_PHYSICAL_TYPES: |
421 | + LOG.warning('Unknown network_data link type (%s); treating as' |
422 | + ' physical ethernet', link_type) |
423 | + if v2_key not in net_config: |
424 | + net_config[v2_key] = {} |
425 | + var_map = copy.deepcopy(NETWORK_DATA_TO_V2.get(v2_key)) |
426 | + if not var_map: |
427 | + var_map = copy.deepcopy(NETWORK_DATA_TO_V2.get(link_type)) |
428 | + |
429 | + # Add v2 config parameters map for this link_type if present |
430 | + if link_type in network_state.NET_CONFIG_TO_V2: |
431 | + var_map.update(dict( |
432 | + (k.replace('-', '_'), 'parameters.{v}'.format(v=v)) |
433 | + for k, v in network_state.NET_CONFIG_TO_V2[link_type].items())) |
434 | + intf_id, intf_cfg = _convert_network_json_to_net_v2(link, var_map) |
435 | + if v2_key in ('ethernets', 'bonds') and 'name' not in intf_cfg: |
436 | + if known_macs is None: |
437 | + known_macs = net.get_interfaces_by_mac() |
438 | + lower_known_macs = dict( |
439 | + (k.lower(), v) for k, v in known_macs.items()) |
440 | + mac = intf_cfg.get( # top-level macaddress and match.macaddress |
441 | + 'macaddress', intf_cfg.get('match', {}).get('macaddress')) |
442 | + if not mac: |
443 | + raise ValueError("No mac_address or name entry for %s" % d) |
444 | + mac = mac.lower() |
445 | + if mac not in lower_known_macs: |
446 | + raise ValueError( |
447 | + 'Unable to find a system nic for %s' % mac) |
448 | + intf_cfg['key_rename'] = lower_known_macs[mac] |
449 | + net_config[v2_key].update({intf_id: intf_cfg}) |
450 | + for network in network_json.get('networks', []): |
451 | + v2_key = _find_v2_device_type(network['link'], net_config) |
452 | + intf_id, network_cfg = _convert_network_json_network_to_net_v2(network) |
453 | + for key, val in network_cfg.items(): |
454 | + if isinstance(val, list): |
455 | + if key not in net_config[v2_key][intf_id]: |
456 | + net_config[v2_key][intf_id][key] = [] |
457 | + net_config[v2_key][intf_id][key].extend(val) |
458 | + else: |
459 | + net_config[v2_key][intf_id][key] = val |
460 | + |
461 | + # Inject global nameserver values under each all interface which |
462 | + # has addresses and do not already have a DNS configuration |
463 | + ignored_keys = set() |
464 | + global_dns = [] |
465 | + for service in network_json.get('services', []): |
466 | + if service['type'] != 'dns': |
467 | + ignored_keys.update('services.type(%s)' % service['type']) |
468 | + continue |
469 | + global_dns.append(service['address']) |
470 | + |
471 | + # Handle renames and global_dns |
472 | + for dev_type in NET_V2_PHYSICAL_TYPES: |
473 | + if dev_type not in net_config: |
474 | + continue |
475 | + renames = {} |
476 | + for dev in net_config[dev_type]: |
477 | + renames[dev] = net_config[dev_type][dev].pop('key_rename', None) |
478 | + if not global_dns: |
479 | + continue |
480 | + dev_keys = set(net_config[dev_type][dev].keys()) |
481 | + if set(['nameservers', 'dhcp4', 'dhcp6']).intersection(dev_keys): |
482 | + # Do not add nameservers if we already have dns config |
483 | + continue |
484 | + if 'addresses' not in net_config[dev_type][dev]: |
485 | + # No configured address, needs no nameserver |
486 | + continue |
487 | + net_config[dev_type][dev]['nameservers'] = { |
488 | + 'addresses': copy.copy(global_dns), 'search': []} |
489 | + for dev, rename in renames.items(): |
490 | + if rename: |
491 | + net_config[dev_type][rename] = net_config[dev_type].pop(dev) |
492 | + if 'set-name' in net_config[dev_type][rename]: |
493 | + net_config[dev_type][rename]['set-name'] = rename |
494 | + |
495 | + if ignored_keys: |
496 | + LOG.debug( |
497 | + 'Ignoring the network_data.json config keys %s', |
498 | + ', '.join(ignored_keys)) |
499 | + |
500 | + return net_config |
501 | + |
502 | + |
503 | +# Convert OpenStack ConfigDrive NetworkData json to network_config yaml |
504 | +def convert_net_json1(network_json=None, known_macs=None): |
505 | """Return a dictionary of network_config by parsing provided |
506 | OpenStack ConfigDrive NetworkData json format |
507 | |
508 | diff --git a/tests/unittests/test_datasource/test_configdrive.py b/tests/unittests/test_datasource/test_configdrive.py |
509 | index 520c50f..f4181f5 100644 |
510 | --- a/tests/unittests/test_datasource/test_configdrive.py |
511 | +++ b/tests/unittests/test_datasource/test_configdrive.py |
512 | @@ -719,11 +719,11 @@ class TestConvertNetworkData(CiTestCase): |
513 | for link in my_netdata['links']: |
514 | link['ethernet_mac_address'] = link['ethernet_mac_address'].upper() |
515 | |
516 | + import pdb; pdb.set_trace() |
517 | ncfg = openstack.convert_net_json(my_netdata, known_macs=KNOWN_MACS) |
518 | config_name2mac = {} |
519 | - for n in ncfg['config']: |
520 | - if n['type'] == 'physical': |
521 | - config_name2mac[n['name']] = n['mac_address'] |
522 | + for name, if_cfg in ncfg['ethernets'].items(): |
523 | + config_name2mac[name] = if_cfg['match']['macaddress'] |
524 | |
525 | expected = {'nic0': 'fa:16:3e:05:30:fe', 'enp0s1': 'fa:16:3e:69:b0:58', |
526 | 'enp0s2': 'fa:16:3e:d4:57:ad'} |