install.sh
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env bash
# firecrab host installer (public-docs/getting-started/installation.md).
#
# Downloads a host bundle from a GitHub Release (or installs local binaries
# via --bin-dir), then lays out systemd units so the dashboard is served on
# http://127.0.0.1:5523/.
#
# Bundles are Linux x86_64/aarch64 × gnu (glibc) / musl. glibc hosts get the
# gnu bundle; musl hosts (Alpine) get musl. Override with --libc.
#
# Every step checks before it changes anything, so re-running is safe and
# doubles as a repair for a half-broken install.
set -euo pipefail
FIRECRAB_USER=${FIRECRAB_USER:-firecrab}
FIRECRAB_GROUP=${FIRECRAB_GROUP:-firecrab}
PREFIX=${PREFIX:-/usr/local}
LIBDIR="$PREFIX/lib/firecrab"
SHAREDIR="$PREFIX/share/firecrab"
DATADIR=${DATADIR:-/var/lib/firecrab}
CONFDIR=${CONFDIR:-/etc/firecrab}
UNITDIR=${UNITDIR:-/etc/systemd/system}
# stdin (`curl | bash`) leaves BASH_SOURCE unset under `set -u`.
# `bash <(curl …)` sets it to a /dev/fd path that is not the checkout.
_src=${BASH_SOURCE[0]:-${0:-}}
case "$_src" in
""|-|bash|sh|main|/dev/fd/*|/proc/self/fd/*)
REPO_ROOT=$(pwd)
;;
*)
REPO_ROOT=$(cd -- "$(dirname -- "$_src")" && pwd)
;;
esac
unset _src
UNITS=(firecrab-helper.service firecrab-api.service)
# Placeholder @FIRECRAB_RELEASE_TAG@ is replaced by the tag on a published
# install.sh (v0.1.0). The repo copy keeps the placeholder and means "latest".
FIRECRAB_RELEASE_TAG="@FIRECRAB_RELEASE_TAG@"
MODE=install
INSTALL_DEPS=1
WITH_FRONTEND=1
WITH_PIPE=0
PURGE=0
# Set by ensure_account when it grants the /dev/kvm ACL, so --uninstall can take it back.
KVM_ACL_ADDED=0
BIN_DIR=
DASHBOARD_DIR=
RELEASE_VERSION=
LIBC=
PAYLOAD_TMP=
PAYLOAD_ROOT=
PAYLOAD_BIN=
PAYLOAD_UNITS=
PAYLOAD_EXTRACT=
PAYLOAD_DASHBOARD=
PAYLOAD_LICENSE=
PAYLOAD_THIRD_PARTY=
PAYLOAD_INVENTORY=
PAYLOAD_GPL=
# BEGIN RELEASE_HELPERS
if [ -f "$REPO_ROOT/scripts/firecrab-release.sh" ]; then
# Optional checkout helper; the published installer inlines this block.
# shellcheck disable=SC1091
. "$REPO_ROOT/scripts/firecrab-release.sh"
fi
if ! type firecrab_host_arch >/dev/null 2>&1; then
printf 'xx release helpers missing — run from a firecrab checkout or use the install.sh from a GitHub Release\n' >&2
exit 1
fi
# END RELEASE_HELPERS
PKG='' # detected package manager
PKG_UPDATED=0 # index refreshed at most once
# Help text, also printed before failing on an unknown option.
usage() {
cat <<'USAGE'
Usage: ./install.sh [OPTIONS]
(no options) download the latest release binaries and start firecrab
--check report what is missing and what would be installed
--doctor run host diagnostics (UFW, socket, KVM, nft, …); no root required
--deps-only install the dependencies, then stop (no host changes)
--no-deps never install packages, only report gaps
--no-frontend do not install the dashboard
--with-pipe also install the verified cloudflared connector (ingress is bundled)
--version VER GitHub Release tag (default: latest, or the baked-in tag)
--libc gnu|musl host libc (default: detect; gnu = glibc)
--bin-dir DIR install these local binaries instead of the release
--dashboard-dir DIR install this built dashboard (with --bin-dir)
--print-install-url print the curl install command and exit
--print-release-url print the host-bundle URL and exit
--uninstall stop and remove units, binaries and dashboard
--purge with --uninstall, also delete /var/lib/firecrab (VM data!)
-h, --help this text
curl -fsSL https://github.com/SteelCrab/firecrab/releases/latest/download/install.sh | bash
Host bundles (install.sh picks arch + libc):
firecrab-host-x86_64-gnu.tar.gz Linux x86_64, glibc
firecrab-host-x86_64-musl.tar.gz Linux x86_64, musl (static)
firecrab-host-aarch64-gnu.tar.gz Linux aarch64, glibc
firecrab-host-aarch64-musl.tar.gz Linux aarch64, musl (static)
Environment: FIRECRAB_USER, FIRECRAB_GROUP, PREFIX, DATADIR, CONFDIR, UNITDIR
FIRECRAB_RELEASE_REPO, FIRECRAB_DEFAULT_VERSION
USAGE
}
log() { printf '\033[1;32m==>\033[0m %s\n' "$*"; }
step() { printf '\033[1;36m--\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m!!\033[0m %s\n' "$*" >&2; }
die() { printf '\033[1;31mxx\033[0m %s\n' "$*" >&2; exit 1; }
have() { command -v "$1" >/dev/null 2>&1; }
# True when SELinux is loaded, enforcing or not. Permissive counts: the labels
# still have to be right, or turning enforcement back on breaks the install.
selinux_active() {
have getenforce || return 1
case "$(getenforce 2>/dev/null || printf Disabled)" in
Enforcing|Permissive) return 0 ;;
*) return 1 ;;
esac
}
# Privilege escalation: empty when already root, "sudo" otherwise.
# Individual commands that need root use $SUDO; the whole script runs as the
# invoking user so that $HOME, SSH keys, and cargo are the user's, not root's.
if [ "$(id -u)" -eq 0 ]; then
SUDO=""
else
have sudo || die "sudo is required to run privileged steps; install it first"
SUDO="sudo"
fi
# Extra args after --doctor are forwarded to `firecrab doctor`.
DOCTOR_ARGS=()
while [ $# -gt 0 ]; do
case "$1" in
--check) MODE=check ;;
--doctor)
MODE=doctor
shift
# Everything after --doctor belongs to the doctor script.
DOCTOR_ARGS=("$@")
break
;;
--deps-only) MODE=deps ;;
--no-deps) INSTALL_DEPS=0 ;;
--no-frontend) WITH_FRONTEND=0 ;;
--with-pipe) WITH_PIPE=1 ;;
--version)
[ $# -ge 2 ] || die "--version needs a tag (for example v0.1.0)"
RELEASE_VERSION=$2
shift
;;
--libc)
[ $# -ge 2 ] || die "--libc needs gnu or musl"
LIBC=$2
firecrab_normalize_libc "$LIBC" >/dev/null \
|| die "--libc must be gnu, glibc, or musl"
LIBC=$(firecrab_normalize_libc "$LIBC")
shift
;;
--bin-dir)
[ $# -ge 2 ] || die "--bin-dir needs a directory"
BIN_DIR=$2
shift
;;
--dashboard-dir)
[ $# -ge 2 ] || die "--dashboard-dir needs a directory"
DASHBOARD_DIR=$2
shift
;;
--print-install-url) MODE=print-install-url ;;
--print-release-url) MODE=print-release-url ;;
--uninstall) MODE=uninstall ;;
--purge) PURGE=1 ;;
-h|--help) usage; exit 0 ;;
*) usage >&2; die "unknown option: $1" ;;
esac
shift
done
if [ -z "$RELEASE_VERSION" ]; then
RELEASE_VERSION=${FIRECRAB_DEFAULT_VERSION:-}
fi
# A published installer substitutes the placeholder with the tag. The repo
# copy keeps @FIRECRAB_RELEASE_TAG@, which means "latest".
case "$FIRECRAB_RELEASE_TAG" in
""|@*) ;;
*)
if [ -z "$RELEASE_VERSION" ]; then
RELEASE_VERSION=$FIRECRAB_RELEASE_TAG
fi
;;
esac
if [ -z "$LIBC" ]; then
LIBC=$(firecrab_host_libc)
else
LIBC=$(firecrab_normalize_libc "$LIBC") || die "--libc must be gnu, glibc, or musl"
fi
cleanup_payload() {
if [ -n "${PAYLOAD_TMP:-}" ] && [ -d "$PAYLOAD_TMP" ]; then
rm -rf "$PAYLOAD_TMP"
fi
}
trap cleanup_payload EXIT
if [ "$WITH_PIPE" = 1 ] && [ "$MODE" != install ] && [ "$MODE" != check ]; then
die "--with-pipe is supported with installation or --check"
fi
# --- package manager -------------------------------------------------------
# Sets PKG to the first package manager found; returns 1 if none is.
detect_pkg() {
for candidate in apt-get dnf zypper pacman apk; do
if have "$candidate"; then PKG=$candidate; return 0; fi
done
return 1
}
# Package name per manager, since only a few of these agree.
pkg_name() {
local generic=$1
case "$PKG:$generic" in
# Fedora/RHEL call it iproute; everyone else iproute2.
dnf:iproute2) echo "iproute" ;;
*:iproute2) echo "iproute2" ;;
*:nftables) echo "nftables" ;;
apt-get:conntrack) echo "conntrack" ;;
dnf:conntrack) echo "conntrack-tools" ;;
zypper:conntrack) echo "conntrack-tools" ;;
*:conntrack) echo "conntrack-tools" ;;
*:dnsmasq) echo "dnsmasq" ;;
# dhcp_release is shipped separately on Debian/Ubuntu and Alpine.
# On RHEL/Fedora it does not exist as a separate package.
apt-get:dnsmasq-utils) echo "dnsmasq-utils" ;;
apk:dnsmasq-utils) echo "dnsmasq-utils" ;;
dnf:dnsmasq-utils) echo "dnsmasq" ;;
*:dnsmasq-utils) echo "dnsmasq-utils" ;;
apk:e2fsprogs) echo "e2fsprogs-extra" ;;
*:e2fsprogs) echo "e2fsprogs" ;;
*:curl) echo "curl" ;;
apt-get:openssh-client) echo "openssh-client" ;;
dnf:openssh-client) echo "openssh-clients" ;;
apk:openssh-client) echo "openssh-client-default" ;;
*:openssh-client) echo "openssh" ;;
# scripts/install-firecracker.sh needs find and tar; a minimal
# openSUSE or Debian install has neither.
*:findutils) echo "findutils" ;;
*:tar) echo "tar" ;;
apt-get:xz) echo "xz-utils" ;;
apt-get:kernel-compiler) echo "build-essential" ;;
dnf:kernel-compiler) echo "gcc make" ;;
zypper:kernel-compiler) echo "gcc make" ;;
pacman:kernel-compiler) echo "base-devel" ;;
apk:kernel-compiler) echo "build-base" ;;
apt-get:kernel-headers) echo "libelf-dev libssl-dev" ;;
dnf:kernel-headers) echo "elfutils-libelf-devel openssl-devel" ;;
zypper:kernel-headers) echo "libelf-devel libopenssl-devel" ;;
pacman:kernel-headers) echo "libelf openssl" ;;
apk:kernel-headers) echo "elfutils-dev openssl-dev" ;;
*:xz) echo "xz" ;;
# Native logical backups use client tools, never database servers.
apt-get:mysql-client) echo "mariadb-client" ;;
dnf:postgresql-client) echo "postgresql" ;;
dnf:mysql-client) echo "mariadb" ;;
pacman:postgresql-client) echo "postgresql-libs" ;;
pacman:mysql-client) echo "mariadb-clients" ;;
zypper:postgresql-client) echo "postgresql" ;;
zypper:mysql-client) echo "mariadb-client" ;;
# Dashboard M2Image install decompresses `{alias}.tar.zst` packages.
*:zstd) echo "zstd" ;;
# semanage lives in a python tooling package on the SELinux distros.
dnf:selinux-tools) echo "policycoreutils-python-utils" ;;
apt-get:selinux-tools) echo "policycoreutils-python-utils" ;;
zypper:selinux-tools) echo "policycoreutils-python-utils" ;;
*:selinux-tools) echo "policycoreutils" ;;
*) echo "$generic" ;;
esac
}
# Refreshes the package index at most once per run.
pkg_refresh() {
[ "$PKG_UPDATED" -eq 1 ] && return 0
case "$PKG" in
apt-get) DEBIAN_FRONTEND=noninteractive $SUDO apt-get update -qq ;;
apk) $SUDO apk update -q ;;
*) : ;; # dnf/zypper/pacman refresh as part of install
esac
PKG_UPDATED=1
}
# Installs the named packages with whichever manager was detected.
# One retry: Fedora metalink/mirror writes fail transiently.
pkg_install() {
pkg_refresh
local attempt
for attempt in 1 2; do
if pkg_install_once "$@"; then
return 0
fi
[ "$attempt" -eq 1 ] || return 1
warn "package install failed; retrying once"
sleep 2
done
}
pkg_install_once() {
case "$PKG" in
apt-get) DEBIAN_FRONTEND=noninteractive $SUDO apt-get install -y -qq "$@" ;;
dnf) $SUDO dnf install -y -q "$@" ;;
zypper) $SUDO zypper --non-interactive install -y "$@" ;;
pacman) $SUDO pacman -Sy --noconfirm --needed "$@" ;;
apk) $SUDO apk add --no-cache "$@" ;;
*) return 1 ;;
esac
}
# `ensure <command> <generic package>`: install only what is actually absent.
ensure() {
local command_name=$1 generic=$2 packages
have "$command_name" && return 0
# One generic name can map to two packages (nodejs + npm), so the words are
# split here, deliberately, into an array rather than left to the shell.
read -ra packages <<<"$(pkg_name "$generic")"
if [ "$MODE" = check ]; then
warn "missing: $command_name (would install: ${packages[*]})"
return 1
fi
[ "$INSTALL_DEPS" -eq 1 ] || { warn "missing: $command_name (--no-deps, install '${packages[*]}' yourself)"; return 1; }
[ -n "$PKG" ] || { warn "missing: $command_name and no known package manager — install '${packages[*]}' yourself"; return 1; }
step "installing ${packages[*]} (for $command_name)"
pkg_install "${packages[@]}" || { warn "failed to install ${packages[*]}"; return 1; }
have "$command_name"
}
# --- host checks -----------------------------------------------------------
# A VM cannot start without /dev/kvm, and this script cannot supply it.
check_kvm() {
if [ -e /dev/kvm ]; then
log "/dev/kvm present"
return 0
fi
# Not installable: it is the CPU, the BIOS, or a VM without nested virt.
warn "/dev/kvm missing — enable virtualization in BIOS, or nested virt if this host is itself a VM"
return 1
}
# The units need a running systemd, not merely the binary.
check_systemd() {
if [ -d /run/systemd/system ]; then
log "systemd is running"
return 0
fi
warn "not a systemd host — the daemons can still be started by hand, but units will not work"
return 1
}
# UFW is still reported here; the helper punches DHCP/DNS on each bridge
# for UFW, firewalld, iptables, and nftables.
report_ufw() {
have ufw || return 0
local status
if status=$(ufw status 2>/dev/null); then
case "$status" in
*"Status: active"*)
log "UFW is active — net-helper opens DHCP/DNS on each MicroNetwork bridge" ;;
*) log "UFW installed but inactive" ;;
esac
else
warn "UFW installed; re-run as root to see whether it is active"
fi
}
# --- dependency resolution -------------------------------------------------
# The commands the two daemons shell out to while running, plus the handful the
# installer's own helper scripts need.
ensure_runtime_deps() {
local failed=0
ensure ip iproute2 || failed=1
ensure ss iproute2 || failed=1
ensure nft nftables || failed=1
ensure conntrack conntrack || warn "conntrack is required when using security groups"
ensure dnsmasq dnsmasq || failed=1
# dhcp_release releases DHCP leases on VM stop; VMs can still run without
# it but leases accumulate until they expire (default 1 h). Fedora/RHEL
# ship dnsmasq without that binary — do not reinstall dnsmasq for it.
if ! have dhcp_release; then
if [ "$PKG" = dnf ]; then
warn "dhcp_release is not available on this distribution; leases expire on their own"
else
ensure dhcp_release dnsmasq-utils \
|| warn "dhcp_release not found; install dnsmasq-utils to release leases on VM stop"
fi
fi
# semanage records the bin_t file context for the installed binaries. Without
# it the services stay in SELinux's init_t domain, where they cannot reach a
# registry or exec nft — an install that looks successful and works for
# nothing. Fedora does not ship it in a minimal install.
if selinux_active && ! have semanage; then
ensure semanage selinux-tools \
|| warn "SELinux is on but semanage is missing; the services will be confined to init_t"
fi
ensure mkfs.ext4 e2fsprogs || failed=1
ensure fakeroot fakeroot || failed=1
ensure curl curl || failed=1
ensure git git || failed=1
ensure ssh openssh-client || failed=1
ensure find findutils || failed=1
ensure tar tar || failed=1
ensure zstd zstd || failed=1
ensure sha256sum coreutils || failed=1
# Kernel builds are optional, but install their prerequisites by default.
ensure_kernel_build_deps || warn "kernel builds are unavailable until their dependencies are installed"
# Missing backup clients do not prevent VM or read-only DB operation.
# Install them by default; --no-deps and --check never change the host.
ensure pg_dump postgresql-client || warn "pg_dump unavailable; PostgreSQL backups need a compatible client"
ensure pg_restore postgresql-client || warn "pg_restore unavailable; PostgreSQL restore is unavailable"
ensure psql postgresql-client || warn "psql unavailable; PostgreSQL restore is unavailable"
ensure mysqldump mysql-client || warn "mysqldump unavailable; MySQL backups are unavailable"
ensure mysql mysql-client || warn "mysql unavailable; MySQL restore is unavailable"
return $failed
}
# The dashboard builds kernels from verified upstream sources on this host.
ensure_kernel_build_deps() {
local failed=0 packages
ensure make kernel-compiler || failed=1
ensure cc kernel-compiler || failed=1
ensure flex flex || failed=1
ensure bison bison || failed=1
ensure bc bc || failed=1
ensure perl perl || failed=1
ensure xz xz || failed=1
if ! printf '#include <libelf.h>\n#include <gelf.h>\n#include <openssl/opensslv.h>\n' | cc -x c -fsyntax-only - >/dev/null 2>&1; then
read -ra packages <<<"$(pkg_name kernel-headers)"
if [ "$MODE" = check ] || [ "$INSTALL_DEPS" -ne 1 ] || [ -z "$PKG" ]; then
warn "missing kernel build headers (install: ${packages[*]})"
failed=1
else
step "installing ${packages[*]} (kernel build headers)"
pkg_install "${packages[@]}" || failed=1
printf '#include <libelf.h>\n#include <gelf.h>\n#include <openssl/opensslv.h>\n' | cc -x c -fsyntax-only - >/dev/null 2>&1 || failed=1
fi
fi
return "$failed"
}
# Comes from its own upstream script rather than a distro package.
# A piped release installer has no checkout, so fetch the same script.
ensure_firecracker() {
have firecracker && { log "firecracker present"; return 0; }
if [ "$MODE" = check ]; then
warn "missing: firecracker (would install the upstream Firecracker binary)"
return 1
fi
[ "$INSTALL_DEPS" -eq 1 ] || { warn "missing: firecracker (--no-deps; install it or re-run without --no-deps)"; return 1; }
step "installing firecracker"
local script fetched=
script=$REPO_ROOT/scripts/install-firecracker.sh
if [ ! -f "$script" ]; then
fetched=$(mktemp)
script=$fetched
if ! curl --proto '=https' --tlsv1.2 -fsSL \
"$(firecrab_repo_raw_url "${RELEASE_VERSION:-latest}" scripts/install-firecracker.sh)" \
-o "$fetched"; then
rm -f "$fetched"
warn "firecracker install failed (could not download the installer)"
return 1
fi
fi
if $SUDO env FIRECRACKER_NOTICE_DIR="$SHAREDIR/firecracker" bash "$script"; then
rm -f "$fetched"
have firecracker && return 0
fi
rm -f "$fetched"
warn "firecracker install failed"
return 1
}
# Whether this script is running from a firecrab git checkout.
is_checkout() {
[ -f "$REPO_ROOT/Cargo.toml" ] && [ -f "$REPO_ROOT/packaging/systemd/firecrab-api.service" ]
}
# Downloads the host bundle for this arch + libc.
download_host_bundle() {
local arch tarball url sums_url
arch=$(firecrab_host_arch) || die "unsupported architecture: $(uname -m) (need x86_64 or aarch64)"
tarball=$(firecrab_host_tarball "$arch" "$LIBC")
PAYLOAD_TMP=$(mktemp -d)
url=$(firecrab_release_asset_url "$RELEASE_VERSION" "$tarball")
sums_url=$(firecrab_release_asset_url "$RELEASE_VERSION" SHA256SUMS)
step "downloading $tarball"
if ! curl --proto '=https' --tlsv1.2 -fsSL "$url" -o "$PAYLOAD_TMP/$tarball"; then
die "failed to download $url
Tag a GitHub Release, or install local binaries:
cargo build --release -p firecrab-api -p firecrab-helper -p firecrab-cli
./install.sh --bin-dir target/release --dashboard-dir firecrab-frontend/dist"
fi
if ! curl --proto '=https' --tlsv1.2 -fsSL "$sums_url" -o "$PAYLOAD_TMP/SHA256SUMS"; then
die "failed to download $sums_url"
fi
firecrab_verify_sha256 "$PAYLOAD_TMP/SHA256SUMS" "$PAYLOAD_TMP/$tarball" \
|| die "checksum mismatch for $tarball"
mkdir -p "$PAYLOAD_TMP/root"
tar -xzf "$PAYLOAD_TMP/$tarball" -C "$PAYLOAD_TMP/root"
PAYLOAD_ROOT=$PAYLOAD_TMP/root
local name
for name in firecrab-api firecrab-helper firecrab firecrab-ingress; do
local member
member=$(firecrab_resolve_binary "$name" "$PAYLOAD_ROOT" "") \
|| die "release bundle is missing $name"
firecrab_assert_binary_arch "$member" "$arch" \
|| die "$tarball: $name is not a $arch binary"
done
for name in LICENSE THIRD_PARTY_NOTICES.txt release-license-inventory.json \
licenses/GPL-2.0-only.txt ingress/LICENSE.nginx ingress/LICENSE.pcre2 \
ingress/LICENSE.musl ingress/sources.json; do
[ -f "$PAYLOAD_ROOT/$name" ] || die "release bundle is missing compliance artifact $name"
done
log "host bundle $arch/$LIBC"
}
# Sets PAYLOAD_* to either --bin-dir + this checkout, or a downloaded release.
resolve_payload() {
if [ "$(firecrab_payload_mode "$BIN_DIR")" = dir ]; then
[ -d "$BIN_DIR" ] || die "--bin-dir is not a directory: $BIN_DIR"
is_checkout || die "--bin-dir needs a firecrab checkout for units and helper scripts"
PAYLOAD_BIN=$BIN_DIR
PAYLOAD_UNITS=$REPO_ROOT/packaging/systemd
PAYLOAD_EXTRACT=$REPO_ROOT/scripts/firecracker-menual
PAYLOAD_LICENSE=$REPO_ROOT/LICENSE
PAYLOAD_GPL=$REPO_ROOT/licenses/GPL-2.0-only.txt
if [ -f "$REPO_ROOT/dist/compliance/THIRD_PARTY_NOTICES.txt" ] \
&& [ -f "$REPO_ROOT/dist/compliance/release-license-inventory.json" ]; then
PAYLOAD_THIRD_PARTY=$REPO_ROOT/dist/compliance/THIRD_PARTY_NOTICES.txt
PAYLOAD_INVENTORY=$REPO_ROOT/dist/compliance/release-license-inventory.json
else
PAYLOAD_THIRD_PARTY=
PAYLOAD_INVENTORY=
fi
if [ -n "$DASHBOARD_DIR" ]; then
PAYLOAD_DASHBOARD=$DASHBOARD_DIR
elif [ -f "$REPO_ROOT/firecrab-frontend/dist/index.html" ]; then
PAYLOAD_DASHBOARD=$REPO_ROOT/firecrab-frontend/dist
else
PAYLOAD_DASHBOARD=
fi
return 0
fi
download_host_bundle
PAYLOAD_BIN=$PAYLOAD_ROOT
PAYLOAD_UNITS=$PAYLOAD_ROOT/systemd
PAYLOAD_EXTRACT=$PAYLOAD_ROOT
PAYLOAD_DASHBOARD=$PAYLOAD_ROOT/dashboard
PAYLOAD_LICENSE=$PAYLOAD_ROOT/LICENSE
PAYLOAD_THIRD_PARTY=$PAYLOAD_ROOT/THIRD_PARTY_NOTICES.txt
PAYLOAD_INVENTORY=$PAYLOAD_ROOT/release-license-inventory.json
PAYLOAD_GPL=$PAYLOAD_ROOT/licenses/GPL-2.0-only.txt
}
report_payload() {
local tarball
if [ "$(firecrab_payload_mode "$BIN_DIR")" = dir ]; then
log "binaries: would install from $BIN_DIR"
if [ ! -d "$BIN_DIR" ]; then
warn "not a directory: $BIN_DIR"
return 1
fi
local name gaps=0
for name in firecrab-api firecrab-helper firecrab; do
if [ -x "$BIN_DIR/$name" ]; then
log " $name"
else
warn " missing $name (ok if already installed under $LIBDIR)"
gaps=1
fi
done
return 0
fi
if ! tarball=$(firecrab_host_tarball "" "$LIBC"); then
warn "unsupported architecture: $(uname -m) (need x86_64 or aarch64)"
return 1
fi
log "binaries: would download $(firecrab_release_asset_url "$RELEASE_VERSION" "$tarball") ($LIBC)"
return 0
}
# --- host layout -----------------------------------------------------------
# The account the API runs as, in the kvm group for firecracker.
ensure_account() {
if getent group "$FIRECRAB_GROUP" >/dev/null; then
log "group $FIRECRAB_GROUP exists"
else
step "creating group $FIRECRAB_GROUP"
$SUDO groupadd --system "$FIRECRAB_GROUP"
fi
if id "$FIRECRAB_USER" >/dev/null 2>&1; then
log "user $FIRECRAB_USER exists"
else
step "creating user $FIRECRAB_USER"
$SUDO useradd --system --gid "$FIRECRAB_GROUP" --home-dir "$DATADIR" \
--shell /usr/sbin/nologin "$FIRECRAB_USER"
fi
# The API spawns firecracker, which opens /dev/kvm.
if getent group kvm >/dev/null && ! id -nG "$FIRECRAB_USER" | tr ' ' '\n' | grep -qx kvm; then
step "adding $FIRECRAB_USER to the kvm group"
$SUDO usermod --append --groups kvm "$FIRECRAB_USER"
fi
# Group membership above is necessary but not always sufficient: some
# hosts' /dev/kvm doesn't actually land in the kvm group the standard
# udev rule (KERNEL=="kvm", GROUP="kvm") asks for — seen live on a
# Parallels ARM host where it came up group "sgx" instead, which made
# every VM start fail with "Kvm error: Permission denied (os error 13)"
# even though $FIRECRAB_USER was correctly in the kvm group. An ACL
# entry is a narrow, idempotent belt-and-suspenders fix: it grants the
# kvm group access without touching whatever else owns the device node,
# and re-running this is a no-op once the entry is already there.
if [ -e /dev/kvm ]; then
if have setfacl; then
if ! getfacl -p /dev/kvm 2>/dev/null | grep -qx 'group:kvm:rw-'; then
step "granting the kvm group ACL access to /dev/kvm"
$SUDO setfacl -m g:kvm:rw /dev/kvm
KVM_ACL_ADDED=1
fi
else
warn "setfacl not found — skipping the /dev/kvm ACL fixup (install the 'acl' package if VMs fail with a KVM permission error)"
fi
fi
}
# Data, config and install directories with their intended owners.
ensure_directories() {
$SUDO install -d -o root -g root -m 0755 "$LIBDIR" "$SHAREDIR"
$SUDO install -d -o "$FIRECRAB_USER" -g "$FIRECRAB_GROUP" -m 0750 \
"$DATADIR" "$DATADIR/data" "$DATADIR/images" "$DATADIR/updates"
$SUDO install -d -o root -g "$FIRECRAB_GROUP" -m 0750 "$CONFDIR"
log "directories ready under $DATADIR, $CONFDIR"
}
# What this host looked like before firecrab changed it, so --uninstall can put
# back what the installer and the helper changed and nothing else. Written once:
# a re-install must not record values the helper has already switched on.
record_host_baseline() {
local file="$CONFDIR/host-baseline.env" content
[ -f "$file" ] && return 0
content="# written by install.sh; --uninstall restores what is listed here"$'\n'
if [ "$KVM_ACL_ADDED" -eq 1 ]; then
content+="kvm_acl=added"$'\n'
fi
# A helper that already ran has turned forwarding on, so those values are no
# longer the host's own.
if ! systemctl is-active --quiet firecrab-helper.service firecrab-net-helper.service 2>/dev/null; then
content+="ip_forward=$(cat /proc/sys/net/ipv4/ip_forward 2>/dev/null || echo 0)"$'\n'
if [ -r /proc/sys/net/ipv6/conf/all/forwarding ]; then
content+="ipv6_forwarding=$(cat /proc/sys/net/ipv6/conf/all/forwarding)"$'\n'
fi
fi
printf '%s' "$content" | $SUDO tee "$file" >/dev/null
$SUDO chown root:"$FIRECRAB_GROUP" "$file"
$SUDO chmod 0640 "$file"
}
# Keep the same release attribution available after the archive is unpacked.
# Firecracker's separately installed upstream notices live below firecracker/.
install_compliance() {
[ -f "$PAYLOAD_LICENSE" ] || die "missing FireCrab LICENSE in install payload"
[ -f "$PAYLOAD_GPL" ] || die "missing GPL-2.0 text in install payload"
$SUDO install -d -o root -g root -m 0755 "$SHAREDIR/licenses"
$SUDO install -o root -g root -m 0644 "$PAYLOAD_LICENSE" "$SHAREDIR/LICENSE"
$SUDO install -o root -g root -m 0644 \
"$PAYLOAD_GPL" "$SHAREDIR/licenses/GPL-2.0-only.txt"
if [ -n "$PAYLOAD_THIRD_PARTY" ] && [ -n "$PAYLOAD_INVENTORY" ]; then
$SUDO install -o root -g root -m 0644 \
"$PAYLOAD_THIRD_PARTY" "$SHAREDIR/THIRD_PARTY_NOTICES.txt"
$SUDO install -o root -g root -m 0644 \
"$PAYLOAD_INVENTORY" "$SHAREDIR/release-license-inventory.json"
else
$SUDO rm -f "$SHAREDIR/THIRD_PARTY_NOTICES.txt" \
"$SHAREDIR/release-license-inventory.json"
fi
log "license and attribution material installed to $SHAREDIR"
}
# Puts the payload binaries, and the dashboard, where the units expect them.
install_binaries() {
local name src ingress_notices guest_notices
# Validate the new required component before replacing any installed binary.
src=$(firecrab_resolve_binary firecrab-ingress "$PAYLOAD_BIN" "$LIBDIR") \
|| die "missing firecrab-ingress; build with scripts/build-ingress.sh or install a host release"
ingress_notices="$PAYLOAD_BIN/ingress"
if [ "$src" -ef "$LIBDIR/firecrab-ingress" ]; then
ingress_notices="$LIBDIR/ingress"
fi
for name in LICENSE.nginx LICENSE.pcre2 LICENSE.musl sources.json; do
[ -f "$ingress_notices/$name" ] || die "missing ingress/$name in install payload"
done
for name in firecrab-api firecrab-helper firecrab-ingress; do
src=$(firecrab_resolve_binary "$name" "$PAYLOAD_BIN" "$LIBDIR") \
|| die "no $name in ${PAYLOAD_BIN:-<release>} or $LIBDIR — pass it via --bin-dir or install a release"
if [ "$src" -ef "$LIBDIR/$name" ]; then
log "keeping existing $LIBDIR/$name"
else
$SUDO install -o root -g root -m 0755 "$src" "$LIBDIR/$name"
fi
done
# Old custom payloads retain a usable serial console without resize support.
if src=$(firecrab_resolve_binary firecrab-guest-console "$PAYLOAD_BIN" "$LIBDIR"); then
guest_notices="$PAYLOAD_BIN/guest-console/LICENSE.musl"
if [ "$src" -ef "$LIBDIR/firecrab-guest-console" ]; then guest_notices="$LIBDIR/guest-console/LICENSE.musl"; fi
[ -f "$guest_notices" ] || die "missing guest-console/LICENSE.musl in install payload"
if [ ! "$src" -ef "$LIBDIR/firecrab-guest-console" ]; then
$SUDO install -o root -g root -m 0755 "$src" "$LIBDIR/firecrab-guest-console"
fi
if [ ! "$guest_notices" -ef "$LIBDIR/guest-console/LICENSE.musl" ]; then
$SUDO install -d -o root -g root -m 0755 "$LIBDIR/guest-console"
$SUDO install -o root -g root -m 0644 "$guest_notices" "$LIBDIR/guest-console/LICENSE.musl"
fi
fi
# Keep the historical executable path usable by old units and clients.
$SUDO ln -sfn firecrab-helper "$LIBDIR/firecrab-net-helper"
# extract-vmlinux and extract-arm64-image are shell scripts used at
# runtime by the API to turn a distro vmlinuz into the kernel format
# Firecracker boots on each architecture. Installing them next to the
# binary lets the API find them without needing access to the repo
# checkout (which may be in a user home the service account cannot reach).
for helper in extract-vmlinux extract-arm64-image; do
[ -f "$PAYLOAD_EXTRACT/$helper" ] || die "missing $PAYLOAD_EXTRACT/$helper"
$SUDO install -o root -g root -m 0755 \
"$PAYLOAD_EXTRACT/$helper" "$LIBDIR/$helper"
done
local cli_src
cli_src=$(firecrab_resolve_binary "firecrab" "$PAYLOAD_BIN" "$PREFIX/bin") \
|| die "no firecrab (CLI) in ${PAYLOAD_BIN:-<release>} or $PREFIX/bin — pass it via --bin-dir or install a release"
if [ "$cli_src" -ef "$PREFIX/bin/firecrab" ]; then
log "keeping existing $PREFIX/bin/firecrab"
else
$SUDO install -d -o root -g root -m 0755 "$PREFIX/bin"
$SUDO install -o root -g root -m 0755 "$cli_src" "$PREFIX/bin/firecrab"
fi
if [ "$ingress_notices" != "$LIBDIR/ingress" ]; then
$SUDO install -d -o root -g root -m 0755 "$LIBDIR/ingress"
for name in LICENSE.nginx LICENSE.pcre2 LICENSE.musl sources.json; do
$SUDO install -o root -g root -m 0644 "$ingress_notices/$name" "$LIBDIR/ingress/$name"
done
fi
log "binaries installed to $LIBDIR (cli → $PREFIX/bin/firecrab)"
[ "$WITH_FRONTEND" -eq 1 ] || return 0
if [ -n "$PAYLOAD_DASHBOARD" ] && [ -f "$PAYLOAD_DASHBOARD/index.html" ]; then
$SUDO rm -rf "$SHAREDIR/dashboard"
$SUDO install -d -o root -g root -m 0755 "$SHAREDIR/dashboard"
$SUDO cp -r "$PAYLOAD_DASHBOARD/." "$SHAREDIR/dashboard/"
$SUDO chown -R root:root "$SHAREDIR/dashboard"
log "dashboard installed to $SHAREDIR/dashboard"
elif [ -f "$SHAREDIR/dashboard/index.html" ]; then
log "keeping existing $SHAREDIR/dashboard"
else
die "dashboard not found — pass --dashboard-dir, or omit --no-frontend only when using a release bundle"
fi
}
# On an enforcing SELinux host the services must not keep systemd's own
# domain.
#
# $PREFIX/lib/firecrab is labelled lib_t, and the policy rule that moves a
# service out of init_t into unconfined_service_t only fires for bin_t
# executables. Without this the API stays in init_t, where an outbound HTTPS
# connect is denied — every registry read fails with EACCES while a shell on
# the same account reaches the registry fine (public-docs/guides/troubleshooting.md).
label_selinux_binaries() {
local pattern="${LIBDIR}(/.*)?"
selinux_active || return 0
# restorecon alone cannot help: it applies the policy's default context,
# which for this path is lib_t. The rule has to be recorded first.
if ! have semanage; then
warn "semanage missing; cannot label $LIBDIR bin_t — the services will"
warn "stay in init_t and every registry read will fail (see --doctor)"
return 0
fi
# -a fails when the rule already exists, which a re-run always hits. Both
# spellings narrate on stdout ("already defined, modifying instead"); the
# log line below is the one an operator needs.
$SUDO semanage fcontext -a -t bin_t "$pattern" >/dev/null 2>&1 \
|| $SUDO semanage fcontext -m -t bin_t "$pattern" >/dev/null 2>&1 \
|| { warn "could not record an SELinux file context for $LIBDIR"; return 0; }
if have restorecon; then
$SUDO restorecon -R "$LIBDIR" >/dev/null 2>&1 \
|| warn "restorecon failed for $LIBDIR"
fi
log "SELinux: $LIBDIR labelled bin_t"
}
# Seeds api.env once so an operator's edits survive a re-run.
install_config() {
if [ -f "$CONFDIR/api.env" ]; then
log "keeping existing $CONFDIR/api.env"
return 0
fi
$SUDO tee "$CONFDIR/api.env" > /dev/null <<'ENVFILE'
# firecrab API settings. The unit already sets the image root, the dashboard
# assets and the working directory; uncomment only what you want to change.
#
# FIRECRAB_BIND_ADDR=127.0.0.1:5523
# A non-loopback address requires authentication AND TLS to be enabled.
# FIRECRAB_ALLOWED_ORIGINS=
# Empty is correct while the dashboard is served from this same origin.
# FIRECRAB_FIRECRACKER_BIN=firecracker
#
# M2Image install (Images page / POST /api/images/{alias}/install).
# Points at a directory-style base that serves `{alias}.tar.zst` packages
# (see scripts/package-m2images.sh). Unset = use the public MicroRegistry.
# Example local mirror:
# FIRECRAB_IMAGE_BASE_URL=http://127.0.0.1:8765
# Requires host tools: tar, zstd. Restart firecrab-api after changing this.
ENVFILE
$SUDO chown root:"$FIRECRAB_GROUP" "$CONFDIR/api.env"
$SUDO chmod 0640 "$CONFDIR/api.env"
log "wrote $CONFDIR/api.env"
}
# Renders the unit templates with this host's paths and account.
install_units() {
local uid source legacy="$UNITDIR/firecrab-net-helper.service"
uid=$(id -u "$FIRECRAB_USER")
local unit
for unit in "${UNITS[@]}"; do
source="$PAYLOAD_UNITS/$unit"
if [ "$unit" = firecrab-helper.service ] && [ ! -f "$source" ]; then
source="$PAYLOAD_UNITS/firecrab-net-helper.service"
fi
[ -r "$source" ] || die "missing unit template $source"
done
# Only a real old unit needs migration. An alias from a previous install
# already denotes the new service and must not be disabled separately.
if [ -f "$legacy" ] && [ ! -L "$legacy" ]; then
$SUDO systemctl disable --now firecrab-net-helper.service
$SUDO rm -f "$legacy"
fi
for unit in "${UNITS[@]}"; do
source="$PAYLOAD_UNITS/$unit"
if [ "$unit" = firecrab-helper.service ] && [ ! -f "$source" ]; then
source="$PAYLOAD_UNITS/firecrab-net-helper.service"
fi
sed -e 's/firecrab-net-helper/firecrab-helper/g' \
-e '/^Alias=/d' \
-e "s|@LIBDIR@|$LIBDIR|g" \
-e "s|@SHAREDIR@|$SHAREDIR|g" \
-e "s|@DATADIR@|$DATADIR|g" \
-e "s|@CONFDIR@|$CONFDIR|g" \
-e "s|@UNITDIR@|$UNITDIR|g" \
-e "s|@PREFIX@|$PREFIX|g" \
-e "s|@FIRECRAB_USER@|$FIRECRAB_USER|g" \
-e "s|@FIRECRAB_GROUP@|$FIRECRAB_GROUP|g" \
-e "s|@FIRECRAB_UID@|$uid|g" \
"$source" | $SUDO tee "$UNITDIR/$unit" > /dev/null
if [ "$unit" = firecrab-helper.service ]; then
printf 'Alias=firecrab-net-helper.service\n' | $SUDO tee -a "$UNITDIR/$unit" >/dev/null
fi
$SUDO chmod 0644 "$UNITDIR/$unit"
done
$SUDO ln -sfn firecrab-helper.service "$legacy"
$SUDO systemctl daemon-reload
log "units installed to $UNITDIR"
}
# --- guest images ----------------------------------------------------------
# Whether a guest rootfs is already installed.
images_present() {
compgen -G "$DATADIR/images/rootfs/*.ext4" >/dev/null 2>&1
}
# --- run -------------------------------------------------------------------
# Enables and starts both units, then confirms they really came up.
start_units() {
$SUDO systemctl enable "${UNITS[@]}"
$SUDO systemctl restart "${UNITS[@]}"
sleep 1
local failed=0
for unit in "${UNITS[@]}"; do
if systemctl is-active --quiet "$unit"; then
log "$unit is running"
else
warn "$unit failed to start — journalctl -u $unit -n 30"
failed=1
fi
done
if [ "$failed" -eq 0 ]; then
wait_for_api || failed=1
fi
return $failed
}
# `is-active` on a Type=simple unit is true as soon as the process forks —
# not once it is actually serving. At startup firecrab-api hashes every
# present template artifact before binding its listener (see
# TemplateRegistry::from_specs in firecrab-api/src/templates.rs), and a
# large rootfs from a previous OCI import can take noticeably longer to hash
# than the process takes to fork. Poll the real HTTP port so callers relying
# on start_units's return don't race it.
wait_for_api() {
local bind=${FIRECRAB_BIND_ADDR:-127.0.0.1:5523}
local _attempt
for _attempt in $(seq 1 60); do
curl -fs -o /dev/null "http://$bind/" 2>/dev/null && return 0
sleep 1
done
warn "firecrab-api did not answer http://$bind/ within 60s — journalctl -u firecrab-api -n 30"
return 1
}
# Report-only path: every gap, no changes, no root required.
do_check() {
log "checking host readiness (nothing will be changed)"
local gaps=0
if detect_pkg; then
log "package manager: $PKG"
else
warn "no known package manager (apt/dnf/zypper/pacman/apk)"
gaps=1
fi
ensure_runtime_deps || gaps=1
if [ "$WITH_PIPE" = 1 ]; then
[ -x "$LIBDIR/firecrab-ingress" ] || { warn "missing bundled firecrab-ingress"; gaps=1; }
if [ -x "$LIBDIR/pipe/cloudflared" ]; then
log "pipe engine present: $LIBDIR/pipe/cloudflared"
else
warn "missing pipe engine: $LIBDIR/pipe/cloudflared (installed with --with-pipe)"
gaps=1
fi
fi
ensure_firecracker || gaps=1
report_payload || gaps=1
check_kvm || gaps=1
check_systemd || gaps=1
report_ufw
if [ "$gaps" -eq 0 ]; then
log "host is ready — nothing missing"
else
warn "run './install.sh' to install the above"
fi
step "for runtime host diagnostics (UFW, helper socket, wrong DB path): ./install.sh --doctor"
return 0
}
# Runtime host diagnostics — delegates to `firecrab doctor` (no root).
do_doctor() {
local cli
if have firecrab; then
cli=firecrab
elif [ -x "$REPO_ROOT/target/release/firecrab" ]; then
cli=$REPO_ROOT/target/release/firecrab
elif [ -x "$REPO_ROOT/target/debug/firecrab" ]; then
cli=$REPO_ROOT/target/debug/firecrab
else
die "missing firecrab CLI (run 'cargo build -p firecrab-cli', or install first)"
fi
# Pass DATADIR through so the doctor matches this install layout.
DATADIR="$DATADIR" FIRECRAB_API_USER="$FIRECRAB_USER" \
exec "$cli" doctor "${DOCTOR_ARGS[@]}"
}
# Installs what firecrab needs and stops there — no account, directories, units
# or services. This is the part that differs per distribution, so it is what a
# container without systemd (and CI's distro matrix) can usefully exercise.
do_deps() {
require_sudo_ticket
detect_pkg || die "no known package manager (apt/dnf/zypper/pacman/apk)"
ensure_runtime_deps || die "missing runtime dependencies (see above)"
ensure_firecracker || die "firecracker is required"
check_kvm || warn "no KVM here; that only matters where VMs actually run"
report_ufw
log "dependencies ready — run without --deps-only to install firecrab"
}
do_print_install_url() {
firecrab_install_curl_url "${RELEASE_VERSION:-latest}"
}
do_print_release_url() {
local tarball
tarball=$(firecrab_host_tarball "" "$LIBC") || die "unsupported architecture: $(uname -m) (need x86_64 or aarch64)"
firecrab_release_asset_url "$RELEASE_VERSION" "$tarball"
}
# `curl | bash` occupies stdin, so sudo cannot read a password from it.
# Ask once on the real terminal; later $SUDO calls reuse the ticket.
require_sudo_ticket() {
[ -n "$SUDO" ] || return 0
sudo -n true >/dev/null 2>&1 && return 0
[ -e /dev/tty ] || die "sudo needs a password and this session has no terminal"
step "sudo password required"
# Redirects attach sudo's prompt to the controlling tty, not stdin.
# shellcheck disable=SC2024
sudo -v </dev/tty >/dev/tty 2>/dev/tty || die "sudo authentication failed"
}
# The default path: fill the gaps, fetch binaries, lay out, start.
do_install() {
require_sudo_ticket
# Checked before anything is installed: refusing after pulling in five
# packages would be a waste of the operator's time.
check_systemd || die "this installer manages systemd units"
detect_pkg || warn "no known package manager — anything missing has to be installed by hand"
ensure_runtime_deps || die "missing runtime dependencies (see above)"
ensure_firecracker || die "firecracker is required"
check_kvm || warn "continuing, but VMs will not start until KVM is available"
resolve_payload
ensure_account
ensure_directories
record_host_baseline
install_compliance
install_binaries
label_selinux_binaries
install_config
install_units
if [ "$WITH_PIPE" = 1 ]; then
$SUDO env PREFIX="$PREFIX" FIRECRAB_LIBDIR="$LIBDIR" DATADIR="$DATADIR" \
CONFDIR="$CONFDIR" UNITDIR="$UNITDIR" "$PREFIX/bin/firecrab" pipe install
fi
# Guest images are not part of the host install — import one afterwards
# with POST /api/oci/import or the dashboard Images page.
start_units || die "installed, but a unit did not come up"
report_ufw
local bind=${FIRECRAB_BIND_ADDR:-127.0.0.1:5523}
log "done — dashboard at http://$bind/"
if ! images_present; then
step "guest images: install from the dashboard Images page"
fi
}
# Each MicroVM runs in its own transient unit, which is meant to outlive the API
# so that restarting the API never kills a guest. Stopping the API therefore
# leaves every shim and its firecracker process running with nothing to manage
# them, so the VMs are stopped first, while the helper can still clean up after
# them.
stop_vm_units() {
local units
units=$(systemctl list-units --all --plain --no-legend --no-pager 'firecrab-vm-*.service' 2>/dev/null \
| awk '{print $1}') || true
[ -n "$units" ] || return 0
step "stopping $(printf '%s\n' "$units" | wc -l | tr -d ' ') MicroVM unit(s)"
# shellcheck disable=SC2086 # one word per unit
$SUDO systemctl stop $units \
|| warn "some MicroVM units did not stop — their firecracker processes may keep running"
# A failed transient unit is not collected on its own.
# shellcheck disable=SC2086
$SUDO systemctl reset-failed $units 2>/dev/null || true
}
# Whether something besides firecrab's own (already removed) networks may rely on
# IP forwarding: a bridge or a veth is left. When `ip` cannot tell, assume so.
others_may_route() {
local kind out
for kind in bridge veth; do
out=$(ip -o link show type "$kind" 2>/dev/null) || return 0
[ -z "$out" ] || return 0
done
return 1
}
# Puts back what record_host_baseline recorded: the /dev/kvm ACL the installer
# added, and IP forwarding where the host had it off and still has it on. A host
# installed before the record existed is left exactly as it is.
restore_host_baseline() {
local file="$CONFDIR/host-baseline.env" content spec key was
local -a to_disable=()
content=$($SUDO cat "$file" 2>/dev/null) || return 0
if printf '%s\n' "$content" | grep -qx 'kvm_acl=added' \
&& have getfacl && getfacl -p /dev/kvm 2>/dev/null | grep -qx 'group:kvm:rw-'; then
if $SUDO setfacl -x g:kvm /dev/kvm; then
# `setfacl -x` leaves a bare mask behind; with nothing else named in
# the ACL take it all off. An entry another tool added is kept.
if ! getfacl -p /dev/kvm 2>/dev/null | grep -Eq '^(user|group):[^:]+:'; then
$SUDO setfacl -b /dev/kvm
fi
log "removed the kvm group ACL the installer added to /dev/kvm"
else
warn "could not remove the kvm group ACL from /dev/kvm — it resets at reboot"
fi
fi
for spec in ip_forward:net.ipv4.ip_forward ipv6_forwarding:net.ipv6.conf.all.forwarding; do
was=$(printf '%s\n' "$content" | sed -n "s/^${spec%%:*}=//p" | head -n 1)
key=${spec#*:}
if [ "$was" = 0 ] && [ "$(sysctl -n "$key" 2>/dev/null)" = 1 ]; then
to_disable+=("$key")
fi
done
if [ "${#to_disable[@]}" -gt 0 ]; then
if others_may_route; then
warn "left ${to_disable[*]} on: other bridges or containers are present (resets at reboot)"
else
for key in "${to_disable[@]}"; do
if $SUDO sysctl -qw "$key=0"; then
log "restored $key=0"
else
warn "could not restore $key=0"
fi
done
fi
fi
# The next install records the host as it is then.
$SUDO rm -f "$file"
}
# Removes what this script installed; data only with --purge.
do_uninstall() {
require_sudo_ticket
stop_vm_units
for unit in firecrab-address-updater.service firecrab-tunnel.service firecrab-ingress.service firecrab-api.service firecrab-helper.service firecrab-net-helper.service; do
$SUDO systemctl disable --now "$unit" 2>/dev/null || true
$SUDO rm -f "$UNITDIR/$unit"
done
$SUDO systemctl daemon-reload
log "units removed"
$SUDO rm -rf "$LIBDIR/pipe"
# SIGTERM (above) only stops the helper's socket loop — it never deletes
# the bridges, TAP devices or nftables tables it created, only a reboot
# would. Run its own teardown while the binary is still on disk.
local teardown_helper
if teardown_helper=$(firecrab_resolve_binary firecrab-helper "" "$LIBDIR"); then
$SUDO "$teardown_helper" --teardown \
|| warn "network teardown failed — bridges/nftables tables may remain until reboot"
fi
$SUDO rm -f "$LIBDIR/firecrab-api" "$LIBDIR/firecrab-helper" "$LIBDIR/firecrab-net-helper" \
"$LIBDIR/extract-vmlinux" "$LIBDIR/extract-arm64-image"
$SUDO rm -f "$PREFIX/bin/firecrab" "$LIBDIR/firecrab-ingress" "$LIBDIR/firecrab-guest-console"
$SUDO rm -rf "$LIBDIR/guest-console"
$SUDO rm -rf "$LIBDIR/ingress"
$SUDO rm -rf "$SHAREDIR/dashboard"
$SUDO rm -f "$SHAREDIR/LICENSE" "$SHAREDIR/THIRD_PARTY_NOTICES.txt" \
"$SHAREDIR/release-license-inventory.json"
$SUDO rm -rf "$SHAREDIR/licenses"
# Firecracker is installed independently and is intentionally not removed;
# keep its upstream notices alongside that remaining binary.
$SUDO rmdir --ignore-fail-on-non-empty "$LIBDIR" "$SHAREDIR" 2>/dev/null || true
# The file-context rule outlives the directory, so leaving it behind would
# silently relabel whatever is installed there next.
if have semanage; then
$SUDO semanage fcontext -d "${LIBDIR}(/.*)?" 2>/dev/null || true
fi
log "binaries and dashboard removed"
# Before the purge below: the record lives in $CONFDIR.
restore_host_baseline
# Left alone on purpose: the account, the config and the data directory.
if [ "$PURGE" -eq 1 ]; then
warn "purging $DATADIR and $CONFDIR (all VM disks and the database)"
$SUDO rm -rf "$DATADIR" "$CONFDIR"
else
log "kept $DATADIR and $CONFDIR — pass --purge to delete them too"
fi
}
case "$MODE" in
check) do_check ;;
doctor) do_doctor ;;
deps) do_deps ;;
print-install-url) do_print_install_url ;;
print-release-url) do_print_release_url ;;
install) do_install ;;
uninstall) do_uninstall ;;
esac