The bundle pool is ~884 MB and now travels through Git LFS, so a plain clone materialises all of it. Measured on this repository: 2.7 GB working tree, against 20 MB with smudge disabled -- the same 135x that decides whether one device or seven hundred can be provisioned from a git remote. bf-bootstrap exports GIT_LFS_SKIP_SMUDGE=1 for every git operation it performs (clone and fetch), so provisioning a device no longer implies downloading the package payload. The trade is explicit and documented rather than hidden: with smudge off the pool files on disk are 131-byte pointer texts, and the installer must not mistake one for a package. So the installer gained a guard. A pointer text satisfies every existence check, so without it the run died later inside `sha256sum -c` and reported a checksum mismatch -- a misdiagnosis that sends an operator looking for corruption when the payload was simply never fetched. It now recognises the pointer by content and names the remedy instead. Getting the payload is a deliberate second step, and both documented routes are real: a local mirror (the offline-correct path, and the one the fleet uses) or `git lfs pull --include='provisioning/offline-repo/built/pool/*'`, which fetches only the delivery pool and leaves the intake copy under packages/ unfetched -- verified: 1.8 GB and a real .deb in built/pool, 131-byte stub still in packages/. Tests: test-field-os-lifecycle.sh gains a scenario that lays down a pointer text where a .deb belongs and asserts three things -- the run refuses, the message names the cause, and it does not blame a checksum. Also asserts apt-get is never invoked. 16 scenarios, all passing. docs/27 is corrected where it had gone stale: it claimed a clone carries the packages and the installer downloads nothing, which stopped being true the moment the payload moved to LFS.
399 lines
16 KiB
Bash
Executable file
399 lines
16 KiB
Bash
Executable file
#!/usr/bin/env bash
|
||
#
|
||
# bf-bootstrap.sh — one-command install of the Blueforce Linux admin toolbox.
|
||
#
|
||
# Documented entry points:
|
||
# curl -fsSL https://raw.githubusercontent.com/emirhanmamak-blueforce/Blueforce-Linux/main/admin/bf-bootstrap.sh | sudo bash
|
||
# sudo admin/bf-bootstrap.sh # from a checkout
|
||
#
|
||
# Steps, in order:
|
||
# 1. root check (apply mode only; --check is read-only and needs no root),
|
||
# 2. Ubuntu release check against the supported baseline (24.04 / 26.04),
|
||
# 3. dependency install: git, curl, ansible-core,
|
||
# 4. clone the repository into <dir> (default /opt/blueforce-linux) or
|
||
# fast-forward an existing checkout (never resets, never discards work),
|
||
# 5. link admin/bf-*, scripts/diagnostics/bf-* and scripts/maintenance/bf-*
|
||
# into /usr/local/bin (idempotent; a re-run refreshes the links),
|
||
# 6. make admin/bf-menu executable so `sudo bf-menu` starts the on-device
|
||
# installer menu,
|
||
# 7. print a clear summary.
|
||
#
|
||
# Safety
|
||
# * This script writes no secret and installs code only: credentials stay on
|
||
# the device and are never fetched by the installer (see admin/bf-creds).
|
||
# * Re-running it is safe: the checkout is fast-forwarded and the tool links
|
||
# are re-created. Nothing is deleted and no `git reset` is ever performed,
|
||
# so local work in the checkout is never discarded.
|
||
# * --check reports the same plan without touching the system.
|
||
#
|
||
set -euo pipefail
|
||
|
||
REPO_URL="${BF_REPO_URL:-https://github.com/emirhanmamak-blueforce/Blueforce-Linux.git}"
|
||
BRANCH_DEFAULT="main"
|
||
INSTALL_DIR_DEFAULT="/opt/blueforce-linux"
|
||
BIN_DIR="${BF_BIN_DIR:-/usr/local/bin}"
|
||
SUPPORTED_UBUNTU=("24.04" "26.04")
|
||
# Command name / apt package name pairs for the installer prerequisites.
|
||
DEPENDENCY_COMMANDS=(git curl ansible-playbook)
|
||
DEPENDENCY_PACKAGES=(git curl ansible-core)
|
||
|
||
CHECK_MODE=0
|
||
ASSUME_YES=0
|
||
BRANCH="$BRANCH_DEFAULT"
|
||
DIR=""
|
||
LINKED=0
|
||
MENU_READY=0
|
||
|
||
usage() {
|
||
cat <<'EOF'
|
||
Usage: sudo bf-bootstrap.sh [--check] [--dir DIR] [--branch BRANCH] [--yes] [--help]
|
||
|
||
Install (or refresh) the Blueforce Linux admin toolbox from git: fetch the
|
||
repository, install the prerequisites, link the bf-* tools into
|
||
/usr/local/bin, and make `sudo bf-menu` available for the on-device setup.
|
||
|
||
Options:
|
||
--check Read-only: report what would happen. It never mutates
|
||
anything and never requires root.
|
||
--dir DIR Checkout directory. Default: the checkout this script lives
|
||
in, otherwise /opt/blueforce-linux.
|
||
--branch BRANCH Branch to track (default: main).
|
||
--yes, -y Do not ask for confirmation.
|
||
--help, -h Show this help and exit.
|
||
|
||
Environment:
|
||
BF_REPO_URL Repository URL (default: the upstream GitHub repository).
|
||
BF_INSTALL_DIR Checkout directory; same as --dir.
|
||
BF_BIN_DIR Tool link directory (default: /usr/local/bin).
|
||
|
||
Notes:
|
||
* Installer prerequisites are git, curl and ansible-core (ansible-playbook).
|
||
* Supported baseline is Ubuntu 24.04 / 26.04; any other release only warns.
|
||
* This script installs code only. It never writes, fetches, or prints a
|
||
secret; device credentials are handled by admin/bf-creds.
|
||
EOF
|
||
}
|
||
|
||
say() { printf 'bf-bootstrap: %s\n' "$*"; }
|
||
warn() { printf 'bf-bootstrap: WARN: %s\n' "$*" >&2; }
|
||
die() { printf 'bf-bootstrap: ERROR: %s\n' "$*" >&2; exit 1; }
|
||
|
||
# Git-LFS smudge is disabled for every git operation this script performs.
|
||
#
|
||
# Why: the offline package pool is ~884 MB of .deb payload tracked through Git
|
||
# LFS, and a default clone materialises all of it (measured: 2.7 GB working tree
|
||
# against 20 MB with smudge off, a 135x difference). A device that only needs to
|
||
# reach the console and the manifest pays that cost on every clone for bytes it
|
||
# may never install from.
|
||
#
|
||
# The trade is explicit: with smudge off the pool files on disk are 131-byte
|
||
# pointer texts, and the installer refuses such a bundle rather than installing
|
||
# from it ("Nokta dosyası" below). Fetching the payload is therefore a separate,
|
||
# deliberate step -- `git lfs pull --include='provisioning/offline-repo/built/pool/*'`
|
||
# -- so that "I cloned the repository" and "I have the packages" stay two facts.
|
||
#
|
||
# This is deliberately the smudge-off half only. Enabling a full `git lfs pull`
|
||
# here would reintroduce the 884 MB download this prevents.
|
||
export GIT_LFS_SKIP_SMUDGE=1
|
||
|
||
run() {
|
||
if [[ "$CHECK_MODE" -eq 1 ]]; then
|
||
printf 'bf-bootstrap: check: would run: %s\n' "$*"
|
||
return 0
|
||
fi
|
||
"$@"
|
||
}
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--check) CHECK_MODE=1; shift ;;
|
||
--dir) DIR="${2:?missing value for --dir}"; shift 2 ;;
|
||
--branch) BRANCH="${2:?missing value for --branch}"; shift 2 ;;
|
||
--yes|-y) ASSUME_YES=1; shift ;;
|
||
--help|-h) usage; exit 0 ;;
|
||
*) printf 'bf-bootstrap: ERROR: unknown argument: %s\n\n' "$1" >&2; usage >&2; exit 2 ;;
|
||
esac
|
||
done
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Where does this script live? When it runs from a checkout, that checkout is
|
||
# the default target; the piped one-command form has no checkout, so the
|
||
# default install directory is used.
|
||
# --------------------------------------------------------------------------- #
|
||
SCRIPT_SOURCE="${BASH_SOURCE[0]:-}"
|
||
if [[ -n "$SCRIPT_SOURCE" && -r "$SCRIPT_SOURCE" ]]; then
|
||
SCRIPT_REAL="$(readlink -f "$SCRIPT_SOURCE" 2>/dev/null || printf '%s' "$SCRIPT_SOURCE")"
|
||
CANDIDATE="$(cd "$(dirname "$SCRIPT_REAL")/.." 2>/dev/null && pwd || true)"
|
||
if [[ -n "$CANDIDATE" && -f "$CANDIDATE/admin/bf-bootstrap.sh" && -d "$CANDIDATE/scripts/diagnostics" ]]; then
|
||
CHECKOUT_ROOT="$CANDIDATE"
|
||
fi
|
||
fi
|
||
if [[ -z "$DIR" ]]; then
|
||
DIR="${BF_INSTALL_DIR:-${CHECKOUT_ROOT:-$INSTALL_DIR_DEFAULT}}"
|
||
fi
|
||
|
||
if [[ -z "$BRANCH" ]]; then
|
||
die 'branch must not be empty'
|
||
fi
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# 1. Root check. --check only reports, so it works as an unprivileged user.
|
||
# --------------------------------------------------------------------------- #
|
||
if [[ "$CHECK_MODE" -eq 1 ]]; then
|
||
if [[ "$(id -u)" -ne 0 ]]; then
|
||
say 'check: not running as root; apply mode will require sudo'
|
||
fi
|
||
else
|
||
if [[ "$(id -u)" -ne 0 ]]; then
|
||
die "must run as root: sudo $0 (or: sudo bf-bootstrap.sh)"
|
||
fi
|
||
fi
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# 2. Ubuntu release check. A different release is only a warning: the goal is
|
||
# to install the toolbox, not to block a working host.
|
||
# --------------------------------------------------------------------------- #
|
||
DISTRO_ID=""
|
||
DISTRO_VERSION=""
|
||
DISTRO_PRETTY="unknown"
|
||
if [[ -r /etc/os-release ]]; then
|
||
# shellcheck disable=SC1091
|
||
. /etc/os-release
|
||
DISTRO_ID="${ID:-}"
|
||
DISTRO_VERSION="${VERSION_ID:-}"
|
||
DISTRO_PRETTY="${PRETTY_NAME:-unknown}"
|
||
fi
|
||
if [[ "$DISTRO_ID" == "ubuntu" ]]; then
|
||
SUPPORTED=0
|
||
for release in "${SUPPORTED_UBUNTU[@]}"; do
|
||
if [[ "$DISTRO_VERSION" == "$release" ]]; then
|
||
SUPPORTED=1
|
||
fi
|
||
done
|
||
if [[ "$SUPPORTED" -eq 1 ]]; then
|
||
say "ubuntu ${DISTRO_VERSION} is a supported baseline"
|
||
else
|
||
warn "Ubuntu ${DISTRO_VERSION:-unknown} is outside the tested baseline (${SUPPORTED_UBUNTU[*]}); continuing"
|
||
fi
|
||
else
|
||
warn "this is not an Ubuntu host (ID=${DISTRO_ID:-unknown}, ${DISTRO_PRETTY}); the Field OS baseline is Ubuntu ${SUPPORTED_UBUNTU[*]}"
|
||
fi
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Operator confirmation. Only a terminal prompts: the documented one-command
|
||
# form (`curl ... | sudo bash`) has no terminal on stdin and never blocks.
|
||
# --------------------------------------------------------------------------- #
|
||
if [[ "$CHECK_MODE" -eq 0 && "$ASSUME_YES" -eq 0 && -t 0 ]]; then
|
||
printf 'Install or update the Blueforce toolbox in %s? [y/N] ' "$DIR"
|
||
ANSWER=""
|
||
read -r ANSWER || ANSWER=""
|
||
if [[ ! "$ANSWER" =~ ^[Yy]([Ee][Ss])?$ ]]; then
|
||
say 'aborted by the operator'
|
||
exit 2
|
||
fi
|
||
fi
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# 3. Dependencies.
|
||
# --------------------------------------------------------------------------- #
|
||
MISSING_COMMANDS=()
|
||
MISSING_PACKAGES=()
|
||
say 'dependency check'
|
||
for index in "${!DEPENDENCY_COMMANDS[@]}"; do
|
||
if ! command -v "${DEPENDENCY_COMMANDS[$index]}" >/dev/null 2>&1; then
|
||
MISSING_COMMANDS+=("${DEPENDENCY_COMMANDS[$index]}")
|
||
MISSING_PACKAGES+=("${DEPENDENCY_PACKAGES[$index]}")
|
||
fi
|
||
done
|
||
if [[ ${#MISSING_PACKAGES[@]} -eq 0 ]]; then
|
||
say "dependencies present: ${DEPENDENCY_COMMANDS[*]}"
|
||
elif [[ "$CHECK_MODE" -eq 1 ]]; then
|
||
say "check: would install with apt-get: ${MISSING_PACKAGES[*]} (missing: ${MISSING_COMMANDS[*]})"
|
||
else
|
||
if ! command -v apt-get >/dev/null 2>&1; then
|
||
die "apt-get not found; install these packages first: ${MISSING_PACKAGES[*]}"
|
||
fi
|
||
say "installing dependencies: ${MISSING_PACKAGES[*]}"
|
||
export DEBIAN_FRONTEND=noninteractive
|
||
run apt-get update
|
||
run apt-get install -y --no-install-recommends "${MISSING_PACKAGES[@]}"
|
||
fi
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# 4. Repository: clone once, then fast-forward only.
|
||
# --------------------------------------------------------------------------- #
|
||
update_checkout() {
|
||
local remote="" origin_url="" dirty=""
|
||
if ! git -C "$DIR" rev-parse --verify --quiet HEAD >/dev/null 2>&1; then
|
||
warn "checkout has no commits yet: $DIR; skipping the update"
|
||
return 0
|
||
fi
|
||
# Prefer `origin`, otherwise accept the checkout's single remote (a local
|
||
# clone of this project may name it `github`, as the upstream checkout does).
|
||
if git -C "$DIR" remote get-url origin >/dev/null 2>&1; then
|
||
remote="origin"
|
||
else
|
||
remote="$(git -C "$DIR" remote 2>/dev/null | head -n 1 || true)"
|
||
fi
|
||
if [[ -z "$remote" ]]; then
|
||
warn "checkout has no git remote: $DIR; skipping the update"
|
||
return 0
|
||
fi
|
||
origin_url="$(git -C "$DIR" remote get-url "$remote" 2>/dev/null || true)"
|
||
if [[ "$origin_url" != *"Blueforce-Linux"* ]]; then
|
||
warn "remote '$remote' looks unexpected: $origin_url"
|
||
fi
|
||
dirty="$(git -C "$DIR" status --porcelain 2>/dev/null || true)"
|
||
if [[ -n "$dirty" ]]; then
|
||
warn "local changes present in $DIR; skipping the update (nothing was reset or stashed)"
|
||
return 0
|
||
fi
|
||
if [[ "$CHECK_MODE" -eq 1 ]]; then
|
||
say "check: would fast-forward $DIR to $remote/$BRANCH"
|
||
return 0
|
||
fi
|
||
say "updating $DIR (fast-forward only, remote $remote)"
|
||
if ! git -C "$DIR" fetch --prune --quiet "$remote" "$BRANCH"; then
|
||
warn "fetch failed (offline?); keeping the current checkout"
|
||
return 0
|
||
fi
|
||
if ! git -C "$DIR" merge --ff-only --quiet "$remote/$BRANCH"; then
|
||
warn "cannot fast-forward to $remote/$BRANCH; keeping the current checkout (no reset performed)"
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
if [[ -e "$DIR" && ! -d "$DIR" ]]; then
|
||
die "install directory exists and is not a directory: $DIR"
|
||
fi
|
||
if [[ -d "$DIR" ]]; then
|
||
if [[ -d "$DIR/.git" ]]; then
|
||
say "existing checkout: $DIR"
|
||
update_checkout
|
||
else
|
||
die "directory exists but is not a git checkout; refusing to touch it: $DIR"
|
||
fi
|
||
elif [[ "$CHECK_MODE" -eq 1 ]]; then
|
||
say "check: would clone $REPO_URL (branch $BRANCH) into $DIR"
|
||
else
|
||
say "cloning $REPO_URL (branch $BRANCH) into $DIR"
|
||
run install -d -m 0755 "$(dirname "$DIR")"
|
||
run git clone --branch "$BRANCH" --single-branch -- "$REPO_URL" "$DIR"
|
||
fi
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# 5. Link the tools into /usr/local/bin. Idempotent: `ln -sfn` refreshes an
|
||
# existing link, and a re-run after `git pull` needs no extra step.
|
||
# --------------------------------------------------------------------------- #
|
||
link_tools() {
|
||
local source="" base=""
|
||
for source in "$DIR"/admin/bf-* "$DIR"/scripts/diagnostics/bf-* "$DIR"/scripts/maintenance/bf-*; do
|
||
if [[ ! -f "$source" ]]; then
|
||
continue
|
||
fi
|
||
base="$(basename "$source")"
|
||
if [[ "$CHECK_MODE" -eq 1 ]]; then
|
||
say "check: would link $source -> $BIN_DIR/$base"
|
||
else
|
||
chmod 0755 "$source"
|
||
ln -sfn "$source" "$BIN_DIR/$base"
|
||
fi
|
||
LINKED=$((LINKED + 1))
|
||
done
|
||
}
|
||
|
||
if [[ -d "$DIR" ]]; then
|
||
run install -d -m 0755 "$BIN_DIR"
|
||
link_tools
|
||
if [[ "$LINKED" -eq 0 ]]; then
|
||
warn "no bf-* tools found under $DIR (admin/, scripts/diagnostics/, scripts/maintenance/)"
|
||
elif [[ "$CHECK_MODE" -eq 0 ]]; then
|
||
say "linked $LINKED tools into $BIN_DIR"
|
||
fi
|
||
elif [[ "$CHECK_MODE" -eq 1 ]]; then
|
||
say "check: would link the bf-* tools from $DIR into $BIN_DIR"
|
||
fi
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# 6. Operator console entry point.
|
||
#
|
||
# The Python console (`admin/bf` -> bfos) is the primary path; the bash
|
||
# `bf-menu` stays linked as a fallback so a host without python3 can still be
|
||
# operated.
|
||
# --------------------------------------------------------------------------- #
|
||
PY_CONSOLE="$DIR/admin/bf"
|
||
MENU_SOURCE="$DIR/admin/bf-menu"
|
||
MENU_READY=0
|
||
|
||
if [[ -f "$PY_CONSOLE" ]]; then
|
||
if [[ "$CHECK_MODE" -eq 1 ]]; then
|
||
say "check: would make $PY_CONSOLE executable and link $BIN_DIR/bf"
|
||
else
|
||
chmod 0755 "$PY_CONSOLE"
|
||
ln -sfn "$PY_CONSOLE" "$BIN_DIR/bf"
|
||
say "bf ready: $BIN_DIR/bf (Python console)"
|
||
fi
|
||
MENU_READY=1
|
||
else
|
||
warn "admin/bf is not present in $DIR; the Python console arrives with the next revision of the checkout"
|
||
fi
|
||
|
||
if [[ -f "$MENU_SOURCE" ]]; then
|
||
if [[ "$CHECK_MODE" -eq 1 ]]; then
|
||
say "check: would make $MENU_SOURCE executable and link $BIN_DIR/bf-menu"
|
||
else
|
||
chmod 0755 "$MENU_SOURCE"
|
||
ln -sfn "$MENU_SOURCE" "$BIN_DIR/bf-menu"
|
||
say "bf-menu ready: $BIN_DIR/bf-menu (bash fallback)"
|
||
fi
|
||
MENU_READY=1
|
||
else
|
||
warn "admin/bf-menu is not present in $DIR; the bash fallback will work once it is in the checkout"
|
||
fi
|
||
|
||
if [[ "$MENU_READY" -eq 0 ]]; then
|
||
warn "no operator console was found in $DIR; link one of admin/bf or admin/bf-menu manually"
|
||
fi
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# 7. Summary.
|
||
# --------------------------------------------------------------------------- #
|
||
if [[ "$CHECK_MODE" -eq 1 ]]; then
|
||
printf '\nbf-bootstrap: check complete -- no changes were made.\n'
|
||
printf ' checkout : %s\n' "$DIR"
|
||
printf ' branch : %s\n' "$BRANCH"
|
||
printf ' tools : %s tool(s) would be linked into %s\n' "$LINKED" "$BIN_DIR"
|
||
printf ' next : sudo bf-bootstrap.sh --yes\n'
|
||
exit 0
|
||
fi
|
||
|
||
REVISION=""
|
||
if [[ -d "$DIR/.git" ]]; then
|
||
REVISION="$(git -C "$DIR" rev-parse --short HEAD 2>/dev/null || true)"
|
||
fi
|
||
printf '\nbf-bootstrap: summary\n'
|
||
printf ' repository : %s\n' "$DIR"
|
||
printf ' branch : %s\n' "$BRANCH"
|
||
if [[ -n "$REVISION" ]]; then
|
||
printf ' revision : %s\n' "$REVISION"
|
||
fi
|
||
printf ' tools : %s linked into %s\n' "$LINKED" "$BIN_DIR"
|
||
if [[ -x "$BIN_DIR/bf" ]]; then
|
||
printf ' console : %s (Python, primary)\n' "$BIN_DIR/bf"
|
||
else
|
||
printf ' console : pending (admin/bf is not in the checkout yet)\n'
|
||
fi
|
||
if [[ -x "$BIN_DIR/bf-menu" ]]; then
|
||
printf ' fallback : %s (bash)\n' "$BIN_DIR/bf-menu"
|
||
else
|
||
printf ' fallback : pending (admin/bf-menu is not in the checkout yet)\n'
|
||
fi
|
||
# The Python console is the recommended entry point; the bash menu stays as the
|
||
# fallback for a host without python3.
|
||
if [[ -x "$BIN_DIR/bf" ]]; then
|
||
printf ' next : sudo bf\n'
|
||
else
|
||
printf ' next : sudo bf-menu\n'
|
||
fi
|
||
printf '\nInstallation complete. To get started: sudo bf\n'
|
||
printf 'Kurulum tamam. Başlamak için: sudo bf\n'
|