From b2b4b6dd1953e0d090352a0eefa0b4a5074fdb1b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 02:29:02 +0000 Subject: [PATCH] Add cloud-init.sh for provider install-script/user-data fields IONOS Cloud Server, DigitalOcean, and Hetzner all offer an "install script"/user-data field that runs as root with no TTY while the image is still provisioning, so bootstrap.sh's interactive tail can't run there. cloud-init.sh clones the repo unattended and drops a one-shot /etc/profile.d hook that launches the normal whiptail setup.sh wizard on the first interactive login, then removes itself. --- README.md | 14 +++++++++++ cloud-init.sh | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100755 cloud-init.sh diff --git a/README.md b/README.md index 1993f87..fe5ae33 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,20 @@ the wizard — the USB can be unplugged once setup starts. sudo bash bootstrap.sh --pat ghp_xxxxxxxxxxxxxxxxxxxx ``` Use a fine-grained read-only PAT scoped to just this repo (Contents: Read). + +**Cloud provider install-script / user-data field (IONOS, DigitalOcean, +Hetzner, ...):** these run as root with no terminal attached while the +image is still being provisioned, so `bootstrap.sh`'s interactive hand-off +doesn't apply yet. Paste `cloud-init.sh` into that field instead: +``` +https://raw.githubusercontent.com/outis1one/ubuntu-post-install/main/cloud-init.sh +``` +It clones the repo in the background during provisioning and installs a +one-shot login hook. The provider boots Ubuntu 24.04, this runs unattended, +and by the time you SSH in the whiptail service menu is already waiting for +you — same experience as `bootstrap.sh`, just already started. Assumes a +root login (the default for all three providers above); see the comments in +`cloud-init.sh` if you've provisioned a separate sudo user instead. The PAT is stripped from the stored remote URL after cloning. ## Usage diff --git a/cloud-init.sh b/cloud-init.sh new file mode 100755 index 0000000..3aba119 --- /dev/null +++ b/cloud-init.sh @@ -0,0 +1,70 @@ +#!/bin/bash +# cloud-init.sh — payload for a cloud provider's "install script" / user-data +# field (IONOS Cloud Server image deploy, DigitalOcean droplet user-data, +# Hetzner Cloud user-data, etc). The provider runs this as root, unattended, +# with no TTY, while the box is still being provisioned — before you have +# ever logged in. +# +# It deliberately does NOT run the interactive wizard itself (there's no +# terminal for whiptail to talk to yet). Instead it does two things: +# +# 1. Clones this repo to /root/ubuntu-post-install (pulls if already there). +# 2. Installs a one-shot /etc/profile.d hook that launches setup.sh — +# the normal whiptail service menu — the first time you actually log +# in over SSH, then deletes itself so it never fires again. +# +# End result: the provider boots Ubuntu 24.04, this runs in the background, +# and by the time you SSH in the checklist menu is sitting there waiting — +# the same experience as running bootstrap.sh by hand, just already started. +# +# Usage: paste this file's raw URL into the provider's install-script / +# user-data field, e.g. +# https://raw.githubusercontent.com/outis1one/ubuntu-post-install/main/cloud-init.sh +# +# Assumes the provider logs you in as root (the default for IONOS Cloud +# Server, DigitalOcean droplets, and Hetzner Cloud server images). If you've +# provisioned a separate non-root sudo user instead, the hook won't reach +# you automatically — SSH in and run: +# sudo bash /root/ubuntu-post-install/setup.sh +set -euo pipefail + +if [ "$(id -u)" -ne 0 ]; then + echo "cloud-init.sh must run as root — that's how provider install-script hooks already run it." >&2 + exit 1 +fi + +REPO_URL="https://github.com/outis1one/ubuntu-post-install.git" +DEST="/root/ubuntu-post-install" +MARKER="/root/.ubuntu-post-install-pending" +HOOK="/etc/profile.d/99-ubuntu-post-install.sh" + +command -v git >/dev/null 2>&1 || { apt-get update -qq && apt-get install -y git; } + +if [ -d "$DEST/.git" ]; then + git -C "$DEST" pull --ff-only || true +else + git clone "$REPO_URL" "$DEST" +fi + +touch "$MARKER" + +# POSIX sh, not bash — /etc/profile.d/*.sh gets sourced by whatever shell +# the login uses, not necessarily bash. +cat > "$HOOK" << 'EOF' +# Installed by cloud-init.sh — launches the ubuntu-post-install wizard on +# the first interactive login, then removes itself so it never fires again. +MARKER="/root/.ubuntu-post-install-pending" +HOOK="/etc/profile.d/99-ubuntu-post-install.sh" +DEST="/root/ubuntu-post-install" + +if [ -f "$MARKER" ] && [ -t 0 ] && [ "$(id -u)" -eq 0 ] && [ -f "$DEST/setup.sh" ]; then + rm -f "$MARKER" "$HOOK" + echo "" + echo "ubuntu-post-install: launching the setup wizard..." + echo "" + bash "$DEST/setup.sh" +fi +EOF +chmod 644 "$HOOK" + +echo "cloud-init.sh: repo cloned to $DEST — the setup wizard will launch on first login."