Merge pull request #114 from outis1one/claude/happy-knuth-hwnvbn
Claude/happy knuth hwnvbn
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
# Builds the all-in-one Ubuntu Server 26.04 + kiosk install ISO
|
||||
# (iso/build-iso.sh) on demand: Actions tab -> "Build kiosk ISO" ->
|
||||
# Run workflow. The ISO (~2.8 GB) is attached to the run as a downloadable
|
||||
# artifact for a few days - see the run's Summary page.
|
||||
#
|
||||
# Pre-setting the admin account (needed for "fully-automatic") uses the
|
||||
# repository secret KIOSK_ADMIN_PASSWORD (Settings -> Secrets and
|
||||
# variables -> Actions) - never a workflow input, since inputs are shown
|
||||
# in the run log. Only a SHA-512 hash of it goes on the ISO, but anyone
|
||||
# who downloads the ISO can read that hash, so treat the artifact as
|
||||
# private and use a password you'll change after install.
|
||||
name: Build kiosk ISO
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
mode:
|
||||
description: >-
|
||||
interactive: installer asks network/disk/user, kiosk install.sh
|
||||
runs with prompts on first boot. unattended-kiosk: same installer,
|
||||
kiosk installs with defaults and reboots into the kiosk.
|
||||
fully-automatic: no questions at all - WIPES THE LARGEST DISK
|
||||
(needs username + KIOSK_ADMIN_PASSWORD secret).
|
||||
type: choice
|
||||
default: interactive
|
||||
options:
|
||||
- interactive
|
||||
- unattended-kiosk
|
||||
- fully-automatic
|
||||
username:
|
||||
description: Admin username to pre-set (blank = asked during install). Needs the KIOSK_ADMIN_PASSWORD secret. Not "kiosk"/"admin".
|
||||
type: string
|
||||
default: ""
|
||||
hostname:
|
||||
description: Hostname (only used when username is set)
|
||||
type: string
|
||||
default: kiosk
|
||||
ssh_public_key:
|
||||
description: Optional SSH public key for the admin user (ssh-ed25519 AAAA...)
|
||||
type: string
|
||||
default: ""
|
||||
retention_days:
|
||||
description: Days to keep the ISO artifact
|
||||
type: number
|
||||
default: 3
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install build tools
|
||||
run: |
|
||||
sudo apt-get update -q
|
||||
sudo apt-get install -y -q xorriso
|
||||
|
||||
- name: Build ISO
|
||||
env:
|
||||
MODE: ${{ inputs.mode }}
|
||||
ADMIN_USERNAME: ${{ inputs.username }}
|
||||
ADMIN_HOSTNAME: ${{ inputs.hostname }}
|
||||
SSH_PUBLIC_KEY: ${{ inputs.ssh_public_key }}
|
||||
KIOSK_ADMIN_PASSWORD: ${{ secrets.KIOSK_ADMIN_PASSWORD }}
|
||||
run: |
|
||||
mkdir -p "$RUNNER_TEMP/iso"
|
||||
args=(--output "$RUNNER_TEMP/iso/ubuntu-kiosk-amd64.iso")
|
||||
case "$MODE" in
|
||||
unattended-kiosk) args+=(--unattended) ;;
|
||||
fully-automatic) args+=(--fully-automatic) ;;
|
||||
esac
|
||||
if [[ -n "$ADMIN_USERNAME" ]]; then
|
||||
if [[ -z "$KIOSK_ADMIN_PASSWORD" ]]; then
|
||||
echo "::error::username given but the KIOSK_ADMIN_PASSWORD repository secret isn't set"
|
||||
exit 1
|
||||
fi
|
||||
args+=(--username "$ADMIN_USERNAME" --hostname "$ADMIN_HOSTNAME")
|
||||
fi
|
||||
if [[ -n "$SSH_PUBLIC_KEY" ]]; then
|
||||
printf '%s\n' "$SSH_PUBLIC_KEY" > "$RUNNER_TEMP/ssh_key.pub"
|
||||
args+=(--ssh-key "$RUNNER_TEMP/ssh_key.pub")
|
||||
fi
|
||||
iso/build-iso.sh "${args[@]}"
|
||||
# Name the file after the Ubuntu point release the script picked.
|
||||
src="$(find iso/build/cache -name "*.iso" -print -quit)"
|
||||
name="$(basename "$src" .iso | sed 's/-live-server/-kiosk/')-${MODE}-${GITHUB_SHA::7}.iso"
|
||||
mv "$RUNNER_TEMP/iso/ubuntu-kiosk-amd64.iso" "$RUNNER_TEMP/iso/$name"
|
||||
# Free the ~3 GB source ISO before the upload step needs scratch space.
|
||||
rm -rf iso/build/cache
|
||||
(cd "$RUNNER_TEMP/iso" && sha256sum "$name" > "$name.sha256")
|
||||
echo "ISO_NAME=$name" >> "$GITHUB_ENV"
|
||||
{
|
||||
echo "### $name"
|
||||
echo
|
||||
echo "- Mode: \`$MODE\`"
|
||||
echo "- Admin user: ${ADMIN_USERNAME:-chosen during install}"
|
||||
echo "- Built from: \`${GITHUB_REF_NAME}\` @ \`${GITHUB_SHA::7}\`"
|
||||
echo "- SHA-256: \`$(cut -d' ' -f1 "$RUNNER_TEMP/iso/$name.sha256")\`"
|
||||
echo
|
||||
echo "Download it from **Artifacts** below (GitHub zips it; unzip, then write the .iso to a USB stick)."
|
||||
if [[ "$MODE" == "fully-automatic" ]]; then
|
||||
echo
|
||||
echo "> **Warning:** this ISO wipes the target machine's largest disk without asking."
|
||||
fi
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload ISO
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.ISO_NAME }}
|
||||
path: |
|
||||
${{ runner.temp }}/iso/${{ env.ISO_NAME }}
|
||||
${{ runner.temp }}/iso/${{ env.ISO_NAME }}.sha256
|
||||
# An ISO is already compressed - zipping it again only costs time.
|
||||
compression-level: 0
|
||||
retention-days: ${{ inputs.retention_days }}
|
||||
if-no-files-found: error
|
||||
@@ -1,2 +1,3 @@
|
||||
node_modules/
|
||||
webui/test/fixtures/.fake-state/
|
||||
iso/build/
|
||||
|
||||
@@ -61,6 +61,61 @@ 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. |
|
||||
|
||||
**No Linux machine handy?** Build it on GitHub instead: **Actions → Build
|
||||
kiosk ISO → Run workflow**, pick a mode, and download the ISO from the run's
|
||||
**Artifacts** once it finishes (about 10 minutes; kept for 3 days by default).
|
||||
To pre-set the admin account (required for `fully-automatic`), first add a
|
||||
repository secret named `KIOSK_ADMIN_PASSWORD` (Settings → Secrets and
|
||||
variables → Actions). Passwords are never typed into the workflow form,
|
||||
because its inputs show up in the run log.
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
Executable
+354
@@ -0,0 +1,354 @@
|
||||
#!/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, or read from $KIOSK_ADMIN_PASSWORD -
|
||||
# better than this flag on shared machines/CI, where
|
||||
# command lines are visible). Stored on the ISO 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="${KIOSK_ADMIN_PASSWORD:-}"
|
||||
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=""
|
||||
|
||||
# Usernames the Ubuntu installer rejects (system users/groups) - copied
|
||||
# from subiquity's reserved-usernames so a bad --username fails here
|
||||
# instead of at boot with "Username is reserved by the system".
|
||||
RESERVED_USERNAMES="
|
||||
root daemon bin sys sync games man lp mail news uucp proxy www-data
|
||||
backup list irc gnats nobody adm tty disk kmem dialout fax voice cdrom
|
||||
floppy tape sudo audio dip operator src shadow utmp video sasl plugdev
|
||||
staff users nogroup netplan ftn mysql tac-plus alias qmail qmaild
|
||||
qmails qmailr qmailq qmaill qmailp asterisk vpopmail vchkpw slurm
|
||||
hacluster haclient grsec-tpe grsec-sock-all grsec-sock-clt
|
||||
grsec-sock-srv grsec-proc ceph opensrf libvirt-qemu admin Debian-exim
|
||||
bind crontab cupsys dcc dhcp dictd dnsmasq dovecot fetchmail firebird
|
||||
ftp fuse gdm haldaemon hplilp identd input jwhois klog kvm lpadmin maas
|
||||
messagebus mythtv netdev powerdev radvd render saned sbuild scanner sgx
|
||||
slocate ssh sshd ssl-cert sslwrap statd syslog telnetd tftpd
|
||||
"
|
||||
|
||||
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)"
|
||||
[[ " $(echo $RESERVED_USERNAMES) " != *" $ADMIN_USER "* ]] \
|
||||
|| die "username '$ADMIN_USER' is reserved by Ubuntu (a system user/group) - pick another"
|
||||
if [[ -z "$ADMIN_PASS" ]]; then
|
||||
[[ -t 0 ]] || die "--username needs a password: pass --password or set KIOSK_ADMIN_PASSWORD"
|
||||
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
|
||||
################################################################################
|
||||
|
||||
# Branch the copied repo is on - what Advanced -> Upgrade pulls. CI
|
||||
# checkouts (actions/checkout) are a detached HEAD, so fall back to the
|
||||
# branch CI says it built, then main.
|
||||
BRANCH="$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD)"
|
||||
[[ "$BRANCH" != "HEAD" ]] || BRANCH="${GITHUB_REF_NAME:-main}"
|
||||
COMMIT="$(git -C "$REPO_ROOT" rev-parse HEAD)"
|
||||
|
||||
info "Staging repo ($BRANCH @ ${COMMIT:0:7})..."
|
||||
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"
|
||||
# init + fetch by commit rather than `git clone --branch`, which can't
|
||||
# clone a detached HEAD.
|
||||
stage_repo="$WORK/kiosk/ubuntu-based-kiosk"
|
||||
git init --quiet "$stage_repo"
|
||||
git -C "$stage_repo" fetch --quiet --depth 1 "file://$REPO_ROOT" "$COMMIT"
|
||||
git -C "$stage_repo" checkout --quiet -B "$BRANCH" FETCH_HEAD
|
||||
git -C "$stage_repo" remote add origin "$REPO_URL"
|
||||
git -C "$stage_repo" config "branch.$BRANCH.remote" origin
|
||||
git -C "$stage_repo" config "branch.$BRANCH.merge" "refs/heads/$BRANCH"
|
||||
|
||||
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
|
||||
+10
@@ -118,6 +118,16 @@ ask_yes_no() {
|
||||
local default="${2:-n}"
|
||||
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
|
||||
read -r -p "$prompt (y/n) [$default]: " answer
|
||||
answer="${answer:-$default}"
|
||||
|
||||
+12
-1
@@ -268,7 +268,11 @@ provision_configure_kiosk_settings() {
|
||||
echo "password protection, WiFi, and schedules. Skip and configure"
|
||||
echo "later via ./install.sh if you'd rather do this after reboot."
|
||||
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
|
||||
fi
|
||||
|
||||
@@ -297,6 +301,13 @@ provision_finish() {
|
||||
echo "(CUPS, LMS/Squeezelite, Remote Access, Authelia, Asterisk Intercom),"
|
||||
echo "or Advanced options."
|
||||
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
|
||||
echo "Rebooting in 3 seconds..."
|
||||
sleep 3
|
||||
|
||||
Reference in New Issue
Block a user