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:
Claude
2026-10-03 01:18:54 +00:00
parent 08b33ef823
commit aee6b779eb
7 changed files with 648 additions and 1 deletions
+1
View File
@@ -1,2 +1,3 @@
node_modules/ node_modules/
webui/test/fixtures/.fake-state/ webui/test/fixtures/.fake-state/
iso/build/
+47
View File
@@ -61,6 +61,53 @@ The installer will guide you through configuration during setup.
--- ---
## All-in-One Install ISO (Ubuntu Server 26.04 + Kiosk)
Instead of installing Ubuntu Server and then running the script, build a
single ISO that does both: it installs Ubuntu Server 26.04, and on the first
boot of the installed system it runs `./install.sh` automatically on the
console.
```bash
sudo apt install xorriso curl git openssl
git clone https://github.com/outis1one/ubuntu-based-kiosk.git
cd ubuntu-based-kiosk
iso/build-iso.sh # downloads + verifies the latest 26.04.x server ISO
# -> iso/build/ubuntu-26.04.x-kiosk-amd64.iso
sudo dd if=iso/build/ubuntu-26.04.*-kiosk-amd64.iso of=/dev/sdX bs=4M status=progress oflag=sync
```
By default the installer only asks for **network** (WiFi works here),
**disk**, and your **admin username/password** — language, keyboard and the
SSH server are answered for you. On first boot, `install.sh` runs
interactively on the screen, exactly like a manual install. The repo ends up
at `~/ubuntu-based-kiosk` for the admin user, so `./install.sh` and
Advanced → Upgrade work afterwards as usual.
Useful options (`iso/build-iso.sh --help` for all):
| Option | Effect |
|---|---|
| `--unattended` | First-boot install takes every default with no prompts (skips the Core Settings menu) and reboots into the kiosk. Set sites afterwards in the Web UI (`http://<ip>:8090`) or `./install.sh`. |
| `--username NAME [--password PASS]` | Pre-set the admin account (password is prompted for if omitted; only a hash goes on the ISO). Not `kiosk`. |
| `--hostname`, `--ssh-key FILE`, `--locale`, `--keyboard` | Pre-set those. |
| `--auto-network` / `--auto-storage` | Skip the network screen (DHCP on ethernet) / wipe the largest disk without asking. |
| `--no-confirm` | Skip the "Continue with autoinstall?" prompt at boot. |
| `--fully-automatic` | All of the above (needs `--username`): boot it and walk away. **Wipes a disk with no questions — label the USB stick.** |
| `--iso PATH` | Use an ISO you already have instead of downloading. |
Notes:
- The ISO carries a copy of this checkout's current commit (uncommitted
changes are not included).
- The kiosk machine still needs internet on first boot (packages, Node.js,
Electron). If there's none, the console waits for it and offers a shell to
fix networking (`/etc/netplan/*.yaml`, then `sudo netplan apply`).
- A failed or interrupted first-boot install runs again on the next boot, or
now with `sudo systemctl start kiosk-firstboot`. Its log is
`/var/lib/kiosk-firstboot/install.log`.
---
## Offline / Air-Gapped Download ## Offline / Air-Gapped Download
If the kiosk machine can't reach GitHub directly (no browser, restrictive proxy, or you just prefer to grab the script on another computer and carry it over via USB), download it ahead of time instead of using the `curl`/`wget` one-liner above. If the kiosk machine can't reach GitHub directly (no browser, restrictive proxy, or you just prefer to grab the script on another computer and carry it over via USB), download it ahead of time instead of using the `curl`/`wget` one-liner above.
+319
View File
@@ -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
+226
View File
@@ -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
+33
View File
@@ -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
+10
View File
@@ -118,6 +118,16 @@ ask_yes_no() {
local default="${2:-n}" local default="${2:-n}"
local answer local answer
# Unattended first-boot install (iso/ - KIOSK_UNATTENDED=1 in
# /etc/kiosk-firstboot.conf): nobody is at the keyboard, so take the
# default every prompt already offers and echo it so the log still
# shows what was decided.
if [[ "${KIOSK_UNATTENDED:-0}" == "1" ]]; then
echo "$prompt (y/n) [$default]: $default (unattended)"
validate_yes_no "$default"
return
fi
while true; do while true; do
read -r -p "$prompt (y/n) [$default]: " answer read -r -p "$prompt (y/n) [$default]: " answer
answer="${answer:-$default}" answer="${answer:-$default}"
+12 -1
View File
@@ -268,7 +268,11 @@ provision_configure_kiosk_settings() {
echo "password protection, WiFi, and schedules. Skip and configure" echo "password protection, WiFi, and schedules. Skip and configure"
echo "later via ./install.sh if you'd rather do this after reboot." echo "later via ./install.sh if you'd rather do this after reboot."
echo echo
if ask_yes_no "Configure Core Settings now?" "y"; then if [[ "${KIOSK_UNATTENDED:-0}" == "1" ]]; then
# The menu itself is interactive - no default to take. Sites etc
# are set afterwards via the Web UI or ./install.sh instead.
log_info "Unattended install - skipping Core Settings (configure later via the Web UI or ./install.sh)"
elif ask_yes_no "Configure Core Settings now?" "y"; then
core_settings_menu core_settings_menu
fi fi
@@ -297,6 +301,13 @@ provision_finish() {
echo "(CUPS, LMS/Squeezelite, Remote Access, Authelia, Asterisk Intercom)," echo "(CUPS, LMS/Squeezelite, Remote Access, Authelia, Asterisk Intercom),"
echo "or Advanced options." echo "or Advanced options."
echo echo
# Lets a caller (iso/firstboot/kiosk-firstboot) tell a finished
# install apart from an interrupted one - is_kiosk_installed is
# already true from step 4 on, and the reboot below can kill the
# caller before it sees this function return.
if [[ -n "${KIOSK_PROVISION_DONE_MARKER:-}" ]]; then
sudo install -D -m 644 /dev/null "$KIOSK_PROVISION_DONE_MARKER"
fi
if ask_yes_no "Reboot now to start the kiosk?" "y"; then if ask_yes_no "Reboot now to start the kiosk?" "y"; then
echo "Rebooting in 3 seconds..." echo "Rebooting in 3 seconds..."
sleep 3 sleep 3