Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add static network setup: #251

Merged
merged 2 commits into from
Nov 23, 2024
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Add static network script:
For environments where DHCP is not available,
I've added a script that will statically configure
network interfaces. The IPAM info must be passed in
via kernel cmdline parameters and be in the appropriate
format.

ipam=<mac-address>:<vlan-id>:<ip-address>:<netmask>:<gateway>:<hostname>:<dns>:<search-domains>:<ntp>

This is probably only useful for the HookOS ISO.
For the Tinkerbell stack, Smee handles patching the ISO
at runtime to include this `ipam=` parameter.

To facilitate this static ip configuration, scripts were placed
into the host filesystem at /etc/init.d. Files in this location
are run at startup my the init system. This makes it possible
to just add the scripts as files in the linuxkit yaml file.
The vlan.sh script was moved to an init.d script because it
needs to run before the static-network script. The vlan.sh script
was updated to use posix /bin/sh instead of bash because the host
only has /bin/sh.

Signed-off-by: Jacob Weinstock <jakobweinstock@gmail.com>
jacobweinstock committed Nov 23, 2024
commit a5766439d16fecd89d9e55cdc5c6790f92bb2dcf
1 change: 0 additions & 1 deletion bash/hook-lk-containers.sh
Original file line number Diff line number Diff line change
@@ -7,7 +7,6 @@ function build_all_hook_linuxkit_containers() {
# when adding new container builds here you'll also want to add them to the
# `linuxkit_build` function in the linuxkit.sh file.
# # NOTE: linuxkit containers must be in the images/ directory
build_hook_linuxkit_container hook-ip HOOK_CONTAINER_IP_IMAGE
build_hook_linuxkit_container hook-bootkit HOOK_CONTAINER_BOOTKIT_IMAGE
build_hook_linuxkit_container hook-docker HOOK_CONTAINER_DOCKER_IMAGE
build_hook_linuxkit_container hook-mdev HOOK_CONTAINER_MDEV_IMAGE
5 changes: 2 additions & 3 deletions bash/linuxkit.sh
Original file line number Diff line number Diff line change
@@ -50,7 +50,7 @@ function linuxkit_build() {
fi

# Build the containers in this repo used in the LinuxKit YAML;
build_all_hook_linuxkit_containers # sets HOOK_CONTAINER_IP_IMAGE, HOOK_CONTAINER_BOOTKIT_IMAGE, HOOK_CONTAINER_DOCKER_IMAGE, HOOK_CONTAINER_MDEV_IMAGE, HOOK_CONTAINER_CONTAINERD_IMAGE
build_all_hook_linuxkit_containers # sets HOOK_CONTAINER_BOOTKIT_IMAGE, HOOK_CONTAINER_DOCKER_IMAGE, HOOK_CONTAINER_MDEV_IMAGE, HOOK_CONTAINER_CONTAINERD_IMAGE

# Template the linuxkit configuration file.
# - You'd think linuxkit would take --build-args or something by now, but no.
@@ -64,14 +64,13 @@ function linuxkit_build() {
# shellcheck disable=SC2016 # I'm using single quotes to avoid shell expansion, envsubst wants the dollar signs.
cat "linuxkit-templates/${kernel_info['TEMPLATE']}.template.yaml" |
HOOK_KERNEL_IMAGE="${kernel_oci_image}" HOOK_KERNEL_ID="${inventory_id}" HOOK_KERNEL_VERSION="${kernel_oci_version}" \
HOOK_CONTAINER_IP_IMAGE="${HOOK_CONTAINER_IP_IMAGE}" \
HOOK_CONTAINER_BOOTKIT_IMAGE="${HOOK_CONTAINER_BOOTKIT_IMAGE}" \
HOOK_CONTAINER_DOCKER_IMAGE="${HOOK_CONTAINER_DOCKER_IMAGE}" \
HOOK_CONTAINER_MDEV_IMAGE="${HOOK_CONTAINER_MDEV_IMAGE}" \
HOOK_CONTAINER_CONTAINERD_IMAGE="${HOOK_CONTAINER_CONTAINERD_IMAGE}" \
HOOK_CONTAINER_RUNC_IMAGE="${HOOK_CONTAINER_RUNC_IMAGE}" \
HOOK_CONTAINER_EMBEDDED_IMAGE="${HOOK_CONTAINER_EMBEDDED_IMAGE}" \
envsubst '$HOOK_VERSION $HOOK_KERNEL_IMAGE $HOOK_KERNEL_ID $HOOK_KERNEL_VERSION $HOOK_CONTAINER_IP_IMAGE $HOOK_CONTAINER_BOOTKIT_IMAGE $HOOK_CONTAINER_DOCKER_IMAGE $HOOK_CONTAINER_MDEV_IMAGE $HOOK_CONTAINER_CONTAINERD_IMAGE $HOOK_CONTAINER_RUNC_IMAGE $HOOK_CONTAINER_EMBEDDED_IMAGE' \
envsubst '$HOOK_VERSION $HOOK_KERNEL_IMAGE $HOOK_KERNEL_ID $HOOK_KERNEL_VERSION $HOOK_CONTAINER_BOOTKIT_IMAGE $HOOK_CONTAINER_DOCKER_IMAGE $HOOK_CONTAINER_MDEV_IMAGE $HOOK_CONTAINER_CONTAINERD_IMAGE $HOOK_CONTAINER_RUNC_IMAGE $HOOK_CONTAINER_EMBEDDED_IMAGE' \
> "hook.${inventory_id}.yaml"

declare -g linuxkit_bin=""
9 changes: 8 additions & 1 deletion files/dhcp.sh
Original file line number Diff line number Diff line change
@@ -18,7 +18,9 @@ run_dhcp_client() {

if [ "$one_shot" = "true" ]; then
# always return true for the one shot dhcp call so it doesn't block Hook from starting up.
/sbin/dhcpcd --nobackground -f /dhcpcd.conf --allowinterfaces "${al}" -1 || true
# the --nobackground is not used here because when it is used, dhcpcd doesn't honor the --timeout option
# and waits indefinitely for a response. For one shot, we want to timeout after the 30 second default.
/sbin/dhcpcd -f /dhcpcd.conf --allowinterfaces "${al}" -1 || true
# use busybox's ntpd to set the time after getting an IP address; don't fail
echo 'sleep 1 second before calling ntpd' && sleep 1
/usr/sbin/ntpd -n -q -dd -p pool.ntp.org || true
@@ -28,6 +30,11 @@ run_dhcp_client() {

}

if [ -f /run/network/interfaces ]; then
echo "the /run/network/interfaces file exists, so static IP's are in use. we will not be running the dhcp client."
exit 0
fi

# we always return true so that a failure here doesn't block the next container service from starting. Ideally, we always
# want the getty service to start so we can debug failures.
run_dhcp_client "$1" || true
50 changes: 50 additions & 0 deletions files/setup-dns.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/bin/sh

# This script is intended to be run on the HookOS/Linuxkit host so it must use /bin/sh.
# No other shells are available on the host.

# modified from alpine setup-dns
# apk add alpine-conf

exec 3>&1 4>&2
trap 'exec 2>&4 1>&3' 0 1 2 3
exec 1>/var/log/setup-dns.log 2>&1

while getopts "d:n:h" opt; do
case $opt in
d) DOMAINNAME="$OPTARG";;
n) NAMESERVERS="$OPTARG";;
esac
done
shift $(($OPTIND - 1))


conf="${ROOT}resolv.conf"

if [ -f "$conf" ] ; then
domain=$(awk '/^domain/ {print $2}' $conf)
dns=$(awk '/^nameserver/ {printf "%s ",$2}' $conf)
elif fqdn="$(get_fqdn)" && [ -n "$fqdn" ]; then
domain="$fqdn"
fi

if [ -n "$DOMAINNAME" ]; then
domain="$DOMAINNAME"
fi

if [ -n "$NAMESERVERS" ] || [ $# -gt 0 ];then
dns="$NAMESERVERS"
fi

if [ -n "$domain" ]; then
mkdir -p "${conf%/*}"
echo "search $domain" > $conf
fi

if [ -n "$dns" ] || [ $# -gt 0 ] && [ -f "$conf" ]; then
sed -i -e '/^nameserver/d' $conf
fi
for i in $dns $@; do
mkdir -p "${conf%/*}"
echo "nameserver $i" >> $conf
done
138 changes: 138 additions & 0 deletions files/static-network.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#!/bin/sh

# This script is intended to be run on the HookOS/Linuxkit host so it must use /bin/sh.
# No other shells are available on the host.

# this script will statically configure a single network interface based on the ipam= parameter
# passed in the kernel command line. The ipam parameter is a colon separated string with the following fields:
# ipam=<mac-address>:<vlan-id>:<ip-address>:<netmask>:<gateway>:<hostname>:<dns>:<search-domains>:<ntp>
# Example: ipam=de-ad-be-ef-fe-ed::192.168.2.193:255.255.255.0:192.168.2.1:myserver:1.1.1.1,8.8.8.8::132.163.97.1,132.163.96.1
# the mac address format requires it to be hyphen separated.

exec 3>&1 4>&2
trap 'exec 2>&4 1>&3' 0 1 2 3
exec 1>/var/log/network_config.log 2>&1

set -xeuo pipefail

# Define the location of the interfaces file
INTERFACES_FILE="/var/run/network/interfaces"

parse_ipam_from_cmdline() {
local cmdline
local ipam_value

# Read the contents of /proc/cmdline
cmdline=$(cat /proc/cmdline)

# Use grep to find the ipam= parameter and awk to extract its value
ipam_value=$(echo "$cmdline" | grep -o 'ipam=[^ ]*' | awk -F= '{print $2}')

# Check if ipam= parameter was found
if [ -n "$ipam_value" ]; then
echo "$ipam_value"
return 0
else
echo "ipam= parameter not found in /proc/cmdline" >&2
return 1
fi
}

# Function to get interface name from MAC address
# TODO(jacobweinstock): if a vlan id is provided we should match for the vlan interface
get_interface_name() {
local mac=$1
for interface in /sys/class/net/*; do
if [ -f "$interface/address" ]; then
if [ "$(cat "$interface/address")" == "$mac" ]; then
echo "$(basename "$interface")"
return 0
fi
fi
done
return 1
}

convert_hyphen_to_colon() {
echo "$1" | tr '-' ':'
}

ipam=$(parse_ipam_from_cmdline)
if [ $? -ne 0 ]; then
echo "Failed to get IPAM value, not statically configuring network"
cat /proc/cmdline
exit 0
fi
echo "IPAM value: $ipam"

mkdir -p $(dirname "$INTERFACES_FILE")

# Parse the IPAM string
IFS=':' read -r mac vlan_id ip netmask gateway hostname dns search_domains ntp <<EOF
${ipam}
EOF

# Check for required fields
if [ -z "$mac" ] || [ -z "$ip" ] || [ -z "$netmask" ] || [ -z "$dns" ]; then
echo "Error: MAC address, IP address, netmask, and DNS are required."
echo "$ipam"
exit 1
fi

# convert Mac address to colon separated format
mac=$(convert_hyphen_to_colon "$mac")

# convert , (comma) separated values to space separated values
dns=$(echo "$dns" | tr ',' ' ')
search_domains=$(echo "$search_domains" | tr ',' ' ')
ntp=$(echo "$ntp" | tr ',' ' ')

# Get interface name
interface=$(get_interface_name "$mac")
if [ -z "$interface" ]; then
echo "Error: No interface found with MAC address $mac"
exit 1
fi

# Start writing to the interfaces file
{
echo "# Static Network configuration for $interface"
echo ""
echo "auto $interface"

if [ -n "$vlan_id" ]; then
echo "iface $interface inet manual"
echo ""
echo "auto $interface.$vlan_id"
echo "iface $interface.$vlan_id inet static"
else
echo "iface $interface inet static"
fi

echo " address $ip"
echo " netmask $netmask"

[ -n "$gateway" ] && echo " gateway $gateway"
[ -n "$hostname" ] && echo " hostname $hostname"

if [ -n "$dns" ]; then
echo " dns-nameserver $dns"
fi

if [ -n "$search_domains" ]; then
echo " dns-search $search_domains"
fi

if [ -n "$ntp" ]; then
echo " ntp-servers $ntp"
fi

} > "$INTERFACES_FILE"

echo "Network configuration has been written to $INTERFACES_FILE"

# Run ifup on the interface
ifup -v -a -i "$INTERFACES_FILE"

# setup DNS
ROOT=/run/resolvconf/ setup-dns -d "$search_domains" "$dns"
73 changes: 42 additions & 31 deletions files/vlan.sh
Original file line number Diff line number Diff line change
@@ -1,63 +1,74 @@
#!/bin/bash
#!/bin/sh

# This script is intended to be run on the HookOS/Linuxkit host so it must use /bin/sh.
# No other shells are available on the host.

exec 3>&1 4>&2
trap 'exec 2>&4 1>&3' 0 1 2 3
exec 1>/var/log/vlan.log 2>&1

set -e # exit on error

# This script will set up VLAN interfaces if `vlan_id=xxxx` in `/proc/cmdline` has a value.
# It will use the MAC address specified in `hw_addr=` to find the interface to add the VLAN to.

function parse_with_regex_power() {
declare stdin_data cmdline_rest
stdin_data="$(cat)" # read stdin
declare search_argument="${1}"
declare normal_matcher="([a-zA-Z0-9/\\@#\$%^&\!*\(\)'\"=:,._-]+)"
declare quoted_matcher="\"([a-zA-Z0-9/\\@#\$%^&\!*\(\)',=: ._-]+)\""
[ $# -gt 1 ] && normal_matcher="$2" && quoted_matcher="$2"
cmdline_rest="$(printf '%s' "$stdin_data" | sed -rn "s/.* ?${search_argument}=${normal_matcher} ?(.*)+?/\1/p")"
if echo "$cmdline_rest" | grep -Eq '^"'; then
cmdline_rest="$(printf "%s\n" "$stdin_data" | sed -rn "s/.* ?${search_argument}=${quoted_matcher} ?(.*)+?/\1/p")"
fi
printf "%s\n" "$cmdline_rest"
}
parse_from_cmdline() {
local key="${1}"
local cmdline
local ipam_value

function parse_kernel_cmdline_for() {
declare result
# shellcheck disable=SC2002
result=$(cat /proc/cmdline | parse_with_regex_power "$@")
if [ -z "${result}" ]; then
return 1
else
printf "%s" "$result"
fi
# Read the contents of /proc/cmdline
cmdline=$(cat /proc/cmdline)

# Use grep to find the ipam= parameter and awk to extract its value
value=$(echo "$cmdline" | grep -o "${key}=[^ ]*" | awk -F= '{print $2}')

# Check if parameter was found
if [ -n "$value" ]; then
echo "$value"
return 0
else
echo "${key}= parameter not found in /proc/cmdline" >&2
return 1
fi
}

function kernel_cmdline_exists() {
parse_kernel_cmdline_for "$@" > /dev/null
get_interface_name() {
local mac=$1
for interface in /sys/class/net/*; do
if [ -f "$interface/address" ]; then
if [ "$(cat "$interface/address")" == "$mac" ]; then
echo "$(basename "$interface")"
return 0
fi
fi
done
return 1
}

function add_vlan_interface() {
# check if vlan_id are set in the kernel commandline, otherwise return.
if ! kernel_cmdline_exists vlan_id; then
if ! parse_from_cmdline vlan_id; then
echo "No vlan_id=xxxx set in kernel commandline; no VLAN handling." >&2
return
fi

# check if hw_addr are set in the kernel commandline, otherwise return.
if ! kernel_cmdline_exists hw_addr; then
if ! parse_from_cmdline hw_addr; then
echo "No hw_addr=xx:xx:xx:xx:xx:xx set in kernel commandline." >&2
fi

echo "Starting VLAN handling, parsing..." >&2

declare vlan_id hw_addr
vlan_id="$(parse_kernel_cmdline_for vlan_id)"
hw_addr="$(parse_kernel_cmdline_for hw_addr)"
vlan_id="$(parse_from_cmdline vlan_id)"
hw_addr="$(parse_from_cmdline hw_addr)"

echo "VLAN handling - vlan_id: '${vlan_id}', hw_addr: '${hw_addr}'" >&2

if [ -n "$vlan_id" ]; then
if [ -n "$hw_addr" ]; then
echo "VLAN handling - vlan_id: '${vlan_id}', hw_addr: '${hw_addr}', searching for interface..." >&2
ifname="$(ip -br link | awk '$3 ~ /'"${hw_addr}"'/ {print $1}')"
ifname="$(get_interface_name ${hw_addr})"
echo "VLAN handling - vlan_id: '${vlan_id}', hw_addr: '${hw_addr}', found interface: '${ifname}'" >&2
else
echo "VLAN handling - vlan_id: '${vlan_id}', hw_addr: '${hw_addr}', no hw_addr found in kernel commandline; default ifname to eth0." >&2
22 changes: 12 additions & 10 deletions linuxkit-templates/hook.template.yaml
Original file line number Diff line number Diff line change
@@ -3,7 +3,6 @@
# - HOOK_KERNEL_IMAGE: ${HOOK_KERNEL_IMAGE}
# - HOOK_KERNEL_ID: ${HOOK_KERNEL_ID}
# - HOOK_KERNEL_VERSION: ${HOOK_KERNEL_VERSION}
# - HOOK_CONTAINER_IP_IMAGE: ${HOOK_CONTAINER_IP_IMAGE}
# - HOOK_CONTAINER_BOOTKIT_IMAGE: ${HOOK_CONTAINER_BOOTKIT_IMAGE}
# - HOOK_CONTAINER_DOCKER_IMAGE: ${HOOK_CONTAINER_DOCKER_IMAGE}
# - HOOK_CONTAINER_MDEV_IMAGE: ${HOOK_CONTAINER_MDEV_IMAGE}
@@ -42,14 +41,6 @@ onboot:
image: linuxkit/modprobe:v1.0.0
command: [ "modprobe", "cdc_ncm" ] # for usb ethernet dongles

- name: vlan
image: "${HOOK_CONTAINER_IP_IMAGE}"
capabilities:
- all
binds.add:
- /etc/ip/vlan.sh:/etc/ip/vlan.sh
command: [ "/etc/ip/vlan.sh" ]

- name: dhcpcd-once
image: linuxkit/dhcpcd:v1.0.0
command: [ "/etc/ip/dhcp.sh", "true" ] # 2nd paramter is one-shot true/false: true for onboot, false for services
@@ -275,10 +266,21 @@ files:
ANSI_COLOR="1;34"
HOME_URL="https://github.com/tinkerbell/hook"

- path: etc/ip/vlan.sh
# Putting scripts in /etc/init.d/ allows them to be run at boot time
- path: etc/init.d/002-vlan.sh
source: "files/vlan.sh"
mode: "0777"

# Putting scripts in /etc/init.d/ allows them to be run at boot time
- path: etc/init.d/003-static-network.sh
source: "files/static-network.sh"
mode: "0777"

# This makes the script available in the host PATH
- path: sbin/setup-dns
source: "files/setup-dns.sh"
mode: "0777"

- path: etc/ip/dhcp.sh
source: "files/dhcp.sh"
mode: "0777"