diff --git a/Makefile b/Makefile index 453d7a2..1d8c862 100644 --- a/Makefile +++ b/Makefile @@ -23,6 +23,7 @@ core: $(MAN1PAGES) $(MAN5PAGES) $(MAN8PAGES) install -Dm644 -t "$(PREFIX)"/usr/lib/tiny-cloud \ lib/tiny-cloud/common \ lib/tiny-cloud/init \ + lib/tiny-cloud/network-config \ lib/tiny-cloud/tiny-cloud.conf install -Dm644 -t "$(PREFIX)"/usr/lib/tiny-cloud/user-data \ lib/tiny-cloud/user-data/* diff --git a/README.md b/README.md index e35d602..1342301 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,13 @@ instance: Other cloud specific and user-data handler specific actions may also occur. +For the NoCloud provider, Tiny Cloud also supports a subset of +`network-config` version 2. The currently supported keys are: +`version: 2`, `ethernets`, `dhcp4`, `dhcp6`, `accept-ra`, +`match.macaddress`, `set-name`, `addresses`, `gateway4`, `gateway6`, +`nameservers.addresses`, `dhcp4-overrides.route-metric`, and +`dhcp6-overrides.route-metric`. + Also included is a handy `imds` client script for easy access to an instance's IMDS data. diff --git a/TODO.md b/TODO.md index 2491408..fa7efbb 100644 --- a/TODO.md +++ b/TODO.md @@ -18,7 +18,7 @@ * Support for multipart `user-data` payload? -* Support for `#network-config`? +* Support for `#network-config` v1? * Support non-`ext[234]` filesystems? diff --git a/docs/tiny-cloud.8.scd b/docs/tiny-cloud.8.scd index 17c2f44..4e3edea 100644 --- a/docs/tiny-cloud.8.scd +++ b/docs/tiny-cloud.8.scd @@ -79,6 +79,35 @@ Otherwise, the selected phase action list is loaded from the tiny-cloud init library. Each action is run in order. If an action fails, *tiny-cloud* exits with a non-zero status. +# NOCLOUD NETWORKING + +For the *nocloud* provider, *tiny-cloud* supports the legacy +*meta-data* *network-interfaces* and a subset of NoCloud +*network-config* version 2. + +Supported *network-config* keys are: + +- *version: 2* +- *ethernets* +- *dhcp4* +- *dhcp6* +- *accept-ra* +- *match.macaddress* +- *set-name* +- *addresses* +- *gateway4* +- *gateway6* +- *nameservers.addresses* +- *dhcp4-overrides.route-metric* +- *dhcp6-overrides.route-metric* + +When `match.macaddress` and `set-name` are used, *tiny-cloud* writes +_/etc/iftab_, configures _/etc/conf.d/macifrename_ to use it, and enables +and starts the `macifrename` OpenRC service if available. + +Unsupported `network-config` versions or unsupported v2 layouts are ignored +with a warning, and normal network fallback behavior applies. + # FILES _/etc/tiny-cloud.conf_ @@ -87,6 +116,16 @@ _/etc/tiny-cloud.conf_ _/etc/tiny-cloud.disabled_ When present, suppresses phase execution. +_/etc/network/interfaces_ + Generated network interface configuration. + +_/etc/iftab_ + MAC-based interface rename rules generated from NoCloud *network-config* + version 2. + +_/etc/conf.d/macifrename_ + OpenRC service configuration used when _/etc/iftab_ is generated. + _/var/lib/cloud/.bootstrap-complete_ Bootstrap completion marker. diff --git a/lib/tiny-cloud/cloud/nocloud/init b/lib/tiny-cloud/cloud/nocloud/init index 8795a4e..7487455 100644 --- a/lib/tiny-cloud/cloud/nocloud/init +++ b/lib/tiny-cloud/cloud/nocloud/init @@ -45,12 +45,16 @@ want_ephemeral_network() { init__set_network_interfaces() { local interfaces="$(imds meta-data/network-interfaces)" + local network_config="$TINY_CLOUD_VAR/network-config" mkdir -p "$ETC"/network if [ -n "$interfaces" ]; then printf "%s\n" "$interfaces" > "$ETC"/network/interfaces + elif network_config_set "$network_config" "NoCloud"; then + : elif ! [ -f "$ETC"/network/interfaces ]; then init__set_default_interfaces fi + network_config_setup_macifrename if ! grep -q dhcp "$ETC"/network/interfaces; then set_resolv_conf fi diff --git a/lib/tiny-cloud/init b/lib/tiny-cloud/init index 196441e..429be5a 100644 --- a/lib/tiny-cloud/init +++ b/lib/tiny-cloud/init @@ -6,6 +6,7 @@ : "${PREFIX:=/usr}" : "${LIBDIR:=$PREFIX/lib}" . "$LIBDIR/tiny-cloud/common" +. "$LIBDIR/tiny-cloud/network-config" : "${SKIP_INIT_ACTIONS:=}" diff --git a/lib/tiny-cloud/network-config b/lib/tiny-cloud/network-config new file mode 100644 index 0000000..d44b77b --- /dev/null +++ b/lib/tiny-cloud/network-config @@ -0,0 +1,165 @@ +# Tiny Cloud - Network Config Functions +# vim:set filetype=sh: +# shellcheck shell=sh + +network_config_get() { + local file="$1" path="$2" + IFS="/" + yx -f "$file" $path 2>/dev/null + unset IFS +} + +network_config_set_v2() { + local file="$1" source_name="${2:-network-config}" + + [ -s "$file" ] || return 1 + + local ethernets="$(network_config_get "$file" ethernets)" + if [ -z "$ethernets" ]; then + log -i -t "$phase" warning "$ACTION: $source_name network-config v2 has no supported ethernets entries" + return 1 + fi + + printf "%s\n%s\n\n" \ + "auto lo" \ + "iface lo inet loopback" \ + > "$ETC/network/interfaces" + rm -f "$ETC/iftab" + + local iface configured= target_name macaddress + for iface in $ethernets; do + local stanza= dhcp4= dhcp6= accept_ra= dhcp_program= dhcp_opts= + local address= gateway= nameservers= server= + local route_metric= + + target_name="$(network_config_get "$file" ethernets/$iface/set-name)" + macaddress="$(network_config_get "$file" ethernets/$iface/match/macaddress)" + [ -n "$target_name" ] || target_name="$iface" + + if [ -n "$target_name" ] && [ -n "$macaddress" ]; then + printf "%s %s\n" "$target_name" "$macaddress" >> "$ETC/iftab" + fi + + dhcp4="$(network_config_get "$file" ethernets/$iface/dhcp4)" + dhcp6="$(network_config_get "$file" ethernets/$iface/dhcp6)" + accept_ra="$(network_config_get "$file" ethernets/$iface/accept-ra)" + + case "$dhcp4" in + true|yes) + stanza="$stanza\tuse dhcp\n" + route_metric="$(network_config_get "$file" ethernets/$iface/dhcp4-overrides/route-metric)" + if [ -n "$route_metric" ]; then + dhcp_program="dhcpcd" + dhcp_opts="$dhcp_opts -m $route_metric" + fi + ;; + esac + + case "$dhcp6" in + true|yes) + case "$dhcp4" in + true|yes) + route_metric="$(network_config_get "$file" ethernets/$iface/dhcp6-overrides/route-metric)" + if [ -n "$route_metric" ]; then + dhcp_program="dhcpcd" + dhcp_opts="$dhcp_opts -m $route_metric" + fi + ;; + *) + stanza="$stanza\tuse dhcp\n" + dhcp_program="dhcpcd" + dhcp_opts="$dhcp_opts -6" + route_metric="$(network_config_get "$file" ethernets/$iface/dhcp6-overrides/route-metric)" + if [ -n "$route_metric" ]; then + dhcp_opts="$dhcp_opts -m $route_metric" + fi + ;; + esac + ;; + esac + + case "$accept_ra" in + true|yes) + stanza="$stanza\tuse ipv6-ra\n" + ;; + esac + + address="$(network_config_get "$file" ethernets/$iface/addresses/1)" + if [ -n "$address" ]; then + stanza="$stanza\taddress $address\n" + gateway="$(network_config_get "$file" ethernets/$iface/gateway4)" + [ -n "$gateway" ] || gateway="$(network_config_get "$file" ethernets/$iface/gateway6)" + if [ -n "$gateway" ]; then + stanza="$stanza\tgateway $gateway\n" + fi + nameservers="$(network_config_get "$file" ethernets/$iface/nameservers/addresses)" + for server in $nameservers; do + add_once "$ETC/resolv.conf" \ + "nameserver $(network_config_get "$file" ethernets/$iface/nameservers/addresses/$server)" + done + fi + + if [ -n "$dhcp_program" ]; then + stanza="$stanza\tdhcp-program $dhcp_program\n" + fi + if [ -n "$dhcp_opts" ]; then + stanza="$stanza\tdhcp-opts${dhcp_opts}\n" + fi + + [ -n "$stanza" ] || continue + + printf "%s\n%s\n%b\n" \ + "auto $target_name" \ + "iface $target_name" \ + "$stanza" >> "$ETC/network/interfaces" + configured=1 + done + + if [ -z "$configured" ]; then + log -i -t "$phase" warning "$ACTION: $source_name network-config v2 has no supported ethernets entries" + return 1 + fi + + return 0 +} + +network_config_set() { + local file="$1" source_name="${2:-network-config}" + + [ -s "$file" ] || return 1 + + local version="$(network_config_get "$file" version)" + case "$version" in + 2) + network_config_set_v2 "$file" "$source_name" + ;; + *) + log -i -t "$phase" warning "$ACTION: unsupported $source_name network-config version: ${version:-unknown}" + return 1 + ;; + esac +} + +network_config_setup_macifrename() { + [ -s "$ETC/iftab" ] || return 0 + + if ! [ -e "$ETC/init.d/macifrename" ]; then + log -i -t "$phase" warning "$ACTION: macifrename service not installed; interface renames will not be applied" + return 0 + fi + + mkdir -p "$ETC/conf.d" + if [ -f "$ETC/conf.d/macifrename" ]; then + if grep -q '^MACIFRENAME_OPTS=' "$ETC/conf.d/macifrename"; then + sed -i -e 's|^MACIFRENAME_OPTS=.*|MACIFRENAME_OPTS="/etc/iftab"|' \ + "$ETC/conf.d/macifrename" + else + printf '%s\n' 'MACIFRENAME_OPTS="/etc/iftab"' >> "$ETC/conf.d/macifrename" + fi + else + printf '%s\n' 'MACIFRENAME_OPTS="/etc/iftab"' > "$ETC/conf.d/macifrename" + fi + + $MOCK rc-update add macifrename boot + $MOCK rc-service macifrename start +} diff --git a/tests/test_env.sh b/tests/test_env.sh index 70dab9a..275f1d0 100644 --- a/tests/test_env.sh +++ b/tests/test_env.sh @@ -37,7 +37,7 @@ fake_umount() { while ! [ -d "\$1" ]; do shift done - rm -f "\$1"/meta-data "\$1"/user-data + rm -f "\$1"/meta-data "\$1"/user-data "\$1"/network-config EOF } @@ -65,6 +65,10 @@ fake_userdata_nocloud() { fake_data_nocloud user-data } +fake_network_config_nocloud() { + fake_data_nocloud network-config +} + fake_metadata_aws() { cat > "169.254.169.254.yaml" export WGET_STRIP_PREFIX="/${IMDS_API_VERSION:-latest}/meta-data" diff --git a/tests/tiny-cloud-alpine.test b/tests/tiny-cloud-alpine.test index b9024f3..52d0435 100755 --- a/tests/tiny-cloud-alpine.test +++ b/tests/tiny-cloud-alpine.test @@ -11,6 +11,15 @@ export CLOUD=nocloud init_tests \ set_ephemeral_network_cmdline \ set_network_config_network_interfaces \ + set_network_config_v2_dhcp4 \ + set_network_config_v2_match_macaddress_set_name_dhcp4 \ + set_network_config_v2_configures_macifrename_service \ + set_network_config_v2_static_ipv4 \ + set_network_config_v2_static_ipv6 \ + set_network_config_v2_dhcp6 \ + set_network_config_v2_accept_ra \ + set_network_config_v2_dhcp4_route_metric \ + set_network_config_v2_dhcp6_route_metric \ set_network_config_auto \ userdata_user_name \ userdata_user_homedir \ @@ -100,6 +109,254 @@ set_network_config_network_interfaces_body() { cat etc/resolv.conf } +set_network_config_v2_dhcp4_body() { + fake_network_config_nocloud <<-EOF + version: 2 + ethernets: + eth1: + dhcp4: true + EOF + + atf_check \ + -o match:"rc-update" \ + -e match:"set_network_interfaces: done" \ + tiny-cloud boot + + atf_check \ + -o match:"auto eth1" \ + -o match:"iface eth1" \ + -o match:"use dhcp" \ + cat etc/network/interfaces +} + +set_network_config_v2_match_macaddress_set_name_dhcp4_body() { + fake_network_config_nocloud <<-EOF + version: 2 + ethernets: + net0: + match: + macaddress: 52:55:55:1d:2a:0b + set-name: lima0 + dhcp4: true + EOF + + atf_check \ + -o match:"rc-update" \ + -e match:"set_network_interfaces: done" \ + tiny-cloud boot + + atf_check \ + -o match:"auto lima0" \ + -o match:"iface lima0" \ + -o match:"use dhcp" \ + cat etc/network/interfaces + + atf_check \ + -o match:"^lima0 52:55:55:1d:2a:0b$" \ + cat etc/iftab +} + +set_network_config_v2_configures_macifrename_service_body() { + mkdir -p etc/conf.d etc/init.d + cat > etc/conf.d/macifrename <<-EOF + MACIFRENAME_OPTS="/etc/foo" + EOF + touch etc/init.d/macifrename + + fake_network_config_nocloud <<-EOF + version: 2 + ethernets: + net0: + match: + macaddress: 52:55:55:1d:2a:0b + set-name: lima0 + dhcp4: true + EOF + + atf_check \ + -o match:"rc-update add macifrename boot" \ + -o match:"rc-service macifrename start" \ + -o match:"rc-update add sshd default" \ + -e match:"set_network_interfaces: done" \ + tiny-cloud boot + + atf_check \ + -o match:'^MACIFRENAME_OPTS="/etc/iftab"$' \ + cat etc/conf.d/macifrename +} + +set_network_config_v2_static_ipv4_body() { + fake_network_config_nocloud <<-EOF + version: 2 + ethernets: + net0: + match: + macaddress: 52:55:55:1d:2a:0b + set-name: lima0 + addresses: + - 192.168.100.10/24 + gateway4: 192.168.100.1 + nameservers: + addresses: + - 8.8.8.8 + - 8.8.4.4 + EOF + + atf_check \ + -o ignore \ + -e match:"set_network_interfaces: done" \ + tiny-cloud boot + + atf_check \ + -o match:"auto lima0" \ + -o match:"iface lima0" \ + -o match:"address 192.168.100.10/24" \ + -o match:"gateway 192.168.100.1" \ + cat etc/network/interfaces + + atf_check \ + -o match:"^nameserver 8.8.8.8$" \ + -o match:"^nameserver 8.8.4.4$" \ + cat etc/resolv.conf +} + +set_network_config_v2_static_ipv6_body() { + fake_network_config_nocloud <<-EOF + version: 2 + ethernets: + net0: + match: + macaddress: 52:55:55:1d:2a:0b + set-name: lima0 + addresses: + - 2001:db8::10/64 + gateway6: 2001:db8::1 + nameservers: + addresses: + - 2001:4860:4860::8888 + - 2001:4860:4860::8844 + EOF + + atf_check \ + -o ignore \ + -e match:"set_network_interfaces: done" \ + tiny-cloud boot + + atf_check \ + -o match:"auto lima0" \ + -o match:"iface lima0" \ + -o match:"address 2001:db8::10/64" \ + -o match:"gateway 2001:db8::1" \ + cat etc/network/interfaces + + atf_check \ + -o match:"^nameserver 2001:4860:4860::8888$" \ + -o match:"^nameserver 2001:4860:4860::8844$" \ + cat etc/resolv.conf +} + +set_network_config_v2_dhcp6_body() { + fake_network_config_nocloud <<-EOF + version: 2 + ethernets: + net0: + match: + macaddress: 52:55:55:1d:2a:0b + set-name: lima0 + dhcp6: true + EOF + + atf_check \ + -o ignore \ + -e match:"set_network_interfaces: done" \ + tiny-cloud boot + + atf_check \ + -o match:"auto lima0" \ + -o match:"iface lima0" \ + -o match:"use dhcp" \ + -o match:"dhcp-program dhcpcd" \ + -o match:"dhcp-opts -6" \ + cat etc/network/interfaces +} + +set_network_config_v2_accept_ra_body() { + fake_network_config_nocloud <<-EOF + version: 2 + ethernets: + net0: + match: + macaddress: 52:55:55:1d:2a:0b + set-name: lima0 + accept-ra: true + EOF + + atf_check \ + -o ignore \ + -e match:"set_network_interfaces: done" \ + tiny-cloud boot + + atf_check \ + -o match:"auto lima0" \ + -o match:"iface lima0" \ + -o match:"use ipv6-ra" \ + cat etc/network/interfaces +} + +set_network_config_v2_dhcp4_route_metric_body() { + fake_network_config_nocloud <<-EOF + version: 2 + ethernets: + net0: + match: + macaddress: 52:55:55:1d:2a:0b + set-name: lima0 + dhcp4: true + dhcp4-overrides: + route-metric: 200 + EOF + + atf_check \ + -o ignore \ + -e match:"set_network_interfaces: done" \ + tiny-cloud boot + + atf_check \ + -o match:"auto lima0" \ + -o match:"iface lima0" \ + -o match:"use dhcp" \ + -o match:"dhcp-program dhcpcd" \ + -o match:"dhcp-opts -m 200" \ + cat etc/network/interfaces +} + +set_network_config_v2_dhcp6_route_metric_body() { + fake_network_config_nocloud <<-EOF + version: 2 + ethernets: + net0: + match: + macaddress: 52:55:55:1d:2a:0b + set-name: lima0 + dhcp6: true + dhcp6-overrides: + route-metric: 100 + EOF + + atf_check \ + -o ignore \ + -e match:"set_network_interfaces: done" \ + tiny-cloud boot + + atf_check \ + -o match:"auto lima0" \ + -o match:"iface lima0" \ + -o match:"use dhcp" \ + -o match:"dhcp-program dhcpcd" \ + -o match:"dhcp-opts -6 -m 100" \ + cat etc/network/interfaces +} + set_network_config_auto_body() { fake_metadata_nocloud <<-EOF resolv_conf: