From b8218bd1f034742fa24eba5a728810af6ce90eb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jake=20Buchholz=20G=C3=B6kt=C3=BCrk?= Date: Mon, 4 Nov 2024 17:05:50 +0000 Subject: [PATCH] split alpine-config out of cloud-config --- README.md | 9 +- TODO.md | 4 - lib/tiny-cloud/user-data/alpine-config | 310 +------------------------ lib/tiny-cloud/user-data/cloud-config | 308 +++++++++++++++++++++++- 4 files changed, 316 insertions(+), 315 deletions(-) diff --git a/README.md b/README.md index 28df319..fa28943 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,13 @@ works with multiple cloud providers. Currently, the following are supported: * [AWS](https://aws.amazon.com) - Amazon Web Services * [Azure](https://azure.microsoft.com) - Microsoft Azure * [GCP](https://cloud.google.com) - Google Cloud Platform -* [Hetzner](https://www.hetzner.com) -* [Incus](https://linuxcontainers.org/incus) +* [Hetzner](https://www.hetzner.com) - Hetzner Cloud +* [Incus](https://linuxcontainers.org/incus) - Incus Containers and Virtual Machines * [NoCloud]( https://cloudinit.readthedocs.io/en/latest/reference/datasources/nocloud.html ) - cloud-init's NoCloud AWS-compatible user provided data source * [OCI](https://cloud.oracle.com) - Oracle Cloud Infrastructure - [Scaleway](https://www.scaleway.com) + [Scaleway](https://www.scaleway.com) - Scaleway Cloud Tiny Cloud is also used for Alpine Linux's experimental "auto-install" feature. @@ -52,9 +52,6 @@ As Tiny Cloud is meant to be tiny, it has few dependencies: * `sfdisk` * [`yx`](https://gitlab.com/tomalok/yx) (for extracting data from YAML files) -Optional dependencies: -* `nvme-cli` (for AWS nitro NVMe symlinks) - _Tiny Cloud has been developed specifically for use with the [Alpine Cloud Images]( https://gitlab.alpinelinux.org/alpine/cloud/alpine-cloud-images) diff --git a/TODO.md b/TODO.md index 77f8498..40a642f 100644 --- a/TODO.md +++ b/TODO.md @@ -2,10 +2,6 @@ ## SOON-ish -* Move the bulk of `#alpine-config` handler that is compatible with - `#cloud-config` to that handler (which is currently just a stub), and only - `#alpine-config` extensions remain there. - * Support cloud auto-detection, where it's possible to do so. ## FUTURE diff --git a/lib/tiny-cloud/user-data/alpine-config b/lib/tiny-cloud/user-data/alpine-config index 2adde1f..d4e1cfe 100644 --- a/lib/tiny-cloud/user-data/alpine-config +++ b/lib/tiny-cloud/user-data/alpine-config @@ -1,176 +1,14 @@ -# Script UserData Functions +# #alpine-config UserData Functions # vim:set filetype=sh: # shellcheck shell=sh -INIT_ACTIONS_MAIN="$(insert_before create_default_user userdata_user $INIT_ACTIONS_MAIN)" -INIT_ACTIONS_MAIN="$(insert_after set_hostname \ - "userdata_bootcmd userdata_groups userdata_users userdata_write_files userdata_ntp userdata_apk_cache userdata_apk_repositories userdata_package_update userdata_package_upgrade userdata_packages" \ - $INIT_ACTIONS_MAIN)" -INIT_ACTIONS_MAIN="$(insert_after set_ssh_keys ssh_authorized_keys $INIT_ACTIONS_MAIN)" -INIT_ACTIONS_FINAL="$INIT_ACTIONS_FINAL userdata_runcmd userdata_autoinstall" +# NOTE: alpine-config extends cloud-config -get_userdata() { - IFS="/" - yx -f "$TINY_CLOUD_VAR/user-data" $1 2>/dev/null - unset IFS -} +: "${LIBDIR:=$PREFIX/lib}" +. "${LIBDIR}/tiny-cloud/user-data/cloud-config" -init__userdata_user() { - local name="$(get_userdata user/name)" - if [ -z "$name" ]; then - name="$(get_userdata user)" - if [ -n "$(get_userdata user/$name)" ]; then - log -s err "user/name is required" - return - fi - fi - if get_userdata | grep -q -x users; then - local default_user="$(get_userdata users/1)" - if [ "$default_user" != "default" ]; then - CLOUD_USER="$(get_userdata users/1/name)" - return 0 - fi - fi - CLOUD_USER="${name:-$CLOUD_USER}" -} - -set_ssh_authorized_keys_for() { - local user="$1" - local userdata_path="$2" - local sshkeys="$(get_userdata $userdata_path)" - if [ -z "$sshkeys" ]; then - return - fi - - local pwent="$(getent passwd "$user")" - if [ -z "$pwent" ]; then - log -i -t "$phase" err "$ACTION: failed to find user $user" - return 1 - fi - local group=$(echo "$pwent" | cut -d: -f4) - local ssh_dir="${ROOT}$(echo "$pwent" | cut -d: -f6)/.ssh" - local keys_file="$ssh_dir/authorized_keys" - - if [ ! -d "$ssh_dir" ]; then - mkdir -p "$ssh_dir" - chmod 700 "$ssh_dir" - fi - - touch "$keys_file" - chmod 600 "$keys_file" - $MOCK chown -R "$user:$group" "$ssh_dir" - for i in $sshkeys; do - local key="$(get_userdata $userdata_path/$i)" - if [ -n "$key" ]; then - echo "$key" >> "$keys_file" - fi - done -} - -init__ssh_authorized_keys() { - if [ -z "$CLOUD_USER" ]; then - return - fi - set_ssh_authorized_keys_for "$CLOUD_USER" ssh_authorized_keys -} - -init__userdata_bootcmd() { - # run bootcmd - local bootcmds="$(get_userdata bootcmd)" - for i in $bootcmds; do - local cmd="$(get_userdata bootcmd/"$i")" - sh -c "$cmd" - done -} - -# write_file -write_file() { - # Defaults used are the same as for full cloud-init "spec": - # https://cloudinit.readthedocs.io/en/latest/reference/modules.html#write-files - local path="$1" - local mode="${2:-0644}" - local owner="${3:-root:root}" - local encoding="${4:-text/plain}" - local append="${5:-false}" - - if [ "$append" != "true" ] && [ "$append" != "false" ]; then - log err "append must be true or false" - return - fi - - local tmpfile="$(mktemp $TINY_CLOUD_VAR/user-data.write_files.XXXXXX)" - - case "$encoding" in - gzip|gz|gz+base64|gzip+base64|gz+b64|gzip+b64) - base64 -d | gzip -d > "$tmpfile" - ;; - base64|b64) - base64 -d > "$tmpfile" - ;; - text/plain) - cat > "$tmpfile" - ;; - esac - - if [ "$append" = "true" ]; then - cat "$tmpfile" >> "$path" - else - cat "$tmpfile" > "$path" - fi - rm -f "$tmpfile" - - chmod "$mode" "$path" - # mocked as we do not know which users we could use in testing - # this way we can check the proper invocation at least - $MOCK chown "$owner" "$path" -} - -init__userdata_write_files() { - local files="$(get_userdata write_files)" - - for i in $files; do - local path="$(get_userdata write_files/$i/path)" - if [ -z "$path" ]; then - continue - fi - - mkdir -p "$(dirname "$ROOT/$path")" - get_userdata write_files/$i/content | write_file "$ROOT/$path" \ - "$(get_userdata write_files/$i/permissions)" \ - "$(get_userdata write_files/$i/owner)" \ - "$(get_userdata write_files/$i/encoding)" \ - "$(get_userdata write_files/$i/append)" - done -} - -init__userdata_ntp() { - local ntp_enabled="$(get_userdata ntp/enabled)" - if [ "$ntp_enabled" != "yes" ] && [ "$ntp_enabled" != "true" ]; then - return - fi - local ntp_client="$(get_userdata 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_ACTIONS_MAIN="$(insert_after userdata_ntp "userdata_apk_cache userdata_apk_repositories" $INIT_ACTIONS_MAIN)" +INIT_ACTIONS_FINAL="$INIT_ACTIONS_FINAL userdata_autoinstall" init__userdata_apk_cache() { local cache="$(get_userdata apk/cache)" @@ -215,142 +53,8 @@ init__userdata_apk_repositories() { done } -init__userdata_package_update() { - local update="$(get_userdata package_update)" - if [ "$update" = "true" ]; then - $MOCK apk update - fi -} - -init__userdata_package_upgrade() { - local upgrade="$(get_userdata package_upgrade)" - if [ "$upgrade" = "true" ]; then - $MOCK apk upgrade - fi -} - -init__userdata_packages() { - local packages="$(get_userdata packages)" - local pkgs= - for i in $packages; do - pkgs="$pkgs $(get_userdata packages/$i)" - done - if [ -n "$pkgs" ]; then - $MOCK apk add $pkgs - fi -} - -init__userdata_runcmd() { - local runcmds="$(get_userdata runcmd)" - for i in $runcmds; do - local cmd="$(get_userdata runcmd/$i)" - sh -c "$cmd" - done -} - -init__userdata_groups() { - local groups="$(get_userdata groups)" - for i in $groups; do - local group="$(get_userdata groups/$i)" - $MOCK addgroup $group - done -} - -in_list() { - local i needle="$1" - shift - for i in "$@"; do - if [ "$i" = "$needle" ]; then - return 0 - fi - done - return 1 -} - -init__userdata_users() { - local users="$(get_userdata users)" - for i in $users; do - local name gecos homedir shell primary_group groups - local system=false no_create_home=false lock_passwd=true - local keys="$(get_userdata users/$i)" - if [ "$i" = 1 ] && [ "$keys" = "default" ]; then - continue - fi - if in_list name $keys; then - name="$(get_userdata users/$i/name)" - else - continue - fi - if in_list gecos $keys; then - gecos="$(get_userdata users/$i/gecos)" - fi - if in_list homedir $keys; then - homedir="$(get_userdata users/$i/homedir)" - fi - if in_list shell $keys; then - shell="$(get_userdata users/$i/shell)" - fi - if in_list primary_group $keys; then - primary_group="$(get_userdata users/$i/primary_group)" - fi - if in_list system $keys; then - system="$(get_userdata users/$i/system)" - fi - if in_list no_create_home $keys; then - no_create_home="$(get_userdata users/$i/no_create_home)" - fi - - if getent passwd "$user" >/dev/null; then - log -i -t "$phase" info "$ACTION: user $user already exists" - else - if [ "$system" != "true" ]; then - unset system - fi - if [ "$no_create_home" != "true" ]; then - unset no_create_home - fi - $MOCK adduser -D ${gecos:+-g "$gecos"} ${homedir:+-h "$homedir"} ${shell:+-s "$shell"} ${primary_group:+-G "$primary_group"} ${system:+-S} ${no_create_home:+-H} "$name" - fi - - if in_list lock_passwd $keys; then - lock_passwd="$(get_userdata users/$i/lock_passwd)" - fi - - if [ "$lock_passwd" != "false" ]; then - echo "$name:*" | $MOCK chpasswd -e - fi - - if in_list ssh_authorized_keys $keys; then - set_ssh_authorized_keys_for "$name" users/$i/ssh_authorized_keys - fi - - if in_list groups $keys; then - groups="$(get_userdata users/$i/groups | tr ',' ' ')" - local group - for group in $groups; do - $MOCK addgroup "$name" "$group" - done - fi - if in_list doas $keys; then - if [ -d "$ROOT/etc/doas.d" ]; then - touch "$ROOT/etc/doas.d/$name.conf" - chmod 660 "$ROOT/etc/doas.d/$name.conf" - fi - local j - for j in $(get_userdata users/$i/doas); do - local line="$(get_userdata users/$i/doas/$j)" - if [ -d "$ROOT/etc/doas.d" ]; then - echo "$line" >> "$ROOT/etc/doas.d/$name.conf" - elif [ -f "$ROOT/etc/doas.conf" ]; then - add_once "$ROOT/etc/doas.conf" "$line" - fi - done - fi - done -} - find_biggest_empty_disk() { - local d + local d p for d in "$ROOT"/sys/class/block/*/device; do p=${d%/device} if [ -e "$p"/size ] && [ -z "$(blkid /dev/${p##*/})" ]; then diff --git a/lib/tiny-cloud/user-data/cloud-config b/lib/tiny-cloud/user-data/cloud-config index adf4f46..57f7d6f 100644 --- a/lib/tiny-cloud/user-data/cloud-config +++ b/lib/tiny-cloud/user-data/cloud-config @@ -1,5 +1,309 @@ -# CloudConfig UserData Functions +# #cloud-config UserData Functions # vim:set filetype=sh: # shellcheck shell=sh -# TODO +# NOTE: This is only a subset of what cloud-init supports! + +INIT_ACTIONS_MAIN="$(insert_before create_default_user userdata_user $INIT_ACTIONS_MAIN)" +INIT_ACTIONS_MAIN="$(insert_after set_hostname \ + "userdata_bootcmd userdata_groups userdata_users userdata_write_files userdata_ntp userdata_package_update userdata_package_upgrade userdata_packages" \ + $INIT_ACTIONS_MAIN)" +INIT_ACTIONS_MAIN="$(insert_after set_ssh_keys ssh_authorized_keys $INIT_ACTIONS_MAIN)" +INIT_ACTIONS_FINAL="$INIT_ACTIONS_FINAL userdata_runcmd" + +get_userdata() { + IFS="/" + yx -f "$TINY_CLOUD_VAR/user-data" $1 2>/dev/null + unset IFS +} + +init__userdata_user() { + local name="$(get_userdata user/name)" + if [ -z "$name" ]; then + name="$(get_userdata user)" + if [ -n "$(get_userdata user/$name)" ]; then + log -s err "user/name is required" + return + fi + fi + if get_userdata | grep -q -x users; then + local default_user="$(get_userdata users/1)" + if [ "$default_user" != "default" ]; then + CLOUD_USER="$(get_userdata users/1/name)" + return 0 + fi + fi + CLOUD_USER="${name:-$CLOUD_USER}" +} + +set_ssh_authorized_keys_for() { + local user="$1" + local userdata_path="$2" + local sshkeys="$(get_userdata $userdata_path)" + if [ -z "$sshkeys" ]; then + return + fi + + local pwent="$(getent passwd "$user")" + if [ -z "$pwent" ]; then + log -i -t "$phase" err "$ACTION: failed to find user $user" + return 1 + fi + local group=$(echo "$pwent" | cut -d: -f4) + local ssh_dir="${ROOT}$(echo "$pwent" | cut -d: -f6)/.ssh" + local keys_file="$ssh_dir/authorized_keys" + + if [ ! -d "$ssh_dir" ]; then + mkdir -p "$ssh_dir" + chmod 700 "$ssh_dir" + fi + + touch "$keys_file" + chmod 600 "$keys_file" + $MOCK chown -R "$user:$group" "$ssh_dir" + for i in $sshkeys; do + local key="$(get_userdata $userdata_path/$i)" + if [ -n "$key" ]; then + echo "$key" >> "$keys_file" + fi + done +} + +init__ssh_authorized_keys() { + if [ -z "$CLOUD_USER" ]; then + return + fi + set_ssh_authorized_keys_for "$CLOUD_USER" ssh_authorized_keys +} + +init__userdata_bootcmd() { + # run bootcmd + local bootcmds="$(get_userdata bootcmd)" + for i in $bootcmds; do + local cmd="$(get_userdata bootcmd/"$i")" + sh -c "$cmd" + done +} + +# write_file +write_file() { + # Defaults used are the same as for full cloud-init "spec": + # https://cloudinit.readthedocs.io/en/latest/reference/modules.html#write-files + local path="$1" + local mode="${2:-0644}" + local owner="${3:-root:root}" + local encoding="${4:-text/plain}" + local append="${5:-false}" + + if [ "$append" != "true" ] && [ "$append" != "false" ]; then + log err "append must be true or false" + return + fi + + local tmpfile="$(mktemp $TINY_CLOUD_VAR/user-data.write_files.XXXXXX)" + + case "$encoding" in + gzip|gz|gz+base64|gzip+base64|gz+b64|gzip+b64) + base64 -d | gzip -d > "$tmpfile" + ;; + base64|b64) + base64 -d > "$tmpfile" + ;; + text/plain) + cat > "$tmpfile" + ;; + esac + + if [ "$append" = "true" ]; then + cat "$tmpfile" >> "$path" + else + cat "$tmpfile" > "$path" + fi + rm -f "$tmpfile" + + chmod "$mode" "$path" + # mocked as we do not know which users we could use in testing + # this way we can check the proper invocation at least + $MOCK chown "$owner" "$path" +} + +init__userdata_write_files() { + local files="$(get_userdata write_files)" + + for i in $files; do + local path="$(get_userdata write_files/$i/path)" + if [ -z "$path" ]; then + continue + fi + + mkdir -p "$(dirname "$ROOT/$path")" + get_userdata write_files/$i/content | write_file "$ROOT/$path" \ + "$(get_userdata write_files/$i/permissions)" \ + "$(get_userdata write_files/$i/owner)" \ + "$(get_userdata write_files/$i/encoding)" \ + "$(get_userdata write_files/$i/append)" + done +} + +init__userdata_ntp() { + local ntp_enabled="$(get_userdata ntp/enabled)" + if [ "$ntp_enabled" != "yes" ] && [ "$ntp_enabled" != "true" ]; then + return + fi + local ntp_client="$(get_userdata 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_package_update() { + local update="$(get_userdata package_update)" + if [ "$update" = "true" ]; then + $MOCK apk update + fi +} + +init__userdata_package_upgrade() { + local upgrade="$(get_userdata package_upgrade)" + if [ "$upgrade" = "true" ]; then + $MOCK apk upgrade + fi +} + +init__userdata_packages() { + local packages="$(get_userdata packages)" + local pkgs= + for i in $packages; do + pkgs="$pkgs $(get_userdata packages/$i)" + done + if [ -n "$pkgs" ]; then + $MOCK apk add $pkgs + fi +} + +init__userdata_runcmd() { + local runcmds="$(get_userdata runcmd)" + for i in $runcmds; do + local cmd="$(get_userdata runcmd/$i)" + sh -c "$cmd" + done +} + +init__userdata_groups() { + local groups="$(get_userdata groups)" + for i in $groups; do + local group="$(get_userdata groups/$i)" + $MOCK addgroup $group + done +} + +in_list() { + local i needle="$1" + shift + for i in "$@"; do + if [ "$i" = "$needle" ]; then + return 0 + fi + done + return 1 +} + +init__userdata_users() { + local users="$(get_userdata users)" + for i in $users; do + local name gecos homedir shell primary_group groups + local system=false no_create_home=false lock_passwd=true + local keys="$(get_userdata users/$i)" + if [ "$i" = 1 ] && [ "$keys" = "default" ]; then + continue + fi + if in_list name $keys; then + name="$(get_userdata users/$i/name)" + else + continue + fi + if in_list gecos $keys; then + gecos="$(get_userdata users/$i/gecos)" + fi + if in_list homedir $keys; then + homedir="$(get_userdata users/$i/homedir)" + fi + if in_list shell $keys; then + shell="$(get_userdata users/$i/shell)" + fi + if in_list primary_group $keys; then + primary_group="$(get_userdata users/$i/primary_group)" + fi + if in_list system $keys; then + system="$(get_userdata users/$i/system)" + fi + if in_list no_create_home $keys; then + no_create_home="$(get_userdata users/$i/no_create_home)" + fi + + if getent passwd "$user" >/dev/null; then + log -i -t "$phase" info "$ACTION: user $user already exists" + else + if [ "$system" != "true" ]; then + unset system + fi + if [ "$no_create_home" != "true" ]; then + unset no_create_home + fi + $MOCK adduser -D ${gecos:+-g "$gecos"} ${homedir:+-h "$homedir"} ${shell:+-s "$shell"} ${primary_group:+-G "$primary_group"} ${system:+-S} ${no_create_home:+-H} "$name" + fi + + if in_list lock_passwd $keys; then + lock_passwd="$(get_userdata users/$i/lock_passwd)" + fi + + if [ "$lock_passwd" != "false" ]; then + echo "$name:*" | $MOCK chpasswd -e + fi + + if in_list ssh_authorized_keys $keys; then + set_ssh_authorized_keys_for "$name" users/$i/ssh_authorized_keys + fi + + if in_list groups $keys; then + groups="$(get_userdata users/$i/groups | tr ',' ' ')" + local group + for group in $groups; do + $MOCK addgroup "$name" "$group" + done + fi + if in_list doas $keys; then + if [ -d "$ROOT/etc/doas.d" ]; then + touch "$ROOT/etc/doas.d/$name.conf" + chmod 660 "$ROOT/etc/doas.d/$name.conf" + fi + local j + for j in $(get_userdata users/$i/doas); do + local line="$(get_userdata users/$i/doas/$j)" + if [ -d "$ROOT/etc/doas.d" ]; then + echo "$line" >> "$ROOT/etc/doas.d/$name.conf" + elif [ -f "$ROOT/etc/doas.conf" ]; then + add_once "$ROOT/etc/doas.conf" "$line" + fi + done + fi + done +}