Add Varaverk plugin scaffold (PHP, unRAID native)

Plugin lives in Plugin/ — invisible to User Script Plugin.
dev_install.sh symlinks into /usr/local/emhttp/plugins/varaverk/ for development.

Pages: Monitor (5s poll), Scheduler (orchs + Advanced children, toggle + cron),
Config (raw editor, host-aware file access), Docs (markdown + live $VAR substitution).

API endpoints: monitor.php (JSON), scheduler.php (writes schedule.json + cron.d),
config.php (writes conf files with host permission check).

Includes: config.php (parser, host detection, var map), scheduler.php (job tree,
cron.d rebuild), monitor.php (docker, GPU, resources, fallback, transcode),
docs.php (file tree, var substitution, Parsedown renderer).
This commit is contained in:
Gmer4Lfe
2026-05-23 16:19:12 -04:00
parent 5e4f510a42
commit 4ab3ddbc47
15 changed files with 829 additions and 0 deletions
@@ -0,0 +1,72 @@
<?php
// Config file parser and writer.
// Reads master.conf and the appropriate host*.conf based on running host.
define('SCRIPTS_DIR', '/mnt/user/appdata/unraid_scripts');
define('CONF_DIR', SCRIPTS_DIR . '/Configurations');
function vv_get_hostname(): string {
return trim(shell_exec('hostname -s') ?: '');
}
function vv_detect_host(): string {
// Reads master.conf to find HOST1/HOST2 hostnames, returns 'host1', 'host2', or 'unknown'
$master = vv_read_conf_raw('master.conf');
preg_match('/^\s*HOST1_NAME\s*=\s*["\']?(\S+?)["\']?\s*$/m', $master, $m1);
preg_match('/^\s*HOST2_NAME\s*=\s*["\']?(\S+?)["\']?\s*$/m', $master, $m2);
$hostname = vv_get_hostname();
if (!empty($m1[1]) && strcasecmp($hostname, $m1[1]) === 0) return 'host1';
if (!empty($m2[1]) && strcasecmp($hostname, $m2[1]) === 0) return 'host2';
return 'unknown';
}
function vv_is_owner(): bool {
return vv_detect_host() === 'host1';
}
function vv_read_conf_raw(string $filename): string {
$path = CONF_DIR . '/' . $filename;
return file_exists($path) ? file_get_contents($path) : '';
}
function vv_write_conf_raw(string $filename, string $content): bool {
$path = CONF_DIR . '/' . $filename;
return file_put_contents($path, $content) !== false;
}
function vv_get_conf_files(): array {
// Returns conf files this host is allowed to view/edit
$host = vv_detect_host();
$files = [];
if ($host === 'host1') {
$files[] = 'master.conf';
$files[] = 'host1.conf';
} elseif ($host === 'host2') {
$files[] = 'host2.conf';
} else {
// Unknown host — show all for dev/debug
foreach (glob(CONF_DIR . '/*.conf') as $f) {
$files[] = basename($f);
}
}
return $files;
}
// Parse conf into key=>value map for $VAR substitution in docs
function vv_conf_vars(): array {
$vars = [];
$files = ['master.conf'];
$host = vv_detect_host();
if ($host === 'host1') $files[] = 'host1.conf';
if ($host === 'host2') $files[] = 'host2.conf';
foreach ($files as $f) {
$raw = vv_read_conf_raw($f);
// Match: VAR_NAME="value" or VAR_NAME=value (no quotes)
preg_match_all('/^\s*([A-Z0-9_]+)\s*=\s*["\']?([^"\'#\n]*?)["\']?\s*(?:#.*)?$/m', $raw, $m);
foreach ($m[1] as $i => $key) {
$vars[$key] = trim($m[2][$i]);
}
}
return $vars;
}
@@ -0,0 +1,48 @@
<?php
// Docs — markdown file discovery, $VAR substitution, and rendering.
// Requires parsedown or similar. Falls back to <pre> if not available.
define('PARSEDOWN_PATH', '/usr/local/emhttp/plugins/varaverk/lib/Parsedown.php');
function vv_docs_tree(): array {
$base = SCRIPTS_DIR;
$tree = [];
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($base, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($files as $f) {
if ($f->isFile() && strtolower($f->getExtension()) === 'md') {
$rel = ltrim(str_replace($base, '', $f->getPathname()), '/');
$tree[] = $rel;
}
}
sort($tree);
return $tree;
}
function vv_docs_render(string $rel, array $vars): string {
$path = SCRIPTS_DIR . '/' . $rel;
if (!file_exists($path)) return '<p>File not found.</p>';
$md = file_get_contents($path);
// Substitute `$VAR_NAME` markers with live conf values
$md = preg_replace_callback('/`\$([A-Z0-9_]+)`/', function($m) use ($vars) {
$key = $m[1];
return isset($vars[$key])
? '<code class="vv-live-var">' . htmlspecialchars($vars[$key]) . '</code>'
: '<code class="vv-unknown-var">$' . htmlspecialchars($key) . '</code>';
}, $md);
// Render markdown
if (file_exists(PARSEDOWN_PATH)) {
require_once PARSEDOWN_PATH;
$pd = new Parsedown();
$pd->setSafeMode(true);
return $pd->text($md);
}
// Fallback: plain preformatted text
return '<pre>' . htmlspecialchars($md) . '</pre>';
}
@@ -0,0 +1,121 @@
<?php
// Monitor helpers — docker, GPU, resources, transcode sessions, fallback state.
function vv_docker_containers(): array {
$out = shell_exec('docker ps --format \'{"name":"{{.Names}}","status":"{{.Status}}","image":"{{.Image}}"}\' 2>/dev/null');
$containers = [];
foreach (explode("\n", trim($out ?? '')) as $line) {
if (!$line) continue;
$c = json_decode($line, true);
if ($c) $containers[] = $c;
}
return $containers;
}
function vv_docker_stopped(): array {
$out = shell_exec('docker ps -a --filter "status=exited" --filter "status=created" --format \'{"name":"{{.Names}}","status":"{{.Status}}"}\' 2>/dev/null');
$containers = [];
foreach (explode("\n", trim($out ?? '')) as $line) {
if (!$line) continue;
$c = json_decode($line, true);
if ($c) $containers[] = $c;
}
return $containers;
}
function vv_gpu_stats(): array {
$out = shell_exec('nvidia-smi --query-gpu=name,memory.used,memory.total,utilization.gpu,temperature.gpu --format=csv,noheader,nounits 2>/dev/null');
if (!$out) return ['available' => false];
$parts = array_map('trim', explode(',', $out));
return [
'available' => true,
'name' => $parts[0] ?? '',
'memory_used' => (int)($parts[1] ?? 0),
'memory_total' => (int)($parts[2] ?? 0),
'utilization' => (int)($parts[3] ?? 0),
'temperature' => (int)($parts[4] ?? 0),
];
}
function vv_gpu_processes(): array {
$out = shell_exec('nvidia-smi --query-compute-apps=pid,used_gpu_memory,name --format=csv,noheader,nounits 2>/dev/null');
$procs = [];
foreach (explode("\n", trim($out ?? '')) as $line) {
if (!$line) continue;
$parts = array_map('trim', explode(',', $line));
$procs[] = [
'pid' => $parts[0] ?? '',
'memory_mb' => $parts[1] ?? '',
'name' => $parts[2] ?? '',
];
}
return $procs;
}
function vv_system_resources(): array {
// RAM
$mem = [];
foreach (file('/proc/meminfo') ?: [] as $line) {
if (preg_match('/^(MemTotal|MemAvailable):\s+(\d+)/', $line, $m))
$mem[$m[1]] = (int)$m[2];
}
// CPU (1-second sample)
$cpu = (int)trim(shell_exec("top -bn1 | grep 'Cpu(s)' | awk '{print $2}' | cut -d. -f1") ?: '0');
// Disk — ramdisk + cache
$ramdisk = vv_df('/mnt/ramdisk_transcodes');
$cache = vv_df('/mnt/cache');
return [
'ram_total_mb' => (int)(($mem['MemTotal'] ?? 0) / 1024),
'ram_free_mb' => (int)(($mem['MemAvailable'] ?? 0) / 1024),
'cpu_percent' => $cpu,
'ramdisk' => $ramdisk,
'cache' => $cache,
];
}
function vv_df(string $path): array {
$out = shell_exec("df -BM --output=size,used,avail '$path' 2>/dev/null | tail -1");
if (!$out) return ['available' => false, 'path' => $path];
$parts = preg_split('/\s+/', trim($out));
return [
'available' => true,
'path' => $path,
'size_mb' => (int)$parts[0],
'used_mb' => (int)$parts[1],
'free_mb' => (int)$parts[2],
];
}
function vv_fallback_state(): array {
// State file written by fallback.sh
$stateFile = '/tmp/fallback_state.db';
if (!file_exists($stateFile)) return ['state' => 'UNKNOWN'];
$raw = [];
foreach (file($stateFile) ?: [] as $line) {
[$k, $v] = array_pad(explode('=', trim($line), 2), 2, '');
$raw[trim($k)] = trim($v);
}
return [
'state' => $raw['state'] ?? 'UNKNOWN',
'failover_start' => $raw['failover_start'] ?? '0',
'tier2_started' => $raw['tier2_started'] ?? 'false',
'tier3_started' => $raw['tier3_started'] ?? 'false',
'tier4_started' => $raw['tier4_started'] ?? 'false',
];
}
function vv_transcode_sessions(): array {
// Read from transcode state file written by transcode_manager.sh
$stateFile = '/tmp/transcode_state.db';
if (!file_exists($stateFile)) return [];
$raw = [];
foreach (file($stateFile) ?: [] as $line) {
[$k, $v] = array_pad(explode('=', trim($line), 2), 2, '');
$raw[trim($k)] = trim($v);
}
return $raw;
}
@@ -0,0 +1,98 @@
<?php
// Scheduler — manages schedule.json and /etc/cron.d/varaverk.
// schedule.json is per-host, never synced.
define('SCHEDULE_FILE', '/boot/config/plugins/varaverk/schedule.json');
define('CRON_FILE', '/etc/cron.d/varaverk');
define('CRON_USER', 'root');
function vv_schedule_load(): array {
if (!file_exists(SCHEDULE_FILE)) return [];
$data = json_decode(file_get_contents(SCHEDULE_FILE), true);
return is_array($data) ? $data : [];
}
function vv_schedule_save(array $schedule): bool {
$dir = dirname(SCHEDULE_FILE);
if (!is_dir($dir)) mkdir($dir, 0755, true);
return file_put_contents(SCHEDULE_FILE, json_encode($schedule, JSON_PRETTY_PRINT)) !== false;
}
function vv_schedule_update(string $id, bool $enabled, string $cron): bool {
$schedule = vv_schedule_load();
$schedule[$id] = [
'id' => $id,
'enabled' => $enabled,
'cron' => $cron,
'updated' => date('c'),
];
if (!vv_schedule_save($schedule)) return false;
return vv_cron_rebuild($schedule);
}
function vv_cron_rebuild(array $schedule): bool {
$lines = ["# Varaverk — managed by plugin, do not edit manually"];
$lines[] = "# Regenerated: " . date('Y-m-d H:i:s');
$lines[] = "";
$scriptsDir = SCRIPTS_DIR;
foreach ($schedule as $entry) {
if (empty($entry['enabled']) || empty($entry['cron']) || empty($entry['id'])) continue;
$script = "$scriptsDir/{$entry['id']}";
$cron = $entry['cron'];
$lines[] = "$cron " . CRON_USER . " bash \"$script\" >> /var/log/varaverk/jobs.log 2>&1";
}
$lines[] = "";
return file_put_contents(CRON_FILE, implode("\n", $lines)) !== false;
}
// Walk the scripts repo and return the job tree:
// orchestrators as top-level, individual scripts as children
function vv_job_tree(): array {
$scriptsDir = SCRIPTS_DIR;
$schedule = vv_schedule_load();
// Orchestrators are in Orchestrators/ and their children are all scripts they call
// For now: walk top-level folders, treat *_management.sh or *_maintenance.sh as orchs
$orchPattern = "$scriptsDir/Orchestrators/*.sh";
$orchs = [];
foreach (glob($orchPattern) ?: [] as $path) {
$id = 'Orchestrators/' . basename($path);
$entry = $schedule[$id] ?? ['enabled' => false, 'cron' => ''];
$orchs[] = [
'id' => $id,
'label' => basename($path, '.sh'),
'type' => 'orchestrator',
'enabled' => (bool)($entry['enabled'] ?? false),
'cron' => $entry['cron'] ?? '',
'children' => vv_script_children($path, $schedule),
];
}
return $orchs;
}
// Parse an orchestrator script to find which child scripts it calls
function vv_script_children(string $orchPath, array $schedule): array {
$scriptsDir = SCRIPTS_DIR;
$content = file_get_contents($orchPath) ?: '';
$children = [];
// Match: bash "path/to/script.sh" or source path/to/script.sh
preg_match_all('/(?:bash|source|\.)\s+"?([^"\s]+\.sh)"?/m', $content, $m);
foreach ($m[1] as $rel) {
// Normalise relative paths
$rel = ltrim(str_replace($scriptsDir . '/', '', $rel), './');
$id = $rel;
$entry = $schedule[$id] ?? ['enabled' => false, 'cron' => ''];
$children[] = [
'id' => $id,
'label' => basename($rel, '.sh'),
'type' => 'script',
'enabled' => (bool)($entry['enabled'] ?? false),
'cron' => $entry['cron'] ?? '',
];
}
return $children;
}