diff --git a/releasenotes/notes/add-static-network-support-ec0d8ec538d0a5d8.yaml b/releasenotes/notes/add-static-network-support-ec0d8ec538d0a5d8.yaml new file mode 100644 index 0000000..06c7f43 --- /dev/null +++ b/releasenotes/notes/add-static-network-support-ec0d8ec538d0a5d8.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Adds config drive based static network configuration to tinyipa. If ramdisk + is booted from a CD drive, and boot ISO image also contains a config drive + image, and OpenStack network meta data is found on the config drive then + attempt to apply static network configuration to the NICs. Fall back to + DHCP configuration if not a single NIC has been configured successfully. diff --git a/tinyipa/build_files/dhcp.sh b/tinyipa/build_files/dhcp.sh index 8383f26..eeaefd2 100644 --- a/tinyipa/build_files/dhcp.sh +++ b/tinyipa/build_files/dhcp.sh @@ -6,6 +6,10 @@ # This waits until all devices have registered /sbin/udevadm settle --timeout=%UDEV_SETTLE_TIMEOUT% +# First try for static network configuration +/opt/static_network.sh && exit + +# Otherwise fall back to DHCP NETDEVICES="$(awk -F: '/eth.:|tr.:/{print $1}' /proc/net/dev 2>/dev/null)" echo "$0: Discovered network devices: $NETDEVICES" for DEVICE in $NETDEVICES; do diff --git a/tinyipa/build_files/export_network_data.py b/tinyipa/build_files/export_network_data.py new file mode 100755 index 0000000..9dbc4a5 --- /dev/null +++ b/tinyipa/build_files/export_network_data.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +# +# Copyright 2019 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import collections +import json +import sys +import argparse + + +DESCRIPTION = """\ +This tool turns OpenStack network metadata, supplied in a form of JSON +document, into a collection of bash arrays. + +Each array variable holds values belonging to a specific option (e.g. +IP address) indexed by entity number (e.g. NIC). This representation +is thought to be more convenient to use from bash scripts. + +The caller of this tool is expected to `eval` its stdout to get bash +variables into caller's environment. + +Example +------- + +This fragment of L2 configuration: + +.. code-block:: json + + { + "links": [ + { + "id": "interface2", + "type": "vif", + "ethernet_mac_address": "a0:36:9f:2c:e8:70", + "vif_id": "e1c90e9f-eafc-4e2d-8ec9-58b91cebb53d", + "mtu": 1500 + }, + { + "id": "interface0", + "type": "phy", + "ethernet_mac_address": "a0:36:9f:2c:e8:80", + "mtu": 9000 + } + } + +Will turn into these bash variables: + +.. code-block:: bash + + ND_L2_ID=('interface2' 'interface0') + ND_L2_TYPE=('vif' 'phy') + ND_L2_ETHERNET_MAC_ADDRESS=('a0:36:9f:2c:e8:70' 'a0:36:9f:2c:e8:80') + ND_L2_VIF_ID=('e1c90e9f-eafc-4e2d-8ec9-58b91cebb53d' '') + ND_L2_MTU=(1500 9000) + +""" + + +def main(): + parser = argparse.ArgumentParser( + description=DESCRIPTION, formatter_class=argparse.RawDescriptionHelpFormatter) + + parser.add_argument( + '--source', + metavar='', + type=str, + required=True, + help='OpenStack network configuration meta data ' + '(AKA network_data.json)') + + parsed_args = parser.parse_args() + + with open(parsed_args.source) as fl: + network_data = json.load(fl) + + subnets = collections.defaultdict(collections.defaultdict) + + for network in network_data.get('networks', []): + # remap IP address variable to facilitate further CLI usage + if network.get('type') == 'ipv4': + network['ipv4_address'] = network.pop('ip_address', '') + network['ipv4_netmask'] = network.pop('netmask', '') + elif network.get('type') == 'ipv6': + network['ipv6_address'] = network.pop('ip_address', '') + network['ipv6_netmask'] = network.pop('netmask', '') + + subnet = subnets[network['link']] + subnet.update(network) + + nics = collections.defaultdict(dict) + + for link in network_data.get('links', []): + + nd_dev = link['id'] + nic = nics[nd_dev] + + nic.update({'ND_L2_%s' % k.upper(): v + for k, v in link.items()}) + + nic.update( + {'ND_L3_%s' % k.upper(): v + for k, v in subnets.get(nd_dev, {}).items()}) + + # transpose into positional form + + variables = collections.defaultdict(dict) + routes = collections.defaultdict(list) + services = collections.defaultdict(dict) + + for index, nic_id in enumerate(nics): + nic = nics[nic_id] + + for var, val in nic.items(): + if isinstance(val, (str, int)): + variables[var][index] = val + + # routing table is a special case + elif var == 'ND_L3_ROUTES': + for route in val: + for field in ('network', 'netmask', 'gateway'): + key = var + '_' + field.upper() + routes[key].append(route.get(field, '')) + + routes[var + '_ETHERNET_MAC_ADDRESS'].append(nic.get( + 'ND_L2_ETHERNET_MAC_ADDRESS', '')) + + sr_count = 0 + + for index, service in enumerate(network_data.get('services', [])): + for var, val in service.items(): + if isinstance(val, (str, int)): + var = 'ND_SERVICE_' + var.upper() + services[var][index] = val + + sr_count = index + 1 + + # generate bash arrays of L2/L3 NIC info indexed by NIC number + + nd_count = len(nics) + + for var, values in variables.items(): + print('%s=(%s)' % ( + var, ' '.join(repr(values.get(idx, '')) + for idx in range(nd_count)))) + + print('ND_NIC_COUNT=%d' % nd_count) + + # generate bash arrays of IPv4/v6 routing info indexed by route number + + rt_count = 0 + + for var, values in routes.items(): + print('%s=(%s)' % ( + var, ' '.join(repr(v) for v in values))) + + if not rt_count: + rt_count = len(values) + + print('ND_L3_ROUTES_COUNT=%d' % rt_count) + + # generate bash arrays of network service info indexed by service number + + for var, values in services.items(): + print('%s=(%s)' % ( + var, ' '.join(repr(values.get(idx, '')) + for idx in range(sr_count)))) + + print('ND_L3_SERVICES_COUNT=%d' % sr_count) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tinyipa/build_files/static_network.sh b/tinyipa/build_files/static_network.sh new file mode 100755 index 0000000..81172ef --- /dev/null +++ b/tinyipa/build_files/static_network.sh @@ -0,0 +1,121 @@ +#!/bin/sh +# +# Try to configure NICs and routes from OpenStack metadata +# +# Exit with success if at least one NIC has been configured +# + +. /etc/init.d/tc-functions + +NETWORK_DATA=openstack/latest/metadata/network_data.json + +if [ ! -b /dev/cdrom ]; then + echo "CDROM device not present - no static network config" + exit 1 +fi + +mkdir -p /mnt +mount -o ro /dev/cdrom /mnt +if [ "$?" != 0 ]; then + echo "CDROM device cannot be mounted - no static network config" + exit 1 +fi + +if [ ! -f /mnt/config-2.img ]; then + echo "Config drive image not present - no static network config" + exit 1 +fi + +mkdir -p /config-2 +mount -o ro /mnt/config-2.img /config-2 +if [ "$?" != 0 ]; then + echo "Config drive cannot be mounted - no static network config" + exit 1 +fi + +if [ ! -f /config-2/$NETWORK_DATA ]; then + echo "No network_data.json found on config drive - no static network config" + exit 1 +fi + +# If succeeded, this script will print out bash variables +eval $(/opt/export_network_data.py --source /config-2/$NETWORK_DATA) + +eval_rc=$? + +umount -f /config-2 /mnt + +if [ "$eval_rc" != 0 ]; then + echo "Processing network data failed - no static network config" + exit 1 +fi + +rc=1 + +declare -A mac_to_dev + +while [ $ND_NIC_COUNT -gt 0 ]; do + + ND_NIC_COUNT=$((ND_NIC_COUNT - 1)) + + ND_MAC=${ND_L2_ETHERNET_MAC_ADDRESS[$ND_NIC_COUNT]} + ND_L2_ID=${ND_L2_IDS[$ND_NIC_COUNT]} + + if [ ! -z "$ND_L2_ID" ]; then + ip link show $ND_L2_ID > /dev/null 2>&1 + fi + + if [ -z "$ND_L2_ID" -o "$?" != 0 ]; then + # NIC is not known, look up NIC name by MAC + if [ -z "$ND_MAC" ]; then + echo "Neither name nor MAC is configured for a L2 device, continuing" + continue + fi + + ND_L2_ID=$(ip -o link | awk "/.*?BROADCAST.*?LOWER_UP.*?${ND_MAC}.*?/{print \$2}") + ND_L2_ID=${ND_L2_ID/:/} + + if [ -z "$ND_L2_ID" ]; then + echo "No NIC found by MAC $ND_MAC, continuing" + continue + fi + fi + + mac_to_dev[$ND_MAC] = $ND_L2_ID + + ifconfig $ND_L2_ID \ + ${ND_L3_IPV4_ADDRESS[$ND_NIC_COUNT]} \ + netmask ${ND_L3_IPV4_NETMASK[$ND_NIC_COUNT]} \ + up + + if [ "$?" != 0 ]; then + echo "Failed to assign IP address to NIC $ND_L2_ID, continuing" + continue + fi + + # consider it a success if at least one NIC is brought up + rc=0 + + echo "Configured $ND_L2_ID IP ${ND_L3_IPV4_ADDRESS[$ND_NIC_COUNT]} netmask ${ND_L3_IPV4_NETMASK[$ND_NIC_COUNT]}" + +done + +while [ $ND_L3_ROUTES_COUNT -gt 0 ]; do + ND_L3_ROUTES_COUNT=$((ND_L3_ROUTES_COUNT - 1)) + + route add -net ${ND_L3_ROUTES_NETWORK[ND_L3_ROUTES_COUNT]} \ + netmask ${ND_L3_ROUTES_NETMASK[ND_L3_ROUTES_COUNT]} \ + gw ${ND_L3_ROUTES_GATEWAY[ND_L3_ROUTES_COUNT]} \ + dev ${mac_to_dev[$ND_L2_ID]} + + if [ "$?" != 0 ]; then + echo "Failed to set route for network ${ND_L3_ROUTES_NETWORK[ND_L3_ROUTES_COUNT]}, continuing" + continue + fi + + echo "Set route ${ND_L3_ROUTES_NETWORK[ND_L3_ROUTES_COUNT]}" + +done + +exit $rc + diff --git a/tinyipa/finalise-tinyipa.sh b/tinyipa/finalise-tinyipa.sh index 953aec9..7f27384 100755 --- a/tinyipa/finalise-tinyipa.sh +++ b/tinyipa/finalise-tinyipa.sh @@ -150,6 +150,9 @@ cleanup_tce "$DST_DIR" # Copy bootlocal.sh to opt sudo cp "$WORKDIR/build_files/bootlocal.sh" "$FINALDIR/opt/." +# Copy static_network.sh to opt +sudo cp "$WORKDIR/build_files/static_network.sh" "$FINALDIR/opt/." + # Copy udhcpc.script to opt sudo cp "$WORKDIR/udhcpc.script" "$FINALDIR/opt/"