On this host "authelia" matches both Authelia and Authelia-Secondary, and taking the first match writes the wrong instance into the conf every other script then trusts.
556 lines
28 KiB
Bash
Executable File
556 lines
28 KiB
Bash
Executable File
#!/bin/bash
|
|
# ==============================================================================================
|
|
# ============================= Conf Auto-Populate =============================================
|
|
# ==============================================================================================
|
|
#
|
|
# PURPOSE
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Reads credentials and settings from locally running services and writes
|
|
# them into the local host conf. Safe to run multiple times — only populates
|
|
# EMPTY fields, never overwrites existing values unless --overwrite is passed.
|
|
#
|
|
# After populating, pushes the updated conf to all partners via conf_sync.sh
|
|
# so they have the fresh keys in their /tmp/.cache/vv/d/ cache immediately.
|
|
#
|
|
# ==============================================================================================
|
|
# AUTO-DETECTED FIELDS
|
|
# ==============================================================================================
|
|
#
|
|
# HOST CONF (Configurations/<hostid>.conf)
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
# HOSTN_OWNER from hostname (strip unRAID- prefix, lowercase)
|
|
# HOSTN_SSH_KEY from hostname convention (/root/.ssh/<owner>_rsync_automation)
|
|
# HOSTN_STORAGE_MODE_INTERNAL from boot device transport (NVMe/SSD=true, USB=false)
|
|
# HOSTN_RADARR_API_KEY from Radarr config.xml (found via docker volume mount)
|
|
# HOSTN_RADARR_URL from Radarr config.xml port
|
|
# HOSTN_RADARR_MOVIES_ROOT from Radarr rootFolder API
|
|
# HOSTN_RADARR_PATH_MAP from Radarr docker volume mounts vs root folder path
|
|
# HOSTN_SONARR_API_KEY from Sonarr config.xml
|
|
# HOSTN_SONARR_URL from Sonarr config.xml port
|
|
# HOSTN_SONARR_TV_ROOT from Sonarr rootFolder API
|
|
# HOSTN_SONARR_PATH_MAP from Sonarr docker volume mounts vs root folder path
|
|
# HOSTN_LIDARR_API_KEY from Lidarr config.xml
|
|
# HOSTN_LIDARR_URL from Lidarr config.xml port
|
|
# HOSTN_LIDARR_MUSIC_ROOT from Lidarr rootFolder API
|
|
# HOSTN_LIDARR_PATH_MAP from Lidarr docker volume mounts vs root folder path
|
|
# HOSTN_SABNZBD_API_KEY from sabnzbd.ini
|
|
# HOSTN_SABNZBD_URL from sabnzbd.ini port
|
|
# HOSTN_SLSKD_API_KEY from slskd config.yml
|
|
# HOSTN_SLSKD_URL from slskd config.yml port
|
|
# HOSTN_QBIT_URL from qBittorrent.conf WebUI port
|
|
# HOSTN_QBIT_USERNAME from qBittorrent.conf WebUI username
|
|
# HOSTN_QBIT_PASSWORD from qBittorrent.conf WebUI password (plaintext only)
|
|
# HOSTN_EMBY_CONTAINER fuzzy match from docker ps
|
|
# HOSTN_EMBY_URL from docker port binding
|
|
# HOSTN_JELLYFIN_CONTAINER fuzzy match from docker ps
|
|
# HOSTN_JELLYFIN_URL from docker port binding
|
|
# HOSTN_TRANSCODE_SSD from Emby/Jellyfin container /transcode volume mount
|
|
# HOSTN_AUTHELIA_CONTAINER fuzzy match from docker ps
|
|
# HOSTN_AUTHELIA_CONFIG from Authelia container /config volume mount
|
|
# HOSTN_SYS_WATCHDOG_NIC from ip route default gateway interface
|
|
#
|
|
# MASTER CONF (Configurations/master.conf)
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
# HOST1 / HOST2 local hostname written to MY_ID slot
|
|
# GITEA_CONTAINER fuzzy match from docker ps
|
|
#
|
|
# ==============================================================================================
|
|
# OPERATIONAL MODEL
|
|
# ==============================================================================================
|
|
#
|
|
# 1. Resolve this host's conf from MY_ID
|
|
# 2. Per service, locate its container and read its own config file:
|
|
# arrs → config.xml via the container's /config volume mount
|
|
# SABnzbd → sabnzbd.ini
|
|
# slskd → config.yml
|
|
# qBit → qBittorrent.conf (plaintext WebUI credentials only)
|
|
# Emby/JF → docker port bindings and /transcode mount
|
|
# 3. Write each value only if the conf field is EMPTY, unless --overwrite
|
|
# 4. Push the updated conf to partners via conf_sync.sh, unless --no-push
|
|
#
|
|
# Container names are resolved by _resolve_container(): an exact name match wins, otherwise
|
|
# a prefix match must be unambiguous or the field is skipped.
|
|
#
|
|
# ==============================================================================================
|
|
# DESIGN PRINCIPLES
|
|
# ==============================================================================================
|
|
#
|
|
# Never Overwrite a Human's Value
|
|
# Only empty fields are populated. A value already in the conf was either set deliberately
|
|
# or populated from a service that has since changed — either way, the file wins over
|
|
# detection. --overwrite exists for deliberate re-sync after a key rotation.
|
|
#
|
|
# Read From the Service, Not From Assumption
|
|
# Every value comes out of the service's own config file or docker metadata — ports from
|
|
# actual port bindings, paths from actual volume mounts. Nothing is derived from naming
|
|
# convention where the real value is readable.
|
|
#
|
|
# Refuse to Guess a Container
|
|
# An ambiguous prefix skips the field rather than picking one. Writing the wrong container
|
|
# name is worse than writing nothing: an empty field is visibly incomplete and gets fixed,
|
|
# while a wrong one silently points the whole stack at the wrong instance. This host has a
|
|
# live example — "authelia" prefix-matches both Authelia and Authelia-Secondary.
|
|
#
|
|
# Push Immediately After Populating
|
|
# Fresh credentials go to partners right away rather than waiting for the next scheduled
|
|
# conf sync, so a partner is never authenticating with a key this host has already rotated.
|
|
#
|
|
# ==============================================================================================
|
|
# OPERATIONAL SAFEGUARDS
|
|
# ==============================================================================================
|
|
#
|
|
# Root Enforcement
|
|
# Reads service config files owned by container users and writes the host conf.
|
|
#
|
|
# Host Detection
|
|
# detect_hosts() resolves MY_ID, which selects which host conf is written. Populating the
|
|
# wrong host's conf would write this machine's credentials into a partner's file.
|
|
#
|
|
# Conf Existence Guard
|
|
# Aborts if the resolved host conf does not exist, rather than creating a partial one.
|
|
#
|
|
# Empty-Field-Only Writes
|
|
# Existing values are preserved unless --overwrite is passed explicitly.
|
|
#
|
|
# Container Ambiguity Guard
|
|
# _resolve_container() refuses a prefix matching more than one container, warning with the
|
|
# full match list. Exact name matches short-circuit and are never treated as ambiguous.
|
|
#
|
|
# Missing Service Tolerance
|
|
# A service that is not installed on this host is skipped with a log line. Absence is a
|
|
# valid configuration, not a failure.
|
|
#
|
|
# Map Block Validation
|
|
# Associative-array entries are only inserted when the target map actually exists in the
|
|
# conf; a missing map warns and skips instead of appending an orphaned entry.
|
|
#
|
|
# Dry Run Support
|
|
# --dry-run reports every value it would write, truncated, and writes nothing.
|
|
#
|
|
# Credential Truncation in Output
|
|
# Detected secrets are printed truncated (first 8 chars) so a populate run can be pasted
|
|
# into a log or issue without leaking full API keys.
|
|
#
|
|
# ==============================================================================================
|
|
# CONFIGURATION
|
|
# ==============================================================================================
|
|
#
|
|
# Reads and writes: Configurations/<hostid>.conf and Configurations/master.conf
|
|
#
|
|
# master.conf
|
|
#
|
|
# DOCKER_APPDATA_BASE
|
|
# Fallback appdata root when a container exposes no /config mount to read from.
|
|
#
|
|
# Everything else this script touches is a field it populates rather than one it consumes —
|
|
# see AUTO-DETECTED FIELDS above for the full list.
|
|
#
|
|
# ==============================================================================================
|
|
# RUNTIME MODES
|
|
# ==============================================================================================
|
|
#
|
|
# conf_populate.sh Populate empty fields only
|
|
# conf_populate.sh --overwrite Overwrite all detected fields (re-sync after arr key rotation)
|
|
# conf_populate.sh --dry-run Show what would be written, no changes
|
|
# conf_populate.sh --log Verbose output
|
|
# conf_populate.sh --no-push Skip pushing to partners after update
|
|
#
|
|
# ==============================================================================================
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
source "$SCRIPT_DIR/../../../load_config.sh"
|
|
SCRIPTS_ROOT="$SCRIPTS_DIR"
|
|
|
|
OVERWRITE=false
|
|
NO_PUSH=false
|
|
FILTERED_ARGS=()
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--overwrite) OVERWRITE=true ;;
|
|
--no-push) NO_PUSH=true ;;
|
|
*) FILTERED_ARGS+=("$arg") ;;
|
|
esac
|
|
done
|
|
parse_args "${FILTERED_ARGS[@]}"
|
|
detect_hosts
|
|
|
|
[[ "$EUID" -ne 0 ]] && { error "Must be run as root"; exit 1; }
|
|
|
|
CONF_FILE="$SCRIPTS_ROOT/Configurations/${MY_ID,,}.conf"
|
|
[[ ! -f "$CONF_FILE" ]] && { error "Conf file not found: $CONF_FILE"; exit 1; }
|
|
|
|
MASTER_CONF="$SCRIPTS_ROOT/Configurations/master.conf"
|
|
|
|
log "$ICON_GEAR Config: conf=${CONF_FILE} overwrite=${OVERWRITE:-false} no-push=${NO_PUSH:-false}"
|
|
|
|
UPDATED=0
|
|
SKIPPED=0
|
|
|
|
echo ""
|
|
echo "━━━ $ICON_GEAR Conf Auto-Populate — $MY_ID ($LOCAL_SERVER_NAME) ━━━"
|
|
echo ""
|
|
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
|
|
[[ "$OVERWRITE" == true ]] && warn "OVERWRITE mode — existing values will be replaced"
|
|
|
|
# ── Helper: write a var into a conf file if empty (or --overwrite) ────────────
|
|
# Optional 4th arg: target file (defaults to $CONF_FILE)
|
|
_set_conf_var() {
|
|
local var_name="$1" value="$2" label="$3" target="${4:-$CONF_FILE}"
|
|
[[ -z "$value" ]] && return
|
|
|
|
local current
|
|
current=$(grep -oP "(?<=^\s*${var_name}=\")[^\"]*" "$target" 2>/dev/null | head -1)
|
|
|
|
if [[ -n "$current" ]] && [[ "$OVERWRITE" == false ]]; then
|
|
log "$label: already set (${current:0:8}…) — skipping"
|
|
(( SKIPPED++ ))
|
|
return
|
|
fi
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
warn "DRY RUN — would set $var_name = ${value:0:8}…"
|
|
return
|
|
fi
|
|
|
|
if grep -q "^\s*${var_name}=" "$target"; then
|
|
sed -i "s|^\(\s*${var_name}\s*=\s*\)\"[^\"]*\"|\1\"${value}\"|" "$target"
|
|
else
|
|
printf '\n %s="%s"\n' "$var_name" "$value" >> "$target"
|
|
fi
|
|
info "$label: set ✅"
|
|
(( UPDATED++ ))
|
|
}
|
|
|
|
# ── Helper: add or update an entry in a declare -A map block ──────────────────
|
|
# Inserts [key]="value" before the closing ) of the named map in $CONF_FILE.
|
|
# Skips if an identical uncommented entry already exists (unless --overwrite).
|
|
_set_conf_map_entry() {
|
|
local map_name="$1" key="$2" value="$3" label="$4"
|
|
[[ -z "$key" || -z "$value" ]] && return
|
|
|
|
if grep -qP "^\s*\[\"${key//\//\\/}\"\]=" "$CONF_FILE" 2>/dev/null; then
|
|
if [[ "$OVERWRITE" == false ]]; then
|
|
log "$label: already set — skipping"
|
|
(( SKIPPED++ ))
|
|
return
|
|
fi
|
|
[[ "$DRY_RUN" == true ]] && { warn "DRY RUN — would update ${map_name}[\"${key}\"]"; return; }
|
|
sed -i "s|^\(\s*\)\[\"${key}\"\]=\"[^\"]*\"|\1[\"${key}\"]=\"${value}\"|" "$CONF_FILE"
|
|
info "$label: set ✅"
|
|
(( UPDATED++ ))
|
|
return
|
|
fi
|
|
|
|
if ! grep -q "declare -A ${map_name}=" "$CONF_FILE" 2>/dev/null; then
|
|
warn "$label: ${map_name} not found in conf — skipping"
|
|
return
|
|
fi
|
|
|
|
[[ "$DRY_RUN" == true ]] && { warn "DRY RUN — would set ${map_name}[\"${key}\"] = \"${value}\""; return; }
|
|
|
|
awk -v mapname="${map_name}" -v key="$key" -v val="$value" '
|
|
BEGIN { in_map=0; done=0 }
|
|
!done && index($0, "declare -A " mapname) { in_map=1 }
|
|
in_map && !done && /^\s*\)/ {
|
|
printf " [\"%s\"]=\"%s\"\n", key, val
|
|
done=1; in_map=0
|
|
}
|
|
{ print }
|
|
' "$CONF_FILE" > "${CONF_FILE}.tmp" && mv "${CONF_FILE}.tmp" "$CONF_FILE" || {
|
|
rm -f "${CONF_FILE}.tmp"
|
|
warn "$label: failed to update ${map_name}"
|
|
return 1
|
|
}
|
|
info "$label: set ✅"
|
|
(( UPDATED++ ))
|
|
}
|
|
|
|
# ── Helper: resolve a container name from a prefix, refusing ambiguity ────────
|
|
# grep -m1 silently returns whichever name docker happens to list first. On this very host
|
|
# "^authelia" matches both Authelia (9091, primary) and Authelia-Secondary (9092), and -m1
|
|
# picks the secondary — writing the wrong container into the conf that every other script
|
|
# then trusts. An exact match wins outright; otherwise a prefix match must be unambiguous.
|
|
# Same rule detect_hosts() applies to host identity: exactly one candidate, or none.
|
|
_resolve_container() {
|
|
local pattern="$1"
|
|
local all exact matches count
|
|
|
|
all=$(docker ps -a --format '{{.Names}}' 2>/dev/null)
|
|
[[ -z "$all" ]] && return 1
|
|
|
|
# Exact name match short-circuits — "Authelia" is not ambiguous with "Authelia-Secondary"
|
|
exact=$(printf '%s\n' "$all" | grep -ixm1 -- "$pattern")
|
|
[[ -n "$exact" ]] && { echo "$exact"; return 0; }
|
|
|
|
matches=$(printf '%s\n' "$all" | grep -i -- "^${pattern}")
|
|
count=$(printf '%s\n' "$matches" | grep -c .)
|
|
|
|
if [[ "$count" -gt 1 ]]; then
|
|
warn "Container prefix '${pattern}' is ambiguous — matches: $(printf '%s' "$matches" | tr '\n' ' ')"
|
|
warn " Refusing to guess. Set the container name manually in host*.conf."
|
|
return 1
|
|
fi
|
|
|
|
[[ "$count" -eq 1 ]] && { printf '%s\n' "$matches"; return 0; }
|
|
return 1
|
|
}
|
|
|
|
# ── Helper: find arr config dir via docker volume mount ───────────────────────
|
|
_arr_config_dir() {
|
|
local pattern="$1"
|
|
local container_name
|
|
container_name=$(_resolve_container "$pattern") || return 1
|
|
[[ -z "$container_name" ]] && return 1
|
|
|
|
local config_path
|
|
config_path=$(docker inspect "$container_name" 2>/dev/null | \
|
|
jq -r '.[0].Mounts[]? | select(.Destination == "/config") | .Source' 2>/dev/null | head -1)
|
|
[[ -z "$config_path" ]] && config_path="${DOCKER_APPDATA_BASE:-/mnt/user/appdata}/${container_name}"
|
|
|
|
[[ -d "$config_path" ]] && echo "$config_path" || return 1
|
|
}
|
|
|
|
# ── Helper: read XML tag value ────────────────────────────────────────────────
|
|
_xml_val() {
|
|
local file="$1" tag="$2"
|
|
grep -oP "(?<=<${tag}>)[^<]+" "$file" 2>/dev/null | head -1
|
|
}
|
|
|
|
# ── Helper: get host-side port for a container's internal port ────────────────
|
|
_docker_host_port() {
|
|
local container="$1" container_port="$2"
|
|
docker inspect "$container" 2>/dev/null | \
|
|
jq -r --arg p "${container_port}/tcp" \
|
|
'.[0].NetworkSettings.Ports[$p]?[0].HostPort // empty' 2>/dev/null | head -1
|
|
}
|
|
|
|
# ── Helper: get host path for a container destination mount ──────────────────
|
|
_docker_volume_host() {
|
|
local container="$1" dest="$2"
|
|
docker inspect "$container" 2>/dev/null | \
|
|
jq -r --arg d "$dest" \
|
|
'.[0].Mounts[]? | select(.Destination == $d) | .Source' 2>/dev/null | head -1
|
|
}
|
|
|
|
# ── Helper: find host↔container path mapping for a given container path ───────
|
|
# Finds the most specific mount whose destination is a prefix of container_path
|
|
# and where source != destination (i.e. an actual remapping exists).
|
|
# Outputs "source|destination" or nothing if no remapping found.
|
|
_docker_path_map() {
|
|
local container="$1" container_path="$2"
|
|
docker inspect "$container" 2>/dev/null | \
|
|
jq -r '.[0].Mounts[]? | select(.Destination != "/config") | "\(.Source)|\(.Destination)"' \
|
|
2>/dev/null | \
|
|
awk -F'|' -v target="$container_path" '
|
|
$1 != $2 && length($2) > 0 && index(target, $2) == 1 { print length($2), $0 }
|
|
' | sort -rn | head -1 | cut -d' ' -f2-
|
|
}
|
|
|
|
# ==============================================================================================
|
|
# ── Owner short name + SSH key ────────────────────────────────────────────────────────────────
|
|
# ==============================================================================================
|
|
|
|
owner=$(echo "$LOCAL_SERVER_NAME" | sed 's/^[Uu][Nn][Rr][Aa][Ii][Dd]-//i' | tr '[:upper:]' '[:lower:]')
|
|
_set_conf_var "${MY_ID}_OWNER" "$owner" "Owner short name"
|
|
_set_conf_var "${MY_ID}_SSH_KEY" "/root/.ssh/${owner}_rsync_automation" "SSH key path"
|
|
|
|
# ==============================================================================================
|
|
# ── Arr API keys + URLs + root paths + path maps ──────────────────────────────────────────────
|
|
# ==============================================================================================
|
|
|
|
for arr in radarr sonarr lidarr; do
|
|
arr_upper="${arr^^}"
|
|
arr_container=$(_resolve_container "$arr")
|
|
config_dir=$(_arr_config_dir "$arr") || {
|
|
log "${arr_upper}: no running container found — skipping"
|
|
continue
|
|
}
|
|
config_xml="${config_dir}/config.xml"
|
|
|
|
if [[ ! -f "$config_xml" ]]; then
|
|
log "${arr_upper}: config.xml not found at $config_xml — skipping"
|
|
continue
|
|
fi
|
|
|
|
key=$(_xml_val "$config_xml" "ApiKey")
|
|
port=$(_xml_val "$config_xml" "Port")
|
|
port="${port:-$(case $arr in radarr) echo 7878;; sonarr) echo 8989;; lidarr) echo 8686;; esac)}"
|
|
url_base="http://localhost:${port}"
|
|
|
|
_set_conf_var "${MY_ID}_${arr_upper}_API_KEY" "$key" "${arr_upper} API key"
|
|
_set_conf_var "${MY_ID}_${arr_upper}_URL" "$url_base" "${arr_upper} URL"
|
|
|
|
if [[ -n "$key" ]]; then
|
|
case "$arr" in lidarr) api_ver="v1" ;; *) api_ver="v3" ;; esac
|
|
root_json=$(curl -sf --max-time 5 \
|
|
-H "X-Api-Key: $key" "${url_base}/api/${api_ver}/rootfolder" 2>/dev/null)
|
|
root_path=$(echo "$root_json" | jq -r '.[0].path // empty' 2>/dev/null)
|
|
|
|
case "$arr" in
|
|
radarr) _set_conf_var "${MY_ID}_RADARR_MOVIES_ROOT" "$root_path" "Radarr movies root" ;;
|
|
sonarr) _set_conf_var "${MY_ID}_SONARR_TV_ROOT" "$root_path" "Sonarr TV root" ;;
|
|
lidarr) _set_conf_var "${MY_ID}_LIDARR_MUSIC_ROOT" "$root_path" "Lidarr music root" ;;
|
|
esac
|
|
|
|
if [[ -n "$root_path" && -n "$arr_container" ]]; then
|
|
map_entry=$(_docker_path_map "$arr_container" "$root_path")
|
|
if [[ -n "$map_entry" ]]; then
|
|
map_src="${map_entry%%|*}"
|
|
map_dest="${map_entry##*|}"
|
|
case "$arr" in
|
|
radarr) _set_conf_map_entry "${MY_ID}_RADARR_PATH_MAP" "$map_dest" "$map_src" "Radarr path map" ;;
|
|
sonarr) _set_conf_map_entry "${MY_ID}_SONARR_PATH_MAP" "$map_dest" "$map_src" "Sonarr path map" ;;
|
|
lidarr) _set_conf_map_entry "${MY_ID}_LIDARR_PATH_MAP" "$map_dest" "$map_src" "Lidarr path map" ;;
|
|
esac
|
|
fi
|
|
fi
|
|
fi
|
|
done
|
|
|
|
# ==============================================================================================
|
|
# ── SABnzbd API key + URL ─────────────────────────────────────────────────────────────────────
|
|
# ==============================================================================================
|
|
|
|
sab_dir=$(_arr_config_dir "sabnzbd") && {
|
|
sab_ini=$(find "$sab_dir" -maxdepth 2 -name "sabnzbd.ini" 2>/dev/null | head -1)
|
|
if [[ -f "$sab_ini" ]]; then
|
|
sab_key=$(grep -oP '(?<=^api_key\s*=\s*)\S+' "$sab_ini" 2>/dev/null | head -1)
|
|
sab_port=$(grep -oP '(?<=^port\s*=\s*)\d+' "$sab_ini" 2>/dev/null | head -1)
|
|
_set_conf_var "${MY_ID}_SABNZBD_API_KEY" "$sab_key" "SABnzbd API key"
|
|
_set_conf_var "${MY_ID}_SABNZBD_URL" "http://localhost:${sab_port:-8080}" "SABnzbd URL"
|
|
fi
|
|
}
|
|
|
|
# ==============================================================================================
|
|
# ── slskd API key + URL ───────────────────────────────────────────────────────────────────────
|
|
# ==============================================================================================
|
|
|
|
slskd_dir=$(_arr_config_dir "slskd") && {
|
|
slskd_yml=$(find "$slskd_dir" -maxdepth 2 \( -name "*.yml" -o -name "*.yaml" \) 2>/dev/null | head -1)
|
|
if [[ -f "$slskd_yml" ]]; then
|
|
slskd_key=$(grep -oP '(?<=api_key:\s)[\w-]+' "$slskd_yml" 2>/dev/null | head -1)
|
|
[[ -z "$slskd_key" ]] && \
|
|
slskd_key=$(grep -oP '(?<=apikey:\s)[\w-]+' "$slskd_yml" 2>/dev/null | head -1)
|
|
slskd_port=$(grep -oP '(?<=port:\s)\d+' "$slskd_yml" 2>/dev/null | head -1)
|
|
_set_conf_var "${MY_ID}_SLSKD_API_KEY" "$slskd_key" "slskd API key"
|
|
_set_conf_var "${MY_ID}_SLSKD_URL" "http://localhost:${slskd_port:-5030}" "slskd URL"
|
|
fi
|
|
}
|
|
|
|
# ==============================================================================================
|
|
# ── qBittorrent URL + credentials ────────────────────────────────────────────────────────────
|
|
# ==============================================================================================
|
|
|
|
qbit_dir=$(_arr_config_dir "qbittorrent") && {
|
|
qbit_conf=$(find "$qbit_dir" -maxdepth 3 -name "qBittorrent.conf" 2>/dev/null | head -1)
|
|
if [[ -f "$qbit_conf" ]]; then
|
|
qbit_port=$(grep -oP '(?<=WebUI\\Port=)\d+' "$qbit_conf" 2>/dev/null | head -1)
|
|
qbit_user=$(grep -oP '(?<=WebUI\\Username=)\S+' "$qbit_conf" 2>/dev/null | head -1)
|
|
# Only capture plaintext password — PBKDF2 hashes are not usable
|
|
qbit_pass=$(grep -oP '(?<=WebUI\\Password=)[^\r\n]+' "$qbit_conf" 2>/dev/null | \
|
|
grep -v '@ByteArray' | head -1)
|
|
_set_conf_var "${MY_ID}_QBIT_URL" "http://localhost:${qbit_port:-8080}" "qBittorrent URL"
|
|
_set_conf_var "${MY_ID}_QBIT_USERNAME" "$qbit_user" "qBittorrent username"
|
|
_set_conf_var "${MY_ID}_QBIT_PASSWORD" "$qbit_pass" "qBittorrent password"
|
|
fi
|
|
}
|
|
|
|
# ==============================================================================================
|
|
# ── Media server container names + URLs + transcode path ─────────────────────────────────────
|
|
# ==============================================================================================
|
|
|
|
transcode_dir=""
|
|
|
|
for pattern in "emby" "jellyfin"; do
|
|
container=$(_resolve_container "$pattern") || continue
|
|
[[ -z "$container" ]] && continue
|
|
|
|
case "$pattern" in
|
|
emby)
|
|
host_port=$(_docker_host_port "$container" "8096")
|
|
_set_conf_var "${MY_ID}_EMBY_CONTAINER" "$container" "Emby container name"
|
|
_set_conf_var "${MY_ID}_EMBY_URL" "http://localhost:${host_port:-8096}" "Emby URL"
|
|
;;
|
|
jellyfin)
|
|
host_port=$(_docker_host_port "$container" "8096")
|
|
_set_conf_var "${MY_ID}_JELLYFIN_CONTAINER" "$container" "Jellyfin container name"
|
|
_set_conf_var "${MY_ID}_JELLYFIN_URL" "http://localhost:${host_port:-8095}" "Jellyfin URL"
|
|
;;
|
|
esac
|
|
|
|
# Transcode path: first container with a /transcode mount wins
|
|
if [[ -z "$transcode_dir" ]]; then
|
|
transcode_dir=$(_docker_volume_host "$container" "/transcode")
|
|
fi
|
|
done
|
|
|
|
[[ -n "$transcode_dir" ]] && \
|
|
_set_conf_var "${MY_ID}_TRANSCODE_SSD" "${transcode_dir%/}/" "Transcode SSD path"
|
|
|
|
# ==============================================================================================
|
|
# ── Authelia container + config path ──────────────────────────────────────────────────────────
|
|
# ==============================================================================================
|
|
|
|
authelia_container=$(docker ps -a --format '{{.Names}}' 2>/dev/null | grep -im1 "authelia")
|
|
if [[ -n "$authelia_container" ]]; then
|
|
_set_conf_var "${MY_ID}_AUTHELIA_CONTAINER" "$authelia_container" "Authelia container"
|
|
authelia_cfg_dir=$(_docker_volume_host "$authelia_container" "/config")
|
|
[[ -n "$authelia_cfg_dir" ]] && \
|
|
_set_conf_var "${MY_ID}_AUTHELIA_CONFIG" "${authelia_cfg_dir}/configuration.yml" "Authelia config path"
|
|
fi
|
|
|
|
# ==============================================================================================
|
|
# ── Network interface ─────────────────────────────────────────────────────────────────────────
|
|
# ==============================================================================================
|
|
|
|
nic=$(ip route show default 2>/dev/null | grep -oP '(?<=dev )\S+' | head -1)
|
|
_set_conf_var "${MY_ID}_SYS_WATCHDOG_NIC" "$nic" "Default NIC"
|
|
|
|
# ==============================================================================================
|
|
# ── Storage mode (internal NVMe/SSD vs USB flash boot) ───────────────────────────────────────
|
|
# ==============================================================================================
|
|
|
|
boot_src=$(findmnt -n -o SOURCE /boot 2>/dev/null)
|
|
if [[ -n "$boot_src" ]]; then
|
|
boot_dev=$(lsblk -no PKNAME "$boot_src" 2>/dev/null | head -1)
|
|
if [[ "$boot_dev" =~ ^nvme ]]; then
|
|
storage_mode=true
|
|
else
|
|
boot_transport=$(cat "/sys/block/${boot_dev}/device/transport" 2>/dev/null)
|
|
[[ "$boot_transport" == "usb" ]] && storage_mode=false || storage_mode=true
|
|
fi
|
|
_set_conf_var "${MY_ID}_STORAGE_MODE_INTERNAL" "$storage_mode" \
|
|
"Storage mode (NVMe/SSD=true, USB=false)"
|
|
fi
|
|
|
|
# ==============================================================================================
|
|
# ── Master conf: host identity + Gitea container ─────────────────────────────────────────────
|
|
# ==============================================================================================
|
|
|
|
if [[ -f "$MASTER_CONF" ]]; then
|
|
echo ""
|
|
echo " ── Master conf ──"
|
|
_set_conf_var "$MY_ID" "$LOCAL_SERVER_NAME" "master.conf ${MY_ID} hostname" "$MASTER_CONF"
|
|
|
|
gitea_container=$(docker ps -a --format '{{.Names}}' 2>/dev/null | grep -im1 "gitea")
|
|
[[ -n "$gitea_container" ]] && \
|
|
_set_conf_var "GITEA_CONTAINER" "$gitea_container" "Gitea container" "$MASTER_CONF"
|
|
fi
|
|
|
|
# ==============================================================================================
|
|
# ── Summary + push ────────────────────────────────────────────────────────────────────────────
|
|
# ==============================================================================================
|
|
|
|
echo ""
|
|
echo "━━━━━ $ICON_SUMMARY Conf Populate Summary ━━━━━"
|
|
echo " Updated: $UPDATED field(s)"
|
|
echo " Skipped: $SKIPPED already set"
|
|
echo " Conf: $CONF_FILE"
|
|
echo "━━━━━━━━━━━━━━━━━━━━━"
|
|
|
|
if [[ "$UPDATED" -gt 0 ]] && [[ "$DRY_RUN" == false ]] && [[ "$NO_PUSH" == false ]]; then
|
|
echo ""
|
|
info "Pushing updated conf to partners..."
|
|
bash "$SCRIPTS_ROOT/System_Essentials/conf_sync.sh" --push-only "${EXTRA_FLAGS[@]}" || true
|
|
fi
|