Two groups of changes, committed together as the follow-up to the initial push: 1. .gitmodules — submodule Qwen3-TTS now points at the internal Gitea fork (ssh://git@mini-eq:2222/umbragit/Qwen3-TTS.git) instead of upstream GitHub, so a clone actually gets the UmbraAudio branding/integration commit 012e452 rather than vanilla upstream. 2. New infra/ tooling for the cross-machine work in spec 21: agent_bridge, remote_credential_broker (Keychain-backed, secrets read at runtime and generated via read_secret/random_secret — no hardcoded credentials), the enterprise bootstrap scripts, and the oldbox SSH/auth prompts. Plus backup/ restore scripts, two specs (18 UmbraTrans, 19 enterprise control plane), umbraaudio/voices/lmstudio_gate.py, and in-flight edits to the discovery/attribution/profiling modules, config, chapter_builder, emotion, pronunciation, qwen_tts and routes. Also adds *.db to .gitignore: SQLite project databases are runtime state and no .db file was tracked before. Verified before staging: .env's only change is UMBRA_TTS__QWEN_TTS_HELPER_URL and the committed copy has zero secret-looking values; the broker ships only config.example.json.
512 lines
20 KiB
Bash
Executable File
512 lines
20 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Phase-one enterprise control-plane bootstrap for Linux Mint 22.x.
|
|
# This provisions PostgreSQL, database roles, core tenancy/identity tables,
|
|
# and least-privilege service accounts. It does not install application code,
|
|
# change Qwen, manage Docker, or change firewall rules.
|
|
set -Eeuo pipefail
|
|
umask 077
|
|
|
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
|
CONFIG_FILE="${UMBRA_BOOTSTRAP_CONFIG:-${SCRIPT_DIR}/enterprise-bootstrap.env}"
|
|
MODE="check"
|
|
DRY_RUN=0
|
|
LOCK_DIR="/run/lock/umbra-enterprise-bootstrap.lock"
|
|
LOG_FILE="/var/log/umbra-enterprise-bootstrap.log"
|
|
|
|
UMBRA_HOSTNAME="oldbox.bdmurphy.me"
|
|
UMBRA_PUBLIC_URL="https://oldbox.bdmurphy.me"
|
|
UMBRA_DB_NAME="umbracontrol"
|
|
UMBRA_DB_HOST="127.0.0.1"
|
|
UMBRA_DB_PORT="5432"
|
|
UMBRA_PG_VERSION="16"
|
|
UMBRA_APP_ROOT="/opt/umbracontrol"
|
|
UMBRA_DATA_ROOT="/var/lib/umbracontrol"
|
|
UMBRA_BACKUP_ROOT="/var/backups/umbracontrol"
|
|
UMBRA_BOOTSTRAP_ADMIN_EMAIL="umbratools@gmail.com"
|
|
UMBRA_ORGANIZATION_NAME="Umbra"
|
|
UMBRA_ORGANIZATION_SLUG="umbra"
|
|
UMBRA_QWEN_URL="http://192.168.123.14:8881"
|
|
UMBRA_INSTALL_PACKAGES=1
|
|
UMBRA_INSTALL_UV=1
|
|
UMBRA_CREATE_BOOTSTRAP_ADMIN=1
|
|
UMBRA_CONFIG_DIR="/etc/umbracontrol"
|
|
UMBRA_CREDENTIALS_FILE="/etc/umbracontrol/db-credentials.env"
|
|
UMBRA_BOOTSTRAP_TOKEN_FILE="/root/umbra-enterprise-bootstrap.token"
|
|
UMBRA_PG_SUPERUSER="postgres"
|
|
UMBRA_SERVICE_GROUP="umbracontrol"
|
|
UMBRA_API_USER="umbracontrol_api"
|
|
UMBRA_WORKER_USER="umbracontrol_worker"
|
|
UMBRA_READONLY_USER="umbracontrol_readonly"
|
|
UMBRA_BACKUP_USER="umbracontrol_backup"
|
|
UMBRA_MIGRATOR_USER="umbracontrol_migrator"
|
|
UMBRA_OWNER_ROLE="umbracontrol_owner"
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: bootstrap_enterprise_mint.sh [options]
|
|
|
|
Options:
|
|
--check Read-only host and dependency inspection (default)
|
|
--dry-run Show apply actions without changing the host
|
|
--apply Apply the phase-one infrastructure bootstrap; requires root
|
|
--status Show installation/service/database status
|
|
--verify Verify installed services and database connectivity
|
|
--config FILE Read non-secret KEY=VALUE configuration
|
|
-h, --help Show this help
|
|
|
|
This phase intentionally does not install application code, modify Qwen,
|
|
manage Docker containers, or change firewall rules. Run it first on Mint,
|
|
then deploy the application and systemd units in a separate reviewed step.
|
|
EOF
|
|
}
|
|
|
|
while (($#)); do
|
|
case "$1" in
|
|
--check) MODE="check"; shift ;;
|
|
--dry-run) MODE="apply"; DRY_RUN=1; shift ;;
|
|
--apply) MODE="apply"; DRY_RUN=0; shift ;;
|
|
--status) MODE="status"; shift ;;
|
|
--verify) MODE="verify"; shift ;;
|
|
--config) CONFIG_FILE="${2:?--config requires a file}"; shift 2 ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
*) printf 'Unknown option: %s\n' "$1" >&2; usage >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
log() {
|
|
local line
|
|
line="[$(date -Is)] $*"
|
|
printf '%s\n' "$line"
|
|
if [[ -f "$LOG_FILE" && "$(id -u)" -eq 0 ]]; then
|
|
printf '%s\n' "$line" >> "$LOG_FILE"
|
|
fi
|
|
}
|
|
|
|
die() {
|
|
printf 'ERROR: %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
require_command() {
|
|
command -v "$1" >/dev/null 2>&1 || die "Required command is missing: $1"
|
|
}
|
|
|
|
require_root() {
|
|
[[ "$(id -u)" -eq 0 ]] || die "This operation requires root. Use --check for a read-only inspection."
|
|
}
|
|
|
|
load_config() {
|
|
[[ -f "$CONFIG_FILE" ]] || {
|
|
[[ "$MODE" == "check" || "$MODE" == "status" || "$MODE" == "verify" ]] && return 0
|
|
die "Configuration file not found: $CONFIG_FILE"
|
|
}
|
|
|
|
local line key value
|
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
line="${line#"${line%%[![:space:]]*}"}"
|
|
[[ -z "$line" || "${line:0:1}" == "#" ]] && continue
|
|
[[ "$line" == *=* ]] || die "Invalid configuration line (expected KEY=VALUE): $line"
|
|
key="${line%%=*}"
|
|
value="${line#*=}"
|
|
key="${key//[[:space:]]/}"
|
|
value="${value%\"}"; value="${value#\"}"
|
|
value="${value%\'}"; value="${value#\'}"
|
|
case "$key" in
|
|
UMBRA_HOSTNAME|UMBRA_PUBLIC_URL|UMBRA_DB_NAME|UMBRA_DB_HOST|UMBRA_DB_PORT|UMBRA_PG_VERSION|UMBRA_APP_ROOT|UMBRA_DATA_ROOT|UMBRA_BACKUP_ROOT|UMBRA_BOOTSTRAP_ADMIN_EMAIL|UMBRA_ORGANIZATION_NAME|UMBRA_ORGANIZATION_SLUG|UMBRA_QWEN_URL|UMBRA_INSTALL_PACKAGES|UMBRA_INSTALL_UV|UMBRA_CREATE_BOOTSTRAP_ADMIN)
|
|
printf -v "$key" '%s' "$value"
|
|
;;
|
|
*) die "Unsupported configuration key: $key" ;;
|
|
esac
|
|
done < "$CONFIG_FILE"
|
|
}
|
|
|
|
validate_config() {
|
|
[[ "$UMBRA_HOSTNAME" =~ ^[A-Za-z0-9.-]+$ ]] || die "Invalid UMBRA_HOSTNAME"
|
|
[[ "$UMBRA_PUBLIC_URL" =~ ^https?://[A-Za-z0-9._:-]+(/.*)?$ ]] || die "Invalid UMBRA_PUBLIC_URL"
|
|
[[ "$UMBRA_DB_NAME" =~ ^[a-z_][a-z0-9_]*$ ]] || die "Invalid UMBRA_DB_NAME"
|
|
[[ "$UMBRA_DB_HOST" =~ ^[A-Za-z0-9._:-]+$ ]] || die "Invalid UMBRA_DB_HOST"
|
|
[[ "$UMBRA_DB_PORT" =~ ^[0-9]+$ && "$UMBRA_DB_PORT" -ge 1 && "$UMBRA_DB_PORT" -le 65535 ]] || die "Invalid UMBRA_DB_PORT"
|
|
[[ "$UMBRA_PG_VERSION" =~ ^[0-9]+$ ]] || die "Invalid UMBRA_PG_VERSION"
|
|
[[ "$UMBRA_APP_ROOT" == /* && "$UMBRA_DATA_ROOT" == /* && "$UMBRA_BACKUP_ROOT" == /* ]] || die "Application/data paths must be absolute"
|
|
[[ "$UMBRA_BOOTSTRAP_ADMIN_EMAIL" == *@*.* ]] || die "Invalid administrator email"
|
|
[[ "$UMBRA_ORGANIZATION_SLUG" =~ ^[a-z0-9][a-z0-9-]*$ ]] || die "Invalid organization slug"
|
|
[[ "$UMBRA_QWEN_URL" =~ ^https?:// ]] || die "Invalid UMBRA_QWEN_URL"
|
|
for value in "$UMBRA_INSTALL_PACKAGES" "$UMBRA_INSTALL_UV" "$UMBRA_CREATE_BOOTSTRAP_ADMIN"; do
|
|
[[ "$value" == 0 || "$value" == 1 ]] || die "Boolean configuration values must be 0 or 1"
|
|
done
|
|
}
|
|
|
|
run() {
|
|
if ((DRY_RUN)); then
|
|
printf '[dry-run]'
|
|
printf ' %q' "$@"
|
|
printf '\n'
|
|
else
|
|
"$@"
|
|
fi
|
|
}
|
|
|
|
run_root() {
|
|
require_root
|
|
run "$@"
|
|
}
|
|
|
|
read_secret() {
|
|
local key="$1"
|
|
[[ -f "$UMBRA_CREDENTIALS_FILE" ]] || return 1
|
|
awk -F= -v key="$key" '$1 == key {print substr($0, index($0, "=") + 1); exit}' "$UMBRA_CREDENTIALS_FILE"
|
|
}
|
|
|
|
write_secret_line() {
|
|
local key="$1" value="$2" temporary
|
|
if ((DRY_RUN)); then
|
|
printf '[dry-run] store generated secret %s in %s\n' "$key" "$UMBRA_CREDENTIALS_FILE"
|
|
return
|
|
fi
|
|
install -d -m 0750 -o root -g "$UMBRA_SERVICE_GROUP" "$UMBRA_CONFIG_DIR"
|
|
touch "$UMBRA_CREDENTIALS_FILE"
|
|
chown root:"$UMBRA_SERVICE_GROUP" "$UMBRA_CREDENTIALS_FILE"
|
|
chmod 0640 "$UMBRA_CREDENTIALS_FILE"
|
|
temporary="$(mktemp "${UMBRA_CONFIG_DIR}/.credentials.XXXXXX")"
|
|
awk -F= -v key="$key" -v value="$value" '
|
|
$1 == key { print key "=" value; found = 1; next }
|
|
{ print }
|
|
END { if (!found) print key "=" value }
|
|
' "$UMBRA_CREDENTIALS_FILE" > "$temporary"
|
|
chown root:"$UMBRA_SERVICE_GROUP" "$temporary"
|
|
chmod 0640 "$temporary"
|
|
mv -f "$temporary" "$UMBRA_CREDENTIALS_FILE"
|
|
}
|
|
|
|
random_secret() {
|
|
openssl rand -hex 32
|
|
}
|
|
|
|
check_host() {
|
|
log "Read-only host inspection"
|
|
printf 'hostname: '; hostname -f 2>/dev/null || hostname
|
|
printf 'os: '; . /etc/os-release; printf '%s\n' "${PRETTY_NAME:-unknown}"
|
|
printf 'kernel: '; uname -r
|
|
printf 'user: '; id
|
|
printf 'cpu: '; nproc
|
|
free -h | sed -n '1,3p'
|
|
if command -v nvidia-smi >/dev/null 2>&1; then
|
|
nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader 2>/dev/null || true
|
|
fi
|
|
printf '%s\n' 'storage:'
|
|
df -hT / /home /var/lib 2>/dev/null || true
|
|
printf '%s\n' 'listening ports:'
|
|
ss -lntup 2>/dev/null | sed -n '1,100p' || true
|
|
printf '%s\n' 'relevant services:'
|
|
systemctl list-units --type=service --all --no-pager 2>/dev/null | grep -Ei 'docker|ollama|lmstudio|qwen|postgres|umbra' || true
|
|
printf '%s\n' 'DNS:'
|
|
getent ahosts "$UMBRA_HOSTNAME" 2>/dev/null || true
|
|
log "No changes made"
|
|
}
|
|
|
|
check_tools() {
|
|
require_command openssl
|
|
require_command systemctl
|
|
if [[ "$MODE" == "apply" && "$UMBRA_INSTALL_PACKAGES" -eq 1 ]]; then
|
|
require_command apt-get
|
|
fi
|
|
}
|
|
|
|
ensure_group_and_users() {
|
|
log "Ensuring service group and non-login service accounts"
|
|
if ! getent group "$UMBRA_SERVICE_GROUP" >/dev/null; then
|
|
run groupadd --system "$UMBRA_SERVICE_GROUP"
|
|
else
|
|
log "Group already exists: $UMBRA_SERVICE_GROUP"
|
|
fi
|
|
|
|
ensure_service_user "$UMBRA_API_USER"
|
|
ensure_service_user "$UMBRA_WORKER_USER"
|
|
ensure_service_user "$UMBRA_READONLY_USER"
|
|
ensure_service_user "$UMBRA_BACKUP_USER"
|
|
}
|
|
|
|
ensure_service_user() {
|
|
local user="$1"
|
|
if id "$user" >/dev/null 2>&1; then
|
|
log "Service account already exists: $user"
|
|
return
|
|
fi
|
|
run useradd --system --gid "$UMBRA_SERVICE_GROUP" --home-dir /nonexistent --no-create-home --shell /usr/sbin/nologin "$user"
|
|
}
|
|
|
|
ensure_directories() {
|
|
log "Creating application, data, backup, and configuration directories"
|
|
run install -d -m 0755 -o root -g root "$UMBRA_APP_ROOT"
|
|
run install -d -m 0750 -o root -g "$UMBRA_SERVICE_GROUP" "$UMBRA_DATA_ROOT"
|
|
run install -d -m 0700 -o root -g root "$UMBRA_BACKUP_ROOT"
|
|
run install -d -m 0750 -o root -g "$UMBRA_SERVICE_GROUP" "$UMBRA_CONFIG_DIR"
|
|
run install -d -m 0750 -o root -g "$UMBRA_SERVICE_GROUP" "$UMBRA_APP_ROOT/app" "$UMBRA_APP_ROOT/venv" "$UMBRA_APP_ROOT/bin"
|
|
}
|
|
|
|
install_packages() {
|
|
[[ "$UMBRA_INSTALL_PACKAGES" -eq 1 ]] || { log "Package installation disabled by configuration"; return; }
|
|
log "Installing Mint/Ubuntu packages"
|
|
run env DEBIAN_FRONTEND=noninteractive apt-get update
|
|
run env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
|
ca-certificates curl openssl postgresql postgresql-client postgresql-contrib \
|
|
python3 python3-venv python3-pip rsync
|
|
}
|
|
|
|
ensure_uv() {
|
|
[[ "$UMBRA_INSTALL_UV" -eq 1 ]] || { log "uv installation disabled by configuration"; return; }
|
|
if command -v uv >/dev/null 2>&1; then
|
|
log "uv already available: $(command -v uv)"
|
|
return
|
|
fi
|
|
log "Installing pinned uv bootstrap environment"
|
|
run python3 -m venv "$UMBRA_APP_ROOT/bootstrap-venv"
|
|
run "$UMBRA_APP_ROOT/bootstrap-venv/bin/python" -m pip install --upgrade pip
|
|
run "$UMBRA_APP_ROOT/bootstrap-venv/bin/python" -m pip install 'uv==0.11.7'
|
|
run ln -sfn "$UMBRA_APP_ROOT/bootstrap-venv/bin/uv" "$UMBRA_APP_ROOT/bin/uv"
|
|
}
|
|
|
|
psql_as_postgres() {
|
|
runuser -u "$UMBRA_PG_SUPERUSER" -- env PGOPTIONS='-c log_statement=none' \
|
|
psql -v ON_ERROR_STOP=1 "$@"
|
|
}
|
|
|
|
ensure_postgres_cluster() {
|
|
log "Ensuring PostgreSQL cluster and service"
|
|
local version cluster
|
|
if command -v pg_lsclusters >/dev/null 2>&1; then
|
|
version="$(pg_lsclusters --no-header 2>/dev/null | awk 'NR == 1 {print $1}')"
|
|
cluster="$(pg_lsclusters --no-header 2>/dev/null | awk 'NR == 1 {print $2}')"
|
|
else
|
|
version=""
|
|
cluster=""
|
|
fi
|
|
if [[ -z "$version" || -z "$cluster" ]]; then
|
|
version="$UMBRA_PG_VERSION"
|
|
cluster="main"
|
|
run pg_createcluster "$version" "$cluster" --start
|
|
fi
|
|
if command -v pg_lsclusters >/dev/null 2>&1; then
|
|
if ! pg_lsclusters --no-header | awk -v v="$version" -v c="$cluster" '$1 == v && $2 == c && $3 == "online" {found=1} END {exit !found}'; then
|
|
run pg_ctlcluster "$version" "$cluster" start
|
|
fi
|
|
else
|
|
log "pg_lsclusters is not installed; cluster start will be performed after package installation"
|
|
fi
|
|
run systemctl enable postgresql
|
|
}
|
|
|
|
ensure_database_roles() {
|
|
log "Creating PostgreSQL roles"
|
|
local role password login
|
|
for role in \
|
|
"$UMBRA_OWNER_ROLE" \
|
|
"$UMBRA_MIGRATOR_USER" \
|
|
"$UMBRA_API_USER" \
|
|
"$UMBRA_WORKER_USER" \
|
|
"$UMBRA_READONLY_USER" \
|
|
"$UMBRA_BACKUP_USER"; do
|
|
password=""
|
|
login="LOGIN"
|
|
[[ "$role" == "$UMBRA_OWNER_ROLE" ]] && login="NOLOGIN"
|
|
if [[ "$role" != "$UMBRA_OWNER_ROLE" ]]; then
|
|
if ((DRY_RUN)); then
|
|
password="$(random_secret)"
|
|
else
|
|
password="$(read_secret "${role^^}_PASSWORD" 2>/dev/null || true)"
|
|
[[ -n "$password" ]] || password="$(random_secret)"
|
|
fi
|
|
write_secret_line "${role^^}_PASSWORD" "$password"
|
|
fi
|
|
|
|
if ((DRY_RUN)); then
|
|
printf '[dry-run] ensure PostgreSQL role %s (%s)\n' "$role" "$login"
|
|
continue
|
|
fi
|
|
|
|
if [[ -n "$password" ]]; then
|
|
printf 'DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = %s) THEN CREATE ROLE %s %s PASSWORD %s; ELSE ALTER ROLE %s %s PASSWORD %s; END IF; END $$;\n' \
|
|
"'$role'" "$role" "$login" "'$password'" "$role" "$login" "'$password'" \
|
|
| psql_as_postgres postgres >/dev/null
|
|
else
|
|
printf 'DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = %s) THEN CREATE ROLE %s %s; END IF; END $$;\n' \
|
|
"'$role'" "$role" "$login" \
|
|
| psql_as_postgres postgres >/dev/null
|
|
fi
|
|
done
|
|
}
|
|
|
|
ensure_database() {
|
|
log "Creating enterprise database if absent"
|
|
if ((DRY_RUN)); then
|
|
printf '[dry-run] ensure database %s owned by %s\n' "$UMBRA_DB_NAME" "$UMBRA_OWNER_ROLE"
|
|
return
|
|
fi
|
|
if ! psql_as_postgres postgres -tAc "SELECT 1 FROM pg_database WHERE datname = '$UMBRA_DB_NAME'" | grep -q 1; then
|
|
runuser -u "$UMBRA_PG_SUPERUSER" -- createdb --owner="$UMBRA_OWNER_ROLE" "$UMBRA_DB_NAME"
|
|
fi
|
|
psql_as_postgres postgres -c "REVOKE ALL ON DATABASE \"$UMBRA_DB_NAME\" FROM PUBLIC" >/dev/null
|
|
psql_as_postgres postgres -c "GRANT CONNECT ON DATABASE \"$UMBRA_DB_NAME\" TO \"$UMBRA_MIGRATOR_USER\", \"$UMBRA_API_USER\", \"$UMBRA_WORKER_USER\", \"$UMBRA_READONLY_USER\"" >/dev/null
|
|
psql_as_postgres postgres -c "GRANT CONNECT ON DATABASE \"$UMBRA_DB_NAME\" TO \"$UMBRA_BACKUP_USER\"" >/dev/null
|
|
}
|
|
|
|
apply_schema() {
|
|
local schema_file="${SCRIPT_DIR}/postgres/001_control_plane.sql"
|
|
[[ -f "$schema_file" ]] || die "Schema file is missing: $schema_file"
|
|
log "Applying core identity/tenancy schema"
|
|
if ((DRY_RUN)); then
|
|
printf '[dry-run] psql -d %s -f %s\n' "$UMBRA_DB_NAME" "$schema_file"
|
|
return
|
|
fi
|
|
/bin/cat -- "$schema_file" | psql_as_postgres "$UMBRA_DB_NAME" >/dev/null
|
|
log "Core schema applied"
|
|
}
|
|
|
|
write_nonsecret_runtime_config() {
|
|
local config_file="$UMBRA_CONFIG_DIR/control-plane.env"
|
|
run install -d -m 0750 -o root -g "$UMBRA_SERVICE_GROUP" "$UMBRA_CONFIG_DIR"
|
|
if ((DRY_RUN)); then
|
|
printf '[dry-run] write non-secret runtime configuration to %s\n' "$config_file"
|
|
return
|
|
fi
|
|
cat > "${config_file}.tmp" <<EOF
|
|
UMBRA_PUBLIC_URL=$UMBRA_PUBLIC_URL
|
|
UMBRA_HOSTNAME=$UMBRA_HOSTNAME
|
|
UMBRA_DB_NAME=$UMBRA_DB_NAME
|
|
UMBRA_DB_HOST=$UMBRA_DB_HOST
|
|
UMBRA_DB_PORT=$UMBRA_DB_PORT
|
|
UMBRA_QWEN_URL=$UMBRA_QWEN_URL
|
|
UMBRA_APP_ROOT=$UMBRA_APP_ROOT
|
|
UMBRA_DATA_ROOT=$UMBRA_DATA_ROOT
|
|
EOF
|
|
chown root:"$UMBRA_SERVICE_GROUP" "${config_file}.tmp"
|
|
chmod 0640 "${config_file}.tmp"
|
|
mv -f "${config_file}.tmp" "$config_file"
|
|
}
|
|
|
|
create_bootstrap_admin() {
|
|
[[ "$UMBRA_CREATE_BOOTSTRAP_ADMIN" -eq 1 ]] || { log "Bootstrap admin creation disabled"; return; }
|
|
local token token_hash
|
|
if [[ -f "$UMBRA_BOOTSTRAP_TOKEN_FILE" ]]; then
|
|
token="$(cat "$UMBRA_BOOTSTRAP_TOKEN_FILE")"
|
|
else
|
|
token="$(random_secret)"
|
|
if ((DRY_RUN)); then
|
|
printf '[dry-run] create one-time admin token at %s\n' "$UMBRA_BOOTSTRAP_TOKEN_FILE"
|
|
return
|
|
fi
|
|
printf '%s\n' "$token" > "$UMBRA_BOOTSTRAP_TOKEN_FILE"
|
|
chown root:root "$UMBRA_BOOTSTRAP_TOKEN_FILE"
|
|
chmod 0600 "$UMBRA_BOOTSTRAP_TOKEN_FILE"
|
|
fi
|
|
[[ "$token" =~ ^[a-f0-9]{64}$ ]] || die "Bootstrap token file has an unexpected format"
|
|
token_hash="$(printf '%s' "$token" | sha256sum | awk '{print $1}')"
|
|
log "Creating/confirming bootstrap platform administrator"
|
|
if ((DRY_RUN)); then
|
|
printf '[dry-run] create administrator %s and organization %s\n' "$UMBRA_BOOTSTRAP_ADMIN_EMAIL" "$UMBRA_ORGANIZATION_NAME"
|
|
return
|
|
fi
|
|
psql_as_postgres "$UMBRA_DB_NAME" \
|
|
-v admin_email="$UMBRA_BOOTSTRAP_ADMIN_EMAIL" \
|
|
-v token_hash="$token_hash" \
|
|
-v organization_name="$UMBRA_ORGANIZATION_NAME" \
|
|
-v organization_slug="$UMBRA_ORGANIZATION_SLUG" <<'SQL' >/dev/null
|
|
INSERT INTO users (email, display_name, auth_provider, auth_subject, status, mfa_required)
|
|
VALUES (:'admin_email', :'admin_email', 'bootstrap', 'bootstrap:' || gen_random_uuid()::text, 'invited', true)
|
|
ON CONFLICT (email) DO UPDATE
|
|
SET display_name = EXCLUDED.display_name,
|
|
auth_provider = 'bootstrap',
|
|
mfa_required = true,
|
|
updated_at = now();
|
|
|
|
INSERT INTO organizations (name, slug)
|
|
VALUES (:'organization_name', :'organization_slug')
|
|
ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name;
|
|
|
|
INSERT INTO user_roles (user_id, role_id, scope, organization_id, project_id, created_by)
|
|
SELECT u.id, r.id, 'platform', NULL, NULL, u.id
|
|
FROM users u
|
|
JOIN roles r ON r.scope = 'platform' AND r.name = 'platform_admin'
|
|
WHERE u.email = :'admin_email'
|
|
ON CONFLICT DO NOTHING;
|
|
|
|
INSERT INTO user_roles (user_id, role_id, scope, organization_id, project_id, created_by)
|
|
SELECT u.id, r.id, 'organization', o.id, NULL, u.id
|
|
FROM users u
|
|
JOIN organizations o ON o.slug = :'organization_slug'
|
|
JOIN roles r ON r.scope = 'organization' AND r.name = 'organization_admin'
|
|
WHERE u.email = :'admin_email'
|
|
ON CONFLICT DO NOTHING;
|
|
|
|
INSERT INTO bootstrap_tokens (user_id, token_hash, expires_at)
|
|
SELECT id, :'token_hash', now() + interval '24 hours'
|
|
FROM users
|
|
WHERE email = :'admin_email'
|
|
ON CONFLICT (token_hash) DO NOTHING;
|
|
SQL
|
|
log "Bootstrap token stored root-only at $UMBRA_BOOTSTRAP_TOKEN_FILE"
|
|
}
|
|
|
|
apply_mode() {
|
|
if ((DRY_RUN == 0)); then
|
|
require_root
|
|
log "Beginning phase-one enterprise bootstrap"
|
|
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
|
|
die "Another bootstrap process holds $LOCK_DIR"
|
|
fi
|
|
trap 'rmdir "$LOCK_DIR" 2>/dev/null || true' EXIT
|
|
touch "$LOG_FILE"
|
|
chmod 0640 "$LOG_FILE"
|
|
else
|
|
log "Beginning phase-one enterprise bootstrap dry-run"
|
|
fi
|
|
install_packages
|
|
ensure_group_and_users
|
|
ensure_directories
|
|
ensure_uv
|
|
ensure_postgres_cluster
|
|
ensure_database_roles
|
|
ensure_database
|
|
apply_schema
|
|
write_nonsecret_runtime_config
|
|
create_bootstrap_admin
|
|
log "Phase-one bootstrap completed"
|
|
log "Application code, systemd units, Qwen, Docker, and firewall were not modified"
|
|
}
|
|
|
|
status_mode() {
|
|
log "Umbra enterprise installation status"
|
|
printf 'host: '; hostname -f 2>/dev/null || hostname
|
|
printf 'dns: '; getent ahosts "$UMBRA_HOSTNAME" 2>/dev/null || true
|
|
printf 'postgres: '; command -v pg_lsclusters >/dev/null 2>&1 && pg_lsclusters || echo 'not installed'
|
|
printf 'api user: '; id "$UMBRA_API_USER" 2>/dev/null || echo 'not installed'
|
|
printf 'worker user: '; id "$UMBRA_WORKER_USER" 2>/dev/null || echo 'not installed'
|
|
systemctl is-active postgresql 2>/dev/null || true
|
|
printf 'qwen port: '; (curl -sS --max-time 3 "$UMBRA_QWEN_URL/health" 2>&1 | head -c 300) || true; printf '\n'
|
|
}
|
|
|
|
verify_mode() {
|
|
log "Verifying phase-one installation"
|
|
require_command psql
|
|
require_root
|
|
pg_isready -h "$UMBRA_DB_HOST" -p "$UMBRA_DB_PORT" -d "$UMBRA_DB_NAME"
|
|
psql_as_postgres "$UMBRA_DB_NAME" -v ON_ERROR_STOP=1 -c 'SELECT version, applied_at FROM schema_migrations ORDER BY version' -c 'SELECT count(*) AS organizations FROM organizations' -c 'SELECT count(*) AS users FROM users'
|
|
systemctl is-active postgresql
|
|
log "Verification completed"
|
|
}
|
|
|
|
main() {
|
|
load_config
|
|
validate_config
|
|
check_tools
|
|
case "$MODE" in
|
|
check) check_host ;;
|
|
apply) apply_mode ;;
|
|
status) status_mode ;;
|
|
verify) verify_mode ;;
|
|
*) die "Unknown mode: $MODE" ;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|