Diff comments:

> 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
> @@ -496,8 +527,233 @@ class MetadataReader(BaseReader):
>                                                 retries=self.retries)
>  
>  
> -# Convert OpenStack ConfigDrive NetworkData json to network_config yaml
> +def _find_v2_device_type(name, net_v2):
> +    """Return the netv2 physical device type containing matching name."""
> +    for device_type in NET_V2_PHYSICAL_TYPES:
> +        if name in net_v2.get(device_type, {}):
> +            return device_type
> +    return None
> +
> +
> +def _convert_network_json_network_to_net_v2(src_json):
> +    """Parse a single network item from the networks list in 
> network_data.json
> +
> +    @param src_json: One network item from network_data.json 'networks' key.
> +
> +    @return: Tuple of <interface_name>, network v2 configuration dict for the
> +         src_json. For example: eth0, {'addresses': [...], 'dhcp4': True}
> +    """
> +    net_v2 = {'addresses': []}
> +    ignored_keys = set()
> +
> +    # In Liberty spec https://specs.openstack.org/openstack/nova-specs/
> +    # specs/liberty/implemented/metadata-service-network-info.html
> +    if src_json['type'] == 'ipv4_dhcp':
> +        net_v2['dhcp4'] = True
> +    elif src_json['type'] == 'ipv6_dhcp':
> +        net_v2['dhcp6'] = True
> +
> +    for service in src_json.get('services', []):
> +        if service['type'] != 'dns':
> +            ignored_keys.update(['services.type(%s)' % service['type']])
> +            continue
> +        if 'nameservers' not in net_v2:
> +            net_v2['nameservers'] = {'addresses': [], 'search': []}
> +        net_v2['nameservers']['addresses'].append(service['address'])
> +    # In Rocky spec https://specs.openstack.org/openstack/nova-specs/specs/
> +    # rocky/approved/multiple-fixed-ips-network-information.html
> +    dns_nameservers = src_json.get('dns_nameservers', [])
> +    if dns_nameservers:
> +        if 'nameservers' not in net_v2:
> +            net_v2['nameservers'] = {'addresses': [], 'search': []}
> +        net_v2['nameservers']['addresses'] = copy.copy(dns_nameservers)
> +
> +    # Parse routes for network, prefix and gateway
> +    route_keys = set(['netmask', 'network', 'gateway'])
> +    for route in src_json.get('routes', []):
> +        ignored_route_keys = (set(route.keys()).difference(route_keys))
> +        ignored_keys.update(['route.%s' % key for key in ignored_route_keys])
> +        route_cfg = {
> +            'to': '{network}/{prefix}'.format(
> +                network=route['network'],
> +                
> prefix=net.network_state.mask_to_net_prefix(route['netmask'])),
> +            'via': route['gateway']}
> +        if route.get('metric'):
> +            route_cfg['metric'] = route.get('metric')
> +        if 'routes' not in net_v2:
> +            net_v2['routes'] = []
> +        net_v2['routes'].append(route_cfg)
> +
> +    # Parse ip addresses on Rocky and Liberty
> +    for ip_cfg in src_json.get('ip_addresses', []):
> +        if ip_cfg.get('netmask'):
> +            prefix = net.network_state.mask_to_net_prefix(ip_cfg['netmask'])
> +            cidr_fmt = '{ip}/{prefix}'
> +        else:
> +            cidr_fmt = '{ip}'
> +            prefix = None
> +        net_v2['addresses'].append(
> +            cidr_fmt.format(ip=ip_cfg['address'], prefix=prefix))
> +    liberty_ip = src_json.get('ip_address')
> +    if liberty_ip:
> +        if src_json.get('netmask'):
> +            prefix = 
> net.network_state.mask_to_net_prefix(src_json['netmask'])
> +            cidr_fmt = '{ip}/{prefix}'
> +        else:
> +            cidr_fmt = '{ip}'
> +            prefix = None
> +        liberty_cidr = cidr_fmt.format(ip=liberty_ip, prefix=prefix)
> +        if liberty_cidr not in net_v2['addresses']:
> +            net_v2['addresses'].append(liberty_cidr)
> +    if not net_v2['addresses']:
> +        net_v2.pop('addresses')
> +    if ignored_keys:
> +        LOG.debug(
> +            'Ignoring the network_data.json %s config keys %s',
> +            src_json['id'], ', '.join(ignored_keys))
> +    return src_json['link'], net_v2
> +
> +
> +def _convert_network_json_to_net_v2(src_json, var_map):
> +    """Return network v2 for an element of OpenStack NetworkData json.
> +
> +    @param src_json: Dict of network_data.json for a single src_json object
> +    @param var_map: Dict with a variable name map from network_data.json to
> +        network v2
> +
> +    @return Tuple of the interface name and the converted network v2 for the
> +        src_json object. For example: eth0, {'match': {'macaddress': 
> 'AA:BB'}}
> +    """
> +    net_v2 = {}
> +    # Map openstack bond keys to network v2
> +    # Copy key values
> +    current_keys = set(src_json)
> +    for key in current_keys.intersection(set(var_map)):
> +        keyparts = var_map[key].split('.')
> +        tmp_cfg = net_v2  # allow traversing net_v2 dict
> +        while keyparts:
> +            keypart = keyparts.pop(0)
> +            if keyparts:
> +                if keypart not in tmp_cfg:
> +                    tmp_cfg[keypart] = {}
> +                tmp_cfg = tmp_cfg[keypart]
> +            elif isinstance(src_json[key], list):
> +                tmp_cfg[keypart] = copy.copy(src_json[key])
> +            elif src_json[key]:
> +                if keypart in LOWERCASE_KEY_VALUE_V2:
> +                    tmp_cfg[keypart] = src_json[key].lower()
> +                else:
> +                    tmp_cfg[keypart] = src_json[key]
> +    if 'key_rename' in var_map:
> +        net_v2['key_rename'] = var_map['key_rename'].format(**net_v2)
> +    return src_json['id'], net_v2
> +
> +
>  def convert_net_json(network_json=None, known_macs=None):
> +    """Parse OpenStack ConfigDrive NetworkData json, returning network cfg 
> v2.
> +
> +    OpenStack network_data.json provides a 3 element dictionary
> +      - "links" (links are network devices, physical or virtual)
> +      - "networks" (networks are ip network configurations for one or more
> +                    links)
> +      -  services (non-ip services, like dns)
> +
> +    networks and links are combined via network items referencing specific
> +    links via a 'link_id' which maps to a links 'id' field.
> +    """
> +    if network_json is None:
> +        return None
> +    net_config = {'version': 2}
> +    for link in network_json.get('links', []):
> +        link_type = link['type']
> +        v2_key = LINK_TYPE_TO_NETWORK_V2_KEYS.get(link_type)
> +        if not v2_key:
> +            v2_key = 'ethernets'
> +            if link_type not in KNOWN_PHYSICAL_TYPES:
> +                LOG.warning('Unknown network_data link type (%s); treating 
> as'
> +                            ' physical ethernet', link_type)
> +        if v2_key not in net_config:
> +            net_config[v2_key] = {}
> +        var_map = copy.deepcopy(NETWORK_DATA_TO_V2.get(v2_key))
> +        if not var_map:
> +            var_map = copy.deepcopy(NETWORK_DATA_TO_V2.get(link_type))
> +
> +        # Add v2 config parameters map for this link_type if present
> +        if link_type in network_state.NET_CONFIG_TO_V2:
> +            var_map.update(dict(
> +                (k.replace('-', '_'), 'parameters.{v}'.format(v=v))
> +                for k, v in 
> network_state.NET_CONFIG_TO_V2[link_type].items()))
> +        intf_id, intf_cfg = _convert_network_json_to_net_v2(link, var_map)
> +        if v2_key in ('ethernets', 'bonds') and 'name' not in intf_cfg:
> +            if known_macs is None:
> +                known_macs = net.get_interfaces_by_mac()
> +            lower_known_macs = dict(
> +                (k.lower(), v) for k, v in known_macs.items())
> +            mac = intf_cfg.get(  # top-level macaddress and match.macaddress
> +                'macaddress', intf_cfg.get('match', {}).get('macaddress'))
> +            if not mac:
> +                raise ValueError("No mac_address or name entry for %s" % d)
> +            mac = mac.lower()
> +            if mac not in lower_known_macs:
> +                raise ValueError(
> +                    'Unable to find a system nic for %s' % mac)
> +            intf_cfg['key_rename'] = lower_known_macs[mac]
> +        net_config[v2_key].update({intf_id: intf_cfg})
> +    for network in network_json.get('networks', []):
> +        v2_key = _find_v2_device_type(network['link'], net_config)
> +        intf_id, network_cfg = 
> _convert_network_json_network_to_net_v2(network)
> +        for key, val in network_cfg.items():
> +            if isinstance(val, list):
> +                if key not in net_config[v2_key][intf_id]:
> +                    net_config[v2_key][intf_id][key] = []
> +                net_config[v2_key][intf_id][key].extend(val)
> +            else:
> +                net_config[v2_key][intf_id][key] = val
> +
> +    # Inject global nameserver values under each all interface which
> +    # has addresses and do not already have a DNS configuration
> +    ignored_keys = set()
> +    global_dns = []
> +    for service in network_json.get('services', []):
> +        if service['type'] != 'dns':
> +            ignored_keys.update('services.type(%s)' % service['type'])
> +            continue
> +        global_dns.append(service['address'])
> +
> +    # Handle renames and global_dns
> +    for dev_type in NET_V2_PHYSICAL_TYPES:
> +        if dev_type not in net_config:
> +            continue
> +        renames = {}
> +        for dev in net_config[dev_type]:
> +            renames[dev] = net_config[dev_type][dev].pop('key_rename', None)
> +            if not global_dns:
> +                continue
> +            dev_keys = set(net_config[dev_type][dev].keys())
> +            if set(['nameservers', 'dhcp4', 'dhcp6']).intersection(dev_keys):
> +                # Do not add nameservers if we already have dns config
> +                continue
> +            if 'addresses' not in net_config[dev_type][dev]:
> +                # No configured address, needs no nameserver
> +                continue
> +            net_config[dev_type][dev]['nameservers'] = {
> +                'addresses': copy.copy(global_dns), 'search': []}
> +        for dev, rename in renames.items():
> +            if rename:
> +                net_config[dev_type][rename] = net_config[dev_type].pop(dev)
> +                if 'set-name' in net_config[dev_type][rename]:
> +                    net_config[dev_type][rename]['set-name'] = rename
> +
> +    if ignored_keys:
> +        LOG.debug(
> +            'Ignoring the network_data.json config keys %s',
> +            ', '.join(ignored_keys))
> +
> +    return net_config
> +
> +
> +# Convert OpenStack ConfigDrive NetworkData json to network_config yaml
> +def convert_net_json1(network_json=None, known_macs=None):

WIP will drop this entire function (which was the v1 version of 
convert_net_json). Left in place for easy comparison/testing v1 vs v2 output.

>      """Return a dictionary of network_config by parsing provided
>         OpenStack ConfigDrive NetworkData json format
>  


-- 
https://code.launchpad.net/~chad.smith/cloud-init/+git/cloud-init/+merge/372009
Your team cloud-init commiters is requested to review the proposed merge of 
~chad.smith/cloud-init:feature/openstack-network-v2-multi-nic into 
cloud-init:master.

_______________________________________________
Mailing list: https://launchpad.net/~cloud-init-dev
Post to     : cloud-init-dev@lists.launchpad.net
Unsubscribe : https://launchpad.net/~cloud-init-dev
More help   : https://help.launchpad.net/ListHelp

Reply via email to