Merge pull request 'Standardiser les scripts créateurs de LXC (template, autologin, update)' (#16) from chore/standardize-lxc-scripts into main
Reviewed-on: #16
This commit was merged in pull request #16.
This commit is contained in:
@@ -9,7 +9,7 @@ These scripts automate the deployment of personal infrastructure components. The
|
||||
- **Self-contained**: No external dependencies beyond standard Debian packages
|
||||
- **Idempotent-ish**: Safe to re-run (where possible)
|
||||
- **Curl-friendly**: Designed for one-liner deployment from a fresh server
|
||||
- **Multi-OS**: Supports Debian and Alpine-based deployments
|
||||
- **Multi-OS**: Supports Debian and Alpine-based deployments, chosen per-script based on that service's requirements
|
||||
- **Loopback by default**: Services bind to `127.0.0.1`; Tailscale handles the reverse proxy and TLS termination
|
||||
- **Log hygiene**: Every long-running service ships with a `logrotate` config (no unbounded log files)
|
||||
- **Console auto-login**: Proxmox LXCs are configured for root auto-login on `tty1` (fast `pct enter` and Web UI shell access)
|
||||
@@ -23,11 +23,10 @@ These scripts automate the deployment of personal infrastructure components. The
|
||||
| [`netlab/install.sh`](netlab/) | Network lab with ContainerLab | `curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/netlab/install.sh` \| `bash` |
|
||||
| [`gitea-runner/install.sh`](gitea-runner/) | Gitea Act Runner on Alpine LXC (Proxmox) | `bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/gitea-runner/install.sh)"` |
|
||||
| [`openbao/install.sh`](openbao/) | OpenBao secrets manager on Alpine LXC (Proxmox) | `bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/openbao/install.sh)"` |
|
||||
| [`ferretdb/install.sh`](ferretdb/) | FerretDB (MongoDB-compatible) on Debian LXC (Proxmox) | `bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/ferretdb/install.sh)"` |
|
||||
| [`komodo/install.sh`](komodo/) | Komodo (Docker + MongoDB) on Alpine VM | `bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/komodo/install.sh)"` |
|
||||
|
||||
### Requirements
|
||||
|
||||
- Fresh Debian 12/13 installation (proxy, netlab) or Proxmox VE host (gitea-runner, openbao, ferretdb) or Alpine VM (komodo)
|
||||
- User with sudo privileges (do not run as root) — except gitea-runner, openbao, ferretdb, and komodo which run as root
|
||||
- Fresh Debian 12/13 installation (proxy, netlab) or Proxmox VE host (gitea-runner, openbao) or Alpine VM (komodo)
|
||||
- User with sudo privileges (do not run as root) — except gitea-runner, openbao, and komodo which run as root
|
||||
- Internet access
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
# FerretDB
|
||||
|
||||
Automated installation and update script for [FerretDB](https://www.ferretdb.io) — a truly
|
||||
open-source, MongoDB-compatible database — running inside a **Debian LXC** on Proxmox.
|
||||
|
||||
A drop-in MongoDB replacement for any app that speaks the MongoDB wire protocol (e.g.
|
||||
[LibreChat](https://www.librechat.ai)) without running MongoDB itself. The script installs
|
||||
and exposes the database only; wiring it into an application is left to that app's own
|
||||
configuration (a separate script, or manually).
|
||||
|
||||
### Why Debian and not Alpine?
|
||||
|
||||
FerretDB v2 is two pieces:
|
||||
|
||||
1. the **FerretDB proxy** (a static Go binary), and
|
||||
2. **PostgreSQL + Microsoft's DocumentDB extension**, the mandatory storage engine.
|
||||
|
||||
The DocumentDB extension is a compiled C PostgreSQL extension and is published **only** as
|
||||
`deb`/`rpm` packages (`deb11`, `deb12`, `ubuntu`, `rhel`) — there is **no Alpine/musl build**.
|
||||
So, unlike the `openbao`/`gitea-runner` Alpine LXCs in this repo, this stack runs on Debian 12
|
||||
(`deb12`, the newest target the extension ships for).
|
||||
|
||||
### Features
|
||||
|
||||
Single script, automatic mode selection:
|
||||
|
||||
| Context | Action |
|
||||
| ----------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| From Proxmox host, no existing FerretDB container | Detects newest Debian template, creates LXC, installs PostgreSQL + DocumentDB + FerretDB |
|
||||
| From Proxmox host, FerretDB container already present | Reuses the existing LXC, refreshes packages, upgrades the FerretDB stack to latest |
|
||||
| From inside an LXC, no `ferretdb` binary | Installs the full stack from scratch |
|
||||
| From inside an LXC, `ferretdb` already present | Updates the packages only (no config / role / data changes) |
|
||||
|
||||
The container is identified by hostname **and** the `ferretdb` tag, so it is re-found across
|
||||
reruns even if the CTID was auto-allocated the first time.
|
||||
|
||||
### Requirements
|
||||
|
||||
- Proxmox VE host with `pveam`, `pct`, `pvesh`, `jq` available
|
||||
- Internet access from both the host (template download) and the LXC (package downloads)
|
||||
- Script must be run as **root** on the Proxmox host (enforced; the Web UI shell qualifies)
|
||||
|
||||
### Usage
|
||||
|
||||
#### Full install (from Proxmox shell)
|
||||
|
||||
```bash
|
||||
bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/ferretdb/install.sh)"
|
||||
```
|
||||
|
||||
The script prints the generated password and the ready-to-paste `MONGO_URI` at the end.
|
||||
**Save them** — the password is not persisted on the Proxmox host.
|
||||
|
||||
Re-running the exact same command later upgrades packages inside the LXC and brings the
|
||||
FerretDB stack to the latest release, without touching the config, role, or PostgreSQL data.
|
||||
|
||||
#### Customisation
|
||||
|
||||
Every parameter is exposed as an environment variable:
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ---------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `CTID` | auto | Container ID (auto-allocated via `pvesh get /cluster/nextid`) |
|
||||
| `FERRETDB_HOSTNAME` | `ferretdb` | LXC hostname |
|
||||
| `TEMPLATE` | auto-detected | Debian template; auto-detected from `pveam available` |
|
||||
| `STORAGE` | `local-lvm` | Proxmox storage for the LXC root disk |
|
||||
| `TEMPLATE_STORAGE` | `local` | Storage where Debian templates live |
|
||||
| `CORES` | `2` | vCPU cores |
|
||||
| `RAM` | `2048` | RAM in MiB (Postgres + FerretDB) |
|
||||
| `DISK` | `16` | Root disk size in GB |
|
||||
| `BRIDGE` | `vmbr0` | Network bridge |
|
||||
| `LXC_TAG` | `ferretdb` | Stable tag used to re-discover the container |
|
||||
| `PG_VERSION` | `17` | PostgreSQL major version (from PGDG) |
|
||||
| `DOCUMENTDB_TAG` | `latest` | DocumentDB release tag (couples documentdb + FerretDB versions); pin e.g. `v0.107.0-ferretdb-2.7.0` |
|
||||
| `DOCUMENTDB_DISTRO` | `deb12` | Distro target in the documentdb deb filename. Escape hatch for a future `deb13` (set with `TEMPLATE`) |
|
||||
| `FERRETDB_LISTEN_ADDR` | `0.0.0.0:27017` | TCP listener. Exposed on all interfaces — it is a database other hosts must reach. |
|
||||
| `FERRETDB_USER` | `ferretdb` | App user — both the PostgreSQL role and the MongoDB user clients authenticate as |
|
||||
| `FERRETDB_PASSWORD` | auto-generated | Auto-generated (`openssl rand -hex 24`) when unset; printed in the final summary |
|
||||
| `TS_AUTHKEY` | _(unset)_ | Pre-auth key (generate at <https://login.tailscale.com/admin/settings/keys>). If unset, finish `tailscale up` manually inside the LXC. |
|
||||
|
||||
```bash
|
||||
CTID=220 FERRETDB_HOSTNAME=mongo RAM=4096 DISK=32 \
|
||||
bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/ferretdb/install.sh)"
|
||||
```
|
||||
|
||||
#### Connecting a client
|
||||
|
||||
Any MongoDB driver or tool connects with a standard connection string (the script prints the
|
||||
exact one, with the generated password, at the end of the install):
|
||||
|
||||
```
|
||||
mongodb://ferretdb:<password>@<lxc-ip-or-tailnet-fqdn>:27017/
|
||||
```
|
||||
|
||||
Append a database name to target one (e.g. `…:27017/myapp`); it is created on first write.
|
||||
Wiring this into a specific application (setting its Mongo connection string, disabling any
|
||||
bundled MongoDB it ships, etc.) is intentionally out of scope — do it from that app's own
|
||||
config or a dedicated script.
|
||||
|
||||
#### Tailscale
|
||||
|
||||
The LXC joins the tailnet (`tailscale up --ssh`) so a client on another node can reach the
|
||||
database over the tailnet. Unlike the `openbao` script there is **no `tailscale serve`** — the
|
||||
MongoDB wire protocol is raw TCP, not HTTP, so the listener is exposed directly on
|
||||
`0.0.0.0:27017` (LAN + tailnet) by design.
|
||||
|
||||
If `TS_AUTHKEY` was supplied the node is brought up automatically; otherwise finish it
|
||||
manually inside the LXC:
|
||||
|
||||
```bash
|
||||
pct enter <CTID>
|
||||
tailscale up --ssh --hostname ferretdb
|
||||
tailscale status # prints the tailnet FQDN
|
||||
```
|
||||
|
||||
> Because the listener is on `0.0.0.0`, restrict access with your tailnet ACLs and/or a host
|
||||
> firewall — anyone who can route to TCP 27017 can attempt to authenticate.
|
||||
|
||||
#### Update (from inside the LXC)
|
||||
|
||||
```bash
|
||||
curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/ferretdb/install.sh | bash
|
||||
```
|
||||
|
||||
The script auto-detects the presence of `/usr/bin/ferretdb` and switches to update mode:
|
||||
packages are upgraded (including the DocumentDB extension via
|
||||
`ALTER EXTENSION documentdb UPDATE`) and the services are restarted. The config, the
|
||||
PostgreSQL role, and the data are left untouched.
|
||||
|
||||
### Architecture
|
||||
|
||||
- **OS**: latest Debian LXC template (auto-detected), unprivileged, `nesting=1`, `/dev/net/tun` passthrough for Tailscale
|
||||
- **Storage engine**: PostgreSQL `17` (PGDG) + the DocumentDB extension (`pg_documentdb`, `pg_cron`), loopback-only on `127.0.0.1:5432`
|
||||
- **Proxy**: official `ferretdb` deb from `github.com/FerretDB/FerretDB`, systemd unit, listening on `0.0.0.0:27017`
|
||||
- **Auth**: the app user is provisioned through `documentdb_api.create_user` (roles `clusterAdmin` + `readWriteAnyDatabase`), **not** a plain `CREATE ROLE`. DocumentDB builds the SCRAM-SHA-256 verifier with its own 28-byte salt (`documentdb.scramDefaultSaltLen`); a native PostgreSQL role would store a 16-byte salt that MongoDB clients reject (`invalid salt length of 16 in sasl step2`). FerretDB connects to PostgreSQL as the same user.
|
||||
- **Network**: FerretDB exposed on `0.0.0.0:27017`; Tailscale runs in the LXC for tailnet reachability (no `serve`)
|
||||
- **Config**: `/etc/postgresql/17/main/conf.d/documentdb.conf` (extension settings) and `/etc/systemd/system/ferretdb.service.d/override.conf` (`FERRETDB_POSTGRESQL_URL`, `FERRETDB_LISTEN_ADDR`)
|
||||
- **Logs**: PostgreSQL via its stock `logrotate`; FerretDB via journald, capped at `SystemMaxUse=200M`
|
||||
- **Version tracking**: `/opt/ferretdb_version.txt` records the installed DocumentDB tag for idempotent reruns
|
||||
@@ -1,676 +0,0 @@
|
||||
#!/bin/bash
|
||||
# install.sh - FerretDB: LXC creation, installation & update
|
||||
# Usage:
|
||||
# From Proxmox host : bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/ferretdb/install.sh)"
|
||||
# From inside LXC : bash /root/install.sh (updates packages)
|
||||
#
|
||||
# Single entrypoint, three automatic modes:
|
||||
# 1. Proxmox host, no existing container -> create Debian LXC + install FerretDB
|
||||
# 2. Proxmox host, container already present -> update packages + upgrade FerretDB
|
||||
# 3. Inside an LXC -> install if missing, otherwise update
|
||||
#
|
||||
# FerretDB v2 is a MongoDB wire-protocol proxy backed by PostgreSQL + the
|
||||
# DocumentDB extension. The extension is a compiled C PostgreSQL extension and
|
||||
# is only published as deb/rpm packages (no Alpine/musl build), so this stack
|
||||
# runs on Debian — unlike the openbao/gitea-runner Alpine LXCs in this repo.
|
||||
#
|
||||
# The package install/upgrade logic lives in a single reusable function
|
||||
# (install_or_upgrade_packages) shared by both the create and update paths.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Force an always-present locale. A fresh Debian template has not generated the
|
||||
# host's locale (e.g. fr_FR.UTF-8 inherited through pct exec), so apt, perl and
|
||||
# apt-listchanges warn loudly about it. C.UTF-8 ships with glibc and is always
|
||||
# valid; this silences the noise without installing extra locales.
|
||||
export LC_ALL=C.UTF-8
|
||||
export LANG=C.UTF-8
|
||||
|
||||
# --- Config (override via environment) ---
|
||||
CTID="${CTID:-}"
|
||||
HOSTNAME_LXC="${FERRETDB_HOSTNAME:-ferretdb}"
|
||||
TEMPLATE="${TEMPLATE:-}" # auto-detected when empty
|
||||
STORAGE="${STORAGE:-local-lvm}"
|
||||
TEMPLATE_STORAGE="${TEMPLATE_STORAGE:-local}"
|
||||
CORES="${CORES:-2}"
|
||||
RAM="${RAM:-2048}" # Postgres needs more headroom than openbao
|
||||
DISK="${DISK:-16}"
|
||||
BRIDGE="${BRIDGE:-vmbr0}"
|
||||
LXC_TAG="${LXC_TAG:-ferretdb}" # stable identifier for the container
|
||||
PG_VERSION="${PG_VERSION:-17}" # PostgreSQL major version (PGDG)
|
||||
# DocumentDB release tag couples both pieces: it encodes the documentdb package
|
||||
# version AND the matching FerretDB version. "latest" resolves both at once.
|
||||
DOCUMENTDB_TAG="${DOCUMENTDB_TAG:-latest}"
|
||||
# Distro target embedded in the documentdb deb filename. Only deb11/deb12 exist
|
||||
# today (no deb13). Escape hatch: when upstream ships deb13, set this + TEMPLATE
|
||||
# to move to trixie without editing the script.
|
||||
DOCUMENTDB_DISTRO="${DOCUMENTDB_DISTRO:-deb12}"
|
||||
DOCUMENTDB_RELEASES_URL="${DOCUMENTDB_RELEASES_URL:-https://api.github.com/repos/FerretDB/documentdb/releases}"
|
||||
# As a database we deliberately expose the listener on all interfaces so other
|
||||
# hosts can reach it over the LAN / tailnet. PostgreSQL stays local.
|
||||
FERRETDB_LISTEN_ADDR="${FERRETDB_LISTEN_ADDR:-0.0.0.0:27017}"
|
||||
# Application credentials. The same user/password is both the PostgreSQL role
|
||||
# FerretDB connects with AND the MongoDB user clients authenticate as.
|
||||
FERRETDB_USER="${FERRETDB_USER:-ferretdb}"
|
||||
FERRETDB_PASSWORD="${FERRETDB_PASSWORD:-}" # auto-generated when empty
|
||||
# Optional: pre-authorise the LXC's Tailscale non-interactively.
|
||||
# Generate at https://login.tailscale.com/admin/settings/keys
|
||||
TS_AUTHKEY="${TS_AUTHKEY:-}"
|
||||
# SCRIPT_URL is what the host-side flow pipes into the LXC. Override it when
|
||||
# testing from a non-main branch.
|
||||
SCRIPT_URL="${SCRIPT_URL:-https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/ferretdb/install.sh}"
|
||||
VERSION_FILE="${VERSION_FILE:-/opt/ferretdb_version.txt}"
|
||||
|
||||
# --- Colors ---
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Logs go to stderr so callers can safely use $(fn) without capturing log noise.
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $1" >&2; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" >&2; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
|
||||
|
||||
# ============================================================
|
||||
# Generic helpers
|
||||
# ============================================================
|
||||
require_root() {
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
log_error "This script must be run as root (current uid: $(id -u))."
|
||||
log_error "On Proxmox, launch it from the host shell or via the Web UI shell, both of which run as root."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Debian packages use dpkg-style arch names (amd64, arm64), which is also what
|
||||
# both the FerretDB and DocumentDB release assets are named with.
|
||||
get_arch() {
|
||||
case "$(uname -m)" in
|
||||
x86_64) echo "amd64" ;;
|
||||
aarch64) echo "arm64" ;;
|
||||
*) log_error "Unsupported architecture: $(uname -m)"; exit 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Resolve the DocumentDB release tag into the two coupled versions it encodes.
|
||||
# Sets globals: DOC_TAG (full tag), DOC_PKG_VER (documentdb pkg version),
|
||||
# FERRET_VER (matching FerretDB version, no leading v).
|
||||
# Tag shape: v0.107.0-ferretdb-2.7.0
|
||||
resolve_versions() {
|
||||
local endpoint tag
|
||||
if [[ "$DOCUMENTDB_TAG" == "latest" ]]; then
|
||||
endpoint="${DOCUMENTDB_RELEASES_URL}/latest"
|
||||
else
|
||||
endpoint="${DOCUMENTDB_RELEASES_URL}/tags/${DOCUMENTDB_TAG}"
|
||||
fi
|
||||
tag=$(curl -fsSL "$endpoint" | jq -r '.tag_name')
|
||||
if [[ -z "$tag" || "$tag" == "null" ]]; then
|
||||
log_error "Failed to resolve DocumentDB release '${DOCUMENTDB_TAG}' from GitHub API."
|
||||
exit 1
|
||||
fi
|
||||
DOC_TAG="$tag"
|
||||
DOC_PKG_VER="${tag#v}" # 0.107.0-ferretdb-2.7.0
|
||||
DOC_PKG_VER="${DOC_PKG_VER%%-ferretdb-*}" # 0.107.0
|
||||
FERRET_VER="${tag##*-ferretdb-}" # 2.7.0
|
||||
if [[ -z "$DOC_PKG_VER" || -z "$FERRET_VER" || "$FERRET_VER" == "$tag" ]]; then
|
||||
log_error "Could not parse DocumentDB tag '${tag}' (expected vX-ferretdb-Y)."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Reusable: install or upgrade PostgreSQL + DocumentDB + FerretDB.
|
||||
# Used by both fresh-install and update flows. Idempotent.
|
||||
# ============================================================
|
||||
install_or_upgrade_packages() {
|
||||
local arch current doc_url ferret_url tmpdir doc_deb_name doc_full_ver
|
||||
resolve_versions
|
||||
arch=$(get_arch)
|
||||
|
||||
current=""
|
||||
if [[ -f "$VERSION_FILE" ]]; then
|
||||
current=$(cat "$VERSION_FILE")
|
||||
fi
|
||||
|
||||
if [[ "$current" == "$DOC_TAG" && -x /usr/bin/ferretdb ]]; then
|
||||
log_info "FerretDB stack already at ${DOC_TAG}, nothing to do."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Ensure the PGDG repo is present so the requested PostgreSQL major exists.
|
||||
if [[ ! -f /etc/apt/sources.list.d/pgdg.list ]]; then
|
||||
log_info "Adding the PostgreSQL APT (PGDG) repository..."
|
||||
install -d -m 0755 /usr/share/keyrings
|
||||
curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \
|
||||
| gpg --dearmor -o /usr/share/keyrings/postgresql.gpg
|
||||
echo "deb [signed-by=/usr/share/keyrings/postgresql.gpg] https://apt.postgresql.org/pub/repos/apt $(. /etc/os-release && echo "$VERSION_CODENAME")-pgdg main" \
|
||||
> /etc/apt/sources.list.d/pgdg.list
|
||||
apt-get update >/dev/null
|
||||
fi
|
||||
|
||||
log_info "Installing PostgreSQL ${PG_VERSION} + pg_cron..."
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||
"postgresql-${PG_VERSION}" "postgresql-${PG_VERSION}-cron" >/dev/null
|
||||
|
||||
# documentdb deb naming: deb12-postgresql-17-documentdb_0.107.0.ferretdb.2.7.0_amd64.deb
|
||||
doc_full_ver="${DOC_PKG_VER}.ferretdb.${FERRET_VER}"
|
||||
doc_deb_name="${DOCUMENTDB_DISTRO}-postgresql-${PG_VERSION}-documentdb_${doc_full_ver}_${arch}.deb"
|
||||
doc_url="https://github.com/FerretDB/documentdb/releases/download/${DOC_TAG}/${doc_deb_name}"
|
||||
ferret_url="https://github.com/FerretDB/FerretDB/releases/download/v${FERRET_VER}/ferretdb-${arch}-linux.deb"
|
||||
|
||||
tmpdir=$(mktemp -d)
|
||||
log_info "Downloading DocumentDB extension (${doc_deb_name})..."
|
||||
curl -fsSL "$doc_url" -o "${tmpdir}/documentdb.deb"
|
||||
log_info "Downloading FerretDB ${FERRET_VER} (${arch})..."
|
||||
curl -fsSL "$ferret_url" -o "${tmpdir}/ferretdb.deb"
|
||||
|
||||
log_info "Installing DocumentDB extension + FerretDB (apt resolves dependencies)..."
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||
"${tmpdir}/documentdb.deb" "${tmpdir}/ferretdb.deb" >/dev/null
|
||||
|
||||
# If the extension is already created (update path), bring it to the new version.
|
||||
if su -s /bin/sh postgres -c "psql -tAc \"SELECT 1 FROM pg_extension WHERE extname='documentdb'\" -d postgres" 2>/dev/null | grep -q 1; then
|
||||
log_info "Updating documentdb extension to ${DOC_PKG_VER}..."
|
||||
su -s /bin/sh postgres -c "psql -d postgres -c 'ALTER EXTENSION documentdb UPDATE;'" >/dev/null 2>&1 || \
|
||||
log_warn "ALTER EXTENSION documentdb UPDATE failed — check after restart."
|
||||
fi
|
||||
|
||||
echo "$DOC_TAG" > "$VERSION_FILE"
|
||||
rm -rf "$tmpdir"
|
||||
|
||||
log_info "Installed FerretDB: $(/usr/bin/ferretdb --version 2>&1 | head -n1 || true)"
|
||||
|
||||
# Restart services if they already exist (update path); the install path
|
||||
# enables them explicitly after configuration.
|
||||
if systemctl list-unit-files ferretdb.service >/dev/null 2>&1; then
|
||||
systemctl restart postgresql 2>/dev/null || true
|
||||
systemctl restart ferretdb 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Reusable: bring Tailscale up so the LXC joins the tailnet.
|
||||
# Unlike openbao we do NOT use 'tailscale serve' — FerretDB speaks the raw
|
||||
# MongoDB wire protocol (TCP), not HTTP, so serve does not apply. The DB is
|
||||
# reachable directly on 0.0.0.0:27017 over the LAN / tailnet.
|
||||
# ============================================================
|
||||
configure_tailscale() {
|
||||
if ! command -v tailscale >/dev/null 2>&1; then
|
||||
log_warn "tailscale CLI not found, skipping tailnet setup."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local backend_state
|
||||
backend_state=$(tailscale status --json 2>/dev/null | jq -r '.BackendState // "unknown"')
|
||||
if [[ "$backend_state" == "Running" ]]; then
|
||||
log_info "Tailscale already up."
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -n "$TS_AUTHKEY" ]]; then
|
||||
log_info "Bringing Tailscale up with provided auth key..."
|
||||
tailscale up --authkey "$TS_AUTHKEY" --ssh --hostname "$HOSTNAME_LXC" \
|
||||
|| log_warn "tailscale up failed — run it manually inside the LXC."
|
||||
else
|
||||
log_warn "Tailscale not authenticated and TS_AUTHKEY was not supplied."
|
||||
log_warn "Finish setup inside the LXC with: tailscale up --ssh --hostname ${HOSTNAME_LXC}"
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Proxmox-host helpers
|
||||
# ============================================================
|
||||
|
||||
# The installer re-fetches itself inside the LXC from SCRIPT_URL. Verify it is
|
||||
# reachable on the host *before* creating any container, so a wrong branch/path
|
||||
# fails immediately with guidance instead of dying mid-install with a curl 404.
|
||||
preflight_script_url() {
|
||||
if curl -fsSL -o /dev/null "$SCRIPT_URL"; then
|
||||
return 0
|
||||
fi
|
||||
log_error "SCRIPT_URL is not reachable: ${SCRIPT_URL}"
|
||||
log_error "The installer re-fetches itself inside the LXC from SCRIPT_URL, so this"
|
||||
log_error "must resolve. If you are testing from a branch (not yet on main), pass it:"
|
||||
log_error " SCRIPT_URL=https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/<branch>/ferretdb/install.sh \\"
|
||||
log_error " bash -c \"\$(curl -fsSL \"\$SCRIPT_URL\")\""
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Detect newest Debian *12* LXC template available from the Proxmox repos.
|
||||
# Deliberately pinned to debian-12: the DocumentDB extension only ships a deb12
|
||||
# build (DOCUMENTDB_DISTRO default), and a newer template (e.g. debian-13) would
|
||||
# pair that deb against a different libicu soname. To move to trixie, set both
|
||||
# TEMPLATE and DOCUMENTDB_DISTRO yourself once upstream publishes a deb13 build.
|
||||
detect_latest_debian_template() {
|
||||
local tmpl
|
||||
tmpl=$(pveam available --section system 2>/dev/null \
|
||||
| awk '/^system[[:space:]]+debian-12-/ {print $2}' \
|
||||
| sort -V \
|
||||
| tail -n1)
|
||||
|
||||
if [[ -z "$tmpl" ]]; then
|
||||
log_warn "Could not find a debian-12 template via pveam; falling back to a known-good name."
|
||||
tmpl="debian-12-standard_12.7-1_amd64.tar.zst"
|
||||
fi
|
||||
log_info "Selected Debian template: $tmpl"
|
||||
echo "$tmpl"
|
||||
}
|
||||
|
||||
# Find an existing LXC by tag or hostname. Echoes CTID, returns 1 if none.
|
||||
find_existing_lxc() {
|
||||
local id host tags
|
||||
while read -r id _; do
|
||||
[[ -z "$id" || "$id" == "VMID" ]] && continue
|
||||
host=$(pct config "$id" 2>/dev/null | awk -F': ' '/^hostname:/ {print $2}' || true)
|
||||
tags=$(pct config "$id" 2>/dev/null | awk -F': ' '/^tags:/ {print $2}' || true)
|
||||
if [[ "$host" == "$HOSTNAME_LXC" ]] || [[ ",${tags//;/,}," == *",${LXC_TAG},"* ]]; then
|
||||
echo "$id"
|
||||
return 0
|
||||
fi
|
||||
done < <(pct list | awk 'NR>1 {print $1}')
|
||||
return 1
|
||||
}
|
||||
|
||||
ensure_template_present() {
|
||||
local tmpl="$1"
|
||||
if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$tmpl"; then
|
||||
log_info "Downloading template ${tmpl} to storage ${TEMPLATE_STORAGE}..."
|
||||
pveam update >/dev/null
|
||||
pveam download "$TEMPLATE_STORAGE" "$tmpl"
|
||||
else
|
||||
log_info "Template ${tmpl} already present on ${TEMPLATE_STORAGE}."
|
||||
fi
|
||||
}
|
||||
|
||||
# Pick next available CTID if user did not provide one.
|
||||
allocate_ctid() {
|
||||
pvesh get /cluster/nextid 2>/dev/null \
|
||||
|| pvesh get /cluster/resources --type vm --output-format json 2>/dev/null \
|
||||
| jq '[.[].vmid] | max + 1' \
|
||||
|| echo 100
|
||||
}
|
||||
|
||||
# Inject the script into the container and execute it in the requested mode.
|
||||
# Forwards the relevant runtime configuration through the environment so the
|
||||
# inner invocation produces the same config the user requested on the host.
|
||||
exec_in_lxc() {
|
||||
local ctid="$1"
|
||||
local mode="$2" # --install or --update
|
||||
|
||||
# Ensure base tooling exists inside the container before piping the script.
|
||||
pct exec "$ctid" -- sh -c "export DEBIAN_FRONTEND=noninteractive LC_ALL=C.UTF-8 LANG=C.UTF-8; apt-get update >/dev/null 2>&1; apt-get install -y bash curl jq ca-certificates >/dev/null 2>&1"
|
||||
curl -fsSL "$SCRIPT_URL" \
|
||||
| pct exec "$ctid" -- env \
|
||||
LC_ALL=C.UTF-8 \
|
||||
LANG=C.UTF-8 \
|
||||
SCRIPT_URL="$SCRIPT_URL" \
|
||||
PG_VERSION="$PG_VERSION" \
|
||||
DOCUMENTDB_TAG="$DOCUMENTDB_TAG" \
|
||||
DOCUMENTDB_DISTRO="$DOCUMENTDB_DISTRO" \
|
||||
FERRETDB_HOSTNAME="$HOSTNAME_LXC" \
|
||||
FERRETDB_LISTEN_ADDR="$FERRETDB_LISTEN_ADDR" \
|
||||
FERRETDB_USER="$FERRETDB_USER" \
|
||||
FERRETDB_PASSWORD="$FERRETDB_PASSWORD" \
|
||||
TS_AUTHKEY="$TS_AUTHKEY" \
|
||||
bash -s -- "$mode"
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# MODE: Proxmox host — create LXC + install
|
||||
# ============================================================
|
||||
create_lxc() {
|
||||
log_info "=== FerretDB — LXC creation ==="
|
||||
|
||||
# Generate the password on the host so we can both pass it in and print it.
|
||||
if [[ -z "$FERRETDB_PASSWORD" ]]; then
|
||||
FERRETDB_PASSWORD=$(openssl rand -hex 24)
|
||||
log_info "Generated FerretDB password (saved in the summary below)."
|
||||
fi
|
||||
|
||||
if [[ -z "$TEMPLATE" ]]; then
|
||||
TEMPLATE=$(detect_latest_debian_template)
|
||||
else
|
||||
log_info "Using user-provided template: $TEMPLATE"
|
||||
fi
|
||||
ensure_template_present "$TEMPLATE"
|
||||
|
||||
if [[ -z "$CTID" ]]; then
|
||||
CTID=$(allocate_ctid)
|
||||
log_info "Auto-selected CTID: $CTID"
|
||||
fi
|
||||
|
||||
log_info "Creating LXC ${CTID} (${HOSTNAME_LXC})..."
|
||||
pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" \
|
||||
--hostname "$HOSTNAME_LXC" \
|
||||
--cores "$CORES" \
|
||||
--memory "$RAM" \
|
||||
--rootfs "${STORAGE}:${DISK}" \
|
||||
--net0 "name=eth0,bridge=${BRIDGE},ip=dhcp" \
|
||||
--unprivileged 1 \
|
||||
--features "nesting=1" \
|
||||
--tags "infra-script,${LXC_TAG}" \
|
||||
--onboot 1 \
|
||||
--start 0
|
||||
|
||||
# Tailscale needs /dev/net/tun inside the unprivileged container.
|
||||
log_info "Adding /dev/net/tun passthrough for Tailscale..."
|
||||
cat >> "/etc/pve/lxc/${CTID}.conf" <<EOF
|
||||
lxc.cgroup2.devices.allow: c 10:200 rwm
|
||||
lxc.mount.entry: /dev/net dev/net none bind,create=dir
|
||||
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file
|
||||
EOF
|
||||
|
||||
log_info "Starting LXC ${CTID}..."
|
||||
pct start "$CTID"
|
||||
# Wait for network to come up
|
||||
local tries=0
|
||||
until pct exec "$CTID" -- sh -c "ip -4 addr show eth0 | grep -q 'inet '" 2>/dev/null; do
|
||||
tries=$((tries + 1))
|
||||
if (( tries > 30 )); then
|
||||
log_error "LXC ${CTID} did not acquire an IP after 30s."
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
log_info "Running installer inside LXC ${CTID}..."
|
||||
exec_in_lxc "$CTID" "--install"
|
||||
|
||||
local ip
|
||||
ip=$(pct exec "$CTID" -- ip -4 addr show eth0 2>/dev/null | awk '/inet /{print $2}' | cut -d/ -f1 || true)
|
||||
|
||||
echo ""
|
||||
log_info "========================================="
|
||||
log_info "LXC ${CTID} created successfully!"
|
||||
log_info "========================================="
|
||||
echo ""
|
||||
echo " Hostname : ${HOSTNAME_LXC}"
|
||||
echo " IP : ${ip:-pending}"
|
||||
echo " FerretDB : ${FERRETDB_LISTEN_ADDR}"
|
||||
echo ""
|
||||
echo "MongoDB connection string (point your client/app at it):"
|
||||
echo " mongodb://${FERRETDB_USER}:${FERRETDB_PASSWORD}@${ip:-<ip>}:27017/"
|
||||
echo ""
|
||||
echo "Store this password somewhere safe — it is not persisted on the host:"
|
||||
echo " user : ${FERRETDB_USER}"
|
||||
echo " password : ${FERRETDB_PASSWORD}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# MODE: Proxmox host — update existing LXC
|
||||
# ============================================================
|
||||
update_lxc() {
|
||||
local ctid="$1"
|
||||
log_info "=== FerretDB — updating existing LXC ${ctid} ==="
|
||||
|
||||
if ! pct status "$ctid" | grep -q running; then
|
||||
log_info "Starting LXC ${ctid}..."
|
||||
pct start "$ctid"
|
||||
sleep 3
|
||||
fi
|
||||
|
||||
log_info "Refreshing Debian packages inside LXC ${ctid}..."
|
||||
pct exec "$ctid" -- sh -c "export DEBIAN_FRONTEND=noninteractive LC_ALL=C.UTF-8 LANG=C.UTF-8; apt-get update >/dev/null && apt-get upgrade -y >/dev/null"
|
||||
|
||||
log_info "Upgrading FerretDB stack inside LXC ${ctid}..."
|
||||
exec_in_lxc "$ctid" "--update"
|
||||
|
||||
log_info "Update of LXC ${ctid} complete."
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# MODE: inside LXC — fresh install of FerretDB
|
||||
# ============================================================
|
||||
install_inside_lxc() {
|
||||
log_info "=== FerretDB — installation ==="
|
||||
|
||||
if [[ -z "$FERRETDB_PASSWORD" ]]; then
|
||||
FERRETDB_PASSWORD=$(openssl rand -hex 24 2>/dev/null || head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n')
|
||||
log_info "Generated FerretDB password (shown in the summary below)."
|
||||
fi
|
||||
|
||||
log_info "Updating package index..."
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update >/dev/null
|
||||
apt-get upgrade -y >/dev/null
|
||||
|
||||
log_info "Installing base dependencies..."
|
||||
apt-get install -y curl jq ca-certificates gnupg lsb-release sudo logrotate openssl >/dev/null
|
||||
|
||||
log_info "Installing Tailscale..."
|
||||
if ! command -v tailscale >/dev/null 2>&1; then
|
||||
curl -fsSL https://tailscale.com/install.sh | sh >/dev/null 2>&1 \
|
||||
|| log_warn "Tailscale install script failed — install it manually later."
|
||||
fi
|
||||
systemctl enable --now tailscaled >/dev/null 2>&1 \
|
||||
|| log_warn "tailscaled failed to start (is /dev/net/tun mapped into the LXC?)"
|
||||
|
||||
install_or_upgrade_packages
|
||||
|
||||
log_info "Configuring PostgreSQL for DocumentDB..."
|
||||
local pg_confd="/etc/postgresql/${PG_VERSION}/main/conf.d"
|
||||
mkdir -p "$pg_confd"
|
||||
# https://docs.ferretdb.io/installation/documentdb/deb/
|
||||
cat > "${pg_confd}/documentdb.conf" <<EOF
|
||||
# Managed by infra-scripts/ferretdb — DocumentDB extension settings.
|
||||
shared_preload_libraries = 'pg_cron,pg_documentdb_core,pg_documentdb'
|
||||
cron.database_name = 'postgres'
|
||||
|
||||
documentdb.enableCompact = true
|
||||
documentdb.enableLetAndCollationForQueryMatch = true
|
||||
documentdb.enableNowSystemVariable = true
|
||||
documentdb.enableSortbyIdPushDownToPrimaryKey = true
|
||||
documentdb.enableSchemaValidation = true
|
||||
documentdb.enableBypassDocumentValidation = true
|
||||
documentdb.enableUserCrud = true
|
||||
documentdb.maxUserLimit = 100
|
||||
|
||||
# Ensure any password we set is hashed with SCRAM-SHA-256 (PG default, set
|
||||
# explicitly so it is active before roles are provisioned).
|
||||
password_encryption = 'scram-sha-256'
|
||||
|
||||
# Postgres stays loopback-only; FerretDB (same LXC) is the network front door.
|
||||
listen_addresses = '127.0.0.1'
|
||||
EOF
|
||||
|
||||
log_info "Restarting PostgreSQL..."
|
||||
systemctl restart postgresql
|
||||
|
||||
log_info "Creating the documentdb extension..."
|
||||
su -s /bin/sh postgres -c "psql -v ON_ERROR_STOP=1 -d postgres" >/dev/null <<'SQL'
|
||||
CREATE EXTENSION IF NOT EXISTS documentdb CASCADE;
|
||||
SQL
|
||||
|
||||
# Provision the user THROUGH DocumentDB — never a plain CREATE ROLE ... PASSWORD.
|
||||
# DocumentDB builds the SCRAM-SHA-256 verifier with its own salt length
|
||||
# (documentdb.scramDefaultSaltLen = 28 bytes) via documentdb_api.create_user /
|
||||
# update_user. A native PostgreSQL role instead stores a 16-byte salt, which
|
||||
# MongoDB clients reject at SASL step2 with "invalid salt length of 16".
|
||||
# create_user only accepts a read-only role, or the clusterAdmin +
|
||||
# readWriteAnyDatabase pair we use here for full read/write (FerretDB/DocumentDB
|
||||
# commands/users.c::ValidateAndObtainUserRole). The spec is built with jq so the
|
||||
# password is JSON-escaped, then embedded in a $DDB$-dollar-quoted SQL literal.
|
||||
log_info "Provisioning MongoDB user '${FERRETDB_USER}' via DocumentDB (28-byte SCRAM salt)..."
|
||||
local role_exists cmd spec
|
||||
role_exists=$(su -s /bin/sh postgres -c \
|
||||
"psql -tAX -d postgres -c \"SELECT 1 FROM pg_roles WHERE rolname = '${FERRETDB_USER}'\"" 2>/dev/null || true)
|
||||
if [[ "$role_exists" == "1" ]]; then
|
||||
log_info "Role '${FERRETDB_USER}' already exists — resetting its password via DocumentDB."
|
||||
cmd="update_user"
|
||||
spec=$(jq -nc --arg u "$FERRETDB_USER" --arg p "$FERRETDB_PASSWORD" \
|
||||
'{updateUser:$u, pwd:$p}')
|
||||
else
|
||||
cmd="create_user"
|
||||
spec=$(jq -nc --arg u "$FERRETDB_USER" --arg p "$FERRETDB_PASSWORD" \
|
||||
'{createUser:$u, pwd:$p, roles:[{role:"clusterAdmin",db:"admin"},{role:"readWriteAnyDatabase",db:"admin"}]}')
|
||||
fi
|
||||
su -s /bin/sh postgres -c "psql -v ON_ERROR_STOP=1 -d postgres" >/dev/null <<SQL
|
||||
SELECT documentdb_api.${cmd}(\$DDB\$${spec}\$DDB\$);
|
||||
SQL
|
||||
|
||||
log_info "Writing FerretDB systemd override..."
|
||||
mkdir -p /etc/systemd/system/ferretdb.service.d
|
||||
cat > /etc/systemd/system/ferretdb.service.d/override.conf <<EOF
|
||||
[Service]
|
||||
Environment=FERRETDB_POSTGRESQL_URL=postgres://${FERRETDB_USER}:${FERRETDB_PASSWORD}@127.0.0.1:5432/postgres
|
||||
Environment=FERRETDB_LISTEN_ADDR=${FERRETDB_LISTEN_ADDR}
|
||||
Environment=FERRETDB_TELEMETRY=disable
|
||||
EOF
|
||||
chmod 600 /etc/systemd/system/ferretdb.service.d/override.conf
|
||||
|
||||
systemctl daemon-reload
|
||||
log_info "Enabling and starting services..."
|
||||
systemctl enable --now postgresql >/dev/null 2>&1 || true
|
||||
systemctl enable ferretdb >/dev/null 2>&1 || true
|
||||
# Explicit restart: the deb postinst may have already started ferretdb with
|
||||
# default settings, in which case 'enable --now' would not re-read our override.
|
||||
systemctl restart ferretdb || log_warn "ferretdb failed to start — check 'journalctl -u ferretdb'."
|
||||
|
||||
# --- Log hygiene ---
|
||||
# PostgreSQL ships /etc/logrotate.d/postgresql-common already. FerretDB logs to
|
||||
# journald, so bound the journal instead of adding a logrotate stanza.
|
||||
log_info "Bounding the systemd journal size..."
|
||||
mkdir -p /etc/systemd/journald.conf.d
|
||||
cat > /etc/systemd/journald.conf.d/ferretdb.conf <<'EOF'
|
||||
[Journal]
|
||||
SystemMaxUse=200M
|
||||
EOF
|
||||
systemctl restart systemd-journald >/dev/null 2>&1 || true
|
||||
|
||||
# --- Console auto-login on tty1 (Proxmox web console / pct console) ---
|
||||
log_info "Enabling console auto-login on tty1..."
|
||||
mkdir -p /etc/systemd/system/container-getty@1.service.d
|
||||
cat > /etc/systemd/system/container-getty@1.service.d/autologin.conf <<'EOF'
|
||||
[Service]
|
||||
ExecStart=
|
||||
ExecStart=-/sbin/agetty --autologin root --noclear --keep-baud tty%I 115200,38400,9600 $TERM
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
systemctl restart container-getty@1.service 2>/dev/null || true
|
||||
|
||||
configure_tailscale
|
||||
|
||||
log_info "Configuring MOTD..."
|
||||
# /etc/profile.d/ runs for every interactive login shell — works for both the
|
||||
# auto-login tty and Tailscale SSH. Quoted heredoc except the values we want
|
||||
# frozen at install time, which we inject via a small companion env file.
|
||||
cat > /etc/ferretdb-motd.env <<EOF
|
||||
FERRETDB_USER='${FERRETDB_USER}'
|
||||
FERRETDB_LISTEN_ADDR='${FERRETDB_LISTEN_ADDR}'
|
||||
EOF
|
||||
chmod 600 /etc/ferretdb-motd.env
|
||||
|
||||
cat > /etc/profile.d/00-ferretdb.sh <<'MOTD'
|
||||
[ -f /etc/ferretdb-motd.env ] && . /etc/ferretdb-motd.env
|
||||
|
||||
TS_FQDN=$(tailscale status --json 2>/dev/null | awk -F'"' '
|
||||
/"Self"/ { in_self=1 }
|
||||
in_self && /"DNSName"/ { gsub(/\.$/, "", $4); print $4; exit }
|
||||
')
|
||||
[ -z "$TS_FQDN" ] && TS_FQDN="$(hostname).ts.net"
|
||||
LAN_IP=$(ip -4 addr show eth0 2>/dev/null | awk '/inet /{print $2}' | cut -d/ -f1)
|
||||
FERRET_VERSION=$(/usr/bin/ferretdb --version 2>/dev/null | head -n1 || echo "unknown")
|
||||
|
||||
systemctl is-active --quiet ferretdb && FERRET_STATE="active" || FERRET_STATE="DOWN"
|
||||
systemctl is-active --quiet postgresql && PG_STATE="active" || PG_STATE="DOWN"
|
||||
|
||||
echo ""
|
||||
echo " _____ _ ____ ____ "
|
||||
echo "| ___|__ _ __ _ __ ___| |_| _ \\| __ ) "
|
||||
echo "| |_ / _ \\ '__| '__/ _ \\ __| | | | _ \\ "
|
||||
echo "| _| __/ | | | | __/ |_| |_| | |_) |"
|
||||
echo "|_| \\___|_| |_| \\___|\\__|____/|____/ "
|
||||
echo ""
|
||||
echo "FerretDB (MongoDB-compatible) — ${FERRET_VERSION}"
|
||||
echo "─────────────────────────────────────────"
|
||||
echo "Status:"
|
||||
echo " • FerretDB : ${FERRET_STATE} (listening on ${FERRETDB_LISTEN_ADDR})"
|
||||
echo " • PostgreSQL : ${PG_STATE} (127.0.0.1:5432)"
|
||||
echo ""
|
||||
echo "Connection string (MongoDB URI):"
|
||||
echo " mongodb://${FERRETDB_USER}:<password>@${LAN_IP:-<ip>}:27017/"
|
||||
echo " (tailnet) mongodb://${FERRETDB_USER}:<password>@${TS_FQDN}:27017/"
|
||||
echo ""
|
||||
echo "Useful commands:"
|
||||
echo " systemctl status ferretdb postgresql"
|
||||
echo " journalctl -u ferretdb -f"
|
||||
echo " mongosh \"mongodb://${FERRETDB_USER}:<password>@127.0.0.1:27017/\""
|
||||
echo "─────────────────────────────────────────"
|
||||
echo ""
|
||||
MOTD
|
||||
chmod +x /etc/profile.d/00-ferretdb.sh
|
||||
|
||||
log_info "Cleaning up..."
|
||||
apt-get clean >/dev/null 2>&1 || true
|
||||
|
||||
local ip
|
||||
ip=$(ip -4 addr show eth0 2>/dev/null | awk '/inet /{print $2}' | cut -d/ -f1 || true)
|
||||
|
||||
echo ""
|
||||
log_info "========================================="
|
||||
log_info "FerretDB installation complete!"
|
||||
log_info "========================================="
|
||||
echo ""
|
||||
echo "MongoDB connection string (point your client/app at it):"
|
||||
echo " mongodb://${FERRETDB_USER}:${FERRETDB_PASSWORD}@${ip:-<ip>}:27017/"
|
||||
echo ""
|
||||
echo "Credentials (store safely — not persisted on the Proxmox host):"
|
||||
echo " user : ${FERRETDB_USER}"
|
||||
echo " password : ${FERRETDB_PASSWORD}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# MODE: inside LXC — update only
|
||||
# ============================================================
|
||||
update_inside_lxc() {
|
||||
log_info "=== FerretDB — update ==="
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update >/dev/null
|
||||
apt-get upgrade -y >/dev/null
|
||||
install_or_upgrade_packages
|
||||
configure_tailscale
|
||||
log_info "Update complete."
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Main — dispatch on explicit mode flag or auto-detect context
|
||||
# ============================================================
|
||||
main() {
|
||||
case "${1:-}" in
|
||||
--install)
|
||||
install_inside_lxc
|
||||
return
|
||||
;;
|
||||
--update)
|
||||
update_inside_lxc
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
if command -v pct >/dev/null 2>&1; then
|
||||
# Running on a Proxmox host
|
||||
require_root
|
||||
preflight_script_url
|
||||
|
||||
local existing=""
|
||||
if existing=$(find_existing_lxc); then
|
||||
log_info "Found existing FerretDB LXC (CTID ${existing}, hostname/tag match) — switching to update mode."
|
||||
update_lxc "$existing"
|
||||
else
|
||||
create_lxc
|
||||
fi
|
||||
else
|
||||
# Inside a container (no Proxmox tooling)
|
||||
require_root
|
||||
if [[ -x /usr/bin/ferretdb ]]; then
|
||||
update_inside_lxc
|
||||
else
|
||||
install_inside_lxc
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -20,7 +20,7 @@ Single script, three automatic modes:
|
||||
bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/gitea-runner/install.sh)"
|
||||
```
|
||||
|
||||
The script automatically creates an Alpine 3.23 LXC with Docker and act_runner.
|
||||
The script automatically creates an Alpine LXC (template auto-detected from `pveam available`) with Docker and act_runner.
|
||||
|
||||
#### Customization
|
||||
|
||||
@@ -34,6 +34,7 @@ CTID=120 HOSTNAME=runner-02 CORES=4 RAM=4096 bash -c "$(curl -fsSL https://gitea
|
||||
|----------|---------|-------------|
|
||||
| `CTID` | auto | Container ID |
|
||||
| `RUNNER_HOSTNAME` | `gitea-runner` | LXC Hostname |
|
||||
| `TEMPLATE` | auto-detected | Alpine template; auto-detected from `pveam available` |
|
||||
| `CORES` | `2` | CPU cores |
|
||||
| `RAM` | `2048` | RAM in MiB |
|
||||
| `DISK` | `8` | Disk in GB |
|
||||
@@ -59,7 +60,7 @@ From inside the LXC:
|
||||
curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/gitea-runner/install.sh | bash
|
||||
```
|
||||
|
||||
The script detects that act_runner is already installed and switches to update mode automatically.
|
||||
The script detects that act_runner is already installed and switches to update mode automatically. Re-running from the Proxmox host does the same, plus refreshes the LXC's Alpine packages first (`apk update && apk upgrade`).
|
||||
|
||||
### Architecture
|
||||
|
||||
|
||||
+124
-39
@@ -8,15 +8,19 @@ set -euo pipefail
|
||||
|
||||
# --- Config (override via environment) ---
|
||||
CTID="${CTID:-}"
|
||||
HOSTNAME="${RUNNER_HOSTNAME:-gitea-runner}"
|
||||
TEMPLATE="${TEMPLATE:-alpine-3.23-default_20260116_amd64.tar.xz}"
|
||||
HOSTNAME_LXC="${RUNNER_HOSTNAME:-gitea-runner}"
|
||||
TEMPLATE="${TEMPLATE:-}" # auto-detected when empty
|
||||
STORAGE="${STORAGE:-local-lvm}"
|
||||
TEMPLATE_STORAGE="${TEMPLATE_STORAGE:-local}"
|
||||
CORES="${CORES:-2}"
|
||||
RAM="${RAM:-2048}"
|
||||
DISK="${DISK:-8}"
|
||||
BRIDGE="${BRIDGE:-vmbr0}"
|
||||
SCRIPT_URL="https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/gitea-runner/install.sh"
|
||||
LXC_TAG="${LXC_TAG:-gitea-runner}" # stable identifier for the container
|
||||
# SCRIPT_URL is what the host-side flow pipes into the LXC. Override it when
|
||||
# testing from a non-main branch, e.g.
|
||||
# SCRIPT_URL="https://gitea.arnodo.fr/.../branch/chore/standardize-lxc-scripts/gitea-runner/install.sh"
|
||||
SCRIPT_URL="${SCRIPT_URL:-https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/gitea-runner/install.sh}"
|
||||
GITEA_HOSTNAME="${GITEA_HOSTNAME:-gitea.taila5ad8.ts.net}"
|
||||
GITEA_API="https://gitea.com/api/v1/repos/gitea/act_runner/releases"
|
||||
VERSION_FILE="/opt/gitea-runner_version.txt"
|
||||
@@ -27,9 +31,47 @@ GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
# Logs go to stderr so callers can safely use $(fn) without capturing log noise.
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $1" >&2; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" >&2; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
|
||||
|
||||
require_root() {
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
log_error "This script must be run as root (current uid: $(id -u))."
|
||||
log_error "On Proxmox, launch it from the host shell or via the Web UI shell, both of which run as root."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Load shared helpers (lib/common.sh: detect_latest_alpine_template,
|
||||
# enable_tty1_autologin, find_existing_lxc, refresh_os_packages).
|
||||
#
|
||||
# Same reasoning as openbao/install.sh: a local checkout has the file
|
||||
# on disk right next to us, but the documented curl one-liner (host or
|
||||
# piped into `pct exec` inside the LXC) has no BASH_SOURCE path worth
|
||||
# trusting, so fall back to fetching lib/common.sh over HTTP next to
|
||||
# SCRIPT_URL. The LXC already needs outbound network to curl this very
|
||||
# script and to download the act_runner binary, so this adds no new
|
||||
# failure mode.
|
||||
# ============================================================
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-.}")" 2>/dev/null && pwd || true)"
|
||||
LIB_COMMON_URL="$(dirname "$(dirname "$SCRIPT_URL")")/lib/common.sh"
|
||||
if [[ -n "$SCRIPT_DIR" && -f "${SCRIPT_DIR}/../lib/common.sh" ]]; then
|
||||
source "${SCRIPT_DIR}/../lib/common.sh"
|
||||
else
|
||||
source <(curl -fsSL "$LIB_COMMON_URL")
|
||||
fi
|
||||
|
||||
# `source <(curl ...)` swallows curl failures: an empty stream still makes
|
||||
# `source` return 0, so a 404/network error would otherwise only surface
|
||||
# later as a confusing "command not found" for detect_latest_alpine_template
|
||||
# et al. Fail loudly here instead, with the URL that was tried.
|
||||
if ! declare -F detect_latest_alpine_template >/dev/null; then
|
||||
log_error "Failed to load lib/common.sh (tried: ${LIB_COMMON_URL})."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Helpers ---
|
||||
get_latest_release() {
|
||||
@@ -67,12 +109,34 @@ download_runner() {
|
||||
echo "$release" > "$VERSION_FILE"
|
||||
}
|
||||
|
||||
# Inject the script into the container and execute it in the requested mode.
|
||||
# Forwards the runtime configuration the inner invocation needs to reproduce
|
||||
# what the user requested on the host (mirrors openbao/install.sh's helper
|
||||
# of the same name).
|
||||
exec_in_lxc() {
|
||||
local ctid="$1"
|
||||
local mode="$2" # --install or --update
|
||||
|
||||
pct exec "$ctid" -- sh -c "apk add --no-cache bash curl jq ca-certificates > /dev/null 2>&1"
|
||||
curl -fsSL "$SCRIPT_URL" \
|
||||
| pct exec "$ctid" -- env \
|
||||
SCRIPT_URL="$SCRIPT_URL" \
|
||||
GITEA_HOSTNAME="$GITEA_HOSTNAME" \
|
||||
bash -s -- "$mode"
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# MODE 1: Proxmox host — create LXC container
|
||||
# ============================================================
|
||||
create_lxc() {
|
||||
log_info "=== Gitea Act Runner — LXC Creation ==="
|
||||
|
||||
if [[ -z "$TEMPLATE" ]]; then
|
||||
TEMPLATE=$(detect_latest_alpine_template)
|
||||
else
|
||||
log_info "Using user-provided template: $TEMPLATE"
|
||||
fi
|
||||
|
||||
# Auto-select next CTID if not specified
|
||||
if [[ -z "$CTID" ]]; then
|
||||
CTID=$(pvesh get /cluster/resources --type vm --output-format json 2>/dev/null \
|
||||
@@ -80,22 +144,18 @@ create_lxc() {
|
||||
log_info "Auto-selected CTID: $CTID"
|
||||
fi
|
||||
|
||||
# Download template if needed
|
||||
if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$TEMPLATE"; then
|
||||
log_info "Downloading template $TEMPLATE..."
|
||||
pveam download "$TEMPLATE_STORAGE" "$TEMPLATE"
|
||||
fi
|
||||
ensure_template_present "$TEMPLATE"
|
||||
|
||||
log_info "Creating LXC $CTID ($HOSTNAME)..."
|
||||
log_info "Creating LXC $CTID ($HOSTNAME_LXC)..."
|
||||
pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" \
|
||||
--hostname "$HOSTNAME" \
|
||||
--hostname "$HOSTNAME_LXC" \
|
||||
--cores "$CORES" \
|
||||
--memory "$RAM" \
|
||||
--rootfs "${STORAGE}:${DISK}" \
|
||||
--net0 "name=eth0,bridge=${BRIDGE},ip=dhcp" \
|
||||
--unprivileged 1 \
|
||||
--features nesting=1,keyctl=1 \
|
||||
--tags "infra-script,cicd" \
|
||||
--tags "infra-script,${LXC_TAG}" \
|
||||
--start 0
|
||||
|
||||
log_info "Configuring LXC for Docker and Tailscale..."
|
||||
@@ -111,8 +171,7 @@ EOF
|
||||
sleep 5
|
||||
|
||||
log_info "Injecting install script into container..."
|
||||
pct exec "$CTID" -- sh -c "apk add --no-cache bash curl jq > /dev/null 2>&1"
|
||||
curl -fsSL "$SCRIPT_URL" | pct exec "$CTID" -- bash -s -- --install
|
||||
exec_in_lxc "$CTID" "--install"
|
||||
|
||||
local ip
|
||||
ip=$(pct exec "$CTID" -- ip -4 addr show eth0 2>/dev/null | awk '/inet /{print $2}' | cut -d/ -f1)
|
||||
@@ -122,7 +181,7 @@ EOF
|
||||
log_info "LXC $CTID created successfully!"
|
||||
log_info "========================================="
|
||||
echo ""
|
||||
echo " Hostname : $HOSTNAME"
|
||||
echo " Hostname : $HOSTNAME_LXC"
|
||||
echo " IP : ${ip:-pending}"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
@@ -133,6 +192,28 @@ EOF
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# MODE 1b: Proxmox host — update an existing LXC
|
||||
# ============================================================
|
||||
update_lxc() {
|
||||
local ctid="$1"
|
||||
log_info "=== Gitea Act Runner — updating existing LXC ${ctid} ==="
|
||||
|
||||
if ! pct status "$ctid" | grep -q running; then
|
||||
log_info "Starting LXC ${ctid}..."
|
||||
pct start "$ctid"
|
||||
sleep 3
|
||||
fi
|
||||
|
||||
log_info "Refreshing Alpine packages inside LXC ${ctid}..."
|
||||
pct exec "$ctid" -- sh -c "apk update >/dev/null && apk upgrade >/dev/null"
|
||||
|
||||
log_info "Upgrading act_runner binary inside LXC ${ctid}..."
|
||||
exec_in_lxc "$ctid" "--update"
|
||||
|
||||
log_info "Update of LXC ${ctid} complete."
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# MODE 2: Inside LXC — fresh install
|
||||
# ============================================================
|
||||
@@ -232,24 +313,7 @@ LOGROTATE
|
||||
|
||||
ln -sf /usr/sbin/logrotate /etc/periodic/daily/logrotate 2>/dev/null || true
|
||||
|
||||
log_info "Enabling console auto-login on tty1..."
|
||||
# Alpine ships busybox getty by default; agetty (from util-linux) is what
|
||||
# supports --autologin.
|
||||
apk add --no-cache agetty >/dev/null 2>&1 || apk add --no-cache util-linux >/dev/null
|
||||
|
||||
# Replace any existing tty1 entry, then append our autologin line. Doing it
|
||||
# in two steps (delete + append) is more robust than an in-place sed against
|
||||
# a pattern that may drift across Alpine releases.
|
||||
sed -i '/^tty1::/d' /etc/inittab
|
||||
echo 'tty1::respawn:/sbin/agetty --autologin root --noclear 38400 tty1' >> /etc/inittab
|
||||
|
||||
# Tell PID 1 to re-read /etc/inittab so the change takes effect without a reboot.
|
||||
kill -HUP 1 2>/dev/null || true
|
||||
|
||||
# Kick any getty/agetty still attached to tty1 so init respawns it *now* with
|
||||
# the new line — otherwise the first web-console session lands on the stale
|
||||
# process and the operator has to type `exit` once before autologin kicks in.
|
||||
pkill -KILL -f '(getty|agetty).*tty1' 2>/dev/null || true
|
||||
enable_tty1_autologin
|
||||
|
||||
log_info "Cleaning up..."
|
||||
rm -rf /var/cache/apk/*
|
||||
@@ -275,6 +339,8 @@ LOGROTATE
|
||||
update_runner() {
|
||||
log_info "=== Gitea Act Runner — Update ==="
|
||||
|
||||
refresh_os_packages
|
||||
|
||||
local release
|
||||
release=$(get_latest_release)
|
||||
|
||||
@@ -305,19 +371,38 @@ update_runner() {
|
||||
# Main — detect context
|
||||
# ============================================================
|
||||
main() {
|
||||
if [[ "${1:-}" == "--install" ]]; then
|
||||
# Explicitly called in install mode (from pct exec)
|
||||
case "${1:-}" in
|
||||
--install)
|
||||
install_runner
|
||||
elif command -v pct &> /dev/null; then
|
||||
return
|
||||
;;
|
||||
--update)
|
||||
update_runner
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
if command -v pct &> /dev/null; then
|
||||
# We're on the Proxmox host
|
||||
require_root
|
||||
local existing=""
|
||||
if existing=$(find_existing_lxc); then
|
||||
log_info "Found existing gitea-runner LXC (CTID ${existing}, hostname/tag match) — switching to update mode."
|
||||
update_lxc "$existing"
|
||||
else
|
||||
create_lxc
|
||||
elif [[ -f /usr/local/bin/act_runner ]]; then
|
||||
fi
|
||||
else
|
||||
# Inside a container (no Proxmox tooling)
|
||||
require_root
|
||||
if [[ -f /usr/local/bin/act_runner ]]; then
|
||||
# act_runner exists — update mode
|
||||
update_runner
|
||||
else
|
||||
# Fresh LXC — install mode
|
||||
install_runner
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
# lib/common.sh - Shared helpers for Proxmox LXC creator scripts.
|
||||
#
|
||||
# Sourced (not executed) by openbao/install.sh and gitea-runner/install.sh.
|
||||
# Assumes the sourcing script already defines log_info/log_warn/log_error
|
||||
# (both scripts do, identically) — this file does not redefine them.
|
||||
#
|
||||
# Contract for future LXC creator scripts:
|
||||
# - detect_latest_alpine_template(): Alpine only. A future Debian-based
|
||||
# script needs its own detect_latest_debian_template() (same pattern:
|
||||
# pveam available + sort -V + hardcoded fallback) — do not overload
|
||||
# this function with an OS parameter.
|
||||
# - enable_tty1_autologin(): implements the Alpine/OpenRC autologin
|
||||
# mechanism (inittab + agetty). A future Debian-based script needs a
|
||||
# distinct function (systemd container-getty override) rather than a
|
||||
# branch inside this one.
|
||||
# - find_existing_lxc(): OS-agnostic, works by tag/hostname via `pct
|
||||
# config`. Reusable as-is by any LXC creator script.
|
||||
# - refresh_os_packages(): Alpine only (apk update && apk upgrade). A
|
||||
# future Debian-based script needs its own apt-get variant.
|
||||
#
|
||||
# Does not set shell options (set -e/-u/-o pipefail): a sourced file must
|
||||
# not impose those on the caller's shell. Both openbao/install.sh and
|
||||
# gitea-runner/install.sh already set them before sourcing this file.
|
||||
|
||||
# ============================================================
|
||||
# #12 - Detect newest Alpine LXC template available from the Proxmox repos.
|
||||
# Echoes the template filename. Falls back to a hardcoded known-good
|
||||
# template if `pveam` is unavailable or returns nothing.
|
||||
# ============================================================
|
||||
detect_latest_alpine_template() {
|
||||
local tmpl
|
||||
tmpl=$(pveam available --section system 2>/dev/null \
|
||||
| awk '/^system[[:space:]]+alpine-/ {print $2}' \
|
||||
| sort -V \
|
||||
| tail -n1)
|
||||
|
||||
if [[ -z "$tmpl" ]]; then
|
||||
log_warn "Could not query pveam; falling back to a known-good Alpine template."
|
||||
tmpl="alpine-3.22-default_20250617_amd64.tar.xz"
|
||||
fi
|
||||
log_info "Selected Alpine template: $tmpl"
|
||||
echo "$tmpl"
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# #14 - Enable root auto-login on tty1 for an Alpine/OpenRC LXC.
|
||||
# Idempotent: safe to call on every install/update.
|
||||
# ============================================================
|
||||
enable_tty1_autologin() {
|
||||
log_info "Enabling console auto-login on tty1..."
|
||||
# Alpine ships busybox getty by default; agetty (from util-linux) is what
|
||||
# supports --autologin.
|
||||
apk add --no-cache agetty >/dev/null 2>&1 || apk add --no-cache util-linux >/dev/null
|
||||
|
||||
# Replace any existing tty1 entry, then append our autologin line. Doing it
|
||||
# in two steps (delete + append) is more robust than an in-place sed against
|
||||
# a pattern that may drift across Alpine releases.
|
||||
sed -i '/^tty1::/d' /etc/inittab
|
||||
echo 'tty1::respawn:/sbin/agetty --autologin root --noclear 38400 tty1' >> /etc/inittab
|
||||
|
||||
# Tell PID 1 to re-read /etc/inittab so the change takes effect without a reboot.
|
||||
kill -HUP 1 2>/dev/null || true
|
||||
|
||||
# Kick any getty/agetty still attached to tty1 so init respawns it *now* with
|
||||
# the new line — otherwise the first web-console session lands on the stale
|
||||
# process and the operator has to type `exit` once before autologin kicks in.
|
||||
pkill -KILL -f '(getty|agetty).*tty1' 2>/dev/null || true
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# #12 - Ensure the given template is downloaded to TEMPLATE_STORAGE, doing a
|
||||
# `pveam update` first so a stale local cache doesn't silently settle for an
|
||||
# older version than the one detect_latest_alpine_template() just picked.
|
||||
# Expects TEMPLATE_STORAGE to be set by the caller.
|
||||
# ============================================================
|
||||
ensure_template_present() {
|
||||
local tmpl="$1"
|
||||
if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$tmpl"; then
|
||||
log_info "Downloading template ${tmpl} to storage ${TEMPLATE_STORAGE}..."
|
||||
pveam update >/dev/null
|
||||
pveam download "$TEMPLATE_STORAGE" "$tmpl"
|
||||
else
|
||||
log_info "Template ${tmpl} already present on ${TEMPLATE_STORAGE}."
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# #15 - Find an existing LXC by tag or hostname (host-side, requires pct).
|
||||
# Echoes the CTID on match, returns 1 if none found.
|
||||
#
|
||||
# Expects HOSTNAME_LXC and LXC_TAG to be set by the caller (as openbao's
|
||||
# find_existing_lxc already does).
|
||||
# ============================================================
|
||||
find_existing_lxc() {
|
||||
local id host tags
|
||||
while read -r id _; do
|
||||
[[ -z "$id" || "$id" == "VMID" ]] && continue
|
||||
host=$(pct config "$id" 2>/dev/null | awk -F': ' '/^hostname:/ {print $2}' || true)
|
||||
tags=$(pct config "$id" 2>/dev/null | awk -F': ' '/^tags:/ {print $2}' || true)
|
||||
if [[ "$host" == "$HOSTNAME_LXC" ]] || [[ ",${tags//;/,}," == *",${LXC_TAG},"* ]]; then
|
||||
echo "$id"
|
||||
return 0
|
||||
fi
|
||||
done < <(pct list | awk 'NR>1 {print $1}')
|
||||
return 1
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# #15 - Refresh OS packages (Alpine: apk update && apk upgrade).
|
||||
# Callable only from inside the LXC: this is a plain bash function in the
|
||||
# current process, so it cannot run across a `pct exec ... sh -c` boundary
|
||||
# without shipping its definition into the container. Host-side callers
|
||||
# (see openbao/install.sh's update_lxc()) invoke apk update/upgrade inline
|
||||
# via `pct exec` instead — do not try to dedupe that call site onto this
|
||||
# function.
|
||||
# ============================================================
|
||||
refresh_os_packages() {
|
||||
log_info "Refreshing Alpine packages..."
|
||||
apk update >/dev/null && apk upgrade >/dev/null
|
||||
}
|
||||
+3
-1
@@ -110,7 +110,9 @@ curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/feat/lxc-Open
|
||||
|
||||
The script auto-detects the presence of `/usr/local/bin/bao` and switches to
|
||||
update mode. The OpenRC service is stopped, the binary is swapped (the old one
|
||||
is kept as `bao.bak.<ts>`), then the service is restarted.
|
||||
is kept as `bao.bak.<ts>`), then the service is restarted. Re-running from the
|
||||
Proxmox host does the same, plus refreshes the LXC's Alpine packages first
|
||||
(`apk update && apk upgrade`).
|
||||
|
||||
### Architecture
|
||||
|
||||
|
||||
+38
-62
@@ -56,6 +56,39 @@ log_info() { echo -e "${GREEN}[INFO]${NC} $1" >&2; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" >&2; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
|
||||
|
||||
# ============================================================
|
||||
# Load shared helpers (lib/common.sh: detect_latest_alpine_template,
|
||||
# enable_tty1_autologin, find_existing_lxc, refresh_os_packages).
|
||||
#
|
||||
# This script runs in three different contexts, only one of which has a
|
||||
# real file on disk next to it:
|
||||
# - local checkout (`bash openbao/install.sh`) -> lib/common.sh
|
||||
# sits right there at ../lib/common.sh, source it straight from disk.
|
||||
# - Proxmox host, documented one-liner (`bash -c "$(curl ... )"`)
|
||||
# -> no checkout, no BASH_SOURCE path worth trusting.
|
||||
# - inside the LXC (exec_in_lxc does `curl ... | pct exec ... bash -s --`)
|
||||
# -> same story, script arrives on stdin.
|
||||
# For the latter two we fetch lib/common.sh over HTTP, next to SCRIPT_URL.
|
||||
# The LXC already needs outbound network to curl this very script and to
|
||||
# download the bao binary, so this adds no new failure mode.
|
||||
# ============================================================
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-.}")" 2>/dev/null && pwd || true)"
|
||||
LIB_COMMON_URL="$(dirname "$(dirname "$SCRIPT_URL")")/lib/common.sh"
|
||||
if [[ -n "$SCRIPT_DIR" && -f "${SCRIPT_DIR}/../lib/common.sh" ]]; then
|
||||
source "${SCRIPT_DIR}/../lib/common.sh"
|
||||
else
|
||||
source <(curl -fsSL "$LIB_COMMON_URL")
|
||||
fi
|
||||
|
||||
# `source <(curl ...)` swallows curl failures: an empty stream still makes
|
||||
# `source` return 0, so a 404/network error would otherwise only surface
|
||||
# later as a confusing "command not found" for detect_latest_alpine_template
|
||||
# et al. Fail loudly here instead, with the URL that was tried.
|
||||
if ! declare -F detect_latest_alpine_template >/dev/null; then
|
||||
log_error "Failed to load lib/common.sh (tried: ${LIB_COMMON_URL})."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# Generic helpers
|
||||
# ============================================================
|
||||
@@ -204,48 +237,6 @@ configure_tailscale_proxy() {
|
||||
# Proxmox-host helpers
|
||||
# ============================================================
|
||||
|
||||
# Detect newest Alpine LXC template available from the Proxmox repos.
|
||||
detect_latest_alpine_template() {
|
||||
local tmpl
|
||||
tmpl=$(pveam available --section system 2>/dev/null \
|
||||
| awk '/^system[[:space:]]+alpine-/ {print $2}' \
|
||||
| sort -V \
|
||||
| tail -n1)
|
||||
|
||||
if [[ -z "$tmpl" ]]; then
|
||||
log_warn "Could not query pveam; falling back to a known-good Alpine template."
|
||||
tmpl="alpine-3.22-default_20250617_amd64.tar.xz"
|
||||
fi
|
||||
log_info "Selected Alpine template: $tmpl"
|
||||
echo "$tmpl"
|
||||
}
|
||||
|
||||
# Find an existing LXC by tag or hostname. Echoes CTID, returns 1 if none.
|
||||
find_existing_lxc() {
|
||||
local id host tags
|
||||
while read -r id _; do
|
||||
[[ -z "$id" || "$id" == "VMID" ]] && continue
|
||||
host=$(pct config "$id" 2>/dev/null | awk -F': ' '/^hostname:/ {print $2}' || true)
|
||||
tags=$(pct config "$id" 2>/dev/null | awk -F': ' '/^tags:/ {print $2}' || true)
|
||||
if [[ "$host" == "$HOSTNAME_LXC" ]] || [[ ",${tags//;/,}," == *",${LXC_TAG},"* ]]; then
|
||||
echo "$id"
|
||||
return 0
|
||||
fi
|
||||
done < <(pct list | awk 'NR>1 {print $1}')
|
||||
return 1
|
||||
}
|
||||
|
||||
ensure_template_present() {
|
||||
local tmpl="$1"
|
||||
if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$tmpl"; then
|
||||
log_info "Downloading template ${tmpl} to storage ${TEMPLATE_STORAGE}..."
|
||||
pveam update >/dev/null
|
||||
pveam download "$TEMPLATE_STORAGE" "$tmpl"
|
||||
else
|
||||
log_info "Template ${tmpl} already present on ${TEMPLATE_STORAGE}."
|
||||
fi
|
||||
}
|
||||
|
||||
# Pick next available CTID if user did not provide one.
|
||||
allocate_ctid() {
|
||||
pvesh get /cluster/nextid 2>/dev/null \
|
||||
@@ -363,6 +354,9 @@ update_lxc() {
|
||||
fi
|
||||
|
||||
log_info "Refreshing Alpine packages inside LXC ${ctid}..."
|
||||
# refresh_os_packages() is a bash function local to this process; it can't
|
||||
# run over `pct exec ... sh -c` without shipping the function definition
|
||||
# into the container, so this call site stays inline rather than dedupe.
|
||||
pct exec "$ctid" -- sh -c "apk update >/dev/null && apk upgrade >/dev/null"
|
||||
|
||||
log_info "Upgrading bao binary inside LXC ${ctid}..."
|
||||
@@ -474,24 +468,7 @@ EOF
|
||||
log_info "Starting openbao service..."
|
||||
rc-service openbao start || log_warn "openbao failed to start — inspect /var/log/openbao.log"
|
||||
|
||||
log_info "Enabling console auto-login on tty1..."
|
||||
# Alpine ships busybox getty by default; agetty (from util-linux) is what
|
||||
# supports --autologin.
|
||||
apk add --no-cache agetty >/dev/null 2>&1 || apk add --no-cache util-linux >/dev/null
|
||||
|
||||
# Replace any existing tty1 entry, then append our autologin line. Doing it
|
||||
# in two steps (delete + append) is more robust than an in-place sed against
|
||||
# a pattern that may drift across Alpine releases.
|
||||
sed -i '/^tty1::/d' /etc/inittab
|
||||
echo 'tty1::respawn:/sbin/agetty --autologin root --noclear 38400 tty1' >> /etc/inittab
|
||||
|
||||
# Tell PID 1 to re-read /etc/inittab so the change takes effect without a reboot.
|
||||
kill -HUP 1 2>/dev/null || true
|
||||
|
||||
# Kick any getty/agetty still attached to tty1 so init respawns it *now* with
|
||||
# the new line — otherwise the first web-console session lands on the stale
|
||||
# process and the operator has to type `exit` once before autologin kicks in.
|
||||
pkill -KILL -f '(getty|agetty).*tty1' 2>/dev/null || true
|
||||
enable_tty1_autologin
|
||||
|
||||
configure_tailscale_proxy
|
||||
|
||||
@@ -563,8 +540,7 @@ MOTD
|
||||
# ============================================================
|
||||
update_inside_lxc() {
|
||||
log_info "=== OpenBao — update ==="
|
||||
apk update >/dev/null
|
||||
apk upgrade >/dev/null
|
||||
refresh_os_packages
|
||||
install_or_upgrade_bao
|
||||
configure_tailscale_proxy
|
||||
log_info "Update complete."
|
||||
|
||||
Reference in New Issue
Block a user