diff --git a/Makefile b/Makefile index 79f75aa..b0aaef2 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ PREFIX?=/ -SUBPACKAGES = core network openrc aws azure gcp oci nocloud +SUBPACKAGES = core network openrc aws azure gcp oci nocloud alpine .PHONY: check install $(SUBPACKAGES) @@ -56,6 +56,11 @@ nocloud: install -Dm644 -t $(PREFIX)/lib/tiny-cloud/cloud/nocloud \ lib/tiny-cloud/cloud/nocloud/* +alpine: + install -Dm644 -t $(PREFIX)/lib/tiny-cloud/cloud/alpine \ + lib/tiny-cloud/cloud/alpine/init + ln -s ../nocloud/imds $(PREFIX)/lib/tiny-cloud/cloud/alpine/imds + check: tests/Kyuafile Kyuafile kyua test || (kyua report --verbose && exit 1) diff --git a/dist/openrc/tiny-cloud b/dist/openrc/tiny-cloud index a4beae1..9ffdb15 100755 --- a/dist/openrc/tiny-cloud +++ b/dist/openrc/tiny-cloud @@ -5,7 +5,7 @@ description="Tiny Cloud Bootstrap - main phase" extra_commands="complete incomplete" depend() { - need net + need net tiny-cloud-net before sshd } diff --git a/dist/openrc/tiny-cloud-net b/dist/openrc/tiny-cloud-net new file mode 100755 index 0000000..5aa8c9d --- /dev/null +++ b/dist/openrc/tiny-cloud-net @@ -0,0 +1,14 @@ +#!/sbin/openrc-run +# vim:set ts=8 noet ft=sh: + +description="Tiny Cloud Bootstrap - net phase" +depend() { + need net + before * +} + +start() { + ebegin "Tiny Cloud - net" + tiny-cloud net + eend $? +} diff --git a/lib/tiny-cloud/cloud/alpine/imds b/lib/tiny-cloud/cloud/alpine/imds new file mode 120000 index 0000000..104b5ca --- /dev/null +++ b/lib/tiny-cloud/cloud/alpine/imds @@ -0,0 +1 @@ +../nocloud/imds \ No newline at end of file diff --git a/lib/tiny-cloud/cloud/alpine/init b/lib/tiny-cloud/cloud/alpine/init new file mode 100644 index 0000000..75441b8 --- /dev/null +++ b/lib/tiny-cloud/cloud/alpine/init @@ -0,0 +1,26 @@ +# Tiny Cloud - Init Functions +# vim:set ts=4 et ft=sh: + +INIT_ACTIONS_EARLY="$(replace_word set_default_interfaces set_network_interfaces $INIT_ACTIONS_EARLY)" + +set_resolv_conf() { + # resolv.conf + local nameservers="$(imds meta-data/resolv_conf/nameservers)" + for i in $nameservers; do + local server="$(imds meta-data/resolv_conf/nameservers/$i)" + add_once "$ROOT"/etc/resolv.conf "nameserver $server" + done +} + +init__set_network_interfaces() { + local interfaces="$(imds meta-data/network-interfaces)" + mkdir -p "$ROOT"/etc/network + if [ -n "$interfaces" ]; then + printf "%s\n" "$interfaces" > "$ROOT"/etc/network/interfaces + elif ! [ -f "$ROOT"/etc/network/interfaces ]; then + init__set_default_interfaces + fi + if ! grep -q dhcp "$ROOT"/etc/network/interfaces; then + set_resolv_conf + fi +} diff --git a/lib/tiny-cloud/common b/lib/tiny-cloud/common index b0500a7..ce0dd1a 100644 --- a/lib/tiny-cloud/common +++ b/lib/tiny-cloud/common @@ -29,3 +29,40 @@ log() { crit|alert|emerg) exit 1 ;; esac } + +# usage: replace_word ... +replace_word() { + local search="$1" replace="$2" + shift 2 + for word in "$@"; do + if [ "$word" = "$search" ]; then + echo "$replace" + else + echo "$word" + fi + done +} + +# usage: insert_after ... +insert_after() { + local search="$1" addition="$2" + shift 2 + for i in "$@"; do + echo "$i" + if [ "$i" = "$search" ]; then + echo "$addition" + fi + done +} + +# usage: add_once ... +add_once() { + local file="$1" + shift + for line; do + if ! grep -x -F "$line" "$file" 2>/dev/null; then + mkdir -p "${file%/*}" + printf "%s\n" "$line" >> "$file" + fi + done +} diff --git a/lib/tiny-cloud/init b/lib/tiny-cloud/init index db5cf84..8f37744 100644 --- a/lib/tiny-cloud/init +++ b/lib/tiny-cloud/init @@ -17,14 +17,17 @@ DEFAULT_ACTIONS_EARLY=" create_default_user enable_sshd " -DEFAULT_ACTIONS_MAIN=" +DEFAULT_ACTIONS_NET=" save_userdata +" +DEFAULT_ACTIONS_MAIN=" set_hostname set_ssh_keys " DEFAULT_ACTIONS_FINAL="" : "${INIT_ACTIONS_EARLY=$DEFAULT_ACTIONS_EARLY}" +: "${INIT_ACTIONS_NET=$DEFAULT_ACTIONS_NET}" : "${INIT_ACTIONS_MAIN=$DEFAULT_ACTIONS_MAIN}" : "${INIT_ACTIONS_FINAL=$DEFAULT_ACTIONS_FINAL}" @@ -242,6 +245,10 @@ init__set_ssh_keys() { init__save_userdata() { local userdata="$TINY_CLOUD_VAR/user-data" + if [ -f "$userdata" ]; then + log info "user-data already saved" + return + fi local tmpfile=$(mktemp "$userdata.XXXXXX") imds -e @userdata > "$tmpfile" diff --git a/lib/tiny-cloud/user-data/alpine-config b/lib/tiny-cloud/user-data/alpine-config new file mode 100644 index 0000000..c04d86e --- /dev/null +++ b/lib/tiny-cloud/user-data/alpine-config @@ -0,0 +1,116 @@ +# Script UserData Functions +# vim:set ts=4 et ft=sh: + +INIT_ACTIONS_MAIN="$(insert_after set_hostname \ + "userdata_bootcmd userdata_ntp userdata_apk_cache userdata_apk_repositories userdata_packages" \ + $INIT_ACTIONS_MAIN)" +INIT_ACTIONS_FINAL="$INIT_ACTIONS_FINAL userdata_runcmd" + +init__userdata_bootcmd() { + # run bootcmd + local bootcmds="$(imds user-data/bootcmd)" + for i in $bootcmds; do + local cmd="$(imds user-data/bootcmd/"$i")" + sh -c "$cmd" + done +} + +init__userdata_ntp() { + local ntp_enabled="$(imds user-data/ntp/enabled)" + if [ "$ntp_enabled" != "yes" ] && [ "$ntp_enabled" != "true" ]; then + return + fi + local ntp_client="$(imds user-data/ntp/ntp_client)" + local svc= pkg= + case "$ntp_client" in + busybox) + svc=ntpd + ;; + chrony|"") + pkg=chrony + svc=chronyd + ;; + openntpd) + pkg=openntpd + svc=openntpd + ;; + esac + if [ -n "$pkg" ]; then + $MOCK apk add "$pkg" + fi + if [ -n "$svc" ]; then + $MOCK rc-update add "$svc" default + $MOCK rc-service "$svc" start + fi +} + +init__userdata_apk_cache() { + local cache="$(imds user-data/apk/cache)" + if [ -z "$cache" ]; then + return + fi + mkdir -p "$ROOT/$cache" + # make link relative + case "$cache" in + /*) cache="../..$cache";; + esac + mkdir -p "$ROOT"/etc/apk + ln -sf "$cache" "$ROOT"/etc/apk/cache +} + +init__userdata_apk_cache() { + local cache="$(imds user-data/apk/cache)" + if [ -z "$cache" ]; then + return + fi + mkdir -p "$ROOT/$cache" + # make link relative + case "$cache" in + /*) cache="../..$cache";; + esac + mkdir -p "$ROOT"/etc/apk + ln -sf "$cache" "$ROOT"/etc/apk/cache +} + +init__userdata_apk_repositories() { + local repositories="$(imds user-data/apk/repositories)" + mkdir -p "$ROOT"/etc/apk + for r in $repositories; do + local baseurl="$(imds user-data/apk/repositories/$r/base_url)" + local repos="$(imds user-data/apk/repositories/$r/repos)" + local version="$(imds user-data/apk/repositories/$r/version)" + if [ -z "$version" ]; then + local version_id=$( . "$ROOT"/etc/os-release 2>/dev/null && echo "$VERSION_ID") + case "$version_id" in + edge*|*_alpha*) version="edge";; + [0-9]*.[0-9]*.[0-9]*) version="v${version_id%.*}";; + esac + fi + if [ -n "$version" ] && [ "$version" != "." ] && [ "$version" != "/" ]; then + baseurl="${baseurl%/}/$version" + fi + for repo in $repos; do + local uri="${baseurl%/}/$(imds user-data/apk/repositories/$r/repos/$repo)" + add_once "$ROOT"/etc/apk/repositories "$uri" + done + done +} + +init__userdata_packages() { + local packages="$(imds user-data/packages)" + local pkgs= + for i in $packages; do + pkgs="$pkgs $(imds user-data/packages/$i)" + done + if [ -n "$pkgs" ]; then + $MOCK apk add $pkgs + fi +} + +init__userdata_runcmd() { + local runcmds="$(imds user-data/runcmd)" + for i in $runcmds; do + local cmd="$(imds user-data/runcmd/$i)" + sh -c "$cmd" + done +} diff --git a/sbin/tiny-cloud b/sbin/tiny-cloud index 4a960fa..bef2937 100755 --- a/sbin/tiny-cloud +++ b/sbin/tiny-cloud @@ -10,7 +10,7 @@ set -e usage() { cat <&2; exit 1;; esac @@ -92,6 +92,7 @@ INIT_ACTIONS_FINAL="${INIT_ACTIONS_FINAL} bootstrap_complete" case "$phase" in early) INIT_ACTIONS="$INIT_ACTIONS_EARLY";; + net) INIT_ACTIONS="$INIT_ACTIONS_NET";; main) INIT_ACTIONS="$INIT_ACTIONS_MAIN";; final) INIT_ACTIONS="$INIT_ACTIONS_FINAL";; *) usage >&2; exit 1 diff --git a/tests/tiny-cloud-alpine.test b/tests/tiny-cloud-alpine.test new file mode 100755 index 0000000..0229381 --- /dev/null +++ b/tests/tiny-cloud-alpine.test @@ -0,0 +1,241 @@ +#!/usr/bin/env atf-sh + +. $(atf_get_srcdir)/test_env.sh + +export PREFIX="$srcdir" +export MOCK=echo +export CLOUD=alpine +lib="$srcdir"/lib/tiny-cloud/cloud/alpine/init + +init_tests \ + set_network_config_network_interfaces \ + set_network_config_auto \ + userdata_bootcmd \ + userdata_ntp \ + userdata_ntp_busybox \ + userdata_ntp_openntpd \ + userdata_apk_cache \ + userdata_apk_repositories \ + userdata_apk_repositories_version \ + userdata_apk_repositories_version_auto_edge \ + userdata_packages \ + userdata_runcmd + + +set_network_config_network_interfaces_body() { + fake_metadata_nocloud <<-EOF + network-interfaces: | + auto eth1 + iface eth1 + address 192.168.100.1 + netmask 255.255.255.0 + + resolv_conf: + nameservers: + - 8.8.8.8 + - 8.8.4.4 + EOF + + atf_check \ + -o match:"rc-update" \ + -e match:"set_network_interfaces .*DONE" \ + tiny-cloud early + atf_check \ + -o match:"auto eth1" \ + -o match:"iface eth1" \ + -o match:"address 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_auto_body() { + fake_metadata_nocloud <<-EOF + resolv_conf: + nameservers: + - 8.8.8.8 + - 8.8.4.4 + EOF + fake_interfaces eth0 eth1 eth2 + echo up > sys/class/net/eth1/operstate + + atf_check \ + -o match:"rc-update" \ + -e match:"set_network_interfaces .*DONE" \ + tiny-cloud early + atf_check \ + -o match:"auto eth1" \ + -o match:"iface eth1" \ + -o match:"use dhcp" \ + cat etc/network/interfaces + # resolv.conf should be ignored with dhcp + if [ -e etc/resolv.conf ]; then + atf_fail "etc/resolv.conf should not been created with DHCP" + fi +} + +userdata_bootcmd_body() { + fake_userdata_nocloud <<-EOF + #alpine-config + bootcmd: + - echo foo + - echo bar + EOF + atf_check -e ignore -o ignore tiny-cloud net + atf_check \ + -e match:"userdata_bootcmd .*DONE" \ + -o match:"^foo$" -o match:"^bar$" \ + tiny-cloud main +} + +userdata_ntp_body() { + fake_userdata_nocloud <<-EOF + #alpine-config + ntp: + enabled: true + EOF + atf_check -e ignore -o ignore tiny-cloud net + atf_check \ + -e match:"userdata_ntp .*DONE" \ + -o match:"apk add.*chrony" \ + -o match:"rc-update .*chronyd" \ + -o match:"rc-service .*chronyd" \ + tiny-cloud main +} + +userdata_ntp_busybox_body() { + fake_userdata_nocloud <<-EOF + #alpine-config + ntp: + enabled: true + ntp_client: busybox + EOF + atf_check -e ignore -o ignore tiny-cloud net + atf_check \ + -e match:"userdata_ntp .*DONE" \ + -o not-match:"apk add" \ + -o match:"rc-update .*ntpd" \ + -o match:"rc-service .*ntpd" \ + tiny-cloud main +} + +userdata_ntp_openntpd_body() { + fake_userdata_nocloud <<-EOF + #alpine-config + ntp: + enabled: true + ntp_client: openntpd + EOF + atf_check -e ignore -o ignore tiny-cloud net + atf_check \ + -e match:"userdata_ntp .*DONE" \ + -o match:"apk add.*openntpd" \ + -o match:"rc-update .*openntpd" \ + -o match:"rc-service .*openntpd" \ + tiny-cloud main +} + +userdata_apk_cache_body() { + fake_userdata_nocloud <<-EOF + #alpine-config + apk: + cache: /var/cache/apk + EOF + atf_check -e ignore -o ignore tiny-cloud net + atf_check \ + -e match:"userdata_apk_cache .*DONE" \ + -o ignore \ + tiny-cloud main + atf_check -o match:"$PWD/var/cache/apk" readlink -f etc/apk/cache +} + +userdata_apk_repositories_body() { + fake_userdata_nocloud <<-EOF + #alpine-config + apk: + repositories: + - base_url: /srv/packages + repos: [ "main", "community" ] + EOF + atf_check -e ignore -o ignore tiny-cloud net + atf_check \ + -e match:"userdata_apk_repositories .*DONE" \ + -o ignore \ + tiny-cloud main + atf_check -o match:"^/srv/packages/main$" \ + -o match:"^/srv/packages/community$" \ + cat etc/apk/repositories +} + +userdata_apk_repositories_version_body() { + fake_userdata_nocloud <<-EOF + #alpine-config + apk: + repositories: + - base_url: https://cdn.alpinelinux.org/ + version: edge + repos: [ "main", "community" ] + EOF + atf_check -e ignore -o ignore tiny-cloud net + atf_check \ + -e match:"userdata_apk_repositories .*DONE" \ + -o ignore \ + tiny-cloud main + atf_check -o match:"^https://cdn.alpinelinux.org/edge/main$" \ + -o match:"^https://cdn.alpinelinux.org/edge/community$" \ + cat etc/apk/repositories +} + +userdata_apk_repositories_version_auto_edge_body() { + fake_userdata_nocloud <<-EOF + #alpine-config + apk: + repositories: + - base_url: https://cdn.alpinelinux.org/ + repos: [ "main", "community" ] + EOF + mkdir -p etc + echo "VERSION_ID=3.18_alpha20230329" > etc/os-release + + atf_check -e ignore -o ignore tiny-cloud net + atf_check \ + -e match:"userdata_apk_repositories .*DONE" \ + -o ignore \ + tiny-cloud main + atf_check -o match:"^https://cdn.alpinelinux.org/edge/main$" \ + -o match:"^https://cdn.alpinelinux.org/edge/community$" \ + cat etc/apk/repositories +} + +userdata_packages_body() { + fake_userdata_nocloud <<-EOF + #alpine-config + packages: + - tmux + - vim + EOF + atf_check -e ignore -o ignore tiny-cloud net + atf_check \ + -e match:"userdata_packages .*DONE" \ + -o match:"apk add .*tmux" \ + -o match:"apk add .*vim" \ + tiny-cloud main +} + +userdata_runcmd_body() { + fake_userdata_nocloud <<-EOF + #alpine-config + runcmd: + - echo foo + - echo bar + EOF + # run net phase to extract the user data + atf_check -e ignore -o ignore tiny-cloud net + atf_check \ + -e match:"userdata_runcmd .*DONE" \ + -o match:"^foo$" -o match:"^bar$" \ + tiny-cloud final +} diff --git a/tests/tiny-cloud.test b/tests/tiny-cloud.test index 9e0d4c8..157c97d 100755 --- a/tests/tiny-cloud.test +++ b/tests/tiny-cloud.test @@ -9,6 +9,7 @@ PROVIDERS="alpine aws azure gcp nocloud oci" init_tests \ tiny_cloud_help \ no_metadata_early \ + no_userdata_net \ no_userdata_main \ no_userdata_final @@ -27,8 +28,8 @@ tiny_cloud_help_body() { no_metadata_early_body() { fake_netcat for provider in $PROVIDERS; do - # TODO: -e not-match:"UNKNOWN" \ CLOUD="$provider" atf_check \ + -e not-match:"UNKNOWN" \ -e not-match:"not found" \ -e not-match:"o such file" \ -o match:"rc-update add.*sshd" \ @@ -36,14 +37,23 @@ no_metadata_early_body() { done } +no_userdata_net_body() { + fake_netcat + for provider in $PROVIDERS; do + CLOUD="$provider" atf_check \ + -e not-match:"UNKNOWN" \ + -e match:"save_userdata.*DONE" \ + tiny-cloud net + done +} + no_userdata_main_body() { fake_netcat for provider in $PROVIDERS; do # we should not set empty hostname # we should not create .ssh dir for non-existing user - # TODO: -e not-match:"UNKNOWN" \ CLOUD="$provider" atf_check \ - -e ignore \ + -e not-match:"UNKNOWN" \ -o not-match:"hostname.*-F" \ -o not-match:"chown.*/\.ssh" \ tiny-cloud main @@ -58,8 +68,8 @@ no_userdata_main_body() { no_userdata_final_body() { fake_netcat for provider in $PROVIDERS; do - # TODO: -e not-match:"UNKNOWN" \ CLOUD="$provider" atf_check \ + -e not-match:"UNKNOWN" \ -e match:"bootstrap_complete .*" \ tiny-cloud final CLOUD="$provider" atf_check \