#!/bin/bash # ============================================================================================== # ================================= Unraid Platform Adapter ==================================== # ============================================================================================== # # PURPOSE # ───────────────────────────────────────────────────────────────────────────── # Platform abstraction layer for Unraid. Sourced by load_config.sh when # PLATFORM=unraid. Scripts call platform_*() functions instead of OS-specific # commands — the adapter isolates all OS-dependent logic in one place. # # ============================================================================================== # OPERATIONAL MODEL # ============================================================================================== # # API CONTRACT # Every function returns 0 on success / 1 on failure unless noted. # Functions that produce output write to stdout; callers capture with $(). # No function calls exit — callers decide what failure means for their flow. # # ADDING A PLATFORM # Create Plugin/truenas/adapter.sh (or ubuntu/adapter.sh) implementing the # same function names. load_config.sh sources Plugin/$PLATFORM/adapter.sh — # no other changes needed anywhere in the codebase. # # FUNCTIONS # platform_require_cmd — verify a platform command exists and is executable # platform_storage_healthy — array mounted and shfs active on /mnt/user # platform_disk_states_path — path to the platform disk state file # platform_get_disk_states — raw disk state content from platform # platform_get_temp_thresholds — disk warn/crit °C from platform config # platform_is_maintenance_running — parity check/sync in progress # platform_is_service_enabled — docker or libvirt enabled in boot config # platform_restart_service — restart a named service via rc.d # platform_stop_service — stop a named service via rc.d # platform_is_service_running — check if a named service process is alive # platform_is_mover_running — unRAID mover process check # platform_stop_user_scripts — kill all user.scripts background processes # platform_send_os_notification — native unRAID notify (dynamix) # platform_storage_path — root path for user shares/storage (e.g. /mnt/user) # platform_webui_install_path — where the platform serves the WebGUI plugin files from # platform_scripts_dir_probe_cmd — shell command to run on a remote to discover its SCRIPTS_DIR # platform_get_templates_dir — path to Unraid CA docker templates-user directory # platform_setup_db_path — path to the persistent Varaverk setup/wizard state database # platform_get_os_version — local OS version string (e.g. "7.2.3") # platform_os_version_probe_cmd — shell command to run on a remote to retrieve its OS version # platform_rebuild_container — rebuild a container from its stored XML template # platform_push_conf — push master.conf to all listed hosts via WebGUI PHP # platform_push_setup_state — push wizard setup state to WebGUI PHP # # ============================================================================================== # DESIGN PRINCIPLES # ============================================================================================== # # One Place Knows the OS # Every Unraid-specific path, rc.d script and config location lives here. Scripts never # branch on OS and never hardcode /etc/rc.d, /boot/config or dynamix paths. That is what # makes a second platform a single new file rather than a hundred edits. # # Never Exit, Always Return # No function here calls exit. A platform capability being absent is information the # caller needs, not a decision the adapter gets to make — a watchdog may want to skip a # check where an installer wants to abort, and only they know which. # # Report, Do Not Remediate # The adapter answers questions and performs the single action asked of it. It does not # retry, escalate, notify or heal. Every one of those policies belongs to the caller, and # burying them here would make identical calls behave differently per platform. # # Stdout Is the Return Channel # Value-producing functions write to stdout and are captured with $(). Status is carried # by the exit code. Keeping those separate is what lets callers use them in conditionals # without parsing output. # # ============================================================================================== # OPERATIONAL SAFEGUARDS # ============================================================================================== # # No Root, No Lock, No detect_hosts — Deliberate # This is a sourced library, loaded by load_config.sh whenever PLATFORM=unraid. It runs # inside the caller's process and holds no state of its own. A root check here would fire # for every script in the ecosystem including read-only ones, and a lock would be taken # on every source. The executable scripts own those gates. Do not "fix" this to match them. # # Executable Validation Before Use # platform_require_cmd() and the rc.d helpers verify a target exists and is executable # before invoking it, so a missing platform binary returns a clean failure rather than a # command-not-found in the middle of a caller's flow. # # Notification Is Best-Effort # platform_send_os_notification() returns 1 if the dynamix notify script is absent rather # than failing the caller. A missing notifier must never turn a successful operation into # a reported failure. # # Graceful Degradation on Unknown Services # _platform_rc_script() falls through to /etc/rc.d/rc. for any service it does not # explicitly map, so a new service works without an adapter change — and still fails # cleanly if that path does not exist. # # ============================================================================================== # CONFIGURATION # ============================================================================================== # # None of its own. The adapter is selected by PLATFORM in varaverk.cfg, which load_config.sh # uses to source Plugin/$PLATFORM/adapter.sh. # # It reads platform-owned files rather than Varaverk config: # # /boot/config/plugins/dynamix/dynamix.cfg disk temperature thresholds # /boot/config/plugins/varaverk/varaverk.cfg SCRIPTS_DIR probe # /boot/config/plugins/dockerMan/templates-user container rebuild templates # /etc/rc.d/rc.* service control # # Those paths are Unraid's, not Varaverk's — which is exactly why they are confined to this # file. STATE_DIR, DATA_DIR and friends belong to master.conf and are not the adapter's # concern. # # ============================================================================================== # RUNTIME MODES # ============================================================================================== # # None — sourced, never executed: # # source "$LOAD_CONFIG_DIR/Plugin/$PLATFORM/adapter.sh" # # No argument parsing, no --dry-run, no --status. Callers that need a dry run implement it # around the platform_*() call, since only they know which of their actions are destructive. # # ============================================================================================== # ────────────────────────────────────────────────────────────────────────────────────────────── # Internal: map a logical service name → its rc.d script path # ────────────────────────────────────────────────────────────────────────────────────────────── _platform_rc_script() { case "$1" in docker) echo "/etc/rc.d/rc.docker" ;; sshd) echo "/etc/rc.d/rc.sshd" ;; libvirt) echo "/etc/rc.d/rc.libvirt" ;; rsyslog) echo "/etc/rc.d/rc.rsyslogd" ;; nginx) echo "/etc/rc.d/rc.nginx" ;; php-fpm) echo "/etc/rc.d/rc.php-fpm" ;; *) echo "/etc/rc.d/rc.$1" ;; esac } # ────────────────────────────────────────────────────────────────────────────────────────────── # platform_require_cmd [test_arg] [expected_output] [label] # Replaces platform_require_cmd. Returns 0 if the command exists and (optionally) its output # matches expected_output. Returns 1 and prints a warning if not. # ────────────────────────────────────────────────────────────────────────────────────────────── platform_require_cmd() { local cmd="$1" local test_arg="${2:-}" local expected="${3:-}" local label="${4:-$cmd}" if [[ ! -x "$cmd" ]]; then return 1 fi if [[ -n "$test_arg" && -n "$expected" ]]; then "$cmd" $test_arg 2>&1 | grep -q "$expected" || return 1 fi return 0 } # ────────────────────────────────────────────────────────────────────────────────────────────── # platform_storage_healthy # Returns 0 if /mnt/user is mounted as shfs (array is up and healthy). # Returns 1 if the mount is absent or is not shfs (array stopped / degraded). # ────────────────────────────────────────────────────────────────────────────────────────────── platform_storage_healthy() { df --output=fstype /mnt/user 2>/dev/null | grep -q shfs } # ────────────────────────────────────────────────────────────────────────────────────────────── # platform_get_disk_states # Writes raw /var/local/emhttp/disks.ini to stdout. # Returns 1 if the file is absent (array not started or emhttp not running). # ────────────────────────────────────────────────────────────────────────────────────────────── platform_disk_states_path() { echo "/var/local/emhttp/disks.ini" } platform_get_disk_states() { local disks_ini disks_ini=$(platform_disk_states_path) [[ -f "$disks_ini" ]] || return 1 cat "$disks_ini" } # ────────────────────────────────────────────────────────────────────────────────────────────── # platform_get_temp_thresholds # Writes four space-separated values to stdout: HDD_HOT HDD_MAX SSD_HOT SSD_MAX (°C integers). # Falls back to 45/55/60/70 if dynamix.cfg is absent or keys are missing. # ────────────────────────────────────────────────────────────────────────────────────────────── platform_get_temp_thresholds() { local cfg="/boot/config/plugins/dynamix/dynamix.cfg" local hdd_hot hdd_max ssd_hot ssd_max hdd_hot=$(grep -m1 '^hot=' "$cfg" 2>/dev/null | cut -d= -f2 | tr -d '"') hdd_max=$(grep -m1 '^max=' "$cfg" 2>/dev/null | cut -d= -f2 | tr -d '"') ssd_hot=$(grep -m1 '^hotssd=' "$cfg" 2>/dev/null | cut -d= -f2 | tr -d '"') ssd_max=$(grep -m1 '^maxssd=' "$cfg" 2>/dev/null | cut -d= -f2 | tr -d '"') echo "${hdd_hot:-45} ${hdd_max:-55} ${ssd_hot:-60} ${ssd_max:-70}" } # ────────────────────────────────────────────────────────────────────────────────────────────── # platform_is_maintenance_running # Returns 0 if a parity check or sync is currently in progress. # Checks var.ini (Unraid 7.3+) then falls back to parity-date.txt (older). # ────────────────────────────────────────────────────────────────────────────────────────────── platform_is_maintenance_running() { local resync resync=$(awk -F'"' '/^mdResync=/{print $2}' /var/local/emhttp/var.ini 2>/dev/null) if [[ -n "$resync" && "$resync" != "0" ]]; then return 0 fi grep -q "progress" /var/local/emhttp/parity-date.txt 2>/dev/null } # ────────────────────────────────────────────────────────────────────────────────────────────── # platform_is_service_enabled # service: docker | libvirt # Returns 0 if the service is enabled in the Unraid boot config. # ────────────────────────────────────────────────────────────────────────────────────────────── platform_is_service_enabled() { case "$1" in docker) grep -qE '^DOCKER_ENABLED="?yes"?' /boot/config/docker.cfg 2>/dev/null ;; libvirt) grep -q '^SERVICE="enable"' /boot/config/domain.cfg 2>/dev/null ;; *) return 1 ;; esac } # ────────────────────────────────────────────────────────────────────────────────────────────── # platform_restart_service # Restarts the named service via its rc.d script. # Returns 1 if the rc.d script does not exist or is not executable. # ────────────────────────────────────────────────────────────────────────────────────────────── platform_restart_service() { local rc rc=$(_platform_rc_script "$1") [[ -x "$rc" ]] || return 1 "$rc" restart >/dev/null 2>&1 } # ────────────────────────────────────────────────────────────────────────────────────────────── # platform_stop_service # Stops the named service via its rc.d script. # Returns 1 if the rc.d script does not exist or is not executable. # ────────────────────────────────────────────────────────────────────────────────────────────── platform_stop_service() { local rc rc=$(_platform_rc_script "$1") [[ -x "$rc" ]] || return 1 "$rc" stop >/dev/null 2>&1 } # ────────────────────────────────────────────────────────────────────────────────────────────── # platform_is_service_running # Returns 0 if the service's main process is alive. # ────────────────────────────────────────────────────────────────────────────────────────────── platform_is_service_running() { case "$1" in docker) pgrep -x dockerd >/dev/null 2>&1 ;; emhttp) pgrep -x emhttpd >/dev/null 2>&1 ;; nginx) pgrep -x nginx >/dev/null 2>&1 ;; php-fpm) pgrep -x php-fpm >/dev/null 2>&1 ;; sshd) pgrep -x sshd >/dev/null 2>&1 ;; rsyslog) pgrep -x rsyslogd >/dev/null 2>&1 ;; libvirt) pgrep -x libvirtd >/dev/null 2>&1 ;; *) pgrep -x "$1" >/dev/null 2>&1 ;; esac } # ────────────────────────────────────────────────────────────────────────────────────────────── # platform_is_mover_running # Returns 0 if the Unraid mover is currently active. # ────────────────────────────────────────────────────────────────────────────────────────────── platform_is_mover_running() { pgrep -f "emhttp.*Mover" >/dev/null 2>&1 } # ────────────────────────────────────────────────────────────────────────────────────────────── # platform_get_mover_pid # Writes the mover's PID to stdout. Returns 1 if the mover is not running. # ────────────────────────────────────────────────────────────────────────────────────────────── platform_get_mover_pid() { local pid pid=$(pgrep -f "emhttp.*Mover" | head -1) [[ -n "$pid" ]] || return 1 echo "$pid" } # ────────────────────────────────────────────────────────────────────────────────────────────── # platform_stop_user_scripts # Kills all Unraid user.scripts background processes. # Returns 0 whether or not any processes were found (pkill exits 1 on no match). # ────────────────────────────────────────────────────────────────────────────────────────────── platform_stop_user_scripts() { pkill -f "/tmp/user.scripts" 2>/dev/null || true } # ────────────────────────────────────────────────────────────────────────────────────────────── # platform_send_os_notification [subject] [severity] # Sends a native Unraid notification via the dynamix notify script. # severity: normal | warning | alert (default: normal) # Returns 1 if the notify script is absent. # ────────────────────────────────────────────────────────────────────────────────────────────── # ── Platform path defaults ───────────────────────────────────────────────────────────────────── # Set here so master.conf and core scripts never contain OS-specific path literals. # host*.conf may override any of these after the adapter is sourced. DOCKER_APPDATA_BASE="${DOCKER_APPDATA_BASE:-/mnt/user/appdata}" # ────────────────────────────────────────────────────────────────────────────────────────────── # platform_storage_path # Writes the root path where user shares and storage are accessible to stdout. # Used by array health checks and disk verification. # ────────────────────────────────────────────────────────────────────────────────────────────── platform_storage_path() { echo "/mnt/user" } # ────────────────────────────────────────────────────────────────────────────────────────────── # platform_webui_install_path # Writes the path where the platform serves WebGUI plugin files from. # On Unraid the PHP WebGUI is served from /boot/, separate from SCRIPTS_DIR. # Returns 1 (empty) if the platform serves directly from SCRIPTS_DIR (no sync needed). # ────────────────────────────────────────────────────────────────────────────────────────────── platform_webui_install_path() { echo "/boot/config/plugins/varaverk" } # ────────────────────────────────────────────────────────────────────────────────────────────── # platform_scripts_dir_probe_cmd # Writes a shell command suitable for running on a remote via SSH to discover # that remote's SCRIPTS_DIR. Each platform exposes its install path differently. # ────────────────────────────────────────────────────────────────────────────────────────────── platform_scripts_dir_probe_cmd() { echo "grep -m1 'SCRIPTS_DIR' /boot/config/plugins/varaverk/varaverk.cfg 2>/dev/null | cut -d= -f2 | tr -d '\"'" } platform_send_os_notification() { local message="$1" local subject="${2:-Varaverk}" local severity="${3:-normal}" local notify_script="/usr/local/emhttp/plugins/dynamix/scripts/notify" [[ -x "$notify_script" ]] || return 1 "$notify_script" -e "Varaverk" -s "$subject" -d "$message" -i "$severity" 2>/dev/null } # ────────────────────────────────────────────────────────────────────────────────────────────── # platform_get_templates_dir # Writes the path to the Unraid CA docker templates-user directory to stdout. # ────────────────────────────────────────────────────────────────────────────────────────────── platform_get_templates_dir() { echo "/boot/config/plugins/dockerMan/templates-user" } # ────────────────────────────────────────────────────────────────────────────────────────────── # platform_setup_db_path # Writes the path to the persistent Varaverk setup/wizard state database. # ────────────────────────────────────────────────────────────────────────────────────────────── platform_setup_db_path() { echo "${STATE_DIR}/varaverk_setup.db" } # ────────────────────────────────────────────────────────────────────────────────────────────── # platform_get_os_version # Writes the platform OS version string to stdout (e.g. "7.2.3"). # Returns 1 if the version file is absent or unparseable. # ────────────────────────────────────────────────────────────────────────────────────────────── platform_get_os_version() { local ver ver=$(grep -oP '(?<=version=")[^"]+' /etc/unraid-version 2>/dev/null) [[ -n "$ver" ]] || return 1 echo "$ver" } # ────────────────────────────────────────────────────────────────────────────────────────────── # platform_os_version_probe_cmd # Writes a shell command suitable for running on a remote via SSH to retrieve # that remote's OS version string. Output format matches platform_get_os_version. # ────────────────────────────────────────────────────────────────────────────────────────────── platform_os_version_probe_cmd() { echo "grep -oP '(?<=version=\")[^\"]+' /etc/unraid-version 2>/dev/null" } # ────────────────────────────────────────────────────────────────────────────────────────────── # platform_rebuild_container # Rebuilds a container from its stored Unraid CA XML template (stops old, recreates on new # image digest, prunes old image). Returns 1 if the rebuild script is absent or fails. # ────────────────────────────────────────────────────────────────────────────────────────────── platform_rebuild_container() { local container="$1" local rebuild_script="/usr/local/emhttp/plugins/dynamix.docker.manager/scripts/rebuild_container" [[ -x "$rebuild_script" ]] || return 1 "$rebuild_script" "$container" >/dev/null 2>&1 } # ────────────────────────────────────────────────────────────────────────────────────────────── # platform_push_conf # Pushes master.conf to all listed hosts via the WebGUI PHP API. # Writes per-host push results to stdout. Returns 1 if any host failed. # Returns 0 silently if php is unavailable (caller should warn manually). # ────────────────────────────────────────────────────────────────────────────────────────────── platform_push_conf() { command -v php &>/dev/null || return 0 local rc=0 php -r " require_once '/usr/local/emhttp/plugins/varaverk/include/config.php'; \$results = vv_push_master_conf(); if (empty(\$results)) { echo 'no remote hosts'; exit(0); } \$failed = 0; foreach (\$results as \$r) { // 'seed' means the partner had no master.conf anywhere — say so, because that is the case // where the push carried the mesh's identity to a node that could not have known it. \$what = (\$r['mode'] ?? '') === 'seed' ? 'seeded → ' . (\$r['path'] ?? '') : 'pushed → ' . (\$r['path'] ?? ''); echo \$r['host'] . ': ' . (\$r['ok'] ? \$what : 'FAILED — ' . \$r['error']) . PHP_EOL; if (!\$r['ok']) \$failed++; } exit(\$failed > 0 ? 1 : 0); " 2>/dev/null rc=$? return $rc } # ────────────────────────────────────────────────────────────────────────────────────────────── # platform_push_setup_state # Pushes the Varaverk wizard setup state to all partners via the WebGUI PHP API. # Returns 1 if any partner did not take it, 0 otherwise. No-op returning 0 if php is absent. # # This used to end with a bare `return 0` under the comment "Always returns 0", which meant # every caller that wrote `platform_push_setup_state || X=false` was testing a constant. # Wiring a failure branch onto a helper that cannot fail is worse than hardcoding the tick, # because the code reads as though it checked. # ────────────────────────────────────────────────────────────────────────────────────────────── platform_push_setup_state() { command -v php &>/dev/null || return 0 php -r "require_once '/usr/local/emhttp/plugins/varaverk/include/config.php'; exit(vv_push_setup_state() > 0 ? 1 : 0);" 2>/dev/null }