Files
infra-scripts/lib/common.sh
T
Damien 1217b26ff9 fix(lib): ini_set n'écrase plus app.ini si awk échoue, insertion avant les lignes vides
Deux corrections dans ini_set() suite à la review de #24 :

- Le mv s'exécutait même si awk avait échoué en cours de route (OOM,
  signal, valeur exotique) : $tmp, créé par la redirection avant
  qu'awk ne tourne, pouvait être tronqué ou vide et venait quand même
  écraser le fichier original. mv est maintenant gaté sur le code de
  retour d'awk : en cas d'échec, $tmp est supprimé, une erreur est
  logguée, et la fonction retourne 1 sans toucher au fichier d'origine.

- L'insertion d'une clé juste avant l'en-tête de section suivante
  atterrissait après la ou les lignes vides de fin de section plutôt
  qu'avant, rendant le fichier plus difficile à lire au fil des appels
  répétés. Les lignes vides rencontrées section active/clé pas encore
  trouvée sont désormais bufferisées et réémises juste après la clé
  insérée (ou en fin de fichier si la section n'est jamais refermée),
  sans toucher à l'ordre dans tous les autres cas.
2026-08-01 11:18:48 +02:00

228 lines
9.3 KiB
Bash

# 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.
# - ini_set(): OS-agnostic (plain awk/sed, no OS-specific assumptions).
# Reusable as-is by any script that manages an INI-style config file,
# regardless of the underlying distro.
#
# 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
}
# ============================================================
# #18 - Idempotently set KEY = VALUE in SECTION of an INI-style config file
# (e.g. Gitea's app.ini). Merges key by key rather than overwriting the
# whole file, so a rejoué script can add newly-required keys to an already
# customized config without clobbering it.
#
# Usage: ini_set <file> <section> <key> <value>
#
# Behavior:
# - Missing file/section/key: created.
# - Key present with a different value: replaced in place.
# - Key present with the same value: no-op (byte-identical output).
# - Other sections/keys: never touched — the match is scoped to the
# given section, so the same key name in a different section (e.g.
# ENABLED in both [metrics] and [actions]) is left alone.
# Comments, blank lines and section order are preserved. Written atomically
# (tmpfile + mv) so an interrupted run can't leave a corrupt config.
# ============================================================
ini_set() {
local file="$1" section="$2" key="$3" value="$4"
local tmp
if [[ ! -f "$file" ]]; then
mkdir -p "$(dirname "$file")"
: > "$file"
fi
tmp=$(mktemp "${file}.tmp.XXXXXX")
# mktemp defaults to 0600 root:root, which would silently lock the
# service account that owns $file (e.g. gitea:www-data on Gitea's
# app.ini) out of the config this function just wrote. Carry the
# original file's mode/ownership onto the replacement before it lands.
# `stat -c` works identically on GNU coreutils and BusyBox.
chmod "$(stat -c '%a' "$file")" "$tmp" 2>/dev/null || true
chown "$(stat -c '%u:%g' "$file")" "$tmp" 2>/dev/null || true
# awk writes into $tmp regardless of its own exit status — a mid-stream
# death (OOM, signal, an exotic value tripping the regex) would still
# leave a truncated-but-nonempty $tmp for `mv` to install over $file.
# Gate the mv on awk's exit code so a failure leaves the original config
# untouched instead of silently destroying it.
if ! awk -v section="$section" -v key="$key" -v value="$value" '
# Blank lines are buffered rather than printed immediately while a
# section is still awaiting insertion: without this, a key inserted
# right before the next section header lands *after* that section'"'"'s
# trailing blank line(s) instead of before them. Flushed as soon as
# either a non-blank line or the insertion itself happens, so this
# never reorders anything except relative to that pending insertion.
function flush_blanks() {
while (blank_count > 0) { print ""; blank_count-- }
}
/^\[.*\]$/ {
if (in_section && !done) {
printf "%s = %s\n", key, value
done = 1
}
flush_blanks()
cur = $0
gsub(/^\[|\]$/, "", cur)
in_section = (cur == section)
if (in_section) section_found = 1
print
next
}
{
if (in_section && !done) {
if (match($0, "^[ \t]*" key "[ \t]*=")) {
flush_blanks()
printf "%s = %s\n", key, value
done = 1
next
}
if ($0 ~ /^[ \t]*$/) {
blank_count++
next
}
flush_blanks()
print
next
}
print
}
END {
if (in_section && !done) {
printf "%s = %s\n", key, value
done = 1
}
flush_blanks()
if (!section_found) {
if (NR > 0) print ""
printf "[%s]\n", section
printf "%s = %s\n", key, value
}
}
' "$file" > "$tmp"; then
rm -f "$tmp"
log_error "ini_set: awk failed on ${file} (${section}.${key}), config left untouched."
return 1
fi
mv "$tmp" "$file"
}