Add static network configuration support

Adds config drive based static network configuration to tinyipa.
Prerequisites:

* ramdisk is booted from a CD drive
* boot ISO image contains a config drive image
* 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.

Change-Id: I01956d7b71362b288fdb8ef06852043f9c8290a2
Story: 2006691
Task: 36993
This commit is contained in:
Ilya Etingof 2019-10-14 15:09:52 +02:00
parent 2380586d2f
commit fd7bcfab72
5 changed files with 321 additions and 0 deletions

View File

@ -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.

View File

@ -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

View File

@ -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='<network_data.json>',
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())

View File

@ -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

View File

@ -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/"