Add all-in-one Ubuntu Server 26.04 + kiosk install ISO builder
iso/build-iso.sh remasters the official 26.04.x live-server ISO (downloaded and checksum-verified, or --iso PATH) with xorriso, replaying its BIOS/UEFI boot records unchanged. It adds: - /autoinstall.yaml: Subiquity autoinstall that answers locale/keyboard/SSH and by default leaves only network, storage and identity interactive (--username/--auto-storage/--auto-network/--no-confirm/--fully-automatic make those automatic too). late-commands copy the repo (a clone of the current HEAD) to /opt/ubuntu-based-kiosk and enable a first-boot service. - iso/firstboot/kiosk-firstboot(.service): on first boot, takes tty1, waits for cloud-init and internet, copies the repo to the admin user's home and runs ./install.sh as that user with temporary passwordless sudo (removed on every exit path). Failed/interrupted installs re-run next boot. install.sh hooks for this: - KIOSK_UNATTENDED=1 makes ask_yes_no take each prompt's default and skips the Core Settings menu (--unattended). - KIOSK_PROVISION_DONE_MARKER is touched by provision_finish before the reboot prompt, so the caller can tell a finished install from an interrupted one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cjz8a58eB5P9sUweqd3NHm
This commit is contained in:
Executable
+319
@@ -0,0 +1,319 @@
|
||||
#!/bin/bash
|
||||
################################################################################
|
||||
# iso/build-iso.sh - Build an Ubuntu Server 26.04 install ISO with the
|
||||
# kiosk built in, so there's no separate "install Ubuntu, then clone and
|
||||
# run ./install.sh" step.
|
||||
#
|
||||
# What the ISO does:
|
||||
# 1. Boots the stock Ubuntu Server installer (Subiquity), driven by an
|
||||
# autoinstall.yaml at the root of the ISO. By default it still asks
|
||||
# for network, disk and your admin username/password - everything
|
||||
# else (language, keyboard, SSH server, snaps...) is answered for
|
||||
# you. Flags below make any of those automatic too.
|
||||
# 2. At the end of the install (autoinstall late-commands), copies this
|
||||
# repo to /opt/ubuntu-based-kiosk on the new system and enables a
|
||||
# one-shot first-boot service (firstboot/kiosk-firstboot.service).
|
||||
# 3. On the first boot of the installed system, that service takes over
|
||||
# tty1 and runs ./install.sh as your admin user from
|
||||
# ~/ubuntu-based-kiosk - interactively by default (same prompts as a
|
||||
# manual install), or with --unattended taking every default and
|
||||
# rebooting straight into the kiosk. Configure sites afterwards via
|
||||
# the Web UI (http://<ip>:8090) or ./install.sh.
|
||||
#
|
||||
# Nothing in the original ISO is changed except /boot/grub/grub.cfg
|
||||
# (menu title, plus the `autoinstall` kernel argument with --no-confirm);
|
||||
# boot records (BIOS + UEFI) are replayed from the original as-is.
|
||||
#
|
||||
# Requirements (on the build machine, any recent Ubuntu/Debian):
|
||||
# sudo apt install xorriso curl git openssl
|
||||
#
|
||||
# Usage:
|
||||
# iso/build-iso.sh [options]
|
||||
#
|
||||
# --iso PATH Use this Ubuntu Server ISO instead of downloading.
|
||||
# --release VER Ubuntu release to download (default: 26.04 - picks
|
||||
# the newest 26.04.x live-server-amd64 point release).
|
||||
# --output PATH Output ISO (default: iso/build/ubuntu-<ver>-kiosk-amd64.iso)
|
||||
#
|
||||
# Installed system:
|
||||
# --username NAME Admin username (the account you SSH in with and run
|
||||
# ./install.sh as - must not be "kiosk"). Giving this
|
||||
# makes the installer's identity screen automatic.
|
||||
# --password PASS Admin password (prompted for if --username is given
|
||||
# without it). Stored only as a SHA-512 crypt hash.
|
||||
# --hostname NAME Hostname (default: kiosk).
|
||||
# --realname NAME Full name for the admin account (default: Kiosk Admin).
|
||||
# --ssh-key FILE Add this public key file to the admin user's
|
||||
# authorized_keys.
|
||||
# --locale LOCALE Default: en_US.UTF-8
|
||||
# --keyboard LAYOUT Default: us
|
||||
# --auto-storage Don't ask about disks: wipe the largest disk and use
|
||||
# all of it (LVM). DESTROYS DATA on that disk.
|
||||
# --auto-network Don't show the network screen (DHCP on wired
|
||||
# interfaces). Leave this off if you need WiFi.
|
||||
# --no-confirm Don't ask "Continue with autoinstall?" at boot.
|
||||
# --fully-automatic Shorthand for --auto-storage --auto-network
|
||||
# --no-confirm --unattended; needs --username.
|
||||
# Booting this ISO will wipe a disk with NO questions.
|
||||
#
|
||||
# Kiosk install on first boot:
|
||||
# --unattended Run install.sh with all defaults, no prompts
|
||||
# (skips the Core Settings menu), then reboot.
|
||||
# --repo-url URL git remote recorded in the copied repo, used by
|
||||
# Advanced -> Upgrade (default: this checkout's
|
||||
# origin, else the upstream GitHub repo).
|
||||
#
|
||||
# The repo copied onto the ISO is a clone of this checkout's current HEAD
|
||||
# (committed changes only - uncommitted edits are NOT included).
|
||||
################################################################################
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
BUILD_DIR="$SCRIPT_DIR/build"
|
||||
DEFAULT_REPO_URL="https://github.com/outis1one/ubuntu-based-kiosk.git"
|
||||
|
||||
SRC_ISO=""
|
||||
RELEASE="26.04"
|
||||
OUTPUT=""
|
||||
ADMIN_USER=""
|
||||
ADMIN_PASS=""
|
||||
HOSTNAME_="kiosk"
|
||||
REALNAME="Kiosk Admin"
|
||||
SSH_KEY_FILE=""
|
||||
LOCALE="en_US.UTF-8"
|
||||
KEYBOARD="us"
|
||||
AUTO_STORAGE=0
|
||||
AUTO_NETWORK=0
|
||||
NO_CONFIRM=0
|
||||
UNATTENDED=0
|
||||
REPO_URL=""
|
||||
|
||||
die() { echo "ERROR: $*" >&2; exit 1; }
|
||||
info() { echo "==> $*"; }
|
||||
|
||||
usage() {
|
||||
sed -n '/^# Usage:/,/^################/p' "$0" | sed '$d; s/^# \{0,1\}//'
|
||||
exit "${1:-0}"
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--iso) SRC_ISO="$2"; shift 2 ;;
|
||||
--release) RELEASE="$2"; shift 2 ;;
|
||||
--output) OUTPUT="$2"; shift 2 ;;
|
||||
--username) ADMIN_USER="$2"; shift 2 ;;
|
||||
--password) ADMIN_PASS="$2"; shift 2 ;;
|
||||
--hostname) HOSTNAME_="$2"; shift 2 ;;
|
||||
--realname) REALNAME="$2"; shift 2 ;;
|
||||
--ssh-key) SSH_KEY_FILE="$2"; shift 2 ;;
|
||||
--locale) LOCALE="$2"; shift 2 ;;
|
||||
--keyboard) KEYBOARD="$2"; shift 2 ;;
|
||||
--auto-storage) AUTO_STORAGE=1; shift ;;
|
||||
--auto-network) AUTO_NETWORK=1; shift ;;
|
||||
--no-confirm) NO_CONFIRM=1; shift ;;
|
||||
--unattended) UNATTENDED=1; shift ;;
|
||||
--fully-automatic) AUTO_STORAGE=1; AUTO_NETWORK=1; NO_CONFIRM=1; UNATTENDED=1; shift ;;
|
||||
--repo-url) REPO_URL="$2"; shift 2 ;;
|
||||
-h|--help) usage 0 ;;
|
||||
*) echo "Unknown option: $1" >&2; usage 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
################################################################################
|
||||
# Validate
|
||||
################################################################################
|
||||
|
||||
for cmd in xorriso curl git openssl sha256sum; do
|
||||
command -v "$cmd" &>/dev/null || die "'$cmd' not found - install it: sudo apt install xorriso curl git openssl"
|
||||
done
|
||||
|
||||
if [[ -n "$ADMIN_USER" ]]; then
|
||||
[[ "$ADMIN_USER" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || die "invalid username: $ADMIN_USER"
|
||||
[[ "$ADMIN_USER" != "kiosk" && "$ADMIN_USER" != "root" ]] \
|
||||
|| die "username '$ADMIN_USER' is reserved (install.sh creates its own 'kiosk' user)"
|
||||
if [[ -z "$ADMIN_PASS" ]]; then
|
||||
read -r -s -p "Password for $ADMIN_USER: " ADMIN_PASS; echo
|
||||
read -r -s -p "Confirm password: " pass2; echo
|
||||
[[ "$ADMIN_PASS" == "$pass2" ]] || die "passwords don't match"
|
||||
[[ -n "$ADMIN_PASS" ]] || die "empty password"
|
||||
fi
|
||||
elif [[ -n "$ADMIN_PASS" ]]; then
|
||||
die "--password needs --username"
|
||||
fi
|
||||
|
||||
if (( AUTO_STORAGE && AUTO_NETWORK && NO_CONFIRM )) && [[ -z "$ADMIN_USER" ]]; then
|
||||
die "--fully-automatic (or all of --auto-storage --auto-network --no-confirm) needs --username"
|
||||
fi
|
||||
|
||||
[[ "$HOSTNAME_" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$ ]] || die "invalid hostname: $HOSTNAME_"
|
||||
[[ -z "$SSH_KEY_FILE" || -r "$SSH_KEY_FILE" ]] || die "can't read --ssh-key file: $SSH_KEY_FILE"
|
||||
|
||||
if [[ -z "$REPO_URL" ]]; then
|
||||
REPO_URL="$(git -C "$REPO_ROOT" remote get-url origin 2>/dev/null || true)"
|
||||
# Local proxies/mirrors and ssh remotes aren't reachable from a kiosk.
|
||||
[[ "$REPO_URL" == https://github.com/* ]] || REPO_URL="$DEFAULT_REPO_URL"
|
||||
fi
|
||||
|
||||
mkdir -p "$BUILD_DIR"
|
||||
WORK="$(mktemp -d "$BUILD_DIR/work.XXXXXX")"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
################################################################################
|
||||
# Source ISO - download and verify unless one was given
|
||||
################################################################################
|
||||
|
||||
if [[ -z "$SRC_ISO" ]]; then
|
||||
base="https://releases.ubuntu.com/$RELEASE"
|
||||
info "Looking up the latest Ubuntu $RELEASE live-server ISO..."
|
||||
sums="$(curl -fsSL "$base/SHA256SUMS")" || die "couldn't fetch $base/SHA256SUMS"
|
||||
iso_name="$(grep -oE "ubuntu-${RELEASE//./\\.}(\.[0-9]+)?-live-server-amd64\.iso" <<<"$sums" | sort -V | tail -1)"
|
||||
[[ -n "$iso_name" ]] || die "no live-server-amd64 ISO listed in $base/SHA256SUMS"
|
||||
expected="$(grep -F "*$iso_name" <<<"$sums" | awk '{print $1}')"
|
||||
|
||||
mkdir -p "$BUILD_DIR/cache"
|
||||
SRC_ISO="$BUILD_DIR/cache/$iso_name"
|
||||
if [[ -f "$SRC_ISO" ]] && echo "$expected $SRC_ISO" | sha256sum -c --status; then
|
||||
info "Using cached $iso_name"
|
||||
else
|
||||
info "Downloading $iso_name (~3 GB)..."
|
||||
curl -fL --retry 5 -C - -o "$SRC_ISO" "$base/$iso_name" \
|
||||
|| curl -fL --retry 5 -o "$SRC_ISO" "$base/$iso_name"
|
||||
info "Verifying checksum..."
|
||||
echo "$expected $SRC_ISO" | sha256sum -c --status \
|
||||
|| { rm -f "$SRC_ISO"; die "checksum mismatch for $iso_name - deleted, re-run to retry"; }
|
||||
fi
|
||||
fi
|
||||
[[ -f "$SRC_ISO" ]] || die "ISO not found: $SRC_ISO"
|
||||
|
||||
if [[ -z "$OUTPUT" ]]; then
|
||||
src_base="$(basename "$SRC_ISO" .iso)"
|
||||
OUTPUT="$BUILD_DIR/${src_base/-live-server/-kiosk}.iso"
|
||||
[[ "$OUTPUT" != "$BUILD_DIR/$src_base.iso" ]] || OUTPUT="$BUILD_DIR/${src_base}-kiosk.iso"
|
||||
fi
|
||||
|
||||
################################################################################
|
||||
# Stage the repo + first-boot files that go on the ISO under /kiosk
|
||||
################################################################################
|
||||
|
||||
info "Staging repo ($(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD) @ $(git -C "$REPO_ROOT" rev-parse --short HEAD))..."
|
||||
if [[ -n "$(git -C "$REPO_ROOT" status --porcelain)" ]]; then
|
||||
echo " WARNING: uncommitted changes in $REPO_ROOT are NOT included - commit them first if you need them."
|
||||
fi
|
||||
mkdir -p "$WORK/kiosk/firstboot"
|
||||
git clone --quiet --depth 1 --no-local \
|
||||
--branch "$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD)" \
|
||||
"file://$REPO_ROOT" "$WORK/kiosk/ubuntu-based-kiosk"
|
||||
git -C "$WORK/kiosk/ubuntu-based-kiosk" remote set-url origin "$REPO_URL"
|
||||
|
||||
install -m 755 "$SCRIPT_DIR/firstboot/kiosk-firstboot" "$WORK/kiosk/firstboot/"
|
||||
install -m 644 "$SCRIPT_DIR/firstboot/kiosk-firstboot.service" "$WORK/kiosk/firstboot/"
|
||||
cat > "$WORK/kiosk/firstboot/kiosk-firstboot.conf" <<EOF
|
||||
# Read by /usr/local/sbin/kiosk-firstboot. Written by iso/build-iso.sh.
|
||||
# 1 = take every install.sh default and reboot into the kiosk; 0 = interactive.
|
||||
KIOSK_UNATTENDED=$UNATTENDED
|
||||
EOF
|
||||
|
||||
################################################################################
|
||||
# autoinstall.yaml
|
||||
################################################################################
|
||||
|
||||
# Single-quoted YAML scalar.
|
||||
yq() { printf "'%s'" "${1//\'/\'\'}"; }
|
||||
|
||||
interactive=()
|
||||
(( AUTO_NETWORK )) || interactive+=(network)
|
||||
(( AUTO_STORAGE )) || interactive+=(storage)
|
||||
[[ -n "$ADMIN_USER" ]] || interactive+=(identity)
|
||||
|
||||
{
|
||||
echo "#cloud-config"
|
||||
echo "# Generated by iso/build-iso.sh - see that file for what this does."
|
||||
echo "autoinstall:"
|
||||
echo " version: 1"
|
||||
if (( ${#interactive[@]} )); then
|
||||
echo " interactive-sections:"
|
||||
for s in "${interactive[@]}"; do echo " - $s"; done
|
||||
fi
|
||||
echo " locale: $(yq "$LOCALE")"
|
||||
echo " keyboard:"
|
||||
echo " layout: $(yq "$KEYBOARD")"
|
||||
# Pre-selected answer for the storage screen when it's interactive;
|
||||
# the whole answer when --auto-storage.
|
||||
echo " storage:"
|
||||
echo " layout:"
|
||||
echo " name: lvm"
|
||||
echo " sizing-policy: all"
|
||||
if [[ -n "$ADMIN_USER" ]]; then
|
||||
echo " identity:"
|
||||
echo " hostname: $(yq "$HOSTNAME_")"
|
||||
echo " realname: $(yq "$REALNAME")"
|
||||
echo " username: $(yq "$ADMIN_USER")"
|
||||
echo " password: $(yq "$(openssl passwd -6 -stdin <<<"$ADMIN_PASS")")"
|
||||
fi
|
||||
echo " ssh:"
|
||||
echo " install-server: true"
|
||||
echo " allow-pw: true"
|
||||
if [[ -n "$SSH_KEY_FILE" ]]; then
|
||||
echo " authorized-keys:"
|
||||
while IFS= read -r key; do
|
||||
[[ -z "$key" || "$key" == \#* ]] && continue
|
||||
echo " - $(yq "$key")"
|
||||
done < "$SSH_KEY_FILE"
|
||||
fi
|
||||
cat <<'EOF'
|
||||
late-commands:
|
||||
- mkdir -p /target/opt /target/usr/local/sbin /target/var/lib/kiosk-firstboot
|
||||
- cp -a /cdrom/kiosk/ubuntu-based-kiosk /target/opt/ubuntu-based-kiosk
|
||||
- chmod -R u+w /target/opt/ubuntu-based-kiosk
|
||||
- install -m 755 /cdrom/kiosk/firstboot/kiosk-firstboot /target/usr/local/sbin/kiosk-firstboot
|
||||
- install -m 644 /cdrom/kiosk/firstboot/kiosk-firstboot.service /target/etc/systemd/system/kiosk-firstboot.service
|
||||
- install -m 644 /cdrom/kiosk/firstboot/kiosk-firstboot.conf /target/etc/kiosk-firstboot.conf
|
||||
- touch /target/var/lib/kiosk-firstboot/pending
|
||||
- curtin in-target --target=/target -- systemctl enable kiosk-firstboot.service
|
||||
EOF
|
||||
} > "$WORK/autoinstall.yaml"
|
||||
|
||||
################################################################################
|
||||
# grub.cfg - same file serves BIOS and UEFI boot on the server ISO
|
||||
################################################################################
|
||||
|
||||
xorriso -osirrox on -indev "$SRC_ISO" -extract /boot/grub/grub.cfg "$WORK/grub.cfg" 2>/dev/null \
|
||||
|| die "couldn't read /boot/grub/grub.cfg from $SRC_ISO - is it an Ubuntu Server ISO?"
|
||||
chmod u+w "$WORK/grub.cfg"
|
||||
grep -q '/casper/vmlinuz' "$WORK/grub.cfg" || die "unexpected grub.cfg layout in $SRC_ISO"
|
||||
|
||||
sed -i 's/Try or Install Ubuntu Server/Install Ubuntu Based Kiosk/' "$WORK/grub.cfg"
|
||||
if (( NO_CONFIRM )); then
|
||||
sed -i -E '/linux[[:space:]]+\/casper\/vmlinuz/ s/[[:space:]]+---/ autoinstall ---/' "$WORK/grub.cfg"
|
||||
fi
|
||||
|
||||
################################################################################
|
||||
# Write the new ISO
|
||||
################################################################################
|
||||
|
||||
info "Writing $OUTPUT..."
|
||||
rm -f "$OUTPUT"
|
||||
xorriso -indev "$SRC_ISO" -outdev "$OUTPUT" \
|
||||
-map "$WORK/autoinstall.yaml" /autoinstall.yaml \
|
||||
-map "$WORK/kiosk" /kiosk \
|
||||
-map "$WORK/grub.cfg" /boot/grub/grub.cfg \
|
||||
-boot_image any replay \
|
||||
-padding included \
|
||||
> "$WORK/xorriso.log" 2>&1 \
|
||||
|| { cat "$WORK/xorriso.log" >&2; rm -f "$OUTPUT"; die "xorriso failed"; }
|
||||
|
||||
echo
|
||||
info "Done: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))"
|
||||
echo
|
||||
echo "Installer asks for : ${interactive[*]:-nothing}$( (( NO_CONFIRM )) || echo " (plus a 'Continue with autoinstall?' confirmation)")"
|
||||
echo "Admin user : ${ADMIN_USER:-chosen during install}"
|
||||
echo "First boot : $( (( UNATTENDED )) && echo "unattended install.sh, then reboots into the kiosk" || echo "interactive install.sh on the console")"
|
||||
echo
|
||||
echo "Write it to a USB stick (replace /dev/sdX - this erases it):"
|
||||
echo " sudo dd if='$OUTPUT' of=/dev/sdX bs=4M status=progress oflag=sync"
|
||||
if (( AUTO_STORAGE && NO_CONFIRM )); then
|
||||
echo
|
||||
echo "WARNING: this ISO wipes the target's largest disk without asking. Label the stick."
|
||||
fi
|
||||
Executable
+226
@@ -0,0 +1,226 @@
|
||||
#!/bin/bash
|
||||
################################################################################
|
||||
# kiosk-firstboot - runs ubuntu-based-kiosk's ./install.sh on the first
|
||||
# boot of a system installed from the custom ISO (iso/build-iso.sh).
|
||||
#
|
||||
# install.sh refuses to run as root and expects a regular sudo user, so
|
||||
# this runs it as the admin account the Ubuntu installer created (the
|
||||
# first regular user, uid 1000), from a copy of the repo in that user's
|
||||
# home - the same layout as a manual `git clone && ./install.sh`, so
|
||||
# re-running ./install.sh later and Advanced -> Upgrade both work as
|
||||
# usual afterwards.
|
||||
#
|
||||
# For the duration of the install only, that user gets passwordless sudo
|
||||
# (nobody has typed a password yet, and in unattended mode nobody will);
|
||||
# the drop-in is removed on every exit path, including the reboot at the
|
||||
# end of install.sh (see ExecStopPost in kiosk-firstboot.service).
|
||||
#
|
||||
# Settings come from /etc/kiosk-firstboot.conf, written at ISO build time:
|
||||
# KIOSK_UNATTENDED=1 take every prompt's default, skip the Core
|
||||
# Settings menu, reboot into the kiosk when done.
|
||||
# KIOSK_UNATTENDED=0 (default) normal interactive install.sh on tty1.
|
||||
################################################################################
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
CONF=/etc/kiosk-firstboot.conf
|
||||
STATE_DIR=/var/lib/kiosk-firstboot
|
||||
PENDING="$STATE_DIR/pending"
|
||||
DONE_MARKER="$STATE_DIR/done"
|
||||
SUDOERS_DROPIN=/etc/sudoers.d/90-kiosk-firstboot
|
||||
APT_DROPIN=/etc/apt/apt.conf.d/90-kiosk-firstboot
|
||||
REPO_SRC=/opt/ubuntu-based-kiosk
|
||||
LOG="$STATE_DIR/install.log"
|
||||
|
||||
KIOSK_UNATTENDED=0
|
||||
# shellcheck source=/dev/null
|
||||
[[ -f "$CONF" ]] && source "$CONF"
|
||||
|
||||
cleanup() {
|
||||
rm -f "$SUDOERS_DROPIN" "$APT_DROPIN"
|
||||
if [[ -f "$DONE_MARKER" ]]; then
|
||||
rm -f "$PENDING"
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "--cleanup" ]]; then
|
||||
cleanup
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Hand tty1 back to a normal login prompt (the unit Conflicts= with
|
||||
# getty@tty1 so it can own the console; systemd won't restart it alone).
|
||||
release_tty() {
|
||||
systemctl --no-block start getty@tty1.service 2>/dev/null || true
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
trap 'exit 130' INT TERM HUP
|
||||
|
||||
banner() {
|
||||
clear
|
||||
echo "════════════════════════════════════════════════════════════"
|
||||
echo " Ubuntu Based Kiosk - First Boot Setup"
|
||||
echo "════════════════════════════════════════════════════════════"
|
||||
echo
|
||||
}
|
||||
|
||||
# Interactive-mode escape hatch when something needs a human: retry,
|
||||
# drop to a shell as the admin user to fix it, or give up until next boot.
|
||||
# Returns 0 to retry, 1 to give up.
|
||||
offer_retry() {
|
||||
local choice
|
||||
while true; do
|
||||
echo
|
||||
echo " [R] Retry [S] Shell (as $ADMIN_USER, exit to come back) [Q] Quit to login prompt"
|
||||
read -r -p "Choice [R]: " choice
|
||||
case "${choice,,}" in
|
||||
""|r) return 0 ;;
|
||||
s) runuser -l "$ADMIN_USER" ;;
|
||||
q)
|
||||
echo "Setup will run again on next boot, or run it now with:"
|
||||
echo " sudo systemctl start kiosk-firstboot"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
wait_for_internet() {
|
||||
local waited=0
|
||||
until curl -fsS --max-time 10 -o /dev/null https://deb.nodesource.com/ 2>/dev/null \
|
||||
|| curl -fsS --max-time 10 -o /dev/null http://archive.ubuntu.com/ubuntu/ 2>/dev/null; do
|
||||
if (( waited == 0 )); then
|
||||
echo "Waiting for an internet connection (needed for packages,"
|
||||
echo "Node.js and Electron)..."
|
||||
fi
|
||||
sleep 10
|
||||
waited=$((waited + 10))
|
||||
if (( waited % 120 == 0 )); then
|
||||
echo "Still no internet after ${waited}s. Network status:"
|
||||
networkctl list --no-pager 2>/dev/null || ip -brief addr
|
||||
if [[ "$KIOSK_UNATTENDED" != "1" ]]; then
|
||||
echo "WiFi or static IP? Use the Shell option and edit /etc/netplan/*.yaml, then 'sudo netplan apply'."
|
||||
offer_retry || return 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
echo "Internet connection OK."
|
||||
}
|
||||
|
||||
################################################################################
|
||||
|
||||
# Keep kernel/systemd chatter off the screen the installer is drawing on.
|
||||
dmesg -n 1 2>/dev/null || true
|
||||
chvt 1 2>/dev/null || true
|
||||
export TERM=linux
|
||||
|
||||
ADMIN_USER="$(getent passwd 1000 | cut -d: -f1)"
|
||||
banner
|
||||
if [[ -z "$ADMIN_USER" ]]; then
|
||||
echo "ERROR: no regular user (uid 1000) found - can't run install.sh."
|
||||
echo "Create one, then: sudo systemctl start kiosk-firstboot"
|
||||
release_tty
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$ADMIN_USER" == "kiosk" ]]; then
|
||||
echo "ERROR: the admin account is named 'kiosk', which install.sh reserves"
|
||||
echo "for the kiosk's own restricted user. Reinstall with a different username."
|
||||
release_tty
|
||||
exit 1
|
||||
fi
|
||||
ADMIN_HOME="$(getent passwd "$ADMIN_USER" | cut -d: -f6)"
|
||||
REPO_DEST="$ADMIN_HOME/ubuntu-based-kiosk"
|
||||
|
||||
if [[ ! -d "$REPO_DEST" ]]; then
|
||||
cp -a "$REPO_SRC" "$REPO_DEST"
|
||||
chmod -R u+w "$REPO_DEST"
|
||||
chown -R "$ADMIN_USER:$ADMIN_USER" "$REPO_DEST"
|
||||
fi
|
||||
|
||||
echo "Admin user : $ADMIN_USER"
|
||||
echo "Installer : $REPO_DEST/install.sh"
|
||||
echo "Mode : $([[ "$KIOSK_UNATTENDED" == "1" ]] && echo "unattended (defaults, reboots when done)" || echo "interactive")"
|
||||
echo "Log : $LOG"
|
||||
echo
|
||||
|
||||
# cloud-init and the first apt/unattended-upgrades run on a fresh system
|
||||
# both want the dpkg lock; let them finish rather than racing them.
|
||||
if command -v cloud-init &>/dev/null; then
|
||||
echo "Waiting for cloud-init to finish..."
|
||||
cloud-init status --wait >/dev/null 2>&1 || true
|
||||
fi
|
||||
mkdir -p "$STATE_DIR"
|
||||
printf 'DPkg::Lock::Timeout "900";\n' > "$APT_DROPIN"
|
||||
|
||||
wait_for_internet || { release_tty; exit 1; }
|
||||
|
||||
# install.sh's own preflight needs jq; Advanced -> Upgrade needs git.
|
||||
echo "Installing installer prerequisites (jq, git)..."
|
||||
DEBIAN_FRONTEND=noninteractive apt-get update -q >/dev/null
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -q jq git curl >/dev/null
|
||||
|
||||
cat > "$SUDOERS_DROPIN".tmp <<SUDOERS
|
||||
# Temporary - removed by /usr/local/sbin/kiosk-firstboot when it exits.
|
||||
$ADMIN_USER ALL=(ALL) NOPASSWD: ALL
|
||||
Defaults:$ADMIN_USER env_keep += "DEBIAN_FRONTEND"
|
||||
SUDOERS
|
||||
chmod 440 "$SUDOERS_DROPIN".tmp
|
||||
if ! visudo -cf "$SUDOERS_DROPIN".tmp >/dev/null; then
|
||||
rm -f "$SUDOERS_DROPIN".tmp
|
||||
echo "ERROR: generated sudoers drop-in failed validation."
|
||||
release_tty
|
||||
exit 1
|
||||
fi
|
||||
mv "$SUDOERS_DROPIN".tmp "$SUDOERS_DROPIN"
|
||||
|
||||
run_install() {
|
||||
local -a envs=(
|
||||
HOME="$ADMIN_HOME" USER="$ADMIN_USER" LOGNAME="$ADMIN_USER"
|
||||
SHELL=/bin/bash TERM=linux LANG="${LANG:-C.UTF-8}"
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
# config.sh would otherwise derive this from SUDO_USER/whoami.
|
||||
BUILD_USER="$ADMIN_USER"
|
||||
KIOSK_UNATTENDED="$KIOSK_UNATTENDED"
|
||||
KIOSK_PROVISION_DONE_MARKER="$DONE_MARKER"
|
||||
)
|
||||
# Even interactively: stdout goes through tee, which debconf dialogs
|
||||
# cannot draw on, and nothing install.sh installs needs a debconf answer.
|
||||
envs+=(DEBIAN_FRONTEND=noninteractive)
|
||||
|
||||
# runuser rather than sudo -u: sudo would set SUDO_USER=root, which
|
||||
# config.sh reads as "the admin running this tool".
|
||||
runuser -u "$ADMIN_USER" -- env -i "${envs[@]}" \
|
||||
bash -c 'cd "$1" && exec ./install.sh' _ "$REPO_DEST" \
|
||||
2>&1 | tee -a "$LOG"
|
||||
return "${PIPESTATUS[0]}"
|
||||
}
|
||||
|
||||
attempt=1
|
||||
while true; do
|
||||
run_install
|
||||
rc=$?
|
||||
if [[ -f "$DONE_MARKER" ]]; then
|
||||
# Reached only when the final "Reboot now?" was declined.
|
||||
echo
|
||||
echo "Kiosk install complete. Reboot to start the kiosk: sudo reboot"
|
||||
cleanup
|
||||
release_tty
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "install.sh did not finish (exit code $rc). Full output: $LOG"
|
||||
if [[ "$KIOSK_UNATTENDED" == "1" ]]; then
|
||||
if (( attempt >= 3 )); then
|
||||
echo "Giving up after $attempt attempts; will try again next boot."
|
||||
release_tty
|
||||
exit 1
|
||||
fi
|
||||
attempt=$((attempt + 1))
|
||||
echo "Retrying in 60 seconds (attempt $attempt of 3)..."
|
||||
sleep 60
|
||||
else
|
||||
offer_retry || { release_tty; exit 1; }
|
||||
fi
|
||||
done
|
||||
@@ -0,0 +1,33 @@
|
||||
# Installed by the custom ISO (iso/build-iso.sh) and enabled from its
|
||||
# autoinstall late-commands. Runs install.sh once, on tty1, the first
|
||||
# time the freshly installed system boots - takes tty1 over from getty
|
||||
# so an interactive install has a keyboard and screen, then hands tty1
|
||||
# back. /var/lib/kiosk-firstboot/pending is removed once the install
|
||||
# finishes, so this is a no-op on every boot after that; an interrupted
|
||||
# or failed install leaves it in place and simply runs again next boot.
|
||||
[Unit]
|
||||
Description=Ubuntu Based Kiosk first-boot install
|
||||
After=network-online.target systemd-user-sessions.service cloud-final.service plymouth-quit-wait.service
|
||||
Wants=network-online.target
|
||||
Before=getty@tty1.service
|
||||
Conflicts=getty@tty1.service
|
||||
ConditionPathExists=/var/lib/kiosk-firstboot/pending
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/sbin/kiosk-firstboot
|
||||
# Belt and braces for the temporary sudoers/apt drop-ins when install.sh
|
||||
# reboots out from under the script (its own EXIT trap normally does this).
|
||||
ExecStopPost=/usr/local/sbin/kiosk-firstboot --cleanup
|
||||
StandardInput=tty-force
|
||||
StandardOutput=tty
|
||||
StandardError=tty
|
||||
TTYPath=/dev/tty1
|
||||
TTYReset=yes
|
||||
TTYVHangup=yes
|
||||
TimeoutStartSec=infinity
|
||||
TimeoutStopSec=30
|
||||
KillMode=mixed
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Reference in New Issue
Block a user