diff --git a/Partnership/partnership_onboard.sh b/Partnership/partnership_onboard.sh index c53d9d2..950b04b 100755 --- a/Partnership/partnership_onboard.sh +++ b/Partnership/partnership_onboard.sh @@ -42,6 +42,8 @@ # Step 10: Conf push — push master.conf + setup state to all listed hosts # Step 11: Service discovery — conf_populate.sh on the mirror, last, once the stacks it # would discover are actually deployed there +# Step 12: Container grouping — file our containers on the mirror under "-Fallback", +# icon resolved here and passed over: the mirror has no Emby key # # ============================================================================================== # DESIGN PRINCIPLES @@ -1047,6 +1049,56 @@ else unset _mirror_sd _pop_script fi +# ── Step 12: Group our containers on the mirror ─────────────────────────────────────────────── +# The mirror now runs a dozen containers that are ours, scattered among its own. This files them +# under one folder named after us — "-Fallback" — matching the convention the owner +# already keeps for the mirror's containers. +# +# The icon is resolved HERE and passed over, not looked up there. It comes from the closest Emby +# user to our own name, and the mirror has neither our Emby key nor necessarily an Emby at all — +# so a lookup on that side would find nothing and the folder would come up blank. +# +# Not fatal in any direction: folder.view3 absent on the mirror is a clean skip, and a folder +# without a picture is still a folder. +echo "" +echo "━━━ $ICON_GEAR Step 12 — Container Grouping ($MIRROR) ━━━" + +FOLDER_OK=false +if [[ -z "${MIRROR_IP:-}" ]]; then + warn "$MIRROR has no resolved IP — skipping container grouping" + FOLDER_OK=skipped +else + mapfile -t _deployed < <(deployed_stack_container_names) + _deployed_csv=$(IFS=,; echo "${_deployed[*]}") + if [[ -z "$_deployed_csv" ]]; then + log "No stack templates resolved to container names — nothing to group" + FOLDER_OK=skipped + elif [[ "$DRY_RUN" == true ]]; then + warn "DRY RUN — would create ${MY_ID}-named fallback folder on $MIRROR with: $_deployed_csv" + FOLDER_OK=true + else + _ff_local="$SCRIPTS_ROOT/Plugin/$PLATFORM/Tools/fallback_folder.php" + _icon=$(php "$_ff_local" --host="$MY_ID" --icon-only 2>/dev/null || true) + [[ -z "$_icon" ]] && log "No icon resolved for $MY_ID — folder will be created without one" + + _mirror_sd=$(resolve_remote_scripts_dir "$MIRROR_IP" "$MIRROR_SSH_KEY" "no") + _ff_remote="${_mirror_sd}/Plugin/${PLATFORM}/Tools/fallback_folder.php" + if timeout 60 ssh -i "$MIRROR_SSH_KEY" \ + -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes -o StrictHostKeyChecking=no \ + root@"$MIRROR_IP" \ + "[ -f '$_ff_remote' ] || { echo missing; exit 127; } + php '$_ff_remote' --host=$(printf '%q' "$MY_ID") \ + --containers=$(printf '%q' "$_deployed_csv") \ + --icon=$(printf '%q' "$_icon")" 2>/dev/null; then + FOLDER_OK=true + else + warn "Could not group containers on $MIRROR — run $_ff_remote there by hand" + fi + unset _ff_local _ff_remote _mirror_sd _icon + fi + unset _deployed _deployed_csv +fi + # ── Write Phase 2 completion state ──────────────────────────────────────────────────────────── [[ "$ONBOARD_OK" == true && "$DRY_RUN" == false ]] && write_onboard_phase "$MIRROR_ID" 2 @@ -1080,6 +1132,7 @@ echo " Step 9d — Media seed: $( [[ "$SKIP_MEDIA_SEED" == true ]] && ech echo " Step 9e — Webhook listener: $(_skip "$SKIP_WEBHOOK_LISTENER" "$WEBHOOK_LISTENER_OK")" echo " Step 10 — Conf push: $( [[ "$ONBOARD_OK" == false ]] && echo "skipped" || echo "$(_ok "$MASTER_PUSH_OK")" )" echo " Step 11 — Discovery: $( [[ "$POPULATE_OK" == skipped ]] && echo "skipped (unreachable)" || _ok "$POPULATE_OK" )" +echo " Step 12 — Grouping: $( [[ "$FOLDER_OK" == skipped ]] && echo "skipped" || _ok "$FOLDER_OK" )" echo "" if [[ "$ONBOARD_OK" == true ]]; then diff --git a/Plugin/unraid/Partnership/containers.sh b/Plugin/unraid/Partnership/containers.sh index c02b918..2ff207b 100755 --- a/Plugin/unraid/Partnership/containers.sh +++ b/Plugin/unraid/Partnership/containers.sh @@ -454,6 +454,31 @@ deploy_xml_stack() { # # Usage: ensure_stack_networks_on_remote "$MIRROR_IP" "$MIRROR_SSH_KEY" # ============================================================================================== +# ============================================================================================== +# ── Container names this onboard deploys, read from the templates it deploys them from ──────── +# +# Echoes one name per line. The element is the same value deploy_xml_stack() passes to +# `docker create --name`, so this is the deployed set by construction rather than by asking the +# mirror what it ended up with — which would also pick up whatever the mirror already ran. +# +# Usage: mapfile -t names < <(deployed_stack_container_names) +# ============================================================================================== +deployed_stack_container_names() { + local -a xml_names=() + [[ ${#PARTNERSHIP_AUTH_STACK[@]} -gt 0 ]] && xml_names+=("${PARTNERSHIP_AUTH_STACK[@]}") + [[ ${#PARTNERSHIP_ARR_STACK[@]} -gt 0 ]] && xml_names+=("${PARTNERSHIP_ARR_STACK[@]}") + [[ ${#PARTNERSHIP_SERVICES_STACK[@]} -gt 0 ]] && xml_names+=("${PARTNERSHIP_SERVICES_STACK[@]}") + + local xml_name xml_file cname + for xml_name in "${xml_names[@]}"; do + [[ -z "$xml_name" ]] && continue + xml_file="${TEMPLATES_DIR}/${xml_name}" + [[ -f "$xml_file" ]] || continue + cname=$(awk 'match($0,/([^<]+)<\/Name>/,a){print a[1];exit}' "$xml_file") + [[ -n "$cname" ]] && echo "$cname" + done +} + ensure_stack_networks_on_remote() { local remote_ip="$1" ssh_key="$2" local -a xml_names=() nets=() diff --git a/Plugin/unraid/Tools/fallback_folder.php b/Plugin/unraid/Tools/fallback_folder.php index 2e76a87..fee9822 100755 --- a/Plugin/unraid/Tools/fallback_folder.php +++ b/Plugin/unraid/Tools/fallback_folder.php @@ -40,7 +40,8 @@ // Icon resolution failing never blocks the folder: no image is a cosmetic loss, no folder is not. // // REQUEST -// fallback_folder.php --host=HOST2 [--containers=A,B,C] [--dry-run] +// fallback_folder.php --host=HOST2 [--containers=A,B,C] [--icon=URL] [--dry-run] +// fallback_folder.php --host=HOST2 --icon-only resolve and print the icon URL, write nothing // // CONFIGURATION // HOST1/HOST2… master.conf — the hostname the folder is named after @@ -57,9 +58,10 @@ require_once $pluginDir . '/include/config.php'; define('FV3_JSON', '/boot/config/plugins/folder.view3/docker.json'); // ── Args ────────────────────────────────────────────────────────────────────── -$opts = ['host' => '', 'containers' => '', 'dry-run' => false]; +$opts = ['host' => '', 'containers' => '', 'icon' => '', 'dry-run' => false, 'icon-only' => false]; foreach (array_slice($argv, 1) as $a) { - if ($a === '--dry-run') { $opts['dry-run'] = true; continue; } + if ($a === '--dry-run') { $opts['dry-run'] = true; continue; } + if ($a === '--icon-only'){ $opts['icon-only'] = true; continue; } if (preg_match('/^--([a-z-]+)=(.*)$/', $a, $m)) $opts[$m[1]] = $m[2]; } $hostId = strtoupper(trim($opts['host'])); @@ -72,6 +74,7 @@ $vars = vv_conf_vars(); $hostname = trim($vars[$hostId] ?? ''); if ($hostname === '') { fwrite(STDERR, "$hostId is not set in master.conf\n"); exit(1); } + // derive_short_name() in common.sh: lowercase, strip a leading "unraid-", capitalise. $short = preg_replace('/^unraid-/i', '', $hostname); $short = ucfirst($short); @@ -124,9 +127,40 @@ function vv_closest_emby_icon(string $short, array $vars, string $meId): array { } $meId = strtoupper(vv_detect_host()); -[$icon, $iconNote] = vv_closest_emby_icon($short, $vars, $meId); + +// --icon= skips the lookup entirely. The mirror needs this: it is being given a folder named +// after the OWNER, and the avatar lives in the owner's Emby — which the mirror has no key for and +// may not run at all. So the owner resolves the URL with --icon-only and hands it over, rather +// than the mirror guessing from a userbase it cannot see. +if (trim($opts['icon']) !== '') { + $icon = trim($opts['icon']); + $iconNote = 'supplied by caller'; +} else { + [$icon, $iconNote] = vv_closest_emby_icon($short, $vars, $meId); +} + +// --icon-only: resolve and print, touch nothing. Exits non-zero when there is no icon, so a +// caller can tell "no image" from "empty string because something broke". +if ($opts['icon-only']) { + if ($icon === '') { fwrite(STDERR, "no icon: $iconNote\n"); exit(1); } + echo $icon . "\n"; + exit(0); +} // ── Upsert the folder ───────────────────────────────────────────────────────── +// A host does not keep a fallback folder for itself — the folder means "containers I run on +// SOMEONE ELSE's behalf", so naming it after this machine is always wrong. Checked here rather +// than at argument parsing, because --icon-only legitimately asks for THIS host's own avatar: +// the owner resolves its own picture to hand to the mirror, which is the whole point of that mode. +// +// Refused rather than created, because the failure is otherwise silent — an empty folder named +// after yourself looks plausible enough to survive a glance. One appeared on HOST1 exactly this +// way, when an older copy of this script ignored an unrecognised flag and ran the upsert anyway. +if (strcasecmp($hostId, strtoupper(vv_detect_host())) === 0) { + fwrite(STDERR, "$hostId is this host — a fallback folder is named after the PARTNER, not self\n"); + exit(2); +} + if (!file_exists(FV3_JSON)) { echo "folder.view3 is not installed — nothing to do\n"; exit(0);