Phase 1 exists to hand the partner its identity before the install, but the push refused any host without varaverk.cfg — so the conf could only ever reach a node that no longer needed it to be told who it was. It now resolves the remote's conf directory across both install layouts and creates the internal one when neither exists.
439 lines
30 KiB
Bash
Executable File
439 lines
30 KiB
Bash
Executable File
#!/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.<name> 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 <path> [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>
|
|
# 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 <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 <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 <service>
|
|
# 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 <message> [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 <container_name>
|
|
# 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.
|
|
# No-op if php is unavailable. Always returns 0.
|
|
# ──────────────────────────────────────────────────────────────────────────────────────────────
|
|
platform_push_setup_state() {
|
|
command -v php &>/dev/null || return 0
|
|
php -r "require_once '/usr/local/emhttp/plugins/varaverk/include/config.php'; vv_push_setup_state();" 2>/dev/null
|
|
return 0
|
|
}
|