Let one host template upgrade any host's conf
The pull substituted only HOSTN_, so bare HOSTN in comments kept tripping conf_upgrade's own guard and no host conf had upgraded since the guard landed.
This commit is contained in:
+82
-20
@@ -69,15 +69,30 @@
|
||||
# OPERATIONAL SAFEGUARDS
|
||||
# ==============================================================================================
|
||||
#
|
||||
# Unsubstituted HOSTN Placeholders Are Refused
|
||||
# The Host Slot Must Be Declared, Never Inferred
|
||||
# host.conf.template ships HOSTN_ placeholders; a live host conf uses HOST1_ / HOST2_. Key
|
||||
# matching is literal, so merging the raw template against a real host conf classifies every
|
||||
# existing key as deprecated — KEPT 0 — and the install drops every credential in the file.
|
||||
# Verified on HOST1 2026-08-02: it would have removed HOST1_RADARR_API_KEY,
|
||||
# HOST1_EMBY_API_KEY, HOST1_NPM_PASS and 137 others. The template is checked for HOSTN and
|
||||
# the target for a HOST<n> slot; if both are present the run aborts and prints the exact sed
|
||||
# command to substitute. Refused rather than auto-substituted — this rewrites the file holding
|
||||
# every secret on the host, and inferring the slot is not worth being wrong about.
|
||||
# HOST1_EMBY_API_KEY, HOST1_NPM_PASS and 137 others.
|
||||
#
|
||||
# --host-slot HOST<n> is how a caller says which slot the template is being resolved for, and
|
||||
# it works for any slot — HOST1, HOST2, and whatever a third server would be. The slot is
|
||||
# still never inferred from the target and applied silently: the caller declares it, and the
|
||||
# target is read only to contradict a wrong answer. A declared slot that disagrees with the
|
||||
# target's own keys aborts, because substituting for the wrong slot destroys the file just as
|
||||
# thoroughly as not substituting at all. Without the flag, a template containing HOSTN is
|
||||
# refused exactly as before.
|
||||
#
|
||||
# Both cases are substituted. HOSTN_ covers the key prefixes, bare HOSTN appears in section
|
||||
# comments, and lowercase hostn is a real value — the hostn-appdata rsync profile keys. A
|
||||
# substitution handling only HOSTN_ leaves a conf carrying a profile named hostn-appdata that
|
||||
# nothing references.
|
||||
#
|
||||
# A Target Owning Two Slots Is Refused
|
||||
# A host conf describes exactly one server. Finding both HOST1_ and HOST2_ key definitions in
|
||||
# one target means it is not the file it claims to be, so the slot cross-check has nothing
|
||||
# trustworthy to compare against and the run aborts rather than picking one.
|
||||
#
|
||||
# Total Mismatch Is Refused
|
||||
# Keeping nothing from a populated conf is never a real upgrade; it means the two files do
|
||||
@@ -122,6 +137,9 @@
|
||||
#
|
||||
# --template <file> New version conf file (source of structure and defaults)
|
||||
# --target <file> Existing user conf (source of real values — always preserved)
|
||||
# --host-slot <HOSTn> Resolve HOSTN/hostn placeholders to this slot before merging.
|
||||
# Required for host.conf.template; meaningless for master.conf.template,
|
||||
# which has no placeholders. Must match the target's own slot.
|
||||
# --dry-run Show what would change without writing
|
||||
# --backup Write a .bak copy of target before modifying
|
||||
#
|
||||
@@ -138,6 +156,11 @@
|
||||
# conf_upgrade.sh --template Deployment/master.conf.template --target Configurations/master.conf
|
||||
# Apply the upgrade in-place with no backup.
|
||||
#
|
||||
# conf_upgrade.sh --template Deployment/host.conf.template --target Configurations/host1.conf \
|
||||
# --host-slot HOST1 --dry-run
|
||||
# Preview a host conf upgrade. HOSTN/hostn are resolved to HOST1/host1 first. Swap in HOST2
|
||||
# and host2.conf for the other server — the template is the same file for every slot.
|
||||
#
|
||||
# ==============================================================================================
|
||||
|
||||
set -uo pipefail
|
||||
@@ -148,11 +171,14 @@ TEMPLATE=""
|
||||
TARGET=""
|
||||
DRY_RUN=false
|
||||
BACKUP=false
|
||||
HOST_SLOT=""
|
||||
_RESOLVED_TMPL="" # set only when --host-slot triggers a substitution; cleaned on exit
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--template) TEMPLATE="$2"; shift 2 ;;
|
||||
--target) TARGET="$2"; shift 2 ;;
|
||||
--host-slot) HOST_SLOT="$2"; shift 2 ;;
|
||||
--dry-run) DRY_RUN=true; shift ;;
|
||||
--backup) BACKUP=true; shift ;;
|
||||
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||
@@ -164,25 +190,60 @@ done
|
||||
[[ -f "$TEMPLATE" ]] || { echo "Error: template not found: $TEMPLATE" >&2; exit 1; }
|
||||
[[ -f "$TARGET" ]] || { echo "Error: target not found: $TARGET" >&2; exit 1; }
|
||||
|
||||
# ── Guard: unsubstituted HOSTN placeholders ──────────────────────────────────────────────────
|
||||
# ── Host slot resolution ─────────────────────────────────────────────────────────────────────
|
||||
#
|
||||
# host.conf.template ships HOSTN_ placeholders; a live host conf uses HOST1_ / HOST2_. Key
|
||||
# matching below is literal, so merging the raw template against a real host conf classifies
|
||||
# EVERY existing key as deprecated and every template key as new — KEPT 0, and the install
|
||||
# would drop every credential in the file. api/setup.php substitutes before writing; a direct
|
||||
# invocation has no such step. Refused rather than auto-substituted: this rewrites the file
|
||||
# that holds every secret on the host, and guessing the slot is not worth being wrong about.
|
||||
if grep -q 'HOSTN' "$TEMPLATE" 2>/dev/null; then
|
||||
_slot=$(grep -oEm1 '^[[:space:]]*(HOST[0-9]+)_' "$TARGET" 2>/dev/null | grep -oE 'HOST[0-9]+')
|
||||
if [[ -n "$_slot" ]]; then
|
||||
_lower=$(echo "$_slot" | tr '[:upper:]' '[:lower:]')
|
||||
echo "Error: template still contains HOSTN placeholders, but the target uses ${_slot}_." >&2
|
||||
echo " Merging as-is would classify all ${_slot}_ keys as deprecated and remove" >&2
|
||||
echo " them — including every credential. Substitute the slot first:" >&2
|
||||
# would drop every credential in the file.
|
||||
#
|
||||
# --host-slot is how a caller declares which slot the template is for. The slot is never
|
||||
# inferred from the target and silently applied: the caller states it, and the target is used
|
||||
# only to contradict a wrong answer. Substituting for the wrong slot is the same catastrophe as
|
||||
# not substituting at all, so a declared slot that disagrees with the target is refused.
|
||||
#
|
||||
# Both cases matter. HOSTN_ covers the 159 key prefixes; bare HOSTN appears in section comments,
|
||||
# and lowercase hostn is a real value — the hostn-appdata rsync profile keys. A substitution
|
||||
# that only handles HOSTN_ leaves a live conf with a profile named hostn-appdata that nothing
|
||||
# references, which is what the pull script did before this flag existed.
|
||||
_target_slots=$(grep -oE '^[[:space:]]*HOST[0-9]+_' "$TARGET" 2>/dev/null \
|
||||
| grep -oE 'HOST[0-9]+' | sort -u)
|
||||
_target_slot=$(echo "$_target_slots" | head -1)
|
||||
if [[ $(echo "$_target_slots" | grep -c .) -gt 1 ]]; then
|
||||
echo "Error: '$TARGET' defines keys for more than one host slot:" >&2
|
||||
echo " $(echo "$_target_slots" | tr '\n' ' ')" >&2
|
||||
echo " A host conf owns exactly one slot. Refusing rather than picking one." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "$HOST_SLOT" ]]; then
|
||||
if ! [[ "$HOST_SLOT" =~ ^HOST[0-9]+$ ]]; then
|
||||
echo "Error: --host-slot must be HOST<n> (e.g. HOST1, HOST2) — got '$HOST_SLOT'" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -n "$_target_slot" && "$_target_slot" != "$HOST_SLOT" ]]; then
|
||||
echo "Error: --host-slot says $HOST_SLOT but '$TARGET' defines ${_target_slot}_ keys." >&2
|
||||
echo " Substituting for the wrong slot removes every key the target actually has," >&2
|
||||
echo " credentials included. Refusing." >&2
|
||||
exit 1
|
||||
fi
|
||||
if grep -q -e 'HOSTN' -e 'hostn' "$TEMPLATE" 2>/dev/null; then
|
||||
_lower=$(echo "$HOST_SLOT" | tr '[:upper:]' '[:lower:]')
|
||||
_RESOLVED_TMPL="$(mktemp)"
|
||||
trap '[[ -n "${_RESOLVED_TMPL:-}" ]] && rm -f "$_RESOLVED_TMPL"' EXIT
|
||||
sed -e "s/HOSTN/${HOST_SLOT}/g" -e "s/hostn/${_lower}/g" "$TEMPLATE" > "$_RESOLVED_TMPL"
|
||||
TEMPLATE="$_RESOLVED_TMPL"
|
||||
echo " Resolved HOSTN → ${HOST_SLOT} for this host"
|
||||
fi
|
||||
elif grep -q 'HOSTN' "$TEMPLATE" 2>/dev/null; then
|
||||
# No slot declared and the template is still generic — the original refusal, unchanged.
|
||||
if [[ -n "$_target_slot" ]]; then
|
||||
_lower=$(echo "$_target_slot" | tr '[:upper:]' '[:lower:]')
|
||||
echo "Error: template still contains HOSTN placeholders, but the target uses ${_target_slot}_." >&2
|
||||
echo " Merging as-is would classify all ${_target_slot}_ keys as deprecated and remove" >&2
|
||||
echo " them — including every credential. Declare the slot:" >&2
|
||||
echo "" >&2
|
||||
echo " sed -e 's/HOSTN/${_slot}/g' -e 's/hostn/${_lower}/g' \\" >&2
|
||||
echo " $TEMPLATE > /tmp/${_lower}.conf.template" >&2
|
||||
echo " $0 --template /tmp/${_lower}.conf.template --target $TARGET --dry-run" >&2
|
||||
echo " $0 --template $TEMPLATE --target $TARGET --host-slot ${_target_slot} --dry-run" >&2
|
||||
echo "" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -355,7 +416,7 @@ fi
|
||||
# Unraid /tmp is rootfs while the confs live on flash — a cross-device mv silently degrades
|
||||
# to copy-then-unlink, which is exactly the torn write this is meant to prevent.
|
||||
TMPOUT="$(mktemp "${TARGET}.XXXXXX")"
|
||||
trap 'rm -f "$TMPOUT"' EXIT
|
||||
trap 'rm -f "$TMPOUT"; [[ -n "${_RESOLVED_TMPL:-}" ]] && rm -f "$_RESOLVED_TMPL"' EXIT
|
||||
|
||||
# mktemp creates 0600; carry the target's existing mode/owner across so the installed conf
|
||||
# does not come back with different permissions than it went in with.
|
||||
@@ -374,4 +435,5 @@ fi
|
||||
# that constantly. A rename swaps the inode: readers get the old file or the new one.
|
||||
mv -f "$TMPOUT" "$TARGET"
|
||||
trap - EXIT
|
||||
[[ -n "$_RESOLVED_TMPL" ]] && rm -f "$_RESOLVED_TMPL"
|
||||
echo "Updated: $TARGET"
|
||||
|
||||
+7
-6
@@ -343,15 +343,16 @@ elif [[ "$SYNC_SUCCESS" == true ]]; then
|
||||
# This server's host conf only — sparse checkout ensures we have it
|
||||
HOST_CONF="$CONF_DIR/${MY_ID,,}.conf"
|
||||
if [[ -f "$DEPLOY_DIR/host.conf.template" && -f "$HOST_CONF" ]]; then
|
||||
# Template uses HOSTN_ as generic prefix. Substitute MY_ID before merging
|
||||
# so keys match the target (HOST1_* or HOST2_*) and real values are preserved.
|
||||
TMPL_RESOLVED=$(mktemp)
|
||||
sed "s/HOSTN_/${MY_ID}_/g; s/REMOTE_ID/${REMOTE_ID}/g" "$DEPLOY_DIR/host.conf.template" > "$TMPL_RESOLVED"
|
||||
# The template is slot-generic; conf_upgrade resolves HOSTN/hostn to MY_ID and refuses
|
||||
# if that disagrees with the target's own keys. This used to be a local sed that only
|
||||
# replaced HOSTN_, which left bare HOSTN in comments — enough to trip conf_upgrade's
|
||||
# own guard, so the host conf silently never upgraded — and left lowercase hostn alone,
|
||||
# which would have installed an unreferenced hostn-appdata rsync profile.
|
||||
bash "$UPGRADE_SCRIPT" \
|
||||
--template "$TMPL_RESOLVED" \
|
||||
--template "$DEPLOY_DIR/host.conf.template" \
|
||||
--target "$HOST_CONF" \
|
||||
--host-slot "$MY_ID" \
|
||||
--backup $_DRY
|
||||
rm -f "$TMPL_RESOLVED"
|
||||
else
|
||||
warn "${MY_ID,,}.conf or host.conf.template not found — skipping"
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user