Merge ~chad.smith/cloud-init:feature/openstack-network-v2-multi-nic into cloud-init: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)
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

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
diff --git a/cloudinit/cmd/devel/net_convert.py b/cloudinit/cmd/devel/net_convert.py
index 1ad7e0b..7b4ae3f 100755
--- a/cloudinit/cmd/devel/net_convert.py
+++ b/cloudinit/cmd/devel/net_convert.py
@@ -81,12 +81,8 @@ def handle_args(name, args):
81 pre_ns = yaml.load(net_data)81 pre_ns = yaml.load(net_data)
82 if 'network' in pre_ns:82 if 'network' in pre_ns:
83 pre_ns = pre_ns.get('network')83 pre_ns = pre_ns.get('network')
84 if args.debug:
85 sys.stderr.write('\n'.join(
86 ["Input YAML",
87 yaml.dump(pre_ns, default_flow_style=False, indent=4), ""]))
88 elif args.kind == 'network_data.json':84 elif args.kind == 'network_data.json':
89 pre_ns = openstack.convert_net_json(85 pre_ns = openstack.convert_net_json_v2(
90 json.loads(net_data), known_macs=known_macs)86 json.loads(net_data), known_macs=known_macs)
91 elif args.kind == 'azure-imds':87 elif args.kind == 'azure-imds':
92 pre_ns = azure.parse_network_config(json.loads(net_data))88 pre_ns = azure.parse_network_config(json.loads(net_data))
@@ -94,6 +90,10 @@ def handle_args(name, args):
94 config = ovf.Config(ovf.ConfigFile(args.network_data.name))90 config = ovf.Config(ovf.ConfigFile(args.network_data.name))
95 pre_ns = ovf.get_network_config_from_conf(config, False)91 pre_ns = ovf.get_network_config_from_conf(config, False)
9692
93 if args.debug:
94 sys.stderr.write('\n'.join(
95 ["Input YAML",
96 yaml.dump(pre_ns, default_flow_style=False, indent=4), ""]))
97 ns = network_state.parse_net_config_data(pre_ns)97 ns = network_state.parse_net_config_data(pre_ns)
98 if not ns:98 if not ns:
99 raise RuntimeError("No valid network_state object created from"99 raise RuntimeError("No valid network_state object created from"
diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
index c0c415d..a7454f7 100644
--- a/cloudinit/net/network_state.py
+++ b/cloudinit/net/network_state.py
@@ -4,6 +4,7 @@
4#4#
5# This file is part of cloud-init. See LICENSE file for license information.5# This file is part of cloud-init. See LICENSE file for license information.
66
7from collections import defaultdict
7import copy8import copy
8import functools9import functools
9import logging10import logging
@@ -159,6 +160,10 @@ class NetworkState(object):
159 return self._version160 return self._version
160161
161 @property162 @property
163 def _global_dns_counts(self):
164 return self._network_state['global_dns_counts']
165
166 @property
162 def dns_nameservers(self):167 def dns_nameservers(self):
163 try:168 try:
164 return self._network_state['dns']['nameservers']169 return self._network_state['dns']['nameservers']
@@ -234,6 +239,10 @@ class NetworkStateInterpreter(object):
234 self._network_state = copy.deepcopy(self.initial_network_state)239 self._network_state = copy.deepcopy(self.initial_network_state)
235 self._network_state['config'] = config240 self._network_state['config'] = config
236 self._parsed = False241 self._parsed = False
242 # Reference counters to promote to global
243 self._global_dns_refs = {
244 'nameserver': defaultdict(list), 'search': defaultdict(list)}
245 self._network_state['global_dns_refs'] = self._global_dns_refs
237246
238 @property247 @property
239 def network_state(self):248 def network_state(self):
@@ -318,7 +327,7 @@ class NetworkStateInterpreter(object):
318 " command '%s'" % command_type)327 " command '%s'" % command_type)
319 try:328 try:
320 handler(self, command)329 handler(self, command)
321 self._v2_common(command)330 self._maybe_promote_v2_common(command)
322 except InvalidCommand:331 except InvalidCommand:
323 if not skip_broken:332 if not skip_broken:
324 raise333 raise
@@ -326,6 +335,41 @@ class NetworkStateInterpreter(object):
326 LOG.warning("Skipping invalid command: %s", command,335 LOG.warning("Skipping invalid command: %s", command,
327 exc_info=True)336 exc_info=True)
328 LOG.debug(self.dump_network_state())337 LOG.debug(self.dump_network_state())
338 # Post-process v2 dns promotions if needed
339 # count interfaces with ip, compare unpromoted global dns
340 self._cleanup_v2_common_from_interfaces()
341
342 def _cleanup_v2_common_from_interfaces(self):
343 """Strip any promoted global dns/search from specific interfaces."""
344 interfaces = self._network_state.get('interfaces')
345 global_dns = set(self._network_state['dns'].get('nameservers', []))
346 global_search = set(self._network_state['dns'].get('search', []))
347 dns_refs = self._global_dns_refs['nameserver']
348 search_refs = self._global_dns_refs['search']
349 promoted_dns = global_dns.intersection(dns_refs)
350 promoted_search = global_dns.intersection(search_refs)
351 for intf_name, intf_cfg in interfaces.items():
352 for subnet in intf_cfg['subnets']:
353 promote_dns = bool(not promoted_dns and len(interfaces) == 1)
354 subnet_dns = subnet.get('dns_nameservers', [])
355 if promote_dns and (subnet_dns or subnet.get('dns_search')):
356 name_cmd = {'type': 'nameserver',
357 'search': subnet.get('dns_search', []),
358 'address': subnet_dns}
359 self.handle_nameserver(name_cmd)
360 subnet.pop('dns_search', None)
361 subnet.pop('dns_nameservers', None)
362 continue
363 for dns_ip in subnet_dns:
364 if dns_ip in promoted_dns:
365 subnet['dns_nameservers'].remove(dns_ip)
366 if not subnet['dns_nameservers']:
367 subnet.pop('dns_nameservers')
368 for search in subnet.get('dns_search', []):
369 if search in promoted_search:
370 subnet['dns_search'].remove(search)
371 if not subnet['dns_search']:
372 subnet.pop('dns_search')
329373
330 @ensure_command_keys(['name'])374 @ensure_command_keys(['name'])
331 def handle_loopback(self, command):375 def handle_loopback(self, command):
@@ -372,7 +416,6 @@ class NetworkStateInterpreter(object):
372 'subnets': subnets,416 'subnets': subnets,
373 })417 })
374 self._network_state['interfaces'].update({command.get('name'): iface})418 self._network_state['interfaces'].update({command.get('name'): iface})
375 self.dump_network_state()
376419
377 @ensure_command_keys(['name', 'vlan_id', 'vlan_link'])420 @ensure_command_keys(['name', 'vlan_id', 'vlan_link'])
378 def handle_vlan(self, command):421 def handle_vlan(self, command):
@@ -520,13 +563,15 @@ class NetworkStateInterpreter(object):
520 if not type(addrs) == list:563 if not type(addrs) == list:
521 addrs = [addrs]564 addrs = [addrs]
522 for addr in addrs:565 for addr in addrs:
523 dns['nameservers'].append(addr)566 if addr not in dns['nameservers']:
567 dns['nameservers'].append(addr)
524 if 'search' in command:568 if 'search' in command:
525 paths = command['search']569 paths = command['search']
526 if not isinstance(paths, list):570 if not isinstance(paths, list):
527 paths = [paths]571 paths = [paths]
528 for path in paths:572 for path in paths:
529 dns['search'].append(path)573 if path not in dns['search']:
574 dns['search'].append(path)
530575
531 @ensure_command_keys(['destination'])576 @ensure_command_keys(['destination'])
532 def handle_route(self, command):577 def handle_route(self, command):
@@ -689,18 +734,45 @@ class NetworkStateInterpreter(object):
689 LOG.warning('Wifi configuration is only available to distros with'734 LOG.warning('Wifi configuration is only available to distros with'
690 'netplan rendering support.')735 'netplan rendering support.')
691736
692 def _v2_common(self, cfg):737 def _maybe_promote_v2_common(self, cfg):
693 LOG.debug('v2_common: handling config:\n%s', cfg)738 """Possibly promote v2 common/global services from specific devices.
694 if 'nameservers' in cfg:739
695 search = cfg.get('nameservers').get('search', [])740 Since network v2 only supports per-interface DNS config settings, there
696 dns = cfg.get('nameservers').get('addresses', [])741 is no 'global' dns service that can be expressed, unless we set
697 name_cmd = {'type': 'nameserver'}742 the same dns values on every interface. If v2 config has the same
698 if len(search) > 0:743 dns config on every configured interface, it will be assumed that
699 name_cmd.update({'search': search})744 the common dns setting needs to be written to the distribution's
700 if len(dns) > 0:745 'global (read /etc/resolv.conf)' dns config.
701 name_cmd.update({'addresses': dns})746
702 LOG.debug('v2(nameserver) -> v1(nameserver):\n%s', name_cmd)747 Track reference counts in _global_dns_refs so net/sysconfig renderer
703 self.handle_nameserver(name_cmd)748 can determine whether to use /etc/resolv.conf of not for specific
749 device dns configuration.
750 """
751 LOG.debug('maybe_promote_v2_common: handling config:\n%s', cfg)
752 for if_name, iface_cfg in cfg.items():
753 if 'nameservers' in iface_cfg:
754 search = iface_cfg.get('nameservers').get('search', [])
755 if not search:
756 search = []
757 elif not isinstance(search, list):
758 search = [search]
759 dns = iface_cfg.get('nameservers').get('addresses')
760 if not dns:
761 dns = []
762 elif not isinstance(dns, list):
763 dns = [dns]
764 name_cmd = {'type': 'nameserver', 'search': [], 'address': []}
765 for sname in search:
766 if self._global_dns_refs['search'][sname]:
767 name_cmd['search'].append[sname]
768 self._global_dns_refs['search'][sname].append(if_name)
769 for dns_ip in dns:
770 if self._global_dns_refs['nameserver'][dns_ip]:
771 name_cmd['address'].append[dns_ip]
772 self._global_dns_refs['nameserver'][dns_ip].append(if_name)
773 if any([name_cmd['search'], name_cmd['address']]):
774 # promote DNS config seen by multiple interfaces
775 self.handle_nameserver(name_cmd)
704776
705 def _handle_bond_bridge(self, command, cmd_type=None):777 def _handle_bond_bridge(self, command, cmd_type=None):
706 """Common handler for bond and bridge types"""778 """Common handler for bond and bridge types"""
@@ -827,7 +899,7 @@ def _normalize_net_keys(network, address_keys=()):
827899
828 @returns: A dict containing normalized prefix and matching addr_key.900 @returns: A dict containing normalized prefix and matching addr_key.
829 """901 """
830 net = dict((k, v) for k, v in network.items() if v)902 net = dict((k, v) for k, v in network.items() if v is not None)
831 addr_key = None903 addr_key = None
832 for key in address_keys:904 for key in address_keys:
833 if net.get(key):905 if net.get(key):
diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
index be5dede..0fa0508 100644
--- a/cloudinit/net/sysconfig.py
+++ b/cloudinit/net/sysconfig.py
@@ -444,9 +444,9 @@ class Renderer(renderer.Renderer):
444444
445 if _is_default_route(route):445 if _is_default_route(route):
446 if (446 if (
447 (subnet.get('ipv4') and447 (not is_ipv6 and
448 route_cfg.has_set_default_ipv4) or448 route_cfg.has_set_default_ipv4) or
449 (subnet.get('ipv6') and449 (is_ipv6 and
450 route_cfg.has_set_default_ipv6)450 route_cfg.has_set_default_ipv6)
451 ):451 ):
452 raise ValueError("Duplicate declaration of default "452 raise ValueError("Duplicate declaration of default "
diff --git a/cloudinit/sources/helpers/openstack.py b/cloudinit/sources/helpers/openstack.py
index 8f06911..ceaf868 100644
--- a/cloudinit/sources/helpers/openstack.py
+++ b/cloudinit/sources/helpers/openstack.py
@@ -17,6 +17,7 @@ import six
17from cloudinit import ec2_utils17from cloudinit import ec2_utils
18from cloudinit import log as logging18from cloudinit import log as logging
19from cloudinit import net19from cloudinit import net
20from cloudinit.net import network_state
20from cloudinit import sources21from cloudinit import sources
21from cloudinit import url_helper22from cloudinit import url_helper
22from cloudinit import util23from cloudinit import util
@@ -82,6 +83,36 @@ KNOWN_PHYSICAL_TYPES = (
82 'vif',83 'vif',
83)84)
8485
86LINK_TYPE_TO_NETWORK_V2_KEYS = {
87 'bond': 'bonds',
88 'vlan': 'vlans',
89}
90
91NET_V2_PHYSICAL_TYPES = ['ethernets', 'vlans', 'bonds', 'bridges', 'wifi']
92
93NETWORK_DATA_TO_V2 = {
94 'bond': {
95 'mtu': 'mtu',
96 'bond_links': 'interfaces',
97 'ethernet_mac_address': 'macaddress',
98 },
99 'ethernets': {
100 'mtu': 'mtu',
101 'ethernet_mac_address': 'match.macaddress',
102 'id': 'set-name',
103 },
104 'vlan': {
105 'key_rename': '{link}.{id}', # override top level key for object
106 'mtu': 'mtu',
107 'vlan_mac_address': 'macaddress',
108 'vlan_id': 'id',
109 'vlan_link': 'link',
110 },
111}
112
113# network_data.json keys for which converted net v2 values should be lowercase
114LOWERCASE_KEY_VALUE_V2 = ('macaddress',)
115
85116
86class NonReadable(IOError):117class NonReadable(IOError):
87 pass118 pass
@@ -496,8 +527,233 @@ class MetadataReader(BaseReader):
496 retries=self.retries)527 retries=self.retries)
497528
498529
499# Convert OpenStack ConfigDrive NetworkData json to network_config yaml530def _find_v2_device_type(name, net_v2):
531 """Return the netv2 physical device type containing matching name."""
532 for device_type in NET_V2_PHYSICAL_TYPES:
533 if name in net_v2.get(device_type, {}):
534 return device_type
535 return None
536
537
538def _convert_network_json_network_to_net_v2(src_json):
539 """Parse a single network item from the networks list in network_data.json
540
541 @param src_json: One network item from network_data.json 'networks' key.
542
543 @return: Tuple of <interface_name>, network v2 configuration dict for the
544 src_json. For example: eth0, {'addresses': [...], 'dhcp4': True}
545 """
546 net_v2 = {'addresses': []}
547 ignored_keys = set()
548
549 # In Liberty spec https://specs.openstack.org/openstack/nova-specs/
550 # specs/liberty/implemented/metadata-service-network-info.html
551 if src_json['type'] == 'ipv4_dhcp':
552 net_v2['dhcp4'] = True
553 elif src_json['type'] == 'ipv6_dhcp':
554 net_v2['dhcp6'] = True
555
556 for service in src_json.get('services', []):
557 if service['type'] != 'dns':
558 ignored_keys.update(['services.type(%s)' % service['type']])
559 continue
560 if 'nameservers' not in net_v2:
561 net_v2['nameservers'] = {'addresses': [], 'search': []}
562 net_v2['nameservers']['addresses'].append(service['address'])
563 # In Rocky spec https://specs.openstack.org/openstack/nova-specs/specs/
564 # rocky/approved/multiple-fixed-ips-network-information.html
565 dns_nameservers = src_json.get('dns_nameservers', [])
566 if dns_nameservers:
567 if 'nameservers' not in net_v2:
568 net_v2['nameservers'] = {'addresses': [], 'search': []}
569 net_v2['nameservers']['addresses'] = copy.copy(dns_nameservers)
570
571 # Parse routes for network, prefix and gateway
572 route_keys = set(['netmask', 'network', 'gateway'])
573 for route in src_json.get('routes', []):
574 ignored_route_keys = (set(route.keys()).difference(route_keys))
575 ignored_keys.update(['route.%s' % key for key in ignored_route_keys])
576 route_cfg = {
577 'to': '{network}/{prefix}'.format(
578 network=route['network'],
579 prefix=net.network_state.mask_to_net_prefix(route['netmask'])),
580 'via': route['gateway']}
581 if route.get('metric'):
582 route_cfg['metric'] = route.get('metric')
583 if 'routes' not in net_v2:
584 net_v2['routes'] = []
585 net_v2['routes'].append(route_cfg)
586
587 # Parse ip addresses on Rocky and Liberty
588 for ip_cfg in src_json.get('ip_addresses', []):
589 if ip_cfg.get('netmask'):
590 prefix = net.network_state.mask_to_net_prefix(ip_cfg['netmask'])
591 cidr_fmt = '{ip}/{prefix}'
592 else:
593 cidr_fmt = '{ip}'
594 prefix = None
595 net_v2['addresses'].append(
596 cidr_fmt.format(ip=ip_cfg['address'], prefix=prefix))
597 liberty_ip = src_json.get('ip_address')
598 if liberty_ip:
599 if src_json.get('netmask'):
600 prefix = net.network_state.mask_to_net_prefix(src_json['netmask'])
601 cidr_fmt = '{ip}/{prefix}'
602 else:
603 cidr_fmt = '{ip}'
604 prefix = None
605 liberty_cidr = cidr_fmt.format(ip=liberty_ip, prefix=prefix)
606 if liberty_cidr not in net_v2['addresses']:
607 net_v2['addresses'].append(liberty_cidr)
608 if not net_v2['addresses']:
609 net_v2.pop('addresses')
610 if ignored_keys:
611 LOG.debug(
612 'Ignoring the network_data.json %s config keys %s',
613 src_json['id'], ', '.join(ignored_keys))
614 return src_json['link'], net_v2
615
616
617def _convert_network_json_to_net_v2(src_json, var_map):
618 """Return network v2 for an element of OpenStack NetworkData json.
619
620 @param src_json: Dict of network_data.json for a single src_json object
621 @param var_map: Dict with a variable name map from network_data.json to
622 network v2
623
624 @return Tuple of the interface name and the converted network v2 for the
625 src_json object. For example: eth0, {'match': {'macaddress': 'AA:BB'}}
626 """
627 net_v2 = {}
628 # Map openstack bond keys to network v2
629 # Copy key values
630 current_keys = set(src_json)
631 for key in current_keys.intersection(set(var_map)):
632 keyparts = var_map[key].split('.')
633 tmp_cfg = net_v2 # allow traversing net_v2 dict
634 while keyparts:
635 keypart = keyparts.pop(0)
636 if keyparts:
637 if keypart not in tmp_cfg:
638 tmp_cfg[keypart] = {}
639 tmp_cfg = tmp_cfg[keypart]
640 elif isinstance(src_json[key], list):
641 tmp_cfg[keypart] = copy.copy(src_json[key])
642 elif src_json[key]:
643 if keypart in LOWERCASE_KEY_VALUE_V2:
644 tmp_cfg[keypart] = src_json[key].lower()
645 else:
646 tmp_cfg[keypart] = src_json[key]
647 if 'key_rename' in var_map:
648 net_v2['key_rename'] = var_map['key_rename'].format(**net_v2)
649 return src_json['id'], net_v2
650
651
500def convert_net_json(network_json=None, known_macs=None):652def convert_net_json(network_json=None, known_macs=None):
653 """Parse OpenStack ConfigDrive NetworkData json, returning network cfg v2.
654
655 OpenStack network_data.json provides a 3 element dictionary
656 - "links" (links are network devices, physical or virtual)
657 - "networks" (networks are ip network configurations for one or more
658 links)
659 - services (non-ip services, like dns)
660
661 networks and links are combined via network items referencing specific
662 links via a 'link_id' which maps to a links 'id' field.
663 """
664 if network_json is None:
665 return None
666 net_config = {'version': 2}
667 for link in network_json.get('links', []):
668 link_type = link['type']
669 v2_key = LINK_TYPE_TO_NETWORK_V2_KEYS.get(link_type)
670 if not v2_key:
671 v2_key = 'ethernets'
672 if link_type not in KNOWN_PHYSICAL_TYPES:
673 LOG.warning('Unknown network_data link type (%s); treating as'
674 ' physical ethernet', link_type)
675 if v2_key not in net_config:
676 net_config[v2_key] = {}
677 var_map = copy.deepcopy(NETWORK_DATA_TO_V2.get(v2_key))
678 if not var_map:
679 var_map = copy.deepcopy(NETWORK_DATA_TO_V2.get(link_type))
680
681 # Add v2 config parameters map for this link_type if present
682 if link_type in network_state.NET_CONFIG_TO_V2:
683 var_map.update(dict(
684 (k.replace('-', '_'), 'parameters.{v}'.format(v=v))
685 for k, v in network_state.NET_CONFIG_TO_V2[link_type].items()))
686 intf_id, intf_cfg = _convert_network_json_to_net_v2(link, var_map)
687 if v2_key in ('ethernets', 'bonds') and 'name' not in intf_cfg:
688 if known_macs is None:
689 known_macs = net.get_interfaces_by_mac()
690 lower_known_macs = dict(
691 (k.lower(), v) for k, v in known_macs.items())
692 mac = intf_cfg.get( # top-level macaddress and match.macaddress
693 'macaddress', intf_cfg.get('match', {}).get('macaddress'))
694 if not mac:
695 raise ValueError("No mac_address or name entry for %s" % d)
696 mac = mac.lower()
697 if mac not in lower_known_macs:
698 raise ValueError(
699 'Unable to find a system nic for %s' % mac)
700 intf_cfg['key_rename'] = lower_known_macs[mac]
701 net_config[v2_key].update({intf_id: intf_cfg})
702 for network in network_json.get('networks', []):
703 v2_key = _find_v2_device_type(network['link'], net_config)
704 intf_id, network_cfg = _convert_network_json_network_to_net_v2(network)
705 for key, val in network_cfg.items():
706 if isinstance(val, list):
707 if key not in net_config[v2_key][intf_id]:
708 net_config[v2_key][intf_id][key] = []
709 net_config[v2_key][intf_id][key].extend(val)
710 else:
711 net_config[v2_key][intf_id][key] = val
712
713 # Inject global nameserver values under each all interface which
714 # has addresses and do not already have a DNS configuration
715 ignored_keys = set()
716 global_dns = []
717 for service in network_json.get('services', []):
718 if service['type'] != 'dns':
719 ignored_keys.update('services.type(%s)' % service['type'])
720 continue
721 global_dns.append(service['address'])
722
723 # Handle renames and global_dns
724 for dev_type in NET_V2_PHYSICAL_TYPES:
725 if dev_type not in net_config:
726 continue
727 renames = {}
728 for dev in net_config[dev_type]:
729 renames[dev] = net_config[dev_type][dev].pop('key_rename', None)
730 if not global_dns:
731 continue
732 dev_keys = set(net_config[dev_type][dev].keys())
733 if set(['nameservers', 'dhcp4', 'dhcp6']).intersection(dev_keys):
734 # Do not add nameservers if we already have dns config
735 continue
736 if 'addresses' not in net_config[dev_type][dev]:
737 # No configured address, needs no nameserver
738 continue
739 net_config[dev_type][dev]['nameservers'] = {
740 'addresses': copy.copy(global_dns), 'search': []}
741 for dev, rename in renames.items():
742 if rename:
743 net_config[dev_type][rename] = net_config[dev_type].pop(dev)
744 if 'set-name' in net_config[dev_type][rename]:
745 net_config[dev_type][rename]['set-name'] = rename
746
747 if ignored_keys:
748 LOG.debug(
749 'Ignoring the network_data.json config keys %s',
750 ', '.join(ignored_keys))
751
752 return net_config
753
754
755# Convert OpenStack ConfigDrive NetworkData json to network_config yaml
756def convert_net_json1(network_json=None, known_macs=None):
501 """Return a dictionary of network_config by parsing provided757 """Return a dictionary of network_config by parsing provided
502 OpenStack ConfigDrive NetworkData json format758 OpenStack ConfigDrive NetworkData json format
503759
diff --git a/tests/unittests/test_datasource/test_configdrive.py b/tests/unittests/test_datasource/test_configdrive.py
index 520c50f..f4181f5 100644
--- a/tests/unittests/test_datasource/test_configdrive.py
+++ b/tests/unittests/test_datasource/test_configdrive.py
@@ -719,11 +719,11 @@ class TestConvertNetworkData(CiTestCase):
719 for link in my_netdata['links']:719 for link in my_netdata['links']:
720 link['ethernet_mac_address'] = link['ethernet_mac_address'].upper()720 link['ethernet_mac_address'] = link['ethernet_mac_address'].upper()
721721
722 import pdb; pdb.set_trace()
722 ncfg = openstack.convert_net_json(my_netdata, known_macs=KNOWN_MACS)723 ncfg = openstack.convert_net_json(my_netdata, known_macs=KNOWN_MACS)
723 config_name2mac = {}724 config_name2mac = {}
724 for n in ncfg['config']:725 for name, if_cfg in ncfg['ethernets'].items():
725 if n['type'] == 'physical':726 config_name2mac[name] = if_cfg['match']['macaddress']
726 config_name2mac[n['name']] = n['mac_address']
727727
728 expected = {'nic0': 'fa:16:3e:05:30:fe', 'enp0s1': 'fa:16:3e:69:b0:58',728 expected = {'nic0': 'fa:16:3e:05:30:fe', 'enp0s1': 'fa:16:3e:69:b0:58',
729 'enp0s2': 'fa:16:3e:d4:57:ad'}729 'enp0s2': 'fa:16:3e:d4:57:ad'}

Subscribers

People subscribed via source and target branches