refactor: move unraid-specific tools into Plugin/unraid/tools/
api_cache_writer, remote_arr_cache_writer, conf_populate, storage_migrate are
all Unraid-adapter-specific — they read Docker appdata paths, Unraid service
configs, and drive the Unraid web plugin cache. They don't belong in Tools/.
- Tools/{4 scripts + php} → Plugin/unraid/tools/
- Fix source depth: ../ → ../../../ (now 3 levels from repo root)
- Fix conf_populate SCRIPTS_ROOT bug: was never set, would expand to empty
- api/arrs.php, api/storage.php: SCRIPTS_DIR/Tools/ → dirname(__DIR__)/tools/
- schedule.json: key IDs updated to Plugin/unraid/tools/...
- Cron rebuilt — live entries updated immediately
This commit is contained in:
@@ -9,7 +9,7 @@ if ($_action === 'refresh_remote') {
|
||||
if (!preg_match('/^host\d+$/', $host)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid host']); exit;
|
||||
}
|
||||
$script = SCRIPTS_DIR . '/Tools/remote_arr_cache_writer.sh';
|
||||
$script = dirname(__DIR__) . '/tools/remote_arr_cache_writer.sh';
|
||||
if (!file_exists($script)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'remote_arr_cache_writer.sh not found']); exit;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ if ($action === 'migrate' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
exit;
|
||||
}
|
||||
|
||||
$script = SCRIPTS_DIR . '/Tools/storage_migrate.sh';
|
||||
$script = dirname(__DIR__) . '/tools/storage_migrate.sh';
|
||||
if (!file_exists($script)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'storage_migrate.sh not found']);
|
||||
exit;
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
// API cache writer — runs every minute via Varaverk scheduler.
|
||||
// Builds monitor + arrs payloads and writes them to /tmp/vv_cache/ so page
|
||||
// loads can serve instantly from the file instead of making live HTTP calls.
|
||||
//
|
||||
// Called by api_cache_writer.sh (bash wrapper required by the scheduler).
|
||||
|
||||
$_base = dirname(__DIR__) . '/Plugin/unraid';
|
||||
require_once $_base . '/include/monitor.php';
|
||||
require_once $_base . '/include/vms.php';
|
||||
require_once $_base . '/include/docker_folders.php';
|
||||
require_once $_base . '/include/arrs.php';
|
||||
|
||||
$t = microtime(true);
|
||||
|
||||
// ── Monitor payload ───────────────────────────────────────────────────────────
|
||||
// Call vv_api_data() once — result is static-cached for the rest of this process.
|
||||
vv_api_data();
|
||||
|
||||
$monitor = [
|
||||
'system' => vv_system_info(),
|
||||
'fallback' => vv_fallback_state(),
|
||||
'fallback_active' => vv_fallback_active(),
|
||||
'partner' => vv_partner_state(),
|
||||
'resources' => vv_system_resources(),
|
||||
'cpu' => vv_cpu_per_core(),
|
||||
'mem' => vv_memory_breakdown(),
|
||||
'net' => vv_network_stats(),
|
||||
'gpu' => vv_gpu_stats(),
|
||||
'gpu_procs' => vv_gpu_processes(),
|
||||
'containers' => vv_docker_containers(),
|
||||
'stopped' => vv_docker_stopped(),
|
||||
'transcode' => vv_transcode_sessions(),
|
||||
'ups' => vv_ups_stats(),
|
||||
'parity' => vv_parity_status(),
|
||||
'storage' => vv_storage_pools(),
|
||||
'array_disks' => vv_array_disks(),
|
||||
'disk_io' => vv_disk_io_rates(),
|
||||
'watchdog' => vv_watchdog_summary(),
|
||||
'scripts' => vv_scripts_status(),
|
||||
'rsync' => vv_rsync_status(),
|
||||
'thresholds' => vv_disk_thresholds(),
|
||||
'vms' => vv_get_vms(),
|
||||
'docker_folders' => vv_get_docker_folders(),
|
||||
'remote_hosts' => vv_remote_hosts_stats(),
|
||||
'_api_status' => vv_api_get_status(),
|
||||
'ts' => time(),
|
||||
];
|
||||
vv_cache_write('monitor', $monitor);
|
||||
|
||||
// ── Arrs payload ──────────────────────────────────────────────────────────────
|
||||
$arrs = vv_arrs_all();
|
||||
vv_cache_write('arrs', $arrs);
|
||||
|
||||
$elapsed = round((microtime(true) - $t) * 1000);
|
||||
echo "Cache written in {$elapsed}ms — monitor + arrs\n";
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
# ==============================================================================================
|
||||
# ================================= API Cache Writer ===========================================
|
||||
# ==============================================================================================
|
||||
#
|
||||
# PURPOSE
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Builds the monitor and arrs API payloads and writes them to /tmp/vv_cache/
|
||||
# so page loads can serve from the file instantly instead of making live HTTP
|
||||
# calls on every request.
|
||||
#
|
||||
# Runs every minute via the Varaverk scheduler. /tmp is tmpfs — files are
|
||||
# RAM-speed reads and auto-cleared on reboot.
|
||||
#
|
||||
# ==============================================================================================
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
php "$SCRIPT_DIR/api_cache_writer.php"
|
||||
Executable
+229
@@ -0,0 +1,229 @@
|
||||
#!/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/.vv/ cache immediately.
|
||||
#
|
||||
# ==============================================================================================
|
||||
# AUTO-DETECTED FIELDS
|
||||
# ==============================================================================================
|
||||
#
|
||||
# HOSTN_RADARR_API_KEY from Radarr config.xml (found via docker volume mount)
|
||||
# HOSTN_SONARR_API_KEY from Sonarr config.xml
|
||||
# HOSTN_LIDARR_API_KEY from Lidarr config.xml
|
||||
# HOSTN_SLSKD_API_KEY from slskd config.yml
|
||||
# HOSTN_SABNZBD_API_KEY from sabnzbd.ini
|
||||
# HOSTN_EMBY_CONTAINER fuzzy match from docker ps
|
||||
# HOSTN_JELLYFIN_CONTAINER fuzzy match from docker ps
|
||||
# HOSTN_RADARR_MOVIE_ROOT from Radarr rootFolder API
|
||||
# HOSTN_SONARR_TV_ROOT from Sonarr rootFolder API
|
||||
# HOSTN_LIDARR_MUSIC_ROOT from Lidarr rootFolder API
|
||||
# HOSTN_SYS_WATCHDOG_NIC from ip route default gateway interface
|
||||
#
|
||||
# ==============================================================================================
|
||||
# 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; }
|
||||
|
||||
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 conf if empty (or --overwrite) ──────────────────
|
||||
_set_conf_var() {
|
||||
local var_name="$1" value="$2" label="$3"
|
||||
[[ -z "$value" ]] && return
|
||||
|
||||
# Check current value in conf
|
||||
local current
|
||||
current=$(grep -oP "(?<=^\s*${var_name}=\")[^\"]*" "$CONF_FILE" 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
|
||||
|
||||
# Update or append the var line
|
||||
if grep -q "^\s*${var_name}=" "$CONF_FILE"; then
|
||||
sed -i "s|^\(\s*${var_name}\s*=\s*\)\"[^\"]*\"|\1\"${value}\"|" "$CONF_FILE"
|
||||
else
|
||||
printf '\n %s="%s"\n' "$var_name" "$value" >> "$CONF_FILE"
|
||||
fi
|
||||
info "$label: set ✅"
|
||||
(( UPDATED++ ))
|
||||
}
|
||||
|
||||
# ── Helper: find arr config dir via docker volume mount ───────────────────────
|
||||
# Looks for a container matching the pattern, then reads its /config volume path.
|
||||
# Falls back to DOCKER_APPDATA_BASE/<ContainerName> if volume not found.
|
||||
_arr_config_dir() {
|
||||
local pattern="$1"
|
||||
local container_name
|
||||
container_name=$(docker ps -a --format '{{.Names}}' 2>/dev/null | \
|
||||
grep -im1 "^${pattern}")
|
||||
[[ -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
|
||||
}
|
||||
|
||||
# ==============================================================================================
|
||||
# ── Arr API keys + root paths ─────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
for arr in radarr sonarr lidarr; do
|
||||
arr_upper="${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")
|
||||
url_base="http://localhost:${port:-$(case $arr in radarr) echo 7878;; sonarr) echo 8989;; lidarr) echo 8686;; esac)}"
|
||||
|
||||
_set_conf_var "${MY_ID}_${arr_upper}_API_KEY" "$key" "${arr_upper} API key"
|
||||
|
||||
# Root paths from arr's own rootFolder API
|
||||
if [[ -n "$key" ]]; then
|
||||
local api_ver; 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_MOVIE_ROOT" "$root_path" "Radarr movie 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
|
||||
fi
|
||||
done
|
||||
|
||||
# ==============================================================================================
|
||||
# ── SABnzbd API key ───────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
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)
|
||||
_set_conf_var "${MY_ID}_SABNZBD_API_KEY" "$sab_key" "SABnzbd API key"
|
||||
fi
|
||||
}
|
||||
|
||||
# ==============================================================================================
|
||||
# ── slskd API key ─────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
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)
|
||||
_set_conf_var "${MY_ID}_SLSKD_API_KEY" "$slskd_key" "slskd API key"
|
||||
fi
|
||||
}
|
||||
|
||||
# ==============================================================================================
|
||||
# ── Container names ───────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
for pattern in "emby" "jellyfin"; do
|
||||
container=$(docker ps -a --format '{{.Names}}' 2>/dev/null | grep -im1 "^${pattern}")
|
||||
[[ -z "$container" ]] && continue
|
||||
case "$pattern" in
|
||||
emby) _set_conf_var "${MY_ID}_EMBY_CONTAINER" "$container" "Emby container name" ;;
|
||||
jellyfin) _set_conf_var "${MY_ID}_JELLYFIN_CONTAINER" "$container" "Jellyfin container name" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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"
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
#!/bin/bash
|
||||
# ==============================================================================================
|
||||
# ============================= Remote Arr Cache Writer ========================================
|
||||
# ==============================================================================================
|
||||
#
|
||||
# PURPOSE
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# SSHes to each remote partner host, calls vv_arrs_local_node() on their PHP
|
||||
# stack, and caches the result locally in /tmp/vv_cache/arrs_remote_hostN.json.
|
||||
#
|
||||
# The arrs page reads these files for instant initial load without hitting the
|
||||
# remote arr APIs on every page view. This script runs every 2 hours so remote
|
||||
# library counts stay reasonably current without hammering the network.
|
||||
#
|
||||
# Accepts --host=HOST2 to refresh a single host (used by the UI refresh button).
|
||||
#
|
||||
# ==============================================================================================
|
||||
# USAGE
|
||||
# ==============================================================================================
|
||||
#
|
||||
# remote_arr_cache_writer.sh — refresh all remote hosts
|
||||
# remote_arr_cache_writer.sh --host=HOST2 — refresh one host only
|
||||
# remote_arr_cache_writer.sh --dry-run — show what would happen
|
||||
# remote_arr_cache_writer.sh --log — verbose output
|
||||
#
|
||||
# ==============================================================================================
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/../../../load_config.sh"
|
||||
|
||||
parse_args "$@"
|
||||
acquire_lock
|
||||
detect_hosts
|
||||
|
||||
# Parse --host= from raw args
|
||||
TARGET_HOST=""
|
||||
for arg in "$@"; do
|
||||
case "$arg" in --host=*) TARGET_HOST="${arg#--host=}" ;; esac
|
||||
done
|
||||
|
||||
mkdir -p /tmp/vv_cache
|
||||
|
||||
log "$ICON_GEAR Config: target=${TARGET_HOST:-all hosts} ssh-key=${SSH_KEY}"
|
||||
|
||||
FETCH_OK=0
|
||||
FETCH_FAIL=0
|
||||
FETCH_SKIP=0
|
||||
|
||||
echo ""
|
||||
echo "━━━ $ICON_SYNC Remote Arr Cache Writer ━━━"
|
||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
|
||||
[[ -n "$TARGET_HOST" ]] && echo " Target: $TARGET_HOST"
|
||||
echo ""
|
||||
|
||||
for host_var in HOST1 HOST2 HOST3 HOST4 HOST5 HOST6 HOST7 HOST8; do
|
||||
[[ "$host_var" == "$MY_ID" ]] && continue
|
||||
|
||||
hostname="${!host_var:-}"
|
||||
[[ -z "$hostname" ]] && continue
|
||||
|
||||
# If targeting a specific host, skip others
|
||||
if [[ -n "$TARGET_HOST" ]]; then
|
||||
[[ "${host_var,,}" != "${TARGET_HOST,,}" && "$host_var" != "$TARGET_HOST" ]] && continue
|
||||
fi
|
||||
|
||||
host_id="${host_var,,}" # host1, host2, …
|
||||
cache_file="/tmp/vv_cache/arrs_remote_${host_id}.json"
|
||||
|
||||
echo " $host_var ($hostname)…"
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn " DRY RUN — would SSH to $hostname and cache arr data"
|
||||
(( FETCH_SKIP++ ))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Resolve Tailscale IP
|
||||
REMOTE_IP=$(resolve_tailscale_ip "$hostname")
|
||||
if [[ -z "$REMOTE_IP" ]]; then
|
||||
warn " $host_var: cannot resolve Tailscale IP for $hostname — skipping"
|
||||
(( FETCH_FAIL++ ))
|
||||
continue
|
||||
fi
|
||||
log " $host_var: resolved $hostname → $REMOTE_IP"
|
||||
|
||||
# Single SSH connection — fetch arrs + monitor stats together
|
||||
RESULT=$(ssh -i "$SSH_KEY" \
|
||||
-o ConnectTimeout=10 \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o BatchMode=yes \
|
||||
"root@${REMOTE_IP}" \
|
||||
"php -r \"
|
||||
require_once '/usr/local/emhttp/plugins/varaverk/include/arrs.php';
|
||||
require_once '/usr/local/emhttp/plugins/varaverk/include/unraid_api.php';
|
||||
echo json_encode([
|
||||
'arrs' => vv_arrs_local_node(),
|
||||
'monitor' => vv_local_host_stats(),
|
||||
]);
|
||||
\"" 2>/dev/null)
|
||||
|
||||
if [[ -z "$RESULT" ]]; then
|
||||
warn " $host_var: empty SSH response — skipping"
|
||||
(( FETCH_FAIL++ ))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Validate combined JSON
|
||||
if ! echo "$RESULT" | php -r "
|
||||
\$d = json_decode(file_get_contents('php://stdin'), true);
|
||||
exit(!is_array(\$d) || !isset(\$d['arrs'], \$d['monitor']) ? 1 : 0);
|
||||
" 2>/dev/null; then
|
||||
warn " $host_var: invalid JSON — skipping"
|
||||
log " Response: ${RESULT:0:200}"
|
||||
(( FETCH_FAIL++ ))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Save arr cache
|
||||
echo "$RESULT" | php -r "
|
||||
\$d = json_decode(file_get_contents('php://stdin'), true);
|
||||
file_put_contents('$cache_file', json_encode(\$d['arrs']));
|
||||
" 2>/dev/null
|
||||
|
||||
# Save monitor cache
|
||||
MONITOR_CACHE="/tmp/vv_cache/monitor_remote_${host_id}.json"
|
||||
echo "$RESULT" | php -r "
|
||||
\$d = json_decode(file_get_contents('php://stdin'), true);
|
||||
file_put_contents('$MONITOR_CACHE', json_encode(\$d['monitor']));
|
||||
" 2>/dev/null
|
||||
|
||||
CACHED_TYPES=$(echo "$RESULT" | php -r "
|
||||
\$d = json_decode(file_get_contents('php://stdin'), true);
|
||||
echo implode(', ', array_column(\$d['arrs']['arrs'] ?? [], 'type'));
|
||||
" 2>/dev/null)
|
||||
echo " $host_var: arrs [${CACHED_TYPES:-none}] + monitor stats cached ✅"
|
||||
(( FETCH_OK++ ))
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "━━━━━ $ICON_SUMMARY Cache Writer Summary ━━━━━"
|
||||
echo " $FETCH_OK updated · $FETCH_FAIL failed · $FETCH_SKIP skipped"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
Executable
+329
@@ -0,0 +1,329 @@
|
||||
#!/bin/bash
|
||||
# ==============================================================================================
|
||||
# ================================= Storage Migration ==========================================
|
||||
# ==============================================================================================
|
||||
#
|
||||
# PURPOSE
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Migrates Varaverk between internal NVMe and USB flash storage modes.
|
||||
#
|
||||
# Internal mode: SCRIPTS_DIR = /boot/config/plugins/varaverk
|
||||
# All scripts, conf, state, and git repo live on fast internal storage.
|
||||
# Direct git pull/push. Zero write-wear concern.
|
||||
#
|
||||
# Flash mode: SCRIPTS_DIR = /mnt/user/appdata/Varaverk
|
||||
# All scripts, conf, state, and git repo live in appdata.
|
||||
# Preserves USB flash lifetime. Array must be started for Varaverk to function.
|
||||
# git_pull_execute.sh syncs Plugin/ back to /boot/ after each pull so the
|
||||
# Unraid webUI always serves current PHP files.
|
||||
#
|
||||
# What this script updates:
|
||||
# varaverk.cfg SCRIPTS_DIR
|
||||
# master.conf TARGET_DIR
|
||||
# host*.conf HOST*_STORAGE_MODE_INTERNAL
|
||||
# varaverk.cron rebuilt via PHP (job paths regenerated for new SCRIPTS_DIR)
|
||||
#
|
||||
# ==============================================================================================
|
||||
# RUNTIME MODES
|
||||
# ==============================================================================================
|
||||
#
|
||||
# storage_migrate.sh --to=internal
|
||||
# Migrate to /boot/config/plugins/varaverk
|
||||
#
|
||||
# storage_migrate.sh --to=flash
|
||||
# Migrate to /mnt/user/appdata/Varaverk
|
||||
#
|
||||
# storage_migrate.sh --dry-run --to=<mode>
|
||||
# Show what would happen — no changes made
|
||||
#
|
||||
# storage_migrate.sh --status
|
||||
# Show current mode, paths, and boot device info
|
||||
#
|
||||
# ==============================================================================================
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/../../../load_config.sh"
|
||||
|
||||
parse_args "$@"
|
||||
acquire_lock
|
||||
|
||||
VV_CFG="/boot/config/plugins/varaverk/varaverk.cfg"
|
||||
INTERNAL_DIR="/boot/config/plugins/varaverk"
|
||||
FLASH_DIR="/mnt/user/appdata/Varaverk"
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Parse --to= from raw args (parse_args doesn't handle this flag)
|
||||
TO_MODE=""
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--to=internal) TO_MODE="internal" ;;
|
||||
--to=flash) TO_MODE="flash" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Boot device detection
|
||||
detect_boot_storage() {
|
||||
local boot_part boot_disk transport
|
||||
boot_part=$(findmnt -n -o SOURCE /boot 2>/dev/null)
|
||||
boot_disk=$(lsblk -no pkname "$boot_part" 2>/dev/null)
|
||||
transport=$(lsblk -dno TRAN "/dev/$boot_disk" 2>/dev/null | tr '[:upper:]' '[:lower:]')
|
||||
echo "${transport:-unknown}"
|
||||
}
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Status
|
||||
if [[ "$SHOW_STATUS" == true ]]; then
|
||||
TRANSPORT=$(detect_boot_storage)
|
||||
DETECTED=$([[ "$TRANSPORT" == "usb" ]] && echo "flash" || echo "internal")
|
||||
echo ""
|
||||
echo "━━━━━ $ICON_SUMMARY STORAGE STATUS ━━━━━"
|
||||
echo "$ICON_GEAR SCRIPTS_DIR: $SCRIPTS_DIR"
|
||||
echo "$ICON_GEAR varaverk.cfg: $VV_CFG"
|
||||
echo "$ICON_HOST Boot device: transport=$TRANSPORT → detected=$DETECTED"
|
||||
echo "$ICON_GEAR Target dirs:"
|
||||
echo " internal: $INTERNAL_DIR"
|
||||
echo " flash: $FLASH_DIR"
|
||||
CONF_MODE=$(grep -m1 "${MY_ID}_STORAGE_MODE_INTERNAL" "$CONF_FILE" 2>/dev/null | cut -d= -f2 | tr -d '"' | tr -d '[:space:]')
|
||||
echo "$ICON_GEAR conf setting: ${MY_ID}_STORAGE_MODE_INTERNAL=${CONF_MODE:-not set}"
|
||||
if [[ "$SCRIPTS_DIR" == "$INTERNAL_DIR" ]]; then
|
||||
echo "$ICON_DONE Current mode: INTERNAL ✅"
|
||||
elif [[ "$SCRIPTS_DIR" == "$FLASH_DIR" ]]; then
|
||||
echo "$ICON_DONE Current mode: FLASH ✅"
|
||||
else
|
||||
echo "$ICON_WARN Current mode: CUSTOM ($SCRIPTS_DIR)"
|
||||
fi
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
if [[ -z "$TO_MODE" ]]; then
|
||||
error "Usage: storage_migrate.sh --to=internal|flash [--dry-run] [--log]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SRC="$SCRIPTS_DIR"
|
||||
DST=$([[ "$TO_MODE" == "internal" ]] && echo "$INTERNAL_DIR" || echo "$FLASH_DIR")
|
||||
NEW_INTERNAL=$([[ "$TO_MODE" == "internal" ]] && echo "true" || echo "false")
|
||||
|
||||
log "$ICON_GEAR Config: to=${TO_MODE} src=${SRC} dst=${DST} dry-run=${DRY_RUN}"
|
||||
|
||||
echo ""
|
||||
echo "━━━━━ $ICON_SYNC Storage Migration ━━━━━"
|
||||
echo "$ICON_GEAR From: $SRC"
|
||||
echo "$ICON_GEAR To: $DST"
|
||||
echo "$ICON_GEAR Mode: $TO_MODE"
|
||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
|
||||
echo ""
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Pre-flight checks
|
||||
if [[ "$SRC" == "$DST" ]]; then
|
||||
info "Already in $TO_MODE mode — nothing to do"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$TO_MODE" == "flash" ]]; then
|
||||
if ! mountpoint -q /mnt/user 2>/dev/null; then
|
||||
error "Array not started — /mnt/user is not mounted. Start the array before migrating to flash."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ! -f "$SRC/load_config.sh" ]]; then
|
||||
error "Source directory looks invalid: $SRC (load_config.sh not found)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Step 1: Git push — ensure remote has everything before we touch the local repo
|
||||
echo "━━━ $ICON_SYNC Step 1: Git push ━━━"
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
if [[ -d "$SRC/.git" ]]; then
|
||||
log "Pushing to Gitea before migration..."
|
||||
if GIT_SSH_COMMAND="ssh -i $GITEA_SSH_KEY -p $SSH_PORT" \
|
||||
git -C "$SRC" push origin main 2>&1 | while IFS= read -r line; do echo " $line"; done; then
|
||||
echo " Git push complete ✅"
|
||||
else
|
||||
warn "Git push failed — continuing (data safe locally, push manually after migration)"
|
||||
fi
|
||||
else
|
||||
warn "No .git directory in $SRC — skipping push"
|
||||
fi
|
||||
else
|
||||
warn "DRY RUN — would push $SRC to Gitea"
|
||||
fi
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Step 2: Rsync content to destination
|
||||
echo ""
|
||||
echo "━━━ $ICON_SYNC Step 2: Copy files ━━━"
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
mkdir -p "$DST"
|
||||
echo " rsync: $SRC/ → $DST/"
|
||||
if rsync -av --delete \
|
||||
--exclude='.git' \
|
||||
"$SRC/" "$DST/" 2>&1 | \
|
||||
grep -v "/$" | \
|
||||
while IFS= read -r line; do log "$line"; done; then
|
||||
echo " Files copied ✅"
|
||||
else
|
||||
error "rsync failed — aborting migration"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Copy .git separately (rsync --exclude='.git' above skipped it)
|
||||
echo " Copying .git..."
|
||||
if cp -a "$SRC/.git" "$DST/.git" 2>/dev/null || \
|
||||
rsync -a "$SRC/.git/" "$DST/.git/" 2>/dev/null; then
|
||||
echo " .git copied ✅"
|
||||
else
|
||||
error ".git copy failed — aborting"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Mark git safe directory
|
||||
git config --global --add safe.directory "$DST" 2>/dev/null
|
||||
else
|
||||
warn "DRY RUN — would rsync $SRC/ → $DST/ (including .git)"
|
||||
fi
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Step 3: Update varaverk.cfg
|
||||
echo ""
|
||||
echo "━━━ $ICON_GEAR Step 3: Update varaverk.cfg ━━━"
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
if grep -q '^SCRIPTS_DIR' "$VV_CFG"; then
|
||||
sed -i "s|^SCRIPTS_DIR=.*|SCRIPTS_DIR=\"$DST\"|" "$VV_CFG"
|
||||
else
|
||||
echo "SCRIPTS_DIR=\"$DST\"" >> "$VV_CFG"
|
||||
fi
|
||||
echo " SCRIPTS_DIR → $DST ✅"
|
||||
else
|
||||
warn "DRY RUN — would set SCRIPTS_DIR=\"$DST\" in $VV_CFG"
|
||||
fi
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Step 4: Update TARGET_DIR, DATA_DIR, STATE_DIR in master.conf (new location)
|
||||
echo ""
|
||||
echo "━━━ $ICON_GEAR Step 4: Update master.conf paths ━━━"
|
||||
NEW_MASTER="$DST/Configurations/master.conf"
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
if [[ -f "$NEW_MASTER" ]]; then
|
||||
sed -i "s|^\(\s*TARGET_DIR\s*=\s*\)\"[^\"]*\"|\1\"$DST\"|" "$NEW_MASTER"
|
||||
sed -i "s|^\(\s*DATA_DIR\s*=\s*\)\"[^\"]*\"|\1\"$DST/data\"|" "$NEW_MASTER"
|
||||
sed -i "s|^\(\s*STATE_DIR\s*=\s*\)\"[^\"]*\"|\1\"$DST/State_Files\"|" "$NEW_MASTER"
|
||||
echo " TARGET_DIR → $DST ✅"
|
||||
echo " DATA_DIR → $DST/data ✅"
|
||||
echo " STATE_DIR → $DST/State_Files ✅"
|
||||
else
|
||||
error "master.conf not found at $NEW_MASTER"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
warn "DRY RUN — would update TARGET_DIR, DATA_DIR, STATE_DIR in master.conf"
|
||||
fi
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Step 5: Update STORAGE_MODE_INTERNAL in host*.conf (new location)
|
||||
echo ""
|
||||
echo "━━━ $ICON_GEAR Step 5: Update STORAGE_MODE_INTERNAL ━━━"
|
||||
NEW_CONF="$DST/Configurations/${MY_ID,,}.conf"
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
if [[ -f "$NEW_CONF" ]]; then
|
||||
if grep -q "${MY_ID}_STORAGE_MODE_INTERNAL" "$NEW_CONF"; then
|
||||
sed -i "s|^\(\s*${MY_ID}_STORAGE_MODE_INTERNAL\s*=\s*\).*|\1${NEW_INTERNAL}|" "$NEW_CONF"
|
||||
else
|
||||
sed -i "/# ━━━ Storage mode/a\\ ${MY_ID}_STORAGE_MODE_INTERNAL=${NEW_INTERNAL}" "$NEW_CONF"
|
||||
fi
|
||||
echo " ${MY_ID}_STORAGE_MODE_INTERNAL → $NEW_INTERNAL ✅"
|
||||
else
|
||||
warn "${MY_ID,,}.conf not found at $NEW_CONF — skipping conf update"
|
||||
fi
|
||||
else
|
||||
warn "DRY RUN — would set ${MY_ID}_STORAGE_MODE_INTERNAL=$NEW_INTERNAL"
|
||||
fi
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Step 6: Rebuild cron (paths must reference new SCRIPTS_DIR)
|
||||
echo ""
|
||||
echo "━━━ $ICON_GEAR Step 6: Rebuild cron ━━━"
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
RESULT=$(php -r "
|
||||
define('PLUGIN_CFG', '/boot/config/plugins/varaverk/varaverk.cfg');
|
||||
\$_c = @parse_ini_file(PLUGIN_CFG) ?: [];
|
||||
define('SCRIPTS_DIR', \$_c['SCRIPTS_DIR'] ?? '/boot/config/plugins/varaverk');
|
||||
define('CONF_DIR', SCRIPTS_DIR . '/Configurations');
|
||||
define('DATA_DIR', SCRIPTS_DIR . '/data');
|
||||
define('STATE_DIR', SCRIPTS_DIR . '/State_Files');
|
||||
define('LOG_DIR', '/var/log/varaverk');
|
||||
define('VV_SETUP_STATE_FILE', STATE_DIR . '/varaverk_setup.db');
|
||||
require_once '/usr/local/emhttp/plugins/varaverk/include/confform.php';
|
||||
require_once '/usr/local/emhttp/plugins/varaverk/include/scheduler.php';
|
||||
\$ok = vv_cron_rebuild(vv_schedule_load());
|
||||
echo \$ok ? 'ok' : 'fail';
|
||||
" 2>/dev/null)
|
||||
if [[ "$RESULT" == "ok" ]]; then
|
||||
echo " Cron rebuilt ✅"
|
||||
else
|
||||
warn "Cron rebuild failed — run Settings → Scheduler → Save to regenerate"
|
||||
fi
|
||||
else
|
||||
warn "DRY RUN — would rebuild cron with new SCRIPTS_DIR paths"
|
||||
fi
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Step 7: Flash mode — sync Plugin/ to /boot/ so webUI is current
|
||||
if [[ "$TO_MODE" == "flash" && "$DRY_RUN" == false ]]; then
|
||||
echo ""
|
||||
echo "━━━ $ICON_SYNC Step 7: Sync Plugin/ → /boot/ ━━━"
|
||||
if rsync -a --delete "$DST/Plugin/" "/boot/config/plugins/varaverk/Plugin/" 2>/dev/null; then
|
||||
echo " Plugin/ synced to /boot/ ✅"
|
||||
else
|
||||
warn "Plugin/ sync to /boot/ failed — webUI may be stale"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Step 8: Delete old location
|
||||
echo ""
|
||||
echo "━━━ $ICON_GEAR Step $([[ "$TO_MODE" == "flash" ]] && echo 8 || echo 7): Clean up old location ━━━"
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
if [[ "$SRC" == "$INTERNAL_DIR" ]]; then
|
||||
# Migrating internal→flash: keep varaverk.cfg and Plugin/ in /boot/, remove everything else
|
||||
echo " Removing scripts/conf/state from /boot/ (keeping Plugin/ and varaverk.cfg)..."
|
||||
find "$SRC" -mindepth 1 -maxdepth 1 \
|
||||
! -name 'Plugin' \
|
||||
! -name 'varaverk.cfg' \
|
||||
! -name '*.plg' \
|
||||
! -name '*.txz' \
|
||||
-exec rm -rf {} + 2>/dev/null
|
||||
echo " /boot/ cleaned ✅ (Plugin/ and varaverk.cfg preserved)"
|
||||
else
|
||||
# Migrating flash→internal: remove appdata copy entirely
|
||||
echo " Removing $SRC..."
|
||||
rm -rf "$SRC"
|
||||
echo " $SRC removed ✅"
|
||||
fi
|
||||
else
|
||||
if [[ "$SRC" == "$INTERNAL_DIR" ]]; then
|
||||
warn "DRY RUN — would remove scripts/conf/state from /boot/ (keeping Plugin/ and varaverk.cfg)"
|
||||
else
|
||||
warn "DRY RUN — would remove $SRC"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "━━━━━ $ICON_DONE Migration complete ━━━━━"
|
||||
echo "$ICON_GEAR Mode: $TO_MODE"
|
||||
echo "$ICON_GEAR SCRIPTS_DIR: $DST"
|
||||
if [[ "$TO_MODE" == "flash" ]]; then
|
||||
echo ""
|
||||
warn "IMPORTANT: Varaverk requires the array to be started to function in flash mode."
|
||||
warn "The webUI plugin tab will load normally at all times (Plugin/ stays in /boot/)."
|
||||
fi
|
||||
echo ""
|
||||
echo " Reload the Varaverk plugin tab to apply changes."
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
Reference in New Issue
Block a user