# Tiny Cloud - Init Functions # vim:set filetype=sh: # shellcheck shell=sh # set defaults : "${PREFIX:=/usr}" : "${LIBDIR:=$PREFIX/lib}" . "$LIBDIR/tiny-cloud/common" : "${SKIP_INIT_ACTIONS:=}" ### default phase actions (without leading 'init__') DEFAULT_ACTIONS_BOOT=" expand_root set_ephemeral_network set_default_interfaces enable_sshd " DEFAULT_ACTIONS_EARLY=" save_userdata " DEFAULT_ACTIONS_MAIN=" create_default_user set_hostname set_ssh_keys " DEFAULT_ACTIONS_FINAL="" : "${INIT_ACTIONS_BOOT=$DEFAULT_ACTIONS_BOOT}" : "${INIT_ACTIONS_EARLY=$DEFAULT_ACTIONS_EARLY}" : "${INIT_ACTIONS_MAIN=$DEFAULT_ACTIONS_MAIN}" : "${INIT_ACTIONS_FINAL=$DEFAULT_ACTIONS_FINAL}" ### standard boot phase functions... init__expand_root() { local dev=$(awk '$2 == "/" {print $1}' "$PROC"/mounts 2>/dev/null) local filesystem=$(awk '$2 == "/" {print $3}' "$PROC"/mounts 2>/dev/null) local partition=$(cat "$SYS/class/block/${dev#/dev/}/partition" 2>/dev/null) # only support ext2/ext3/ext4 for now case "$filesystem" in ext*) ;; *) return;; esac if [ -n "$partition" ]; then # it's a partition, resize it local volume=$(readlink -f "$SYS/class/block/${dev#/dev/}/..") volume="/dev/${volume##*/}" echo ", +" | $MOCK sfdisk -q --no-reread -N "$partition" "$volume" $MOCK partx -u "$volume" fi # resize filesystem if [ -e "$dev" ] || [ -n "$MOCK" ]; then $MOCK resize2fs "$dev" fi } # collect ethernet interfaces, sorted by index ethernets() { for i in "$SYS/class/net/"*; do local iface="${i##*/}" case "$iface" in eth*) echo "$(cat "$i/ifindex") $iface";; esac done | sort -n | awk '{print $2}' } # find the interface that is has operstate up find_first_interface_up() { local n=0 [ $# -eq 0 ] && return while [ $n -le ${TINY_CLOUD_LINK_WAIT_MAX:-10} ]; do for i in "$@"; do if [ "$(cat "$SYS/class/net/$i/operstate")" = "up" ]; then echo "$i" return fi done sleep 0.1 n=$((n+1)) done } # auto detect which network interface to auto configure # check which is connected or fallback to first # This will set link to down to all eth* except the found auto_detect_ethernet_interface() { local ifaces="$(ethernets)" [ -z "$ifaces" ] && return # find first connected interface for i in $ifaces; do $MOCK ip link set dev $i up >/dev/null done local iface="$(find_first_interface_up $ifaces)" # use first if all are disconnected if [ -z "$iface" ]; then set -- $ifaces iface="$1" fi # we will use the found interface later so lets keep it up for i in $ifaces; do if [ "$i" != "$iface" ]; then $MOCK ip link set dev $i down >/dev/null fi done echo "$iface" } # may be overridded by provider want_ephemeral_network() { false } init__set_ephemeral_network() { if ! want_ephemeral_network; then return fi local iface="$(auto_detect_ethernet_interface)" if [ -z "$iface" ]; then return fi $MOCK udhcpc -i "$iface" -f -q } init__set_default_interfaces() { if [ -f "$ETC"/network/interfaces ]; then log -i -t "$phase" info "$ACTION: already set up" return fi mkdir -p "$ETC/network" printf "%s\n%s\n\n" \ "auto lo" \ "iface lo inet loopback" \ > "$ETC/network/interfaces" local iface="$(auto_detect_ethernet_interface)" if [ -z "$iface" ]; then # TODO: message/log? return fi printf "%s\n%s\n\t%s\n\n" \ "auto $iface" \ "iface $iface" \ "use dhcp" >> "$ETC/network/interfaces" } init__create_default_user() { local user="$CLOUD_USER" if [ "$user" = "none" ] || [ -z "$user" ]; then log -i -t "$phase" info "$ACTION: skip" return fi # don't do anything if it already exists if getent passwd "$user" >/dev/null; then log -i -t "$phase" info "$ACTION: already exists" return fi $MOCK addgroup "$user" $MOCK adduser -h "/home/$user" -s /bin/sh -G "$user" -D "$user" $MOCK addgroup "$user" wheel echo "$user:*" | $MOCK chpasswd -e # setup sudo and/or doas if [ -d "$ETC/sudoers.d" ]; then echo '%wheel ALL=(ALL) NOPASSWD: ALL' > "$ETC/sudoers.d/wheel" fi if [ -d "$ETC/doas.d" ]; then echo 'permit nopass :wheel' > "$TARGET/etc/doas.d/wheel.conf" elif [ -f "$ETC/doas.conf" ]; then add_once "$TARGET/etc/doas.conf" "permit nopass :wheel" fi } init__enable_sshd() { $MOCK rc-update add sshd default # in case something else has enabled/disabled dservices $MOCK rc-update --update } ### standard early phase functions init__save_userdata() { local userdata="$TINY_CLOUD_VAR/user-data" local tmpfile=$(mktemp "$userdata.XXXXXX") imds -e @userdata > "$tmpfile" if printf '\037\213\010' | cmp -s -n 3 "$tmpfile"; then gzip -dc "$tmpfile" > "$userdata" elif printf 'BZh' | cmp -s -n 3 "$tmpfile"; then bzip2 -dc "$tmpfile" > "$userdata" elif printf '\375\067\172\130\132\000' | cmp -s -n 6 "$tmpfile"; then unxz -c "$tmpfile" > "$userdata" elif printf '\135\000\000' | cmp -s -n 3 "$tmpfile"; then lzma -dc "$tmpfile" > "$userdata" elif printf '\211\114\132' | cmp -s -n 3 "$tmpfile"; then lzop -dc "$tmpfile" > "$userdata" elif printf '\004\042\115\030' | cmp -s -n 4 "$tmpfile"; then lz4 -dc "$tmpfile" > "$userdata" elif printf '(\265/\375' | cmp -s -n 4 "$tmpfile"; then zstd -dc "$tmpfile" > "$userdata" else cp "$tmpfile" "$userdata" fi rm "$tmpfile" } ### standard main phase functions init__set_hostname() { local fqdn=$(imds @hostname) if [ -z "$fqdn" ]; then log -i -t "$phase" info "$ACTION: no hostname set" return fi local host="${fqdn%%\.*}" if [ -z "$host" ]; then log -i -t "$phase" warning "$ACTION: invalid hostname '$fqdn'" return 1 fi mkdir -p "$ETC" echo "$host" > "$ETC"/hostname $MOCK hostname -F "$ETC"/hostname echo -e "127.0.1.1\t$fqdn $host" >> "$ETC"/hosts } init__set_ssh_keys() { local sshkeys="$(imds @ssh-keys)" if [ -z "$sshkeys" ]; then log -i -t "$phase" info "$ACTION: no ssh key found" return fi local user="$CLOUD_USER" 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" echo "$sshkeys" > "$keys_file" } ### standard final phase functions would be here, if there were any ### load cloud-specific init functions / vars (potentially overriding) if [ "$CLOUD" = "alpine" ]; then log -i -t "$phase" warning "CLOUD provider alpine is deprecated. Use nocloud" CLOUD="nocloud" fi if [ -f "$LIBDIR/tiny-cloud/cloud/$CLOUD/init" ]; then . "$LIBDIR/tiny-cloud/cloud/$CLOUD/init" fi ### load user-data type-specific init functions / vars (potentially overriding) userdata_type() { if [ ! -f "$TINY_CLOUD_VAR/user-data" ]; then echo missing return fi header=$(head -n1 "$TINY_CLOUD_VAR/user-data" | sed -e 's/[[:space:]].*//g') case "$header" in '#!'*) echo script;; '#'*) echo ${header#\#};; *) echo unknown;; esac } USERDATA_TYPE="$(userdata_type)" if [ -f "$LIBDIR/tiny-cloud/user-data/$USERDATA_TYPE" ]; then . "$LIBDIR/tiny-cloud/user-data/$USERDATA_TYPE" fi