diff --git a/plugin/event/disks_mounted b/plugin/event/disks_mounted index d04025e..4671cb0 100755 --- a/plugin/event/disks_mounted +++ b/plugin/event/disks_mounted @@ -20,6 +20,12 @@ # blocked on podman's full startup sequence (preflight, storage mount, # service start, autostart chain) — mirrors how unassigned.devices # backgrounds its own longer-running "started" hook. +# +# podman-mount-managed-disk.sh runs first, still within the same +# backgrounded subshell: it's a no-op unless the WebUI's "Format a Disk +# for Podman Storage" flow (ajax/disks.php) was ever used, and rc.podman +# start's own storage step needs that disk already mounted at +# $STORAGE_PATH to succeed — see that script's own header comment. # ============================================================================= -/etc/rc.d/rc.podman start > /dev/null 2>&1 & disown +(/usr/local/sbin/podman-mount-managed-disk.sh; /etc/rc.d/rc.podman start) > /dev/null 2>&1 & disown diff --git a/plugin/sbin/podman-common.sh b/plugin/sbin/podman-common.sh index 986962d..0295034 100755 --- a/plugin/sbin/podman-common.sh +++ b/plugin/sbin/podman-common.sh @@ -168,6 +168,33 @@ podman_storage_path_is_safe() { return 0 } +# ----------------------------------------------------------------------------- +# podman_path_has_real_mount_ancestor +# +# True if itself, or its nearest EXISTING ancestor directory, lives +# on a different filesystem than / (root) — i.e. something is genuinely +# mounted along this path (a cache pool, a dedicated disk, ...), even if +# the exact leaf directory doesn't exist yet. False only when nothing real +# is mounted anywhere along the path (root/RAM all the way up), which is +# the one case that's actually unsafe to silently `mkdir -p` into. +# +# This exists because this project never auto-created $STORAGE_PATH +# itself (only podman.img inside it) — found live: a perfectly normal, +# already-mounted cache pool still failed preflight/storage-create with +# "does not exist", because the pool's own .../system/podman subdirectory +# had simply never been created. "Does the exact leaf directory exist" was +# always the wrong question; "is a real filesystem mounted somewhere along +# this path" is the one that actually matters. +# ----------------------------------------------------------------------------- +podman_path_has_real_mount_ancestor() { + local path="$1" + local parent="$path" + while [ ! -d "$parent" ] && [ "$parent" != "/" ]; do + parent="$(dirname "$parent")" + done + [ "$parent" != "/" ] && [ "$(stat -c %d "$parent")" != "$(stat -c %d /)" ] +} + # ----------------------------------------------------------------------------- # podman_require_command # diff --git a/plugin/sbin/podman-mount-managed-disk.sh b/plugin/sbin/podman-mount-managed-disk.sh new file mode 100644 index 0000000..47c3a18 --- /dev/null +++ b/plugin/sbin/podman-mount-managed-disk.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# ============================================================================= +# plugin/sbin/podman-mount-managed-disk.sh +# +# Remounts, on every boot, a disk the WebUI's "Format a Disk for Podman +# Storage" flow (webui/plugins/podman/ajax/disks.php's "format" action) +# formatted and mounted for a single-disk system with no cache pool — +# that disk is deliberately outside Unraid's own array/cache pool +# management (it's just a plain XFS filesystem on an otherwise-unassigned +# disk), so nothing else on the system would remount it after a reboot. +# +# Called from plugin/event/disks_mounted, BEFORE rc.podman start, so +# $STORAGE_PATH (pointed at this disk's mountpoint via Settings) is a real +# mounted filesystem by the time podman-storage.sh's `create`/`mount` +# steps run — see that script's "does not exist or is not mounted" check. +# +# Does nothing (exit 0) if the plugin was never used to format a disk — +# /boot/config/plugins/podman/managed-disk.cfg only exists after that flow +# has actually run at least once. +# ============================================================================= + +set -eu + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=./podman-common.sh +. "$SCRIPT_DIR/podman-common.sh" + +MANAGED_DISK_CFG="$PODMAN_BOOT_DIR/managed-disk.cfg" +[ -f "$MANAGED_DISK_CFG" ] || exit 0 + +UUID="" +MOUNTPOINT="" +# shellcheck source=/dev/null +. "$MANAGED_DISK_CFG" + +if [ -z "$UUID" ] || [ -z "$MOUNTPOINT" ]; then + podman_log_error "mount-managed-disk: $MANAGED_DISK_CFG is missing UUID/MOUNTPOINT, skipping" + exit 0 +fi + +if mountpoint -q "$MOUNTPOINT" 2> /dev/null; then + podman_log "mount-managed-disk: $MOUNTPOINT already mounted" + exit 0 +fi + +mkdir -p "$MOUNTPOINT" +if mount "UUID=$UUID" "$MOUNTPOINT"; then + podman_log "mount-managed-disk: mounted UUID=$UUID at $MOUNTPOINT" +else + podman_log_error "mount-managed-disk: failed to mount UUID=$UUID at $MOUNTPOINT (disk removed/renamed?)" +fi diff --git a/plugin/sbin/podman-preflight.sh b/plugin/sbin/podman-preflight.sh index 9111b6e..301e7b4 100755 --- a/plugin/sbin/podman-preflight.sh +++ b/plugin/sbin/podman-preflight.sh @@ -75,6 +75,14 @@ elif [ -d "$STORAGE_PATH" ]; then else fail "STORAGE_PATH ($STORAGE_PATH) does not appear to be on a mounted filesystem" fi +elif podman_path_has_real_mount_ancestor "$STORAGE_PATH"; then + # The leaf directory doesn't exist yet, but a real filesystem IS mounted + # somewhere along its path (e.g. the cache pool itself) — podman-storage.sh + # create will mkdir -p it. Not a failure; see that check's own comment + # for the live bug this used to cause (a normal, already-mounted cache + # pool failing preflight just because its .../system/podman subdirectory + # had never been created). + ok "STORAGE_PATH ($STORAGE_PATH) doesn't exist yet, but resolves onto a mounted filesystem — will be created" else fail "STORAGE_PATH ($STORAGE_PATH) does not exist — is the configured cache pool/disk present and started?" fi diff --git a/plugin/sbin/podman-storage.sh b/plugin/sbin/podman-storage.sh index 21bae7d..8656c5d 100755 --- a/plugin/sbin/podman-storage.sh +++ b/plugin/sbin/podman-storage.sh @@ -46,9 +46,13 @@ cmd_create() { fi if [ ! -d "$STORAGE_PATH" ]; then - podman_log_error "storage: $STORAGE_PATH does not exist or is not mounted." - podman_log_error "storage: check that the configured cache pool/disk is present before starting podman." - return 1 + if ! podman_path_has_real_mount_ancestor "$STORAGE_PATH"; then + podman_log_error "storage: $STORAGE_PATH does not exist or is not mounted." + podman_log_error "storage: check that the configured cache pool/disk is present before starting podman." + return 1 + fi + podman_log "storage: $STORAGE_PATH doesn't exist yet under an already-mounted filesystem — creating it" + mkdir -p "$STORAGE_PATH" fi # Free space check: refuse to create an image bigger than what's actually diff --git a/scripts/lib/slackbuild-common.sh b/scripts/lib/slackbuild-common.sh index d9f9441..f23a037 100755 --- a/scripts/lib/slackbuild-common.sh +++ b/scripts/lib/slackbuild-common.sh @@ -173,6 +173,23 @@ sb_make_package() { find "$PKG" -type f \( -perm -u+x -o -name '*.so*' \) -exec sh -c \ 'file "$1" | grep -q ELF && strip --strip-unneeded "$1" 2>/dev/null || true' _ {} \; + # Reproducible builds: Slackware's own makepkg already sorts its file + # list (LC_COLLATE=C sort) before archiving, so member ORDER is already + # deterministic — but it only clamps file mtimes in the resulting tar + # when $SOURCE_DATE_EPOCH is set (verified by reading a real + # /sbin/makepkg: `if [ -n "${SOURCE_DATE_EPOCH}" ]; then MTIME= + # "--clamp-mtime --mtime=@${SOURCE_DATE_EPOCH}"; fi`). Without it, every + # separate build run stamps freshly-compiled files with its own wall-clock + # time, so two builds of the *same* source produce byte-different .txz + # files — which is exactly what broke release.yml's "rebuild in CI and + # verify it matches the checksums committed in podman.plg" step (found + # live: aardvark-dns's checksum differed between two separate Gitea + # Actions runs of the identical tagged commit). Deriving it from the + # repo's last commit time (not `date`/a random per-build value) keeps it + # stable across any number of rebuilds of the same commit, while still + # changing whenever the source actually does. + export SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-$(cd "$CWD/../.." && git log -1 --format=%ct 2>/dev/null || echo 0)}" + local pkg_file="$PRGNAM-$version-$arch-$build$tag.txz" ( cd "$PKG" && makepkg --linkadd y --chown y "$OUTPUT/$pkg_file" ) diff --git a/webui/plugins/podman/Podman.page b/webui/plugins/podman/Podman.page index 2fdd7f7..4c1548b 100644 --- a/webui/plugins/podman/Podman.page +++ b/webui/plugins/podman/Podman.page @@ -236,6 +236,21 @@ function podman_asset_version(string $relPath): string
+
+
+

Podman Service

Start, restart, or check the podman.sock backend — no terminal needed.
+
+
+
+ Checking… + + + +
+ +
+
+

Storage

Where podman keeps images, containers and volumes on disk.
@@ -247,6 +262,13 @@ function podman_asset_version(string $relPath): string
Cache pool or dedicated disk — never a path under /mnt/user (FUSE).
+
+ +
+ +
Formats an unused disk with XFS and mounts it, for a single-disk system with no cache pool set up yet.
+
+
diff --git a/webui/plugins/podman/ajax/disks.php b/webui/plugins/podman/ajax/disks.php new file mode 100644 index 0000000..6f5940d --- /dev/null +++ b/webui/plugins/podman/ajax/disks.php @@ -0,0 +1,289 @@ + [{device, sizeBytes, sizeFormatted, model, hasData}, ...] + * format POST {"device": "/dev/sdc"} -> {"mountPath": "/mnt/disks/podman-storage"} + */ + +declare(strict_types=1); + +require __DIR__ . '/../include/bootstrap.php'; + +const MOUNT_PATH = '/mnt/disks/podman-storage'; +const MANAGED_DISK_CFG = '/boot/config/plugins/podman/managed-disk.cfg'; + +$action = $_GET['action'] ?? ''; + +switch ($action) { + case 'list_candidates': + podman_json_response(list_candidate_disks()); + break; + + case 'format': + $body = podman_read_json_body(); + $device = (string) ($body['device'] ?? ''); + podman_json_response(format_disk($device)); + break; + + default: + podman_json_error("Unknown action '{$action}'", 400); +} + +/** + * Runs a command and returns [exitCode, stdout+stderr combined]. Every + * caller here passes a fixed argv array (never a shell string built from + * request input), so there is no injection surface even before the + * device-path validation in format_disk() below. + * + * @param array $argv + * @return array{0:int,1:string} + */ +function run(array $argv, int $timeoutSeconds = 30): array +{ + $descriptors = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']]; + $process = proc_open($argv, $descriptors, $pipes); + if (!is_resource($process)) { + return [127, "could not start {$argv[0]}"]; + } + stream_set_timeout($pipes[1], $timeoutSeconds); + $out = (stream_get_contents($pipes[1]) ?: '') . (stream_get_contents($pipes[2]) ?: ''); + fclose($pipes[1]); + fclose($pipes[2]); + $code = proc_close($process); + return [$code, trim($out)]; +} + +/** + * Every whole disk (not partition) libpod's host isn't already using — + * mounted anywhere (itself or any partition), part of Unraid's own + * array/cache pool (cross-checked against /var/local/emhttp/disks.ini, + * the same state file Unraid's own array management writes — the boot + * flash device is covered by this same check, since Unraid lists it there + * too), or the disk backing the currently-booted root/flash filesystem. + * + * @return array> + */ +function list_candidate_disks(): array +{ + // Nested partitions come back automatically as a "children" array in + // -J's JSON tree — CHILDREN is not a real -o column (lsblk itself + // rejects it: "unknown column: CHILDREN"; verified live against a + // real host's lsblk). LABEL is included specifically to catch the + // Unraid boot flash drive, which FAT-labels itself "UNRAID" — see the + // safety note below for why that check exists at all. + [$code, $out] = run(['lsblk', '-J', '-b', '-p', '-o', 'NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT,MODEL,LABEL'], 10); + if ($code !== 0) { + podman_json_error('Could not list block devices: ' . $out, 500); + } + $tree = json_decode($out, true); + if (!is_array($tree) || !isset($tree['blockdevices'])) { + podman_json_error('Unexpected lsblk output', 500); + } + + $arrayDevices = unraid_array_device_names(); + + $candidates = []; + foreach ($tree['blockdevices'] as $dev) { + if (($dev['type'] ?? '') !== 'disk') { + continue; + } + $name = (string) ($dev['name'] ?? ''); + $baseName = basename($name); + if (in_array($baseName, $arrayDevices, true)) { + continue; + } + // zram devices report TYPE "disk" too (RAM-backed, not persistent + // storage — pointless and misleading to offer for this) and an + // empty card-reader slot with no card inserted reports as a real + // "disk" at 0 bytes; both are excluded outright rather than left + // for the size-based hasData check below. + if (str_starts_with($baseName, 'zram') || (int) ($dev['size'] ?? 0) <= 0) { + continue; + } + if (device_or_children_mounted($dev)) { + continue; + } + if (device_or_children_labeled_unraid($dev)) { + continue; + } + // Deliberately excludes (not just warns about) ANY disk that + // already has a filesystem, partition, or other signature on it + // or any of its partitions — not just ones lsblk reports as + // currently mounted. Found live: a real host's cache pool disks + // showed up here as "safe" with only a soft warning, because + // they're ZFS pool members (fstype "zfs_member"/partition + // present) rather than plain mounts, so the earlier + // mounted-only check missed them entirely — the same blind spot + // that also let the Unraid boot USB stick's own partition + // through (FAT, not reported as "mounted" by lsblk either). A + // disk being reused for podman storage must be genuinely blank; + // asking a user to wipe it themselves first is a small price for + // this being impossible to get wrong. + if (!empty($dev['fstype']) || !empty($dev['children'])) { + continue; + } + + $candidates[] = [ + 'device' => $name, + 'sizeBytes' => (int) ($dev['size'] ?? 0), + 'sizeFormatted' => podman_format_bytes((int) ($dev['size'] ?? 0)), + 'model' => trim((string) ($dev['model'] ?? '')) ?: null, + ]; + } + + return $candidates; +} + +function device_or_children_mounted(array $dev): bool +{ + if (!empty($dev['mountpoint'])) { + return true; + } + foreach ($dev['children'] ?? [] as $child) { + if (device_or_children_mounted($child)) { + return true; + } + } + return false; +} + +/** + * Catches the Unraid boot flash drive specifically: it FAT-labels itself + * "UNRAID" (verified live: `blkid` on a real host's boot partition shows + * LABEL_FATBOOT="UNRAID" LABEL="UNRAID") and — on at least one real, + * modern Unraid setup — /boot is actually backed by a ZFS dataset + * ("flash/boot"), not a direct mount of that partition, so it does NOT + * show up as "mounted" via lsblk's own MOUNTPOINT column at all. This + * label check is a second, independent layer specifically because that + * gap meant the boot drive briefly passed every other check here during + * development — never rely on a single signal for something this + * destructive. + */ +function device_or_children_labeled_unraid(array $dev): bool +{ + if (strtoupper(trim((string) ($dev['label'] ?? ''))) === 'UNRAID') { + return true; + } + foreach ($dev['children'] ?? [] as $child) { + if (device_or_children_labeled_unraid($child)) { + return true; + } + } + return false; +} + +/** + * @return array bare device names ("sda", "nvme0n1", ...) Unraid + * itself has assigned to the array or a cache pool, read from the same + * /var/local/emhttp/disks.ini Unraid's own array management writes — + * NOT parsed/guessed, so this stays correct across whatever array + * layout a given host actually has. + */ +function unraid_array_device_names(): array +{ + $path = '/var/local/emhttp/disks.ini'; + if (!is_readable($path)) { + return []; + } + $ini = @parse_ini_file($path, true); + if (!is_array($ini)) { + return []; + } + $names = []; + foreach ($ini as $section) { + if (is_array($section) && !empty($section['device'])) { + $names[] = basename((string) $section['device']); + } + } + return $names; +} + +/** @return array */ +function format_disk(string $device): array +{ + if (!preg_match('#^/dev/(sd[a-z]+|nvme\d+n\d+|vd[a-z]+)$#', $device)) { + podman_json_error('Invalid or unsupported device path', 400); + } + + $allowed = array_column(list_candidate_disks(), 'device'); + if (!in_array($device, $allowed, true)) { + podman_json_error("{$device} is not a currently-eligible disk (already in use, part of the array, or not found) — refusing to format it.", 400); + } + + $partDevice = preg_match('#nvme\d+n\d+$#', $device) ? "{$device}p1" : "{$device}1"; + + [$code, $out] = run(['wipefs', '-a', $device], 30); + if ($code !== 0) { + podman_json_error("wipefs failed: {$out}", 500); + } + + [$code, $out] = run(['parted', '-s', $device, 'mklabel', 'gpt', 'mkpart', 'primary', '1MiB', '100%'], 30); + if ($code !== 0) { + podman_json_error("parted failed: {$out}", 500); + } + + run(['partprobe', $device], 10); + // Partition device nodes can take a moment to appear after partprobe. + for ($i = 0; $i < 20 && !file_exists($partDevice); $i++) { + usleep(250000); + } + if (!file_exists($partDevice)) { + podman_json_error("Partition {$partDevice} did not appear after partitioning {$device}", 500); + } + + [$code, $out] = run(['mkfs.xfs', '-f', '-n', 'ftype=1', '-L', 'podmanstorage', $partDevice], 60); + if ($code !== 0) { + podman_json_error("mkfs.xfs failed: {$out}", 500); + } + + [$code, $uuid] = run(['blkid', '-s', 'UUID', '-o', 'value', $partDevice], 10); + $uuid = trim($uuid); + if ($code !== 0 || $uuid === '') { + podman_json_error('Could not determine filesystem UUID after formatting', 500); + } + + if (!is_dir(MOUNT_PATH) && !mkdir(MOUNT_PATH, 0755, true) && !is_dir(MOUNT_PATH)) { + podman_json_error('Could not create ' . MOUNT_PATH, 500); + } + [$code, $out] = run(['mount', 'UUID=' . $uuid, MOUNT_PATH], 15); + if ($code !== 0) { + podman_json_error("mount failed: {$out}", 500); + } + + // Persisted so plugin/sbin/podman-mount-managed-disk.sh (called from + // the disks_mounted event hook, before rc.podman start) can remount + // this same disk by UUID on every future boot — nothing else on the + // system knows about this disk, since it's deliberately outside + // Unraid's own array/cache pool management. + $cfgDir = dirname(MANAGED_DISK_CFG); + if (!is_dir($cfgDir) && !mkdir($cfgDir, 0755, true) && !is_dir($cfgDir)) { + podman_json_error('Could not create ' . $cfgDir, 500); + } + $cfgContent = "# Written by the unraid-podman WebUI (ajax/disks.php) — do not edit by hand.\n" + . 'UUID="' . $uuid . "\"\n" + . 'MOUNTPOINT="' . MOUNT_PATH . "\"\n"; + if (file_put_contents(MANAGED_DISK_CFG, $cfgContent, LOCK_EX) === false) { + podman_json_error('Formatted and mounted, but could not persist ' . MANAGED_DISK_CFG . ' for future boots', 500); + } + + return ['mountPath' => MOUNT_PATH]; +} diff --git a/webui/plugins/podman/ajax/settings.php b/webui/plugins/podman/ajax/settings.php index 0b48d08..e2792a5 100644 --- a/webui/plugins/podman/ajax/settings.php +++ b/webui/plugins/podman/ajax/settings.php @@ -16,12 +16,17 @@ * save POST {"storagePath": "...", "storageImageSizeGb": 20, * "enabled": true, "stopTimeoutSeconds": 10} * autostart_save POST {"names": ["postgres", "nextcloud", ...]} + * service_status GET -> {"running": bool, "output": "..."} + * service_start POST -> {"running": bool, "output": "..."} + * service_restart POST -> {"running": bool, "output": "..."} */ declare(strict_types=1); require __DIR__ . '/../include/bootstrap.php'; +const RC_PODMAN = '/etc/rc.d/rc.podman'; + $action = $_GET['action'] ?? ''; switch ($action) { @@ -45,10 +50,60 @@ switch ($action) { podman_json_response(['status' => 'saved']); break; + case 'service_status': + podman_json_response(rc_podman('status', 15)); + break; + + case 'service_start': + podman_json_response(rc_podman('start', 120)); + break; + + case 'service_restart': + podman_json_response(rc_podman('restart', 120)); + break; + default: podman_json_error("Unknown action '{$action}'", 400); } +/** + * Shells out to /etc/rc.d/rc.podman — the plugin's own real + * start/stop/status script, the SAME one the array-start event hook and + * a terminal `rc.podman status` use (see plugin/rc.d/rc.podman's header + * comment). This exists specifically so a fresh install where podman + * failed to start (e.g. no cache pool configured yet, see + * podman-storage.sh's "does not exist or is not mounted" error) can be + * diagnosed and retried from the WebUI itself — no SSH/terminal access + * needed, which is exactly what was missing when this was first needed + * live (a fresh install on a different Unraid box with nobody able to + * reach a terminal to run `rc.podman start` by hand). + * + * @return array{running: bool, output: string} + */ +function rc_podman(string $verb, int $timeoutSeconds): array +{ + $descriptors = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']]; + $process = proc_open([RC_PODMAN, $verb], $descriptors, $pipes); + if (!is_resource($process)) { + podman_json_error("Could not run rc.podman {$verb}", 500); + } + + stream_set_timeout($pipes[1], $timeoutSeconds); + $stdout = stream_get_contents($pipes[1]) ?: ''; + $stderr = stream_get_contents($pipes[2]) ?: ''; + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($process); + + $combined = trim($stdout . $stderr); + // rc.podman status/start both print "service: running (pid ..., socket ...)" + // on success — cheaper and more honest than re-implementing the same + // socket/pid check PHP-side when the shell script already did it. + $running = (bool) preg_match('/service:\s+running/', $combined); + + return ['running' => $running, 'output' => $combined]; +} + /** @return array */ function settings_get(PodmanConfig $config): array { diff --git a/webui/plugins/podman/javascript/settings.js b/webui/plugins/podman/javascript/settings.js index c32c255..f682a1e 100644 --- a/webui/plugins/podman/javascript/settings.js +++ b/webui/plugins/podman/javascript/settings.js @@ -57,6 +57,118 @@ }); } + // --- Podman service status/start/restart ---------------------------------- + + function renderServiceResult(data) { + const chip = P.el('settings-service-chip'); + chip.className = 'podman-chip ' + (data.running ? 'podman-chip-good' : 'podman-chip-bad'); + chip.innerHTML = '' + (data.running ? 'Running' : 'Not running'); + + const log = P.el('settings-service-log'); + if (data.output) { + log.style.display = ''; + log.textContent = data.output; + log.scrollTop = log.scrollHeight; + } + } + + function refreshServiceStatus() { + const buttons = [P.el('settings-service-status-btn'), P.el('settings-service-start-btn'), P.el('settings-service-restart-btn')]; + buttons.forEach(function (b) { b.disabled = true; }); + return P.get('settings', 'service_status').then(renderServiceResult).catch(function (err) { + P.el('settings-service-chip').className = 'podman-chip podman-chip-bad'; + P.el('settings-service-chip').innerHTML = 'Unknown'; + P.el('settings-service-log').style.display = ''; + P.el('settings-service-log').textContent = err.message; + }).finally(function () { + buttons.forEach(function (b) { b.disabled = false; }); + }); + } + + function runServiceAction(action) { + const buttons = [P.el('settings-service-status-btn'), P.el('settings-service-start-btn'), P.el('settings-service-restart-btn')]; + buttons.forEach(function (b) { b.disabled = true; }); + P.el('settings-service-log').style.display = ''; + P.el('settings-service-log').textContent = 'Working…'; + return P.post('settings', action, {}).then(renderServiceResult).catch(function (err) { + P.el('settings-service-log').style.display = ''; + P.el('settings-service-log').textContent = err.message; + }).finally(function () { + buttons.forEach(function (b) { b.disabled = false; }); + }); + } + + // --- Format a disk for podman storage -------------------------------------- + + function openFormatDiskModal() { + const backdrop = document.createElement('div'); + backdrop.className = 'podman-modal-backdrop'; + backdrop.innerHTML = '' + + ''; + + (document.querySelector('.podman-plugin') || document.body).appendChild(backdrop); + + const select = backdrop.querySelector('#fd-device'); + const warning = backdrop.querySelector('#fd-warning'); + const confirmBox = backdrop.querySelector('#fd-confirm'); + const submitBtn = backdrop.querySelector('[data-role="submit"]'); + + function updateSubmitEnabled() { + submitBtn.disabled = !(select.value && confirmBox.checked); + } + + P.get('disks', 'list_candidates').then(function (list) { + select.innerHTML = list.length + ? list.map(function (d) { + return ''; + }).join('') + : ''; + updateSubmitEnabled(); + }).catch(function (err) { + select.innerHTML = ''; + warning.textContent = err.message; + }); + + select.addEventListener('change', updateSubmitEnabled); + confirmBox.addEventListener('change', updateSubmitEnabled); + + function close() { backdrop.remove(); } + + submitBtn.addEventListener('click', function () { + if (!select.value || !confirmBox.checked) return; + if (!confirm('Format ' + select.value + '? This cannot be undone.')) return; + submitBtn.disabled = true; + submitBtn.textContent = 'Formatting…'; + P.post('disks', 'format', { device: select.value }).then(function (data) { + close(); + P.el('settings-storage-path').value = data.mountPath; + alert('Formatted and mounted at ' + data.mountPath + '. Storage path has been filled in below — click "Save Settings" to use it, then Restart Podman.'); + }).catch(function (err) { + submitBtn.disabled = false; + submitBtn.textContent = 'Format Disk'; + alert('Format failed: ' + err.message); + }); + }); + + backdrop.querySelector('[data-role="cancel"]').addEventListener('click', close); + backdrop.addEventListener('click', function (e) { if (e.target === backdrop) close(); }); + } + function save() { const body = { storagePath: P.el('settings-storage-path').value.trim(), @@ -91,6 +203,12 @@ saveAutostart(); }); + P.el('settings-service-status-btn').addEventListener('click', refreshServiceStatus); + P.el('settings-service-start-btn').addEventListener('click', function () { runServiceAction('service_start'); }); + P.el('settings-service-restart-btn').addEventListener('click', function () { runServiceAction('service_restart'); }); + P.el('settings-format-disk-btn').addEventListener('click', openFormatDiskModal); + + refreshServiceStatus(); return load(); } diff --git a/webui/plugins/podman/styles/podman.css b/webui/plugins/podman/styles/podman.css index dbee1f0..e32bef9 100644 --- a/webui/plugins/podman/styles/podman.css +++ b/webui/plugins/podman/styles/podman.css @@ -418,6 +418,9 @@ .podman-input-suffix input[type="number"] { max-width: 100px; width: auto; } .podman-input-suffix span { font-size: 12px; color: var(--text-dim); } +.podman-service-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; } +.podman-service-row:last-child { margin-bottom: 0; } + /* * Toggle switch — a plain checkbox reads as a leftover form control next * to everything else in this panel getting a designed treatment; this