#!/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/.conf) # ────────────────────────────────────────────────────────────────────────── # HOSTN_OWNER from hostname (strip unRAID- prefix, lowercase) # HOSTN_SSH_KEY from hostname convention (/root/.ssh/_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/.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