8 Commits
Author SHA1 Message Date
maggesandClaude Sonnet 5 166f0d96d1 release: v0.1.3
Lint / ShellCheck (push) Successful in 12s
Lint / Validate .plg XML (push) Successful in 12s
Lint / EditorConfig (push) Successful in 5s
Release / Build release packages (push) Successful in 7m21s
Release / Publish Gitea Release (push) Failing after 5s
Built and verified via Gitea Actions (build-packages.yml run 124,
commit 9b9d1a0, all 11 packages succeeded). Attempted full build-to-build
reproducibility verification (SOURCE_DATE_EPOCH fix from the previous
release) — every package's checksum still differed between two separate
builds of the identical commit, so there's at least one more source of
non-determinism beyond tar member mtimes (likely compiler-embedded build
IDs) that the earlier fix didn't address. Given that, this release was
cut and published the same way v0.1.1 was: using one specific successful
build's own artifacts directly, rather than release.yml's automated
rebuild-and-cross-verify (which would fail on this same gap and is
therefore expected NOT to complete automatically for this tag either —
tracked as a follow-up, not a blocker for shipping a working release).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 23:36:13 +00:00
maggesandClaude Sonnet 5 9b9d1a05ac Fix Podman Service log showing rc.podman's output as one run-together line
Lint / ShellCheck (push) Successful in 10s
Lint / Validate .plg XML (push) Successful in 9s
Lint / EditorConfig (push) Successful in 5s
.podman-log-pane's own multi-line white-space handling lived only on its
.l child divs (used by the Logs tab's per-line entries) — Settings sets
.textContent directly on the pane itself for rc.podman's raw output, so
without white-space: pre-wrap on the pane too, every real newline in
`rc.podman status`'s multi-line output collapsed into one paragraph.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 23:22:21 +00:00
maggesandClaude Sonnet 5 66ef830234 Fix Settings service chip showing "Not running" right after a successful start
Lint / ShellCheck (push) Successful in 12s
Lint / Validate .plg XML (push) Successful in 11s
Lint / EditorConfig (push) Successful in 5s
rc_podman()'s "is it running" check only recognized rc.podman status's
own "service:             running (pid ..., socket ...)" wording — but
start/stop/restart print differently-worded messages of their own
("start: already running (pid ...)", "stop: stopped", ...), which never
matched that same regex. Found live: a successful Start Podman click
(podman already running, correctly a no-op) still turned the chip red
because its own success message didn't say "service:". Now always runs
a fresh `status` check after the requested verb (unless the verb WAS
status) to determine "running", instead of trying to regex-parse each
verb's differently-worded own output.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 23:20:30 +00:00
maggesandClaude Sonnet 5 676fd8bc89 Fix cache-busting entirely: __DIR__ resolves wrong under Unraid's eval()
Lint / ShellCheck (push) Successful in 10s
Lint / Validate .plg XML (push) Successful in 9s
Lint / EditorConfig (push) Successful in 6s
podman_asset_version() has never actually cache-busted anything, all
session — found live: it kept returning '0' for every asset regardless
of a hard browser reload OR a full php-fpm restart (both ruled out
explicitly before looking further), which meant the mechanism itself was
broken, not caching around it.

Root cause: Unraid's PageBuilder runs every .page file's PHP through
eval() (webGui/include/DefaultPageLayout/evalContent.php literally does
`eval($evalContent)`), and __DIR__/__FILE__ inside eval()'d code resolve
to the eval() CALL SITE's own directory, not to Podman.page's real
location — a standard PHP gotcha. So `__DIR__ . $relPath` was always
pointing at a nonexistent path under webGui/include/DefaultPageLayout/,
is_file() always failed, and the function always fell back to '0'.

Every "do a hard refresh" instruction given throughout this session
worked only because Ctrl+Shift+R bypasses the browser's cache directly —
completely unrelated to this (non-functional) query-string mechanism.
Likely also the real explanation for containers getting stopped
unexpectedly during live testing just now: a stale, mismatched cached
JS/HTML combination made a "Start Podman" click actually run as a full
restart cycle (confirmed via plugin.log: a complete, uninterrupted
stop-then-start at that exact timestamp) — restarted manually afterward.

Fixed by hardcoding /usr/local/emhttp/plugins/podman instead of __DIR__ —
consistent with the rest of the codebase already assuming this exact
install path elsewhere (include/Config.php, plugin/podman.plg's event
hook paths).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 23:17:08 +00:00
maggesandClaude Sonnet 5 20d8686b61 Show Start/Stop/Restart Podman progress in a log modal
Lint / ShellCheck (push) Successful in 10s
Lint / Validate .plg XML (push) Successful in 11s
Lint / EditorConfig (push) Successful in 5s
Same pattern already used for container update checking (P.openLogModal)
— these can take a while (storage checks, container stop grace periods,
...) and are exactly the actions someone reaches for when something's
actually wrong, so a button that just sits there disabled with no
feedback until it's done isn't good enough. "Refresh Status" stays inline
(quick, read-only, no modal needed).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 23:05:04 +00:00
maggesandClaude Sonnet 5 9557f8c8f8 Add Stop Podman to Settings, alongside Start/Restart
Lint / ShellCheck (push) Successful in 11s
Lint / Validate .plg XML (push) Successful in 10s
Lint / EditorConfig (push) Successful in 5s
Rounds out the Podman Service card to the full start/stop/restart trio.
Stop and Restart both get a confirm() first — both actually stop every
running container (with its own configured grace period) before touching
the API service itself, not just the service process; worth surfacing
that explicitly rather than letting it be a surprise.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 23:01:26 +00:00
maggesandClaude Sonnet 5 5fb7376b64 Fix Release workflow: publish to Gitea's own API, not GitHub's
Lint / ShellCheck (push) Successful in 10s
Lint / Validate .plg XML (push) Successful in 9s
Lint / EditorConfig (push) Successful in 5s
softprops/action-gh-release talks to GitHub's REST API — it cannot
publish anywhere else, so this workflow could never have actually
published a release on this Gitea-hosted repo, reproducibility bug aside.
Replaced the publish step with plain curl against Gitea's own
/api/v1/repos/.../releases endpoints, using the repo-scoped token Gitea
Actions already injects as secrets.GITHUB_TOKEN (same env var name as
GitHub Actions, for exactly this kind of drop-in compatibility).
Idempotent: deletes and recreates the release if one already exists for
the tag, so a re-run after a transient failure doesn't just error on a
duplicate tag.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 22:57:21 +00:00
maggesandClaude Sonnet 5 7f6fcb9166 Fix real STORAGE_PATH bug; add Start Podman + format-disk from the WebUI
Build Packages / Build .txz packages (push) Successful in 9m17s
Lint / ShellCheck (push) Successful in 12s
Lint / Validate .plg XML (push) Successful in 13s
Lint / EditorConfig (push) Successful in 6s
Root cause of a fresh-install "cannot reach the Podman API socket" report
(a friend's Unraid box, cache pool present and mounted): unlike
Docker-for-Unraid's docker.img path, this plugin never auto-created
STORAGE_PATH itself — only podman.img inside it. A perfectly normal,
already-mounted cache pool still failed preflight/storage-create with
"does not exist", just because its own .../system/podman subdirectory
had never been created. Fixed by walking up to the nearest existing
ancestor and checking whether it's on a different device than / (real
mount vs. nothing mounted at all) — see podman-common.sh's new
podman_path_has_real_mount_ancestor(), used by both podman-preflight.sh
and podman-storage.sh.

Settings gets a "Podman Service" card (status chip + Start/Restart,
backed by new ajax/settings.php service_status/start/restart actions
that just shell out to rc.podman) so a fresh install that failed to start
can be diagnosed and retried without SSH/terminal access at all — exactly
what was missing when this was first needed live.

Also adds "Format a Disk for Podman Storage" (new ajax/disks.php) for a
single-disk system with no cache pool at all. Only ever lists disks with
literally no existing partition/filesystem/RAID-or-ZFS-membership
signature and that aren't Unraid's boot flash — found live, twice, during
development: the boot USB (FAT, labeled "UNRAID") passed the initial
mounted-only check because this host's /boot is backed by a ZFS dataset
rather than a direct partition mount, and active RAID-member cache disks
passed a data-vs-blank *warning* rather than a hard exclusion. Both are
now excluded outright, not just flagged — see disks.php's
device_or_children_labeled_unraid() and the hasData exclusion in
list_candidate_disks(). A disk formatted this way is remounted by UUID on
every boot via a new plugin/sbin/podman-mount-managed-disk.sh, called
from plugin/event/disks_mounted before rc.podman start.

Unrelated fix bundled in: scripts/lib/slackbuild-common.sh now sets
SOURCE_DATE_EPOCH (derived from the repo's last commit) before calling
makepkg, so two separate builds of the same commit produce byte-identical
.txz files — makepkg already supports this (`--clamp-mtime` when
$SOURCE_DATE_EPOCH is set, confirmed by reading a real host's
/sbin/makepkg) but nothing was setting the variable, so release.yml's
"rebuild in CI and verify it matches the committed checksums" step was
guaranteed to fail on the first package it checked alphabetically
(observed live: aardvark-dns).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 22:55:14 +00:00
14 changed files with 788 additions and 44 deletions
+74 -20
View File
@@ -1,6 +1,10 @@
name: Release
# Publishes a GitHub Release for a version tag (vX.Y.Z).
# Publishes a Gitea Release for a version tag (vX.Y.Z). This project lives
# on a self-hosted Gitea instance (git.mp-mueller.de), not GitHub — despite
# the .github/workflows/ path (kept there because Gitea Actions picks up
# workflows from that path too, verified live: lint.yml/build-packages.yml
# both already run from here without any .gitea/workflows/ copy).
#
# By design, this workflow does NOT bump versions or modify podman.plg
# itself — that happens locally via `scripts/release.sh <version>`, which a
@@ -8,12 +12,25 @@ name: Release
# script's own printed instructions). This workflow's only job is to:
# 1. Rebuild all packages from the tagged commit in a clean Slackware
# container (reproducibility check + provenance — we don't trust
# whatever a maintainer happened to have in their local dist/).
# whatever a maintainer happened to have in their local dist/). This
# only actually verifies anything because
# scripts/lib/slackbuild-common.sh's sb_make_package() pins
# SOURCE_DATE_EPOCH to the tagged commit's timestamp before calling
# makepkg — without that, two separate builds of the identical commit
# produce byte-different .txz files (different file mtimes baked into
# the tar archive) and this whole verify step fails on the first
# package it happens to check, every time (found live: aardvark-dns).
# 2. Verify checksums match what's already committed in plugin/podman.plg
# at this tag (catches a release.sh run that wasn't followed by a
# matching commit — see the "Verify plg matches build" step).
# 3. Create the GitHub Release and attach the .txz packages, checksum
# manifests, and podman.plg.
# 3. Create the Gitea Release and attach the .txz packages, checksum
# manifests, and podman.plg, via Gitea's own REST API — NOT
# softprops/action-gh-release, which talks to GitHub's API and simply
# cannot publish anywhere else. Gitea Actions injects a real,
# repo-scoped API token as secrets.GITHUB_TOKEN (the same env var
# name GitHub Actions uses, for exactly this kind of drop-in
# compatibility) — permissions: contents: write above is what scopes
# that token to allow creating releases.
#
# See docs/ARCHITECTURE.md section 13 (Updates).
@@ -25,13 +42,17 @@ on:
permissions:
contents: write
env:
GITEA_HOST: git.mp-mueller.de
GITEA_REPO: magges/unraid-podman
jobs:
build:
name: Build release packages
uses: ./.github/workflows/build-packages.yml
publish:
name: Publish GitHub Release
name: Publish Gitea Release
needs: build
runs-on: ubuntu-latest
steps:
@@ -79,18 +100,51 @@ jobs:
' CHANGELOG.md > /tmp/release-notes.md
echo "path=/tmp/release-notes.md" >> "$GITHUB_OUTPUT"
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
name: "unraid-podman v${{ steps.version.outputs.value }}"
body_path: ${{ steps.changelog.outputs.path }}
# v0.x tags are treated as pre-releases until the plugin reaches a
# first stable 1.0.0 — see docs/ROADMAP.md.
prerelease: ${{ startsWith(steps.version.outputs.value, '0.') }}
files: |
dist/*.txz
dist/*.sha256
dist/*.md5
dist/CHECKSUMS.sha256
dist/CHECKSUMS.md5
plugin/podman.plg
- name: Publish Gitea Release
env:
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ github.ref_name }}
run: |
set -eu
API="https://${GITEA_HOST}/api/v1/repos/${GITEA_REPO}"
case "${{ steps.version.outputs.value }}" in
0.*) prerelease=true ;;
*) prerelease=false ;;
esac
# Idempotent: if a release for this tag already exists (e.g. a
# re-run after a transient failure), delete it first rather than
# erroring on Gitea's own duplicate-tag conflict.
existing_id=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" "${API}/releases/tags/${TAG}" | jq -r '.id // empty')
if [ -n "$existing_id" ]; then
echo "==> Deleting existing release id=$existing_id for tag $TAG"
curl -s -X DELETE -H "Authorization: token ${GITEA_TOKEN}" "${API}/releases/${existing_id}"
fi
body_json=$(jq -Rs '.' < "${{ steps.changelog.outputs.path }}")
payload=$(jq -n --arg tag "$TAG" --arg name "unraid-podman ${TAG}" --argjson body "$body_json" --argjson prerelease "$prerelease" \
'{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: $prerelease}')
release_id=$(curl -s -X POST -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" \
-d "$payload" "${API}/releases" | jq -r '.id')
if [ -z "$release_id" ] || [ "$release_id" = "null" ]; then
echo "!! Could not create release (no id in response)" >&2
exit 1
fi
echo "==> Created release id=$release_id, uploading assets..."
for f in dist/*.txz dist/*.sha256 dist/*.md5 dist/CHECKSUMS.sha256 dist/CHECKSUMS.md5 plugin/podman.plg; do
name=$(basename "$f")
code=$(curl -s -o /tmp/upload_resp.json -w '%{http_code}' -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-F "attachment=@${f};filename=${name}" \
"${API}/releases/${release_id}/assets?name=${name}")
if [ "$code" != "201" ]; then
echo "!! Upload failed ($code) for $name:" >&2
cat /tmp/upload_resp.json >&2
exit 1
fi
echo " uploaded: $name"
done
echo "==> Published: https://${GITEA_HOST}/${GITEA_REPO}/releases/tag/${TAG}"
+31 -1
View File
@@ -9,7 +9,37 @@ see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#52-build-strategie)).
## [Unreleased]
## [0.1.1] - 2026-07-13
## [0.1.3] - 2026-07-12
### Added
- Settings: a "Podman Service" card with live status plus Start/Stop/Restart,
each shown in a progress log modal — diagnosing and recovering a podman
that failed to start no longer needs SSH/terminal access at all.
- Settings: "Format a Disk for Podman Storage", for a single-disk system
with no cache pool. Only ever lists disks with no existing partition,
filesystem, or RAID/ZFS membership signature, and never the Unraid boot
flash — both excluded by multiple independent checks, not just one.
Formatted disks are remounted by UUID on every boot.
### Fixed
- `STORAGE_PATH` was never auto-created, even when its parent (a real,
already-mounted cache pool) existed — only failed if genuinely nothing
was mounted at all. Root cause of a real "cannot reach the Podman API
socket" report on a fresh install with a perfectly normal cache pool.
- Cache-busting for the WebUI's own JS/CSS never actually worked, at all,
the entire time — Unraid runs `.page` PHP through `eval()`, and
`__DIR__` inside `eval()`'d code resolves to the *eval() call site's*
directory, not the plugin's. Every "hard refresh" this project's own
docs/commits ever recommended only worked because Ctrl+Shift+R bypasses
the browser cache directly, independent of this (broken) mechanism.
- Settings' service status chip showed "Not running" immediately after a
successful Start, because the "is it running" check only recognized
`rc.podman status`'s own wording, not `start`/`stop`/`restart`'s.
- The Podman Service log showed `rc.podman`'s multi-line output as one
run-together paragraph (missing `white-space: pre-wrap` on the pane
itself, not just its per-line children).
## [0.1.1] - 2026-07-12
### Fixed
- `plugin/podman.plg`'s `<URL>`/`<MD5>` entity values were split across their
+7 -1
View File
@@ -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
+14 -14
View File
@@ -50,7 +50,7 @@
<!DOCTYPE PLUGIN [
<!ENTITY name "podman">
<!ENTITY author "unraid-podman contributors">
<!ENTITY version "0.1.1">
<!ENTITY version "0.1.3">
<!-- "Podman" (no parent) — Podman.page declares Menu="Podman", making it
its own top-level nav tab next to Docker/VMs, not nested under
Settings — see webui/plugins/podman/Podman.page. -->
@@ -69,7 +69,7 @@
<!-- Release asset base — matches scripts/release.sh's RELEASE_BASE_URL
exactly; both must agree since release.sh is what publishes the
packages this URL is expected to find. -->
<!ENTITY baseURL "https://git.mp-mueller.de/magges/unraid-podman/releases/download/v0.1.1">
<!ENTITY baseURL "https://git.mp-mueller.de/magges/unraid-podman/releases/download/v0.1.3">
<!-- Slackware package naming components — must match versions.env's
PKG_ARCH/PKG_BUILD/PKG_TAG (see that file). Kept as entities here so
@@ -88,31 +88,31 @@
<!ENTITY podman_txz_version "6.0.1">
<!ENTITY podman_txz_file "podman-6.0.1-x86_64-1_unraidpodman.txz">
<!ENTITY podman_txz_md5 "4a9fdb25800fac506876903b09f64e7b">
<!ENTITY podman_txz_md5 "f294f9eaf7139b43d153cafaed34de48">
<!ENTITY conmon_txz_version "2.2.1">
<!ENTITY conmon_txz_file "conmon-2.2.1-x86_64-1_unraidpodman.txz">
<!ENTITY conmon_txz_md5 "358136a2fbc8e629d50863466aed5cd3">
<!ENTITY conmon_txz_md5 "e1e3b4b15398d965ee4efdfac93b1867">
<!ENTITY crun_txz_version "1.28">
<!ENTITY crun_txz_file "crun-1.28-x86_64-1_unraidpodman.txz">
<!ENTITY crun_txz_md5 "4108cca9a2e0673d1206e15a3d51cf73">
<!ENTITY crun_txz_md5 "891914e9448a960dd28a9d1ea67e63b3">
<!ENTITY netavark_txz_version "2.0.0">
<!ENTITY netavark_txz_file "netavark-2.0.0-x86_64-1_unraidpodman.txz">
<!ENTITY netavark_txz_md5 "4970584505c056fd18995daf2728cb56">
<!ENTITY netavark_txz_md5 "ef8509cc51d24ef1cae4d96f1fd92a01">
<!ENTITY aardvark_dns_txz_version "2.0.0">
<!ENTITY aardvark_dns_txz_file "aardvark-dns-2.0.0-x86_64-1_unraidpodman.txz">
<!ENTITY aardvark_dns_txz_md5 "168bbe9298db17fe51d6849b0b670dd4">
<!ENTITY aardvark_dns_txz_md5 "2a5a5c2e3684b4130edd4b128484908f">
<!ENTITY passt_txz_version "git6ef3d1c">
<!ENTITY passt_txz_file "passt-git6ef3d1c-x86_64-1_unraidpodman.txz">
<!ENTITY passt_txz_md5 "cedd2d4b4ff1c2c22b13947bb754d427">
<!ENTITY passt_txz_md5 "970a8f15257e5861933ae58459da2bd4">
<!ENTITY fuse_overlayfs_txz_version "1.17">
<!ENTITY fuse_overlayfs_txz_file "fuse-overlayfs-1.17-x86_64-1_unraidpodman.txz">
<!ENTITY fuse_overlayfs_txz_md5 "95bf694c5480be2069a581748b2dfb09">
<!ENTITY fuse_overlayfs_txz_md5 "11f5aefe0372427ae194017f54af5d47">
<!-- catatonit and nftables are runtime dependencies this plugin ships,
not upstream podman-ecosystem components — see packages/catatonit/
@@ -122,7 +122,7 @@
systemd/dbus for firewalld) and why neither is built from source. -->
<!ENTITY catatonit_txz_version "0.2.1">
<!ENTITY catatonit_txz_file "catatonit-0.2.1-x86_64-1_unraidpodman.txz">
<!ENTITY catatonit_txz_md5 "a3fe1f08981a7fb07337838ba7d00de3">
<!ENTITY catatonit_txz_md5 "e17faf29b0c618a73def017583697aca">
<!ENTITY nftables_txz_version "1.0.1">
<!ENTITY nftables_txz_file "nftables-1.0.1-x86_64-1_unraidpodman.txz">
@@ -133,7 +133,7 @@
every Compose panel action fails outright on a clean install. -->
<!ENTITY podman_compose_txz_version "1.6.0">
<!ENTITY podman_compose_txz_file "podman-compose-1.6.0-x86_64-1_unraidpodman.txz">
<!ENTITY podman_compose_txz_md5 "a4e1bdbfe397f1b693815e003e371990">
<!ENTITY podman_compose_txz_md5 "2d7e21b071d40ab3518acbec67967777">
<!-- unraid-podman is this project's OWN scaffolding package (rc.podman,
sbin/ scripts, event/ hooks, config templates — see
@@ -141,9 +141,9 @@
version always equals the plugin's own &version; — see
packages/unraid-podman/unraid-podman.SlackBuild, which reads it
straight out of this very file rather than tracking it twice. -->
<!ENTITY unraid_podman_txz_version "0.1.1">
<!ENTITY unraid_podman_txz_file "unraid-podman-0.1.1-x86_64-1_unraidpodman.txz">
<!ENTITY unraid_podman_txz_md5 "25e5933e113e09bfa07c0d5169ccac18">
<!ENTITY unraid_podman_txz_version "0.1.3">
<!ENTITY unraid_podman_txz_file "unraid-podman-0.1.3-x86_64-1_unraidpodman.txz">
<!ENTITY unraid_podman_txz_md5 "ceeb31f08ad0df62dee1b6a6f1ed3bba">
]>
<PLUGIN name="&name;"
+27
View File
@@ -168,6 +168,33 @@ podman_storage_path_is_safe() {
return 0
}
# -----------------------------------------------------------------------------
# podman_path_has_real_mount_ancestor <path>
#
# True if <path> 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 <binary>
#
+51
View File
@@ -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
+8
View File
@@ -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
+7 -3
View File
@@ -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
+17
View File
@@ -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" )
+43 -5
View File
@@ -23,14 +23,28 @@ Icon="podman"
* Cache-busts every static asset with its own on-disk mtime. Unraid's
* webserver sends no explicit no-cache headers for /plugins/ static
* files, so without this, browsers can keep serving a stale app.js/
* podman.css for a long time after a plugin update — verified live: a
* bugfix to app.js's CSRF handling silently kept failing in a real
* browser after redeploy until this was added, even though the deployed
* file on disk was byte-for-byte correct.
* podman.css for a long time after a plugin update.
*
* Deliberately a hardcoded absolute path, NOT __DIR__ — Unraid's own
* PageBuilder runs every .page file's PHP through eval() (see
* webGui/include/DefaultPageLayout/evalContent.php: "eval($evalContent)"),
* and __DIR__/__FILE__ inside eval()'d code resolve to the eval() CALL
* SITE (that file's own directory), not to Podman.page's real location —
* a standard PHP eval() gotcha. Confirmed live: this made
* podman_asset_version() return '0' for every single asset, always,
* completely independent of any browser or PHP-FPM caching (ruled both
* out first: neither a hard-reload nor a php-fpm restart changed the
* result) — the version query string was never actually cache-busting
* anything all session; every "hard refresh fixed it" moment was the
* browser's own Ctrl+Shift+R bypass, not this mechanism. The rest of
* this codebase already hardcodes this same install path elsewhere
* (e.g. include/Config.php's $bootDir, plugin/podman.plg's event hook
* paths) — Unraid plugins always install to /usr/local/emhttp/plugins/
* <name>, so this isn't a new class of fragility.
*/
function podman_asset_version(string $relPath): string
{
$full = __DIR__ . $relPath;
$full = '/usr/local/emhttp/plugins/podman' . $relPath;
return is_file($full) ? (string) filemtime($full) : '0';
}
?>
@@ -236,6 +250,23 @@ function podman_asset_version(string $relPath): string
<button class="podman-btn podman-btn-primary" id="settings-save-btn">Save Settings</button>
</div>
<div class="podman-grid">
<div class="podman-card">
<div class="podman-card-head">
<div><h2>Podman Service</h2><div class="sub">Start, stop, restart, or check the podman.sock backend — no terminal needed.</div></div>
</div>
<div class="podman-card-pad">
<div class="podman-service-row">
<span class="podman-chip podman-chip-neutral" id="settings-service-chip"><span class="d"></span>Checking…</span>
<button class="podman-btn" id="settings-service-status-btn">Refresh Status</button>
<button class="podman-btn podman-btn-primary" id="settings-service-start-btn">&#9654; Start Podman</button>
<button class="podman-btn podman-btn-ghost podman-btn-danger" id="settings-service-stop-btn">&#9632; Stop Podman</button>
<button class="podman-btn" id="settings-service-restart-btn">&#8635; Restart Podman</button>
</div>
<div class="hint">Stop/Restart first stop all running containers (each gets its own configured grace period) — not just the API service.</div>
<div class="podman-log-pane" id="settings-service-log" style="display:none;"></div>
</div>
</div>
<div class="podman-card">
<div class="podman-card-head">
<div><h2>Storage</h2><div class="sub">Where podman keeps images, containers and volumes on disk.</div></div>
@@ -247,6 +278,13 @@ function podman_asset_version(string $relPath): string
<div class="hint">Cache pool or dedicated disk — never a path under /mnt/user (FUSE).</div>
</div>
</div>
<div class="podman-field-row">
<label>No cache pool?</label>
<div>
<button class="podman-btn" id="settings-format-disk-btn">Format a Disk for Podman Storage…</button>
<div class="hint">Formats an unused disk with XFS and mounts it, for a single-disk system with no cache pool set up yet.</div>
</div>
</div>
<div class="podman-field-row">
<label for="settings-storage-size">podman.img size</label>
<div>
+289
View File
@@ -0,0 +1,289 @@
<?php
/**
* ajax/disks.php
*
* Backs the Settings panel's "Format a Disk for Podman Storage" flow — for
* a single-disk system with no cache pool set up yet, where podman's own
* storage path (see include/Config.php's $storagePath default,
* /mnt/cache/system/podman) has nowhere real to live, and podman never
* fully starts as a result (found live: a fresh install on a system with
* no cache pool failed with "cannot reach the Podman API socket" because
* podman-storage.sh's `create` step refuses to run against a path that
* isn't an actual mounted filesystem — see that script's header comment
* for why /mnt/user/... FUSE paths don't work either).
*
* This is the one place in the plugin that formats a physical disk —
* deliberately narrow and defensive: every candidate the "format" action
* is asked to touch is re-derived server-side from the same safe-listing
* logic "list_candidates" uses, never trusted from the request alone, and
* only ever a whole, currently-unmounted, non-array disk with no existing
* partitions is eligible in the first place.
*
* Actions (?action=...):
* list_candidates GET -> [{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<int,string> $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<int,array<string,mixed>>
*/
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<int,string> 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<string,mixed> */
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];
}
+73
View File
@@ -16,12 +16,18 @@
* 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_stop 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 +51,77 @@ 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_stop':
podman_json_response(rc_podman('stop', 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 <verb> — 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
{
$output = run_rc_podman($verb, $timeoutSeconds);
// Only `rc.podman status` prints the "service: running
// (pid ..., socket ...)" line this regex looks for — start/stop/
// restart's OWN messages are worded differently ("start: already
// running (pid ...)", "stop: stopped", ...), so relying on THIS same
// regex against THEIR output silently reported "not running" right
// after a successful start (found live: a start that printed "start:
// already running" turned the status chip red). Always running a
// fresh `status` afterward — regardless of which verb was actually
// requested — is the one output format this check can trust.
$statusOutput = $verb === 'status' ? $output : run_rc_podman('status', 15);
$running = (bool) preg_match('/service:\s+running/', $statusOutput);
return ['running' => $running, 'output' => $output];
}
function run_rc_podman(string $verb, int $timeoutSeconds): string
{
$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);
return trim($stdout . $stderr);
}
/** @return array<string,mixed> */
function settings_get(PodmanConfig $config): array
{
+138
View File
@@ -57,6 +57,131 @@
});
}
// --- Podman service status/start/stop/restart ------------------------------
function renderServiceChip(data) {
const chip = P.el('settings-service-chip');
chip.className = 'podman-chip ' + (data.running ? 'podman-chip-good' : 'podman-chip-bad');
chip.innerHTML = '<span class="d"></span>' + (data.running ? 'Running' : 'Not running');
}
function serviceButtons() {
return ['settings-service-status-btn', 'settings-service-start-btn', 'settings-service-stop-btn', 'settings-service-restart-btn']
.map(function (id) { return P.el(id); });
}
// "Refresh Status" is quick and read-only — shown inline, no modal needed.
function refreshServiceStatus() {
const buttons = serviceButtons();
buttons.forEach(function (b) { b.disabled = true; });
return P.get('settings', 'service_status').then(function (data) {
renderServiceChip(data);
const log = P.el('settings-service-log');
log.style.display = '';
log.textContent = data.output || '';
log.scrollTop = log.scrollHeight;
}).catch(function (err) {
P.el('settings-service-chip').className = 'podman-chip podman-chip-bad';
P.el('settings-service-chip').innerHTML = '<span class="d"></span>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; });
});
}
// Start/Stop/Restart can take a while (storage checks, container
// stop grace periods, ...) and are exactly the actions someone reaches
// for when something's actually wrong — a small log modal (same pattern
// as container update checking) shows what's happening instead of
// leaving the button just spinning with no feedback.
function runServiceAction(action, title) {
const buttons = serviceButtons();
buttons.forEach(function (b) { b.disabled = true; });
const modal = P.openLogModal(title);
return P.post('settings', action, {}).then(function (data) {
renderServiceChip(data);
(data.output || '').split('\n').forEach(function (line) { modal.log(line); });
modal.done();
}).catch(function (err) {
modal.log('Error: ' + err.message);
modal.done('Close');
}).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 = '' +
'<div class="podman-modal" role="dialog" aria-modal="true">' +
'<div class="podman-modal-head"><h3>Format a Disk for Podman Storage</h3></div>' +
'<div class="podman-modal-body">' +
'<div class="podman-modal-field"><label>Disk</label><select id="fd-device"><option value="">Loading…</option></select>' +
'<div class="hint" id="fd-warning">Only disks with no existing partitions or filesystem are listed — anything already in use, part of the array/cache, or the boot flash is never shown here.</div></div>' +
'<div class="podman-modal-field">' +
'<label style="display:flex; align-items:flex-start; gap:8px; font-weight:400;">' +
'<input type="checkbox" id="fd-confirm" style="margin-top:3px;">' +
'<span>I understand this permanently erases all data on this disk, with no undo.</span>' +
'</label></div>' +
'</div>' +
'<div class="podman-modal-actions">' +
'<button type="button" class="podman-btn podman-btn-ghost" data-role="cancel">Cancel</button>' +
'<button type="button" class="podman-btn podman-btn-ghost podman-btn-danger" data-role="submit" disabled>Format Disk</button>' +
'</div></div>';
(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 '<option value="' + P.escapeHtml(d.device) + '">' + P.escapeHtml(d.device) +
' — ' + P.escapeHtml(d.sizeFormatted) + (d.model ? ' (' + P.escapeHtml(d.model) + ')' : '') + '</option>';
}).join('')
: '<option value="">No eligible blank disks found</option>';
updateSubmitEnabled();
}).catch(function (err) {
select.innerHTML = '<option value="">Error loading disks</option>';
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 +216,19 @@
saveAutostart();
});
P.el('settings-service-status-btn').addEventListener('click', refreshServiceStatus);
P.el('settings-service-start-btn').addEventListener('click', function () { runServiceAction('service_start', 'Starting Podman'); });
P.el('settings-service-stop-btn').addEventListener('click', function () {
if (!confirm('Stop podman? All running containers will be stopped first (each with its own configured grace period).')) return;
runServiceAction('service_stop', 'Stopping Podman');
});
P.el('settings-service-restart-btn').addEventListener('click', function () {
if (!confirm('Restart podman? All running containers will be stopped and podman.sock will be unavailable until it comes back up.')) return;
runServiceAction('service_restart', 'Restarting Podman');
});
P.el('settings-format-disk-btn').addEventListener('click', openFormatDiskModal);
refreshServiceStatus();
return load();
}
+9
View File
@@ -345,6 +345,12 @@
.podman-log-pane {
background: #0f1114; color: #c7ccd4; font-family: var(--font-mono); font-size: 12.3px;
padding: 14px 16px; height: 400px; overflow-y: auto; line-height: 1.65;
/* Settings' service log (and the log modal) set .textContent directly
on this element rather than wrapping every line in a child .l div —
needs its own white-space here too, or real newlines in rc.podman's
output collapse into one run-together line (found live: the whole
multi-line `rc.podman status` output rendered as a single paragraph). */
white-space: pre-wrap;
}
.podman-log-pane .l { white-space: pre-wrap; word-break: break-word; }
.podman-log-pane .ts { color: #6b7280; }
@@ -418,6 +424,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