Add reproducible build system, native Unraid plugin, and WebUI
- versions.env pins podman, conmon, crun, netavark, aardvark-dns, passt, and fuse-overlayfs to verified upstream source checksums; SlackBuild recipes, scripts/build-packages.sh, checksums.sh, release.sh, and update-versions.sh implement the reproducible pipeline; GitHub Actions workflows build in a Slackware container and publish releases without committing any binaries. - plugin/podman.plg installs/updates/removes all eight packages (the seven components plus the plugin's own unraid-podman scaffolding package) via upgradepkg, using the official Unraid array-event hook mechanism (event/disks_mounted, event/stopping) instead of editing /boot/config/go. rc.podman and the sbin/ helper scripts implement storage creation, config seeding/sync, preflight checks, autostart with per-container Safe-Mode, and package verify/update/rollback. - webui/plugins/podman implements the Dashboard, Containers, Pods, Images, Volumes, Networks, Logs, Terminal, Compose, and Settings panels against the approved mockup (webui/mockups/prototype.html), talking to podman system service exclusively via PodmanClient.php (libpod REST API over the Unix socket), with two documented exceptions: Terminal's one-shot exec model and Compose's use of the podman compose CLI, since libpod has no REST equivalent for either. - docs/ARCHITECTURE.md and docs/ROADMAP.md record the design decisions and honest current status (syntax-checked, unit- and integration-tested against fake sockets/servers; not yet run against a real Unraid/Podman/Slackware system). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
# plugin/boot-config/plugins/podman/
|
||||
|
||||
This directory is a staged mirror of what `/boot/config/plugins/podman/` should
|
||||
look like immediately after a first install — see
|
||||
[docs/ARCHITECTURE.md, section 4.1](../../../../docs/ARCHITECTURE.md#41-auf-dem-flash-device-boot-persistent-klein).
|
||||
|
||||
The `.plg` postinstall step (see `plugin/podman.plg`) copies these files to
|
||||
`/boot/config/plugins/podman/` **only if they don't already exist**, so
|
||||
re-running an install/update never clobbers a user's existing configuration.
|
||||
|
||||
Config file *content* templates (`containers.conf`, `storage.conf`,
|
||||
`registries.conf`, `policy.json`) are sourced from the top-level [config/](../../../../config)
|
||||
directory rather than duplicated here — this directory only defines the
|
||||
directory skeleton and the plugin-specific settings files (`podman.cfg`,
|
||||
`autostart`, ...) that have no equivalent elsewhere.
|
||||
|
||||
```
|
||||
plugins/podman/
|
||||
├── podman.cfg # copied from config/podman.cfg.example
|
||||
├── autostart # empty by default, see docs/ARCHITECTURE.md section 12
|
||||
├── autostart-delay # empty by default
|
||||
├── networks/ # empty, populated as custom networks are created
|
||||
├── backup/
|
||||
│ ├── packages/ # empty, populated on first update
|
||||
│ └── config/ # empty, populated on first update
|
||||
└── plugin.log # empty, install/update log
|
||||
```
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# plugin/event/disks_mounted
|
||||
#
|
||||
# Official Unraid plugin event hook — NOT a custom mechanism. Unraid's
|
||||
# emhttpd fires every executable found at
|
||||
# /usr/local/emhttp/plugins/<name>/event/<eventname> as each boot/array
|
||||
# event happens (see e.g. the real-world unassigned.devices plugin, which
|
||||
# uses this same convention for its own disks_mounted/started/stopping_svcs
|
||||
# hooks). This project relies exclusively on this official mechanism —
|
||||
# specifically NOT on hand-editing /boot/config/go, which is unnecessary
|
||||
# and easy to get wrong across plugin updates/uninstalls.
|
||||
#
|
||||
# "disks_mounted" fires once array disks AND cache pools are mounted —
|
||||
# exactly the precondition unraid-podman needs before it can create/mount
|
||||
# podman.img on the configured cache pool/disk (see
|
||||
# docs/ARCHITECTURE.md section 4.3 and section 6.2).
|
||||
#
|
||||
# Backgrounded (& disown) so array startup / the Main GUI page isn't
|
||||
# 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.
|
||||
# =============================================================================
|
||||
|
||||
/etc/rc.d/rc.podman start > /dev/null 2>&1 & disown
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# plugin/event/stopping
|
||||
#
|
||||
# Official Unraid plugin event hook (see plugin/event/disks_mounted for the
|
||||
# full explanation of this mechanism). "stopping" is the FIRST event fired
|
||||
# in Unraid's shutdown sequence — before stopping_docker, stopping_svcs,
|
||||
# unmounting_disks, stopping_array — which gives unraid-podman the largest
|
||||
# possible window to stop containers and unmount podman.img BEFORE Unraid
|
||||
# unmounts the underlying cache pool/disk out from under it.
|
||||
#
|
||||
# Run synchronously (no backgrounding, unlike disks_mounted) — shutdown
|
||||
# must actually wait for containers to stop and storage to unmount
|
||||
# cleanly; racing the array-stop sequence here risks filesystem corruption
|
||||
# on podman.img (see docs/ARCHITECTURE.md section 16.1 on why this project
|
||||
# treats storage integrity conservatively).
|
||||
# =============================================================================
|
||||
|
||||
/etc/rc.d/rc.podman stop
|
||||
@@ -0,0 +1,365 @@
|
||||
<?xml version="1.0" standalone="yes"?>
|
||||
|
||||
<!--
|
||||
podman.plg — Unraid plugin manifest for unraid-podman.
|
||||
|
||||
This file is the single entry point Unraid's Plugin Manager uses to
|
||||
install, update, and remove the plugin. Everything it does is built on
|
||||
OFFICIAL Unraid plugin mechanisms only — verified directly against the
|
||||
real source of two long-established, widely used Unraid plugins
|
||||
(unraid/community.applications and unraid/unassigned.devices) rather than
|
||||
guessed:
|
||||
|
||||
- A FILE block with a Name attribute, an upgradepkg Run command (using
|
||||
its install new and reinstall flags together), and a URL/MD5 child
|
||||
pair is the standard way to download and install a Slackware .txz
|
||||
package, and the same command works for both a fresh install and an
|
||||
update in one step (confirmed: both reference plugins use exactly
|
||||
this pattern rather than separate installpkg/upgradepkg branches).
|
||||
- Plugin uninstall logic lives in a <FILE Run="/bin/bash" Method="remove">
|
||||
block — this exact `Method="remove"` attribute is what Unraid's
|
||||
plugin manager looks for when the user clicks "Remove" (confirmed
|
||||
against community.applications.plg).
|
||||
- Boot/shutdown integration uses the official Unraid plugin EVENT
|
||||
mechanism: any executable at
|
||||
/usr/local/emhttp/plugins/<name>/event/<eventname> is run automatically
|
||||
by emhttpd as the corresponding event fires (confirmed against
|
||||
unassigned.devices, which uses this exact convention for its own
|
||||
disks_mounted/started/stopping_svcs hooks). This project deliberately
|
||||
does NOT hand-edit /boot/config/go — see plugin/event/disks_mounted
|
||||
and plugin/event/stopping for the two hooks used here, and
|
||||
docs/ARCHITECTURE.md section 6.2 for the full event-ordering rationale.
|
||||
|
||||
Structure of this file:
|
||||
1. DOCTYPE entity block — plugin metadata + one version/file/md5 triple
|
||||
per package (the seven upstream components plus this project's own
|
||||
"unraid-podman" scaffolding package, see packages/unraid-podman/).
|
||||
Entities are rewritten automatically by scripts/release.sh; never
|
||||
hand-edit a *_txz_version/_file/_md5 entity — see that script.
|
||||
2. <PLUGIN> body:
|
||||
a. <CHANGES> — kept in sync with CHANGELOG.md by hand for now.
|
||||
b. Pre-install architecture sanity check.
|
||||
c. Eight <FILE> package install/update blocks.
|
||||
d. Postinstall <FILE Run="/bin/bash"> — directory/config seeding,
|
||||
install-manifest generation, first start.
|
||||
e. <FILE Run="/bin/bash" Method="remove"> — uninstall.
|
||||
-->
|
||||
|
||||
<!DOCTYPE PLUGIN [
|
||||
<!ENTITY name "podman">
|
||||
<!ENTITY author "unraid-podman contributors">
|
||||
<!ENTITY version "0.0.0">
|
||||
<!-- "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. -->
|
||||
<!ENTITY launch "Podman">
|
||||
<!ENTITY github "OWNER/unraid-podman">
|
||||
<!ENTITY gitURL "https://raw.githubusercontent.com/&github;/main">
|
||||
<!ENTITY pluginURL "&gitURL;/plugin/podman.plg">
|
||||
<!ENTITY supportURL "https://github.com/&github;/discussions">
|
||||
|
||||
<!-- 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://github.com/&github;/releases/download/v&version;">
|
||||
|
||||
<!-- Slackware package naming components — must match versions.env's
|
||||
PKG_ARCH/PKG_BUILD/PKG_TAG (see that file). Kept as entities here so
|
||||
the eight removepkg calls in the Method="remove" block don't have to
|
||||
repeat "x86_64-1_unraidpodman" eight times by hand. -->
|
||||
<!ENTITY pkgArch "x86_64">
|
||||
<!ENTITY pkgBuild "1">
|
||||
<!ENTITY pkgTag "_unraidpodman">
|
||||
|
||||
<!-- One version/file/md5 entity triple per package, filled in by
|
||||
scripts/release.sh from dist/*.txz.md5 at release time (see that
|
||||
script's "Update per-package entities" step). Until a release has been
|
||||
cut, these are placeholders and the FILE blocks below will correctly
|
||||
fail to download anything — that is intentional; there is nothing to
|
||||
install before the first tagged release. -->
|
||||
|
||||
<!ENTITY podman_txz_version "0.0.0">
|
||||
<!ENTITY podman_txz_file "podman-&podman_txz_version;-&pkgArch;-&pkgBuild;&pkgTag;.txz">
|
||||
<!ENTITY podman_txz_md5 "00000000000000000000000000000000">
|
||||
|
||||
<!ENTITY conmon_txz_version "0.0.0">
|
||||
<!ENTITY conmon_txz_file "conmon-&conmon_txz_version;-&pkgArch;-&pkgBuild;&pkgTag;.txz">
|
||||
<!ENTITY conmon_txz_md5 "00000000000000000000000000000000">
|
||||
|
||||
<!ENTITY crun_txz_version "0.0.0">
|
||||
<!ENTITY crun_txz_file "crun-&crun_txz_version;-&pkgArch;-&pkgBuild;&pkgTag;.txz">
|
||||
<!ENTITY crun_txz_md5 "00000000000000000000000000000000">
|
||||
|
||||
<!ENTITY netavark_txz_version "0.0.0">
|
||||
<!ENTITY netavark_txz_file "netavark-&netavark_txz_version;-&pkgArch;-&pkgBuild;&pkgTag;.txz">
|
||||
<!ENTITY netavark_txz_md5 "00000000000000000000000000000000">
|
||||
|
||||
<!ENTITY aardvark_dns_txz_version "0.0.0">
|
||||
<!ENTITY aardvark_dns_txz_file "aardvark-dns-&aardvark_dns_txz_version;-&pkgArch;-&pkgBuild;&pkgTag;.txz">
|
||||
<!ENTITY aardvark_dns_txz_md5 "00000000000000000000000000000000">
|
||||
|
||||
<!ENTITY passt_txz_version "0.0.0">
|
||||
<!ENTITY passt_txz_file "passt-&passt_txz_version;-&pkgArch;-&pkgBuild;&pkgTag;.txz">
|
||||
<!ENTITY passt_txz_md5 "00000000000000000000000000000000">
|
||||
|
||||
<!ENTITY fuse_overlayfs_txz_version "0.0.0">
|
||||
<!ENTITY fuse_overlayfs_txz_file "fuse-overlayfs-&fuse_overlayfs_txz_version;-&pkgArch;-&pkgBuild;&pkgTag;.txz">
|
||||
<!ENTITY fuse_overlayfs_txz_md5 "00000000000000000000000000000000">
|
||||
|
||||
<!-- unraid-podman is this project's OWN scaffolding package (rc.podman,
|
||||
sbin/ scripts, event/ hooks, config templates — see
|
||||
packages/unraid-podman/README.md), not an upstream component. Its
|
||||
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 "&version;">
|
||||
<!ENTITY unraid_podman_txz_file "unraid-podman-&unraid_podman_txz_version;-&pkgArch;-&pkgBuild;&pkgTag;.txz">
|
||||
<!ENTITY unraid_podman_txz_md5 "00000000000000000000000000000000">
|
||||
]>
|
||||
|
||||
<PLUGIN name="&name;"
|
||||
author="&author;"
|
||||
version="&version;"
|
||||
launch="&launch;"
|
||||
pluginURL="&pluginURL;"
|
||||
support="&supportURL;"
|
||||
icon="cubes"
|
||||
min="6.12.0">
|
||||
<!--
|
||||
min="6.12.0": Unraid 6.12 introduced folder-based (non-loopback) Docker
|
||||
storage and is a reasonably conservative baseline for the cgroup v2 /
|
||||
kernel networking features netavark, aardvark-dns, and passt assume. See
|
||||
docs/ARCHITECTURE.md section 21 (open question — revisit if real-world
|
||||
testing shows an earlier or later minimum is actually required).
|
||||
-->
|
||||
|
||||
<CHANGES>
|
||||
##podman
|
||||
|
||||
###0.0.0
|
||||
- Initial scaffolding. Not a functional release — see CHANGELOG.md for the
|
||||
authoritative, up-to-date history; this block is kept in sync with it by
|
||||
hand at release time (scripts/release.sh does not touch this section).
|
||||
</CHANGES>
|
||||
|
||||
<!--
|
||||
Pre-install sanity check: this project only builds/ships x86_64 packages
|
||||
(see versions.env's PKG_ARCH) — fail with a clear message on any other
|
||||
architecture rather than letting eight package downloads 404 one by one.
|
||||
-->
|
||||
<FILE Run="/bin/bash">
|
||||
<INLINE>
|
||||
if [ "$(uname -m)" != "x86_64" ]; then
|
||||
echo "unraid-podman only supports x86_64 (detected: $(uname -m)) - aborting install."
|
||||
exit 1
|
||||
fi
|
||||
</INLINE>
|
||||
</FILE>
|
||||
|
||||
<!--
|
||||
The seven upstream component packages. Each is downloaded straight into
|
||||
its backup slot under /boot/config/plugins/&name;/backup/packages/&version;/
|
||||
(grouped by PLUGIN version, not each component's own version — a rollback
|
||||
targets "this plugin release" as one unit, see
|
||||
plugin/sbin/podman-backup.sh's header comment) and installed from there
|
||||
in place via upgradepkg's install new / reinstall flags, which handle
|
||||
both a fresh install and an update to a newer version identically (see
|
||||
file header comment). This download-destination-doubles-as-backup approach is
|
||||
what makes plugin/sbin/podman-backup.sh's restore-packages command work
|
||||
with no separate copy step.
|
||||
-->
|
||||
|
||||
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&podman_txz_file;" Run="upgradepkg --install-new --reinstall">
|
||||
<URL>
|
||||
&baseURL;/&podman_txz_file;
|
||||
</URL>
|
||||
<MD5>
|
||||
&podman_txz_md5;
|
||||
</MD5>
|
||||
</FILE>
|
||||
|
||||
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&conmon_txz_file;" Run="upgradepkg --install-new --reinstall">
|
||||
<URL>
|
||||
&baseURL;/&conmon_txz_file;
|
||||
</URL>
|
||||
<MD5>
|
||||
&conmon_txz_md5;
|
||||
</MD5>
|
||||
</FILE>
|
||||
|
||||
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&crun_txz_file;" Run="upgradepkg --install-new --reinstall">
|
||||
<URL>
|
||||
&baseURL;/&crun_txz_file;
|
||||
</URL>
|
||||
<MD5>
|
||||
&crun_txz_md5;
|
||||
</MD5>
|
||||
</FILE>
|
||||
|
||||
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&netavark_txz_file;" Run="upgradepkg --install-new --reinstall">
|
||||
<URL>
|
||||
&baseURL;/&netavark_txz_file;
|
||||
</URL>
|
||||
<MD5>
|
||||
&netavark_txz_md5;
|
||||
</MD5>
|
||||
</FILE>
|
||||
|
||||
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&aardvark_dns_txz_file;" Run="upgradepkg --install-new --reinstall">
|
||||
<URL>
|
||||
&baseURL;/&aardvark_dns_txz_file;
|
||||
</URL>
|
||||
<MD5>
|
||||
&aardvark_dns_txz_md5;
|
||||
</MD5>
|
||||
</FILE>
|
||||
|
||||
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&passt_txz_file;" Run="upgradepkg --install-new --reinstall">
|
||||
<URL>
|
||||
&baseURL;/&passt_txz_file;
|
||||
</URL>
|
||||
<MD5>
|
||||
&passt_txz_md5;
|
||||
</MD5>
|
||||
</FILE>
|
||||
|
||||
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&fuse_overlayfs_txz_file;" Run="upgradepkg --install-new --reinstall">
|
||||
<URL>
|
||||
&baseURL;/&fuse_overlayfs_txz_file;
|
||||
</URL>
|
||||
<MD5>
|
||||
&fuse_overlayfs_txz_md5;
|
||||
</MD5>
|
||||
</FILE>
|
||||
|
||||
<!--
|
||||
The plugin's own scaffolding package — rc.podman, sbin/ helper scripts,
|
||||
the event/ hooks below, and default config templates. See
|
||||
packages/unraid-podman/README.md.
|
||||
-->
|
||||
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&unraid_podman_txz_file;" Run="upgradepkg --install-new --reinstall">
|
||||
<URL>
|
||||
&baseURL;/&unraid_podman_txz_file;
|
||||
</URL>
|
||||
<MD5>
|
||||
&unraid_podman_txz_md5;
|
||||
</MD5>
|
||||
</FILE>
|
||||
|
||||
<!--
|
||||
Postinstall: everything that has to happen AFTER the eight packages above
|
||||
are on disk, but isn't itself package content — directory/config
|
||||
seeding, the install-manifest that plugin/sbin/podman-verify-packages.sh
|
||||
and podman-update-packages.sh read, and the first start. Runs on both
|
||||
fresh install and every update (Unraid re-runs the whole .plg either way)
|
||||
— every step here is written to be idempotent, per
|
||||
docs/ARCHITECTURE.md section 7.
|
||||
-->
|
||||
<!--
|
||||
IMPORTANT for anyone editing this block: it is deliberately NOT wrapped
|
||||
in <![CDATA[ ]]>. CDATA sections suppress XML entity expansion entirely,
|
||||
which would leave literal, unexpanded text like "&version;" in the
|
||||
generated installed-versions.env instead of the real version number —
|
||||
confirmed by direct testing against this exact file. This is also why
|
||||
real Unraid plugins (community.applications, unassigned.devices) never
|
||||
wrap their entity-referencing INLINE scripts in CDATA either. The
|
||||
practical consequence: avoid literal '<' in this script (heredocs
|
||||
included) — a sequence of individual "echo >>" lines is used below
|
||||
instead for exactly that reason. A lone '>' is fine unescaped in XML
|
||||
content (only '<' and '&' are not) — verified empirically, not assumed.
|
||||
-->
|
||||
<FILE Run="/bin/bash">
|
||||
<INLINE>
|
||||
set -u
|
||||
|
||||
echo "Setting permissions..."
|
||||
chmod 0755 /etc/rc.d/rc.podman
|
||||
chmod 0755 /usr/local/sbin/podman-*.sh
|
||||
chmod 0755 /usr/local/emhttp/plugins/podman/event/disks_mounted
|
||||
chmod 0755 /usr/local/emhttp/plugins/podman/event/stopping
|
||||
|
||||
echo "Writing installed-package manifest..."
|
||||
mkdir -p /usr/local/share/unraid-podman
|
||||
MANIFEST=/usr/local/share/unraid-podman/installed-versions.env
|
||||
echo "# Generated by podman.plg postinstall - do not edit by hand." > "$MANIFEST"
|
||||
echo "# Read by podman-verify-packages.sh and podman-update-packages.sh." >> "$MANIFEST"
|
||||
echo "PLUGIN_VERSION=\"&version;\"" >> "$MANIFEST"
|
||||
echo "PODMAN_INSTALLED_VERSION=\"&podman_txz_version;\"" >> "$MANIFEST"
|
||||
echo "CONMON_INSTALLED_VERSION=\"&conmon_txz_version;\"" >> "$MANIFEST"
|
||||
echo "CRUN_INSTALLED_VERSION=\"&crun_txz_version;\"" >> "$MANIFEST"
|
||||
echo "NETAVARK_INSTALLED_VERSION=\"&netavark_txz_version;\"" >> "$MANIFEST"
|
||||
echo "AARDVARK_DNS_INSTALLED_VERSION=\"&aardvark_dns_txz_version;\"" >> "$MANIFEST"
|
||||
echo "PASST_INSTALLED_VERSION=\"&passt_txz_version;\"" >> "$MANIFEST"
|
||||
echo "FUSE_OVERLAYFS_INSTALLED_VERSION=\"&fuse_overlayfs_txz_version;\"" >> "$MANIFEST"
|
||||
echo "UNRAID_PODMAN_INSTALLED_VERSION=\"&unraid_podman_txz_version;\"" >> "$MANIFEST"
|
||||
|
||||
echo "Seeding /boot/config/plugins/podman/ configuration (existing files left untouched)..."
|
||||
/usr/local/sbin/podman-config.sh seed
|
||||
|
||||
echo "Verifying package installation..."
|
||||
if /usr/local/sbin/podman-verify-packages.sh --quiet; then
|
||||
echo "Package verification OK."
|
||||
else
|
||||
echo "WARNING: package verification reported problems - see /boot/config/plugins/podman/plugin.log"
|
||||
fi
|
||||
|
||||
echo "Starting podman..."
|
||||
if /etc/rc.d/rc.podman start; then
|
||||
echo ""
|
||||
echo "----------------------------------------------------"
|
||||
echo " unraid-podman &version; has been installed and started."
|
||||
echo " CLI: podman --url unix:///var/run/podman/podman.sock ..."
|
||||
echo " Docs: https://github.com/&github;"
|
||||
echo "----------------------------------------------------"
|
||||
echo ""
|
||||
else
|
||||
echo ""
|
||||
echo "----------------------------------------------------"
|
||||
echo " unraid-podman &version; was installed but did not start cleanly."
|
||||
echo " It will be retried automatically the next time the array starts"
|
||||
echo " (see /usr/local/emhttp/plugins/podman/event/disks_mounted)."
|
||||
echo " Check /boot/config/plugins/podman/plugin.log for details."
|
||||
echo "----------------------------------------------------"
|
||||
echo ""
|
||||
fi
|
||||
</INLINE>
|
||||
</FILE>
|
||||
|
||||
<!--
|
||||
Uninstall. Triggered by Unraid's Plugin Manager when the user clicks
|
||||
"Remove" — the Method="remove" attribute is what makes this block
|
||||
(rather than any of the install blocks above) run in that case.
|
||||
|
||||
Order matters: podman-uninstall-cleanup.sh runs FIRST, while the
|
||||
unraid-podman package (which contains it, at /usr/local/sbin/) is still
|
||||
on disk — removepkg-ing it out from under itself mid-script would be a
|
||||
mistake. See that script's own header comment for exactly what is and
|
||||
is not removed by default (user data is preserved unless the purge-data
|
||||
flag is passed, which this normal removal flow deliberately never does).
|
||||
-->
|
||||
<!-- Not CDATA-wrapped — see the comment above the postinstall block for why: this content needs its &..._txz_file; entity references expanded, and CDATA would suppress that. Nothing here needs an unescaped '<'. -->
|
||||
<FILE Run="/bin/bash" Method="remove">
|
||||
<INLINE>
|
||||
echo "Running unraid-podman cleanup..."
|
||||
/usr/local/sbin/podman-uninstall-cleanup.sh
|
||||
|
||||
echo "Removing packages..."
|
||||
removepkg &podman_txz_file;
|
||||
removepkg &conmon_txz_file;
|
||||
removepkg &crun_txz_file;
|
||||
removepkg &netavark_txz_file;
|
||||
removepkg &aardvark_dns_txz_file;
|
||||
removepkg &passt_txz_file;
|
||||
removepkg &fuse_overlayfs_txz_file;
|
||||
removepkg &unraid_podman_txz_file;
|
||||
|
||||
echo ""
|
||||
echo "unraid-podman removed. Your configuration, backups, and container"
|
||||
echo "storage under /boot/config/plugins/podman/ and the configured"
|
||||
echo "STORAGE_PATH were left in place - see docs/INSTALL.md if you want"
|
||||
echo "to remove them too."
|
||||
echo ""
|
||||
</INLINE>
|
||||
</FILE>
|
||||
|
||||
</PLUGIN>
|
||||
Executable
+244
@@ -0,0 +1,244 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# plugin/rc.d/rc.podman
|
||||
#
|
||||
# Init script for unraid-podman, in the classic Slackware BSD
|
||||
# start|stop|restart|status style — Unraid has no systemd, so this script
|
||||
# alone owns the entire process lifecycle (see docs/ARCHITECTURE.md
|
||||
# section 6, "Start-/Stop-Skripte", and the top-level "Rahmenbedingungen"
|
||||
# table on why that's the case).
|
||||
#
|
||||
# Staged by plugin/podman.plg to /etc/rc.d/rc.podman. Its boot-time
|
||||
# invocation is registered (also by podman.plg) in /boot/config/go, gated on
|
||||
# Unraid's "array started" event rather than running unconditionally at
|
||||
# boot — the configured storage path (cache pool/disk) is not guaranteed to
|
||||
# be available before that point. See ARCHITECTURE.md section 6.2.
|
||||
#
|
||||
# The individual responsibilities below are deliberately split into small,
|
||||
# single-purpose scripts under /usr/local/sbin/ (staged from plugin/sbin/)
|
||||
# rather than inlined here — see each script's own header comment for why
|
||||
# it's separate:
|
||||
# podman-preflight.sh startup validation
|
||||
# podman-config.sh config seeding + sync
|
||||
# podman-storage.sh podman.img create/mount/unmount
|
||||
# podman-autostart.sh autostart chain
|
||||
# podman-backup.sh config/package snapshots for rollback
|
||||
# podman-verify-packages.sh package integrity checks
|
||||
# podman-update-packages.sh package reconciliation/update
|
||||
# podman-uninstall-cleanup.sh called from podman.plg's removepkg block
|
||||
# =============================================================================
|
||||
|
||||
set -u
|
||||
|
||||
SBIN_DIR="/usr/local/sbin"
|
||||
# shellcheck source=../sbin/podman-common.sh
|
||||
. "$SBIN_DIR/podman-common.sh"
|
||||
|
||||
podman_load_cfg
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# podman_start
|
||||
#
|
||||
# See file header for the full sequence. Every step is expected to be
|
||||
# idempotent (safe to re-run `rc.podman start` against an already-running
|
||||
# instance without breaking anything) — preflight and podman-storage.sh
|
||||
# both already implement this on their own, but we also short-circuit here
|
||||
# if the service is clearly already up, to avoid doing redundant work.
|
||||
# -----------------------------------------------------------------------------
|
||||
podman_start() {
|
||||
if [ "${PODMAN_ENABLED:-yes}" != "yes" ]; then
|
||||
podman_log "start: PODMAN_ENABLED is not 'yes' in podman.cfg, not starting"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -S "$PODMAN_SOCKET" ] && [ -f "$PODMAN_SERVICE_PID_FILE" ] \
|
||||
&& kill -0 "$(cat "$PODMAN_SERVICE_PID_FILE" 2> /dev/null)" 2> /dev/null; then
|
||||
podman_log "start: already running (pid $(cat "$PODMAN_SERVICE_PID_FILE"))"
|
||||
return 0
|
||||
fi
|
||||
|
||||
podman_log "start: beginning startup sequence"
|
||||
|
||||
# 1. Preflight — aborts loudly (and already notified) on failure.
|
||||
if ! "$SBIN_DIR/podman-preflight.sh"; then
|
||||
podman_log_error "start: preflight checks failed, aborting"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 2. Ensure config exists (no-op if already seeded) and sync it to the
|
||||
# RAM-root /etc/containers/ — see podman-config.sh.
|
||||
"$SBIN_DIR/podman-config.sh" seed
|
||||
if ! "$SBIN_DIR/podman-config.sh" sync; then
|
||||
podman_log_error "start: config sync failed, aborting"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 3. Storage: create the image if this is a first start, then mount it.
|
||||
if ! "$SBIN_DIR/podman-storage.sh" create; then
|
||||
podman_log_error "start: storage creation failed, aborting"
|
||||
return 1
|
||||
fi
|
||||
if ! "$SBIN_DIR/podman-storage.sh" mount; then
|
||||
podman_log_error "start: storage mount failed, aborting"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 4. Restore persisted netavark network definitions (see
|
||||
# docs/ARCHITECTURE.md section 8, Netzwerke) from the boot-persistent
|
||||
# copy into the RAM-root config directory netavark reads from.
|
||||
mkdir -p "$PODMAN_ETC_DIR/networks"
|
||||
if [ -d "$PODMAN_NETWORKS_BOOT_DIR" ] && [ -n "$(ls -A "$PODMAN_NETWORKS_BOOT_DIR" 2> /dev/null)" ]; then
|
||||
cp -a "$PODMAN_NETWORKS_BOOT_DIR"/. "$PODMAN_ETC_DIR/networks/"
|
||||
podman_log "start: restored custom network definitions"
|
||||
fi
|
||||
|
||||
# 5. Start the Podman API service, rootful, on a unix socket — see
|
||||
# docs/ARCHITECTURE.md section 6.1 for why this runs as a persistent
|
||||
# service rather than being started fresh per CLI/WebUI call:
|
||||
# a shared process gives consistent state and event streaming.
|
||||
# --time=0 disables the idle-shutdown timeout (this is a long-running
|
||||
# daemon under our process management, not an on-demand activation).
|
||||
mkdir -p "$PODMAN_RUN_DIR"
|
||||
podman_log "start: starting podman system service on unix://$PODMAN_SOCKET"
|
||||
nohup podman system service --time=0 "unix://$PODMAN_SOCKET" \
|
||||
> "$PODMAN_LOG_DIR/podman-service.log" 2>&1 &
|
||||
local service_pid=$!
|
||||
echo "$service_pid" > "$PODMAN_SERVICE_PID_FILE"
|
||||
|
||||
# Wait for the socket to actually appear rather than assuming the fork
|
||||
# succeeded instantly — up to 15s, polled every 200ms.
|
||||
local waited=0
|
||||
while [ ! -S "$PODMAN_SOCKET" ] && [ "$waited" -lt 15000 ]; do
|
||||
sleep 0.2
|
||||
waited=$((waited + 200))
|
||||
if ! kill -0 "$service_pid" 2> /dev/null; then
|
||||
podman_log_error "start: podman system service exited immediately — see $PODMAN_LOG_DIR/podman-service.log"
|
||||
"$SBIN_DIR/podman-storage.sh" unmount || true
|
||||
podman_notify "Podman failed to start" \
|
||||
"podman system service exited immediately. See $PODMAN_LOG_DIR/podman-service.log." \
|
||||
"alert"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ! -S "$PODMAN_SOCKET" ]; then
|
||||
podman_log_error "start: timed out waiting for $PODMAN_SOCKET to appear"
|
||||
return 1
|
||||
fi
|
||||
podman_log "start: podman system service is up (pid $service_pid)"
|
||||
|
||||
# 6. Autostart. A failure here is logged/notified by the script itself
|
||||
# and does not abort rc.podman start — the service is already usable.
|
||||
"$SBIN_DIR/podman-autostart.sh" || podman_log_error "start: autostart chain reported errors (see above)"
|
||||
|
||||
echo "running" > "$PODMAN_STATUS_FILE"
|
||||
podman_log "start: startup sequence complete"
|
||||
return 0
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# podman_stop
|
||||
#
|
||||
# Stops containers first (each with its own STOP_TIMEOUT grace period),
|
||||
# then the API service, then unmounts storage — the reverse of start, so
|
||||
# nothing is torn down out from under something still using it.
|
||||
# -----------------------------------------------------------------------------
|
||||
podman_stop() {
|
||||
if [ ! -S "$PODMAN_SOCKET" ]; then
|
||||
podman_log "stop: not running (no socket at $PODMAN_SOCKET)"
|
||||
# Still attempt an unmount in case a prior stop was interrupted after
|
||||
# the service went down but before the unmount completed.
|
||||
"$SBIN_DIR/podman-storage.sh" unmount || true
|
||||
rm -f "$PODMAN_STATUS_FILE"
|
||||
return 0
|
||||
fi
|
||||
|
||||
podman_log "stop: stopping running containers (timeout ${STOP_TIMEOUT:-10}s each)"
|
||||
local running_ids
|
||||
running_ids=$(podman --url "unix://$PODMAN_SOCKET" ps -q 2> /dev/null || true)
|
||||
if [ -n "$running_ids" ]; then
|
||||
local id
|
||||
for id in $running_ids; do
|
||||
podman --url "unix://$PODMAN_SOCKET" stop -t "${STOP_TIMEOUT:-10}" "$id" \
|
||||
> /dev/null 2>&1 \
|
||||
|| podman_log_error "stop: failed to stop container $id within timeout"
|
||||
done
|
||||
fi
|
||||
|
||||
podman_log "stop: stopping podman system service"
|
||||
if [ -f "$PODMAN_SERVICE_PID_FILE" ]; then
|
||||
local pid
|
||||
pid=$(cat "$PODMAN_SERVICE_PID_FILE")
|
||||
if kill -0 "$pid" 2> /dev/null; then
|
||||
kill "$pid" 2> /dev/null || true
|
||||
local waited=0
|
||||
while kill -0 "$pid" 2> /dev/null && [ "$waited" -lt 10 ]; do
|
||||
sleep 1
|
||||
waited=$((waited + 1))
|
||||
done
|
||||
kill -0 "$pid" 2> /dev/null && kill -9 "$pid" 2> /dev/null || true
|
||||
fi
|
||||
rm -f "$PODMAN_SERVICE_PID_FILE"
|
||||
fi
|
||||
rm -f "$PODMAN_SOCKET"
|
||||
|
||||
"$SBIN_DIR/podman-storage.sh" unmount || podman_log_error "stop: storage unmount failed"
|
||||
|
||||
rm -f "$PODMAN_STATUS_FILE"
|
||||
podman_log "stop: stopped"
|
||||
return 0
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# podman_status
|
||||
#
|
||||
# Human-readable health summary — see docs/ARCHITECTURE.md section 16.2.
|
||||
# -----------------------------------------------------------------------------
|
||||
podman_status() {
|
||||
echo "PODMAN_ENABLED: ${PODMAN_ENABLED:-yes}"
|
||||
|
||||
if [ -S "$PODMAN_SOCKET" ] && [ -f "$PODMAN_SERVICE_PID_FILE" ] \
|
||||
&& kill -0 "$(cat "$PODMAN_SERVICE_PID_FILE" 2> /dev/null)" 2> /dev/null; then
|
||||
echo "service: running (pid $(cat "$PODMAN_SERVICE_PID_FILE"), socket $PODMAN_SOCKET)"
|
||||
else
|
||||
echo "service: stopped"
|
||||
fi
|
||||
|
||||
echo
|
||||
"$SBIN_DIR/podman-storage.sh" status
|
||||
|
||||
echo
|
||||
if [ -f "$PODMAN_AUTOSTART_FILE" ]; then
|
||||
local count
|
||||
count=$(grep -vcE '^\s*(#|$)' "$PODMAN_AUTOSTART_FILE" 2> /dev/null || echo 0)
|
||||
echo "autostart entries: $count (see $PODMAN_AUTOSTART_FILE)"
|
||||
fi
|
||||
|
||||
if [ -S "$PODMAN_SOCKET" ]; then
|
||||
echo
|
||||
echo "podman info:"
|
||||
podman --url "unix://$PODMAN_SOCKET" info --format \
|
||||
' containers: {{.Store.ContainerStore.Number}} images: {{.Store.ImageStore.Number}}' \
|
||||
2> /dev/null || echo " (failed to query — service may still be initializing)"
|
||||
fi
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
start)
|
||||
podman_start
|
||||
;;
|
||||
stop)
|
||||
podman_stop
|
||||
;;
|
||||
restart)
|
||||
podman_stop
|
||||
podman_start
|
||||
;;
|
||||
status)
|
||||
podman_status
|
||||
;;
|
||||
*)
|
||||
echo "usage: $0 {start|stop|restart|status}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Executable
+172
@@ -0,0 +1,172 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# plugin/sbin/podman-autostart.sh
|
||||
#
|
||||
# "Autostart einrichten" — starts containers listed in
|
||||
# /boot/config/plugins/podman/autostart, in file order, via the running
|
||||
# Podman API service. Run by `rc.podman start` after the service is up.
|
||||
# See docs/ARCHITECTURE.md section 12 (Autostart).
|
||||
#
|
||||
# Design constraints from the architecture doc, both implemented below:
|
||||
# - A single container failing to start must NOT abort the rest of the
|
||||
# chain (one broken container shouldn't take down everything else).
|
||||
# - After repeated consecutive failures, a container is automatically
|
||||
# paused out of the autostart chain ("Safe-Mode pro Container") so a
|
||||
# persistently crash-looping container doesn't waste boot time or
|
||||
# resources forever — with a GUI notification explaining why.
|
||||
#
|
||||
# File formats:
|
||||
# autostart one container name per line; '#' starts a comment;
|
||||
# blank lines ignored. Order = start order.
|
||||
# autostart-delay optional "<container-name>=<seconds>" lines — sleep
|
||||
# that many seconds AFTER starting that container
|
||||
# before moving to the next (for startup dependencies,
|
||||
# e.g. a database before the app that needs it).
|
||||
#
|
||||
# Failure tracking: a small per-container counter file under
|
||||
# $PODMAN_BOOT_DIR/autostart-failures/<name> holds the consecutive-failure
|
||||
# count. Reset to 0 on any successful start.
|
||||
# =============================================================================
|
||||
|
||||
set -u
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=./podman-common.sh
|
||||
. "$SCRIPT_DIR/podman-common.sh"
|
||||
|
||||
podman_load_cfg
|
||||
|
||||
# After this many consecutive failed autostart attempts, a container is
|
||||
# skipped and flagged rather than retried again on the next boot, until a
|
||||
# human clears it (see clear_failure_flag below).
|
||||
MAX_CONSECUTIVE_FAILURES=3
|
||||
|
||||
FAILURES_DIR="$PODMAN_BOOT_DIR/autostart-failures"
|
||||
mkdir -p "$FAILURES_DIR"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# get_delay <container-name>
|
||||
#
|
||||
# Looks up an optional post-start delay for a container from
|
||||
# autostart-delay ("name=seconds" lines). Returns 0 if not configured.
|
||||
# -----------------------------------------------------------------------------
|
||||
get_delay() {
|
||||
local name="$1"
|
||||
if [ -f "$PODMAN_AUTOSTART_DELAY_FILE" ]; then
|
||||
awk -F= -v n="$name" '$1 == n { print $2; found=1 } END { if (!found) print 0 }' \
|
||||
"$PODMAN_AUTOSTART_DELAY_FILE"
|
||||
else
|
||||
echo 0
|
||||
fi
|
||||
}
|
||||
|
||||
failure_count() {
|
||||
local name="$1"
|
||||
local f="$FAILURES_DIR/$name"
|
||||
[ -f "$f" ] && cat "$f" || echo 0
|
||||
}
|
||||
|
||||
record_failure() {
|
||||
local name="$1"
|
||||
local count
|
||||
count=$(($(failure_count "$name") + 1))
|
||||
echo "$count" > "$FAILURES_DIR/$name"
|
||||
echo "$count"
|
||||
}
|
||||
|
||||
clear_failure_flag() {
|
||||
local name="$1"
|
||||
rm -f "$FAILURES_DIR/$name"
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# start_one <container-name>
|
||||
#
|
||||
# Starts a single container via `podman start`, which talks to the already
|
||||
# running API service over $PODMAN_SOCKET rather than spawning an unrelated
|
||||
# podman process tree — see docs/ARCHITECTURE.md section 11 (Container).
|
||||
# -----------------------------------------------------------------------------
|
||||
start_one() {
|
||||
local name="$1"
|
||||
|
||||
local prior_failures
|
||||
prior_failures=$(failure_count "$name")
|
||||
if [ "$prior_failures" -ge "$MAX_CONSECUTIVE_FAILURES" ]; then
|
||||
podman_log_error "autostart: skipping '$name' — $prior_failures consecutive prior failures (Safe-Mode). Remove $FAILURES_DIR/$name to re-enable."
|
||||
return 1
|
||||
fi
|
||||
|
||||
podman_log "autostart: starting '$name'"
|
||||
if podman --url "unix://$PODMAN_SOCKET" start "$name" > /tmp/podman-autostart-"$name".log 2>&1; then
|
||||
clear_failure_flag "$name"
|
||||
podman_log "autostart: '$name' started successfully"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local new_count
|
||||
new_count=$(record_failure "$name")
|
||||
podman_log_error "autostart: '$name' failed to start (attempt $new_count/$MAX_CONSECUTIVE_FAILURES) — see /tmp/podman-autostart-$name.log"
|
||||
|
||||
if [ "$new_count" -ge "$MAX_CONSECUTIVE_FAILURES" ]; then
|
||||
podman_notify "Podman container disabled from autostart" \
|
||||
"'$name' failed to start $new_count times in a row and has been paused from autostart. Fix the underlying issue, then remove $FAILURES_DIR/$name to re-enable." \
|
||||
"warning"
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# main
|
||||
# -----------------------------------------------------------------------------
|
||||
if [ ! -f "$PODMAN_AUTOSTART_FILE" ]; then
|
||||
podman_log "autostart: no autostart file at $PODMAN_AUTOSTART_FILE, nothing to do"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -S "$PODMAN_SOCKET" ]; then
|
||||
podman_log_error "autostart: podman API socket ($PODMAN_SOCKET) not present — is the service running?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
started=0
|
||||
skipped=0
|
||||
failed=0
|
||||
|
||||
while IFS= read -r raw_line || [ -n "$raw_line" ]; do
|
||||
# Strip comments and surrounding whitespace; skip blank lines.
|
||||
line="${raw_line%%#*}"
|
||||
line="$(echo "$line" | xargs || true)"
|
||||
[ -z "$line" ] && continue
|
||||
|
||||
if start_one "$line"; then
|
||||
started=$((started + 1))
|
||||
else
|
||||
prior=$(failure_count "$line")
|
||||
if [ "$prior" -ge "$MAX_CONSECUTIVE_FAILURES" ]; then
|
||||
skipped=$((skipped + 1))
|
||||
else
|
||||
failed=$((failed + 1))
|
||||
fi
|
||||
# A single container's failure does not abort the loop — see file
|
||||
# header. Continue to the next entry.
|
||||
continue
|
||||
fi
|
||||
|
||||
delay=$(get_delay "$line")
|
||||
if [ "$delay" -gt 0 ] 2> /dev/null; then
|
||||
podman_log "autostart: waiting ${delay}s after '$line' (configured dependency delay)"
|
||||
sleep "$delay"
|
||||
fi
|
||||
done < "$PODMAN_AUTOSTART_FILE"
|
||||
|
||||
podman_log "autostart: complete (started=$started failed=$failed skipped-safe-mode=$skipped)"
|
||||
|
||||
# Exit non-zero only if EVERY entry failed outright (as opposed to a mix,
|
||||
# or entries already in Safe-Mode) — rc.podman treats that as worth
|
||||
# flagging loudly, whereas a partial failure is already individually
|
||||
# notified above.
|
||||
if [ "$started" -eq 0 ] && [ "$failed" -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
Executable
+168
@@ -0,0 +1,168 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# plugin/sbin/podman-backup.sh
|
||||
#
|
||||
# Rollback support — see docs/ARCHITECTURE.md section 14 (Rollback). Two
|
||||
# independent kinds of backup, matching the two kinds of thing an update can
|
||||
# break:
|
||||
#
|
||||
# PACKAGES plugin/podman.plg downloads each release's .txz files
|
||||
# straight into $PODMAN_BOOT_DIR/backup/packages/<plugin-version>/
|
||||
# — all 7 component packages of one plugin release share a
|
||||
# single directory named after the PLUGIN version (not each
|
||||
# component's own version), since a rollback means "go back to
|
||||
# plugin release X" as one unit — rather than into a scratch
|
||||
# dir that gets discarded, so the previously installed
|
||||
# version's packages are automatically still on disk after an
|
||||
# update, with no separate copy step. `restore-packages
|
||||
# <plugin-version>` reinstalls every package from one of those
|
||||
# directories.
|
||||
#
|
||||
# CONFIG Before a config schema migration (see podman.cfg's
|
||||
# CONFIG_SCHEMA_VERSION), the plg's postinstall calls
|
||||
# `snapshot-config` to copy the current *.conf + podman.cfg
|
||||
# into $PODMAN_BOOT_DIR/backup/config/<timestamp>/.
|
||||
# `restore-config <timestamp>` copies them back.
|
||||
#
|
||||
# `prune` bounds how many old snapshots of each kind are kept, since flash
|
||||
# space is limited (see ARCHITECTURE.md section 14, "Rollback-Historie
|
||||
# begrenzen").
|
||||
#
|
||||
# Usage:
|
||||
# podman-backup.sh snapshot-config
|
||||
# podman-backup.sh restore-config <timestamp>
|
||||
# podman-backup.sh restore-packages <version>
|
||||
# podman-backup.sh list
|
||||
# podman-backup.sh prune [--keep N]
|
||||
# =============================================================================
|
||||
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=./podman-common.sh
|
||||
. "$SCRIPT_DIR/podman-common.sh"
|
||||
|
||||
DEFAULT_KEEP=3
|
||||
|
||||
cmd_snapshot_config() {
|
||||
local ts
|
||||
ts=$(date -u +%Y%m%dT%H%M%SZ)
|
||||
local dest="$PODMAN_BACKUP_DIR/config/$ts"
|
||||
mkdir -p "$dest"
|
||||
|
||||
local f
|
||||
for f in podman.cfg containers.conf storage.conf registries.conf policy.json; do
|
||||
[ -f "$PODMAN_BOOT_DIR/$f" ] && cp "$PODMAN_BOOT_DIR/$f" "$dest/"
|
||||
done
|
||||
|
||||
podman_log "backup: config snapshot saved to $dest"
|
||||
echo "$ts"
|
||||
}
|
||||
|
||||
cmd_restore_config() {
|
||||
local ts="${1:?usage: podman-backup.sh restore-config <timestamp>}"
|
||||
local src="$PODMAN_BACKUP_DIR/config/$ts"
|
||||
|
||||
if [ ! -d "$src" ]; then
|
||||
podman_log_error "backup: no config snapshot found at $src"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local f
|
||||
for f in "$src"/*; do
|
||||
[ -f "$f" ] || continue
|
||||
cp "$f" "$PODMAN_BOOT_DIR/$(basename "$f")"
|
||||
done
|
||||
|
||||
podman_log "backup: config restored from snapshot $ts — restart podman (rc.podman restart) to apply"
|
||||
}
|
||||
|
||||
cmd_restore_packages() {
|
||||
local version="${1:?usage: podman-backup.sh restore-packages <version>}"
|
||||
local src="$PODMAN_BACKUP_DIR/packages/$version"
|
||||
|
||||
if [ ! -d "$src" ]; then
|
||||
podman_log_error "backup: no package backup found for version $version at $src"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
podman_require_command upgradepkg
|
||||
|
||||
local txz
|
||||
local restored=0
|
||||
for txz in "$src"/*.txz; do
|
||||
[ -f "$txz" ] || continue
|
||||
podman_log "backup: reinstalling $(basename "$txz") from backup (version $version)"
|
||||
upgradepkg --reinstall --install-new "$txz"
|
||||
restored=$((restored + 1))
|
||||
done
|
||||
|
||||
if [ "$restored" -eq 0 ]; then
|
||||
podman_log_error "backup: no .txz files found in $src"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
podman_notify "Podman packages rolled back" \
|
||||
"Reinstalled $restored package(s) from the version $version backup. Run 'rc.podman restart' to apply." \
|
||||
"warning"
|
||||
podman_log "backup: package rollback to version $version complete ($restored package(s))"
|
||||
}
|
||||
|
||||
cmd_list() {
|
||||
echo "Config snapshots ($PODMAN_BACKUP_DIR/config/):"
|
||||
if [ -d "$PODMAN_BACKUP_DIR/config" ]; then
|
||||
find "$PODMAN_BACKUP_DIR/config" -mindepth 1 -maxdepth 1 -type d -printf ' %f\n' 2> /dev/null | sort -r
|
||||
fi
|
||||
echo
|
||||
echo "Package backups ($PODMAN_BACKUP_DIR/packages/):"
|
||||
if [ -d "$PODMAN_BACKUP_DIR/packages" ]; then
|
||||
find "$PODMAN_BACKUP_DIR/packages" -mindepth 1 -maxdepth 1 -type d -printf ' %f\n' 2> /dev/null | sort -r
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_prune() {
|
||||
local keep="$DEFAULT_KEEP"
|
||||
if [ "${1:-}" = "--keep" ] && [ -n "${2:-}" ]; then
|
||||
keep="$2"
|
||||
fi
|
||||
|
||||
local kind
|
||||
for kind in config packages; do
|
||||
local dir="$PODMAN_BACKUP_DIR/$kind"
|
||||
[ -d "$dir" ] || continue
|
||||
|
||||
# Sort newest-first by directory name (timestamps and semver both sort
|
||||
# correctly lexicographically here — timestamps are zero-padded ISO8601,
|
||||
# versions are compared via `sort -V`), then remove everything beyond
|
||||
# $keep.
|
||||
local sorted
|
||||
if [ "$kind" = "config" ]; then
|
||||
sorted=$(find "$dir" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort -r)
|
||||
else
|
||||
sorted=$(find "$dir" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort -rV)
|
||||
fi
|
||||
|
||||
local i=0
|
||||
local name
|
||||
while IFS= read -r name; do
|
||||
[ -z "$name" ] && continue
|
||||
i=$((i + 1))
|
||||
if [ "$i" -gt "$keep" ]; then
|
||||
podman_log "backup: pruning old $kind backup: $name"
|
||||
rm -rf "${dir:?}/$name"
|
||||
fi
|
||||
done <<< "$sorted"
|
||||
done
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
snapshot-config) cmd_snapshot_config ;;
|
||||
restore-config) shift; cmd_restore_config "$@" ;;
|
||||
restore-packages) shift; cmd_restore_packages "$@" ;;
|
||||
list) cmd_list ;;
|
||||
prune) shift; cmd_prune "$@" ;;
|
||||
*)
|
||||
echo "usage: $0 {snapshot-config|restore-config <ts>|restore-packages <version>|list|prune [--keep N]}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Executable
+173
@@ -0,0 +1,173 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# plugin/sbin/podman-common.sh
|
||||
#
|
||||
# Shared constants and helper functions sourced by every other script under
|
||||
# plugin/sbin/ and by plugin/rc.d/rc.podman. Centralizing these here means:
|
||||
# - every script agrees on the same paths (no risk of one script writing
|
||||
# config to a path another script reads from a slightly different one),
|
||||
# - logging/notification behavior is consistent everywhere,
|
||||
# - each individual script stays focused on its one job instead of
|
||||
# re-implementing "how do I log this" or "where is podman.cfg".
|
||||
#
|
||||
# This file is NOT meant to be executed directly — it only defines
|
||||
# functions/variables for other scripts to source:
|
||||
# . "$(dirname "${BASH_SOURCE[0]}")/podman-common.sh"
|
||||
#
|
||||
# See docs/ARCHITECTURE.md sections 4 (Verzeichnislayout), 7 (Persistenz),
|
||||
# 15 (Logging), 16 (Fehlerbehandlung) for the design this implements.
|
||||
# =============================================================================
|
||||
|
||||
# We deliberately do NOT `set -e` in this file: it is sourced by scripts
|
||||
# that set their own error-handling mode, and a library changing its
|
||||
# caller's shell options would be a surprising action at a distance.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Path constants.
|
||||
#
|
||||
# PODMAN_BOOT_DIR is the persistent, source-of-truth configuration directory
|
||||
# on the Unraid flash device (survives reboots — see ARCHITECTURE.md 4.1).
|
||||
# Everything under /etc, /var, /usr is RAM-root and rebuilt from here (or
|
||||
# from the array/cache-backed storage dir) on every boot.
|
||||
# -----------------------------------------------------------------------------
|
||||
PODMAN_BOOT_DIR="/boot/config/plugins/podman"
|
||||
PODMAN_CFG_FILE="$PODMAN_BOOT_DIR/podman.cfg"
|
||||
PODMAN_AUTOSTART_FILE="$PODMAN_BOOT_DIR/autostart"
|
||||
PODMAN_AUTOSTART_DELAY_FILE="$PODMAN_BOOT_DIR/autostart-delay"
|
||||
PODMAN_NETWORKS_BOOT_DIR="$PODMAN_BOOT_DIR/networks"
|
||||
PODMAN_BACKUP_DIR="$PODMAN_BOOT_DIR/backup"
|
||||
PODMAN_PLUGIN_LOG="$PODMAN_BOOT_DIR/plugin.log"
|
||||
|
||||
# Runtime (RAM-root) locations rebuilt/synced on every rc.podman start.
|
||||
PODMAN_ETC_DIR="/etc/containers"
|
||||
PODMAN_RUN_DIR="/var/run/podman"
|
||||
PODMAN_SOCKET="$PODMAN_RUN_DIR/podman.sock"
|
||||
PODMAN_STATUS_FILE="$PODMAN_RUN_DIR/rc.podman.status"
|
||||
PODMAN_SERVICE_PID_FILE="$PODMAN_RUN_DIR/podman-service.pid"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# podman_load_cfg
|
||||
#
|
||||
# Sources /boot/config/plugins/podman/podman.cfg (the user-editable settings
|
||||
# file, see config/podman.cfg.example) and applies safe defaults for any
|
||||
# setting that file doesn't define — so every other script can simply
|
||||
# reference $STORAGE_PATH, $PODMAN_ENABLED, etc. after calling this, without
|
||||
# each script having its own copy of the defaults.
|
||||
# -----------------------------------------------------------------------------
|
||||
podman_load_cfg() {
|
||||
# Defaults, applied BEFORE sourcing podman.cfg so the file only needs to
|
||||
# override what the user actually wants to change.
|
||||
STORAGE_PATH="/mnt/cache/system/podman"
|
||||
STORAGE_IMAGE_SIZE_GB="20"
|
||||
PODMAN_ENABLED="yes"
|
||||
STOP_TIMEOUT="10"
|
||||
CONFIG_SCHEMA_VERSION="1"
|
||||
|
||||
if [ -f "$PODMAN_CFG_FILE" ]; then
|
||||
# shellcheck source=/dev/null
|
||||
. "$PODMAN_CFG_FILE"
|
||||
fi
|
||||
|
||||
# Values derived from STORAGE_PATH, computed after sourcing so a custom
|
||||
# STORAGE_PATH is honored.
|
||||
PODMAN_STORAGE_IMAGE="$STORAGE_PATH/podman.img"
|
||||
PODMAN_LOG_DIR="$STORAGE_PATH/logs"
|
||||
PODMAN_GRAPHROOT="/var/lib/containers/storage"
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# podman_log <message>
|
||||
#
|
||||
# Writes a timestamped line to both stdout (so it shows up in `rc.podman`
|
||||
# invocations and CI/manual runs) and to the persistent plugin log on flash
|
||||
# (so it survives a reboot — see ARCHITECTURE.md section 15, Logging). Kept
|
||||
# deliberately terse (no log levels/rotation logic here) — this is for
|
||||
# install/lifecycle events, not container output, which lives under
|
||||
# $PODMAN_LOG_DIR instead.
|
||||
# -----------------------------------------------------------------------------
|
||||
podman_log() {
|
||||
local msg="$1"
|
||||
local line
|
||||
line="$(date -u +'%Y-%m-%dT%H:%M:%SZ') [podman] $msg"
|
||||
echo "$line"
|
||||
# Best-effort: the boot directory should always exist post-install, but
|
||||
# never let logging itself fail a caller that has `set -e`.
|
||||
mkdir -p "$PODMAN_BOOT_DIR" 2> /dev/null || true
|
||||
echo "$line" >> "$PODMAN_PLUGIN_LOG" 2> /dev/null || true
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# podman_log_error <message>
|
||||
#
|
||||
# Like podman_log, but also mirrors the message to the system log via
|
||||
# `logger`, so it is visible in Unraid's Tools -> System Log GUI without the
|
||||
# user having to know where the plugin's own log lives. Reserved for
|
||||
# conditions the user should actually notice (see ARCHITECTURE.md 16.2).
|
||||
# -----------------------------------------------------------------------------
|
||||
podman_log_error() {
|
||||
local msg="$1"
|
||||
podman_log "ERROR: $msg"
|
||||
if command -v logger > /dev/null 2>&1; then
|
||||
logger -t podman-plugin "$msg"
|
||||
fi
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# podman_notify <subject> <description> [importance]
|
||||
#
|
||||
# Surfaces a message via Unraid's own GUI notification system
|
||||
# (/usr/local/emhttp/webGui/scripts/notify) instead of inventing a parallel
|
||||
# notification mechanism — see ARCHITECTURE.md section 16.2. Silently
|
||||
# no-ops if that script isn't present (e.g. when running outside a real
|
||||
# Unraid system, such as in CI/lint contexts).
|
||||
#
|
||||
# importance: "normal" (default), "warning", or "alert".
|
||||
# -----------------------------------------------------------------------------
|
||||
podman_notify() {
|
||||
local subject="$1"
|
||||
local description="$2"
|
||||
local importance="${3:-normal}"
|
||||
local notify_bin="/usr/local/emhttp/webGui/scripts/notify"
|
||||
|
||||
podman_log "notify [$importance] $subject: $description"
|
||||
|
||||
if [ -x "$notify_bin" ]; then
|
||||
"$notify_bin" -e "unraid-podman" -s "$subject" -d "$description" -i "$importance" \
|
||||
> /dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# podman_storage_path_is_safe
|
||||
#
|
||||
# Refuses storage paths under /mnt/user (FUSE/shfs) — see
|
||||
# ARCHITECTURE.md section 4.3 for why the overlay storage driver must live
|
||||
# on a real mounted filesystem (cache pool or a specific disk), not shfs.
|
||||
# Returns 0 (safe) or 1 (unsafe) and prints a reason on failure.
|
||||
# -----------------------------------------------------------------------------
|
||||
podman_storage_path_is_safe() {
|
||||
local path="$1"
|
||||
case "$path" in
|
||||
/mnt/user/*|/mnt/user)
|
||||
echo "STORAGE_PATH ($path) is under /mnt/user (FUSE/shfs)." >&2
|
||||
echo "The overlay storage driver needs a real mounted filesystem —" >&2
|
||||
echo "use a cache pool or a specific disk path instead, e.g. /mnt/cache/system/podman." >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# podman_require_command <binary>
|
||||
#
|
||||
# Fails loudly (rather than letting a script continue and fail confusingly
|
||||
# three steps later) if a required binary isn't on PATH.
|
||||
# -----------------------------------------------------------------------------
|
||||
podman_require_command() {
|
||||
local bin="$1"
|
||||
if ! command -v "$bin" > /dev/null 2>&1; then
|
||||
podman_log_error "required command not found: $bin"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
Executable
+141
@@ -0,0 +1,141 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# plugin/sbin/podman-config.sh
|
||||
#
|
||||
# "Konfiguration anlegen" — creates and synchronizes unraid-podman's
|
||||
# configuration. Has two responsibilities, run as two subcommands:
|
||||
#
|
||||
# podman-config.sh seed
|
||||
# First-install (or "file went missing") step: ensures
|
||||
# /boot/config/plugins/podman/ exists with its full directory skeleton,
|
||||
# and copies default config templates into it — but ONLY for files that
|
||||
# don't already exist. This is what makes plugin updates safe: an
|
||||
# existing user configuration is never overwritten (see
|
||||
# docs/ARCHITECTURE.md section 3.2 and section 7).
|
||||
#
|
||||
# podman-config.sh sync
|
||||
# Boot-time step: copies the current, authoritative config from
|
||||
# /boot/config/plugins/podman/*.conf into /etc/containers/ (which lives
|
||||
# on Unraid's RAM-root and is empty again after every reboot). Run by
|
||||
# rc.podman on every `start`.
|
||||
#
|
||||
# Template source: the plugin ships its default config templates (from this
|
||||
# repo's config/) to /usr/local/share/unraid-podman/templates/ at install
|
||||
# time (see plugin/podman.plg) — that is what `seed` copies FROM, and
|
||||
# /boot/config/plugins/podman/ is what it copies TO.
|
||||
#
|
||||
# Usage:
|
||||
# podman-config.sh seed
|
||||
# podman-config.sh sync
|
||||
# =============================================================================
|
||||
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=./podman-common.sh
|
||||
. "$SCRIPT_DIR/podman-common.sh"
|
||||
|
||||
TEMPLATES_DIR="/usr/local/share/unraid-podman/templates"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# seed_file <template-filename> <dest-path>
|
||||
#
|
||||
# Copies a template into place only if the destination doesn't already
|
||||
# exist. This single rule is what protects user customizations across
|
||||
# plugin updates — see docs/ARCHITECTURE.md section 7, "Persistenz-Strategie".
|
||||
# -----------------------------------------------------------------------------
|
||||
seed_file() {
|
||||
local template_name="$1"
|
||||
local dest_path="$2"
|
||||
local src_path="$TEMPLATES_DIR/$template_name"
|
||||
|
||||
if [ -f "$dest_path" ]; then
|
||||
podman_log "config: $dest_path already exists, leaving untouched"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ ! -f "$src_path" ]; then
|
||||
podman_log_error "config: template missing: $src_path (plugin install incomplete?)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$dest_path")"
|
||||
cp "$src_path" "$dest_path"
|
||||
podman_log "config: seeded $dest_path from template $template_name"
|
||||
}
|
||||
|
||||
cmd_seed() {
|
||||
podman_log "config: seeding /boot/config/plugins/podman/ (first install or repair)"
|
||||
|
||||
# Full directory skeleton — see docs/ARCHITECTURE.md section 4.1 and
|
||||
# plugin/boot-config/plugins/podman/README.md for the reference layout.
|
||||
mkdir -p \
|
||||
"$PODMAN_BOOT_DIR" \
|
||||
"$PODMAN_NETWORKS_BOOT_DIR" \
|
||||
"$PODMAN_BACKUP_DIR/packages" \
|
||||
"$PODMAN_BACKUP_DIR/config"
|
||||
|
||||
seed_file "podman.cfg.example" "$PODMAN_CFG_FILE"
|
||||
seed_file "containers.conf" "$PODMAN_BOOT_DIR/containers.conf"
|
||||
seed_file "storage.conf" "$PODMAN_BOOT_DIR/storage.conf"
|
||||
seed_file "registries.conf" "$PODMAN_BOOT_DIR/registries.conf"
|
||||
seed_file "policy.json" "$PODMAN_BOOT_DIR/policy.json"
|
||||
|
||||
# Autostart files: empty by default, `touch` is the "seed" here (no
|
||||
# template content to copy — see docs/ARCHITECTURE.md section 12).
|
||||
[ -f "$PODMAN_AUTOSTART_FILE" ] || { touch "$PODMAN_AUTOSTART_FILE"; podman_log "config: created empty $PODMAN_AUTOSTART_FILE"; }
|
||||
[ -f "$PODMAN_AUTOSTART_DELAY_FILE" ] || { touch "$PODMAN_AUTOSTART_DELAY_FILE"; podman_log "config: created empty $PODMAN_AUTOSTART_DELAY_FILE"; }
|
||||
[ -f "$PODMAN_PLUGIN_LOG" ] || touch "$PODMAN_PLUGIN_LOG"
|
||||
|
||||
podman_log "config: seeding complete"
|
||||
}
|
||||
|
||||
cmd_sync() {
|
||||
podman_load_cfg
|
||||
|
||||
if [ ! -d "$PODMAN_BOOT_DIR" ]; then
|
||||
podman_log_error "config: $PODMAN_BOOT_DIR does not exist — run 'podman-config.sh seed' first"
|
||||
return 1
|
||||
fi
|
||||
|
||||
podman_log "config: syncing $PODMAN_BOOT_DIR/*.conf -> $PODMAN_ETC_DIR/"
|
||||
mkdir -p "$PODMAN_ETC_DIR"
|
||||
|
||||
local any_missing=0
|
||||
for f in containers.conf storage.conf registries.conf policy.json; do
|
||||
if [ ! -f "$PODMAN_BOOT_DIR/$f" ]; then
|
||||
podman_log_error "config: missing $PODMAN_BOOT_DIR/$f (run 'podman-config.sh seed')"
|
||||
any_missing=1
|
||||
continue
|
||||
fi
|
||||
cp "$PODMAN_BOOT_DIR/$f" "$PODMAN_ETC_DIR/$f"
|
||||
done
|
||||
|
||||
if [ "$any_missing" -ne 0 ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# storage.conf's graphroot/runroot are environment-specific (depend on
|
||||
# STORAGE_PATH from podman.cfg), so they are appended here at sync time
|
||||
# rather than hardcoded in the template — see config/storage.conf's own
|
||||
# comment on this split.
|
||||
{
|
||||
echo ""
|
||||
echo "# --- appended at boot by podman-config.sh sync, from podman.cfg ---"
|
||||
echo "[storage]"
|
||||
echo "driver = \"overlay\""
|
||||
echo "graphroot = \"$PODMAN_GRAPHROOT\""
|
||||
echo "runroot = \"/var/run/containers/storage\""
|
||||
} >> "$PODMAN_ETC_DIR/storage.conf"
|
||||
|
||||
podman_log "config: sync complete"
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
seed) cmd_seed ;;
|
||||
sync) cmd_sync ;;
|
||||
*)
|
||||
echo "usage: $0 {seed|sync}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Executable
+150
@@ -0,0 +1,150 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# plugin/sbin/podman-preflight.sh
|
||||
#
|
||||
# Startup validation, run by `rc.podman start` BEFORE anything is mounted or
|
||||
# started. The goal is to fail fast with one clear message, instead of
|
||||
# letting `podman system service` fail three steps later with a cryptic
|
||||
# error. See docs/ARCHITECTURE.md section 16.1 (Startup-Robustheit).
|
||||
#
|
||||
# Every check below is independent and all of them run even if an earlier
|
||||
# one fails, so a single invocation reports every problem at once rather
|
||||
# than forcing the user through a fix-one-rerun-find-the-next loop.
|
||||
#
|
||||
# Exit code: 0 if every check passed, 1 if any failed (with all failures
|
||||
# already logged/notified by that point).
|
||||
# =============================================================================
|
||||
|
||||
set -u # deliberately not -e: see the "run every check" note above
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=./podman-common.sh
|
||||
. "$SCRIPT_DIR/podman-common.sh"
|
||||
|
||||
podman_load_cfg
|
||||
|
||||
FAILURES=0
|
||||
|
||||
fail() {
|
||||
podman_log_error "preflight: $1"
|
||||
FAILURES=$((FAILURES + 1))
|
||||
}
|
||||
|
||||
ok() {
|
||||
podman_log "preflight: OK - $1"
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 1. Required binaries are actually installed.
|
||||
# -----------------------------------------------------------------------------
|
||||
for bin in podman conmon crun mount umount xfs_repair mkfs.xfs; do
|
||||
if command -v "$bin" > /dev/null 2>&1; then
|
||||
ok "$bin found"
|
||||
else
|
||||
fail "required binary '$bin' not found on PATH — is the plugin fully installed?"
|
||||
fi
|
||||
done
|
||||
|
||||
for helper in /usr/libexec/podman/netavark /usr/libexec/podman/aardvark-dns; do
|
||||
if [ -x "$helper" ]; then
|
||||
ok "$helper found"
|
||||
else
|
||||
fail "required helper '$helper' not found — is the plugin fully installed?"
|
||||
fi
|
||||
done
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 2. Storage path configured, safe, and mounted (cache pool / disk, not
|
||||
# /mnt/user — see docs/ARCHITECTURE.md section 4.3).
|
||||
# -----------------------------------------------------------------------------
|
||||
if podman_storage_path_is_safe "$STORAGE_PATH" 2> /tmp/podman-preflight-storage-safety.log; then
|
||||
ok "STORAGE_PATH ($STORAGE_PATH) is not under /mnt/user"
|
||||
else
|
||||
fail "$(cat /tmp/podman-preflight-storage-safety.log)"
|
||||
fi
|
||||
rm -f /tmp/podman-preflight-storage-safety.log
|
||||
|
||||
if [ -d "$STORAGE_PATH" ] && mountpoint -q "$STORAGE_PATH" 2> /dev/null; then
|
||||
ok "STORAGE_PATH ($STORAGE_PATH) is a mounted filesystem"
|
||||
elif [ -d "$STORAGE_PATH" ]; then
|
||||
# Not every valid target is a mountpoint itself (e.g. a subdirectory of a
|
||||
# cache pool root) — warn rather than fail, but only if the parent chain
|
||||
# is mounted somewhere.
|
||||
if findmnt -T "$STORAGE_PATH" > /dev/null 2>&1; then
|
||||
ok "STORAGE_PATH ($STORAGE_PATH) resolves onto a mounted filesystem"
|
||||
else
|
||||
fail "STORAGE_PATH ($STORAGE_PATH) does not appear to be on a mounted filesystem"
|
||||
fi
|
||||
else
|
||||
fail "STORAGE_PATH ($STORAGE_PATH) does not exist — is the configured cache pool/disk present and started?"
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 3. Free space check (only meaningful once STORAGE_PATH exists).
|
||||
# -----------------------------------------------------------------------------
|
||||
if [ -d "$STORAGE_PATH" ]; then
|
||||
available_kb=$(df --output=avail -k "$STORAGE_PATH" 2> /dev/null | tail -n1 | tr -d '[:space:]')
|
||||
if [ -n "${available_kb:-}" ]; then
|
||||
available_gb=$((available_kb / 1024 / 1024))
|
||||
if [ -f "$PODMAN_STORAGE_IMAGE" ]; then
|
||||
# Image already exists — just warn if the pool itself is nearly full,
|
||||
# since podman.img growth or new image pulls need headroom too.
|
||||
if [ "$available_gb" -lt 2 ]; then
|
||||
fail "less than 2G free on $STORAGE_PATH (${available_gb}G) — image pulls will likely fail"
|
||||
else
|
||||
ok "${available_gb}G free on $STORAGE_PATH"
|
||||
fi
|
||||
elif [ "$available_gb" -lt "$STORAGE_IMAGE_SIZE_GB" ]; then
|
||||
fail "not enough free space on $STORAGE_PATH to create a ${STORAGE_IMAGE_SIZE_GB}G podman.img (only ${available_gb}G free)"
|
||||
else
|
||||
ok "${available_gb}G free on $STORAGE_PATH (enough for a fresh ${STORAGE_IMAGE_SIZE_GB}G podman.img)"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 4. Kernel supports cgroup v2 (required by crun/netavark's expectations).
|
||||
# -----------------------------------------------------------------------------
|
||||
if [ -f /sys/fs/cgroup/cgroup.controllers ]; then
|
||||
ok "cgroup v2 unified hierarchy is active"
|
||||
else
|
||||
fail "cgroup v2 unified hierarchy not detected (/sys/fs/cgroup/cgroup.controllers missing) — check Unraid's syslinux cgroup boot parameters"
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 5. No orphaned socket/pidfile from a previous unclean shutdown.
|
||||
# -----------------------------------------------------------------------------
|
||||
if [ -S "$PODMAN_SOCKET" ]; then
|
||||
if [ -f "$PODMAN_SERVICE_PID_FILE" ] && kill -0 "$(cat "$PODMAN_SERVICE_PID_FILE")" 2> /dev/null; then
|
||||
fail "podman system service already appears to be running (pid $(cat "$PODMAN_SERVICE_PID_FILE")) — is rc.podman already started?"
|
||||
else
|
||||
podman_log "preflight: removing orphaned socket $PODMAN_SOCKET from a previous unclean shutdown"
|
||||
rm -f "$PODMAN_SOCKET"
|
||||
ok "cleared orphaned socket"
|
||||
fi
|
||||
else
|
||||
ok "no orphaned podman.sock"
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 6. Boot-config directory exists (i.e. 'seed' has run at least once).
|
||||
# -----------------------------------------------------------------------------
|
||||
if [ -f "$PODMAN_CFG_FILE" ]; then
|
||||
ok "$PODMAN_CFG_FILE present"
|
||||
else
|
||||
fail "$PODMAN_CFG_FILE missing — run 'podman-config.sh seed' (should happen automatically on install)"
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Summary
|
||||
# -----------------------------------------------------------------------------
|
||||
if [ "$FAILURES" -gt 0 ]; then
|
||||
podman_notify "Podman preflight checks failed" \
|
||||
"$FAILURES check(s) failed — podman was not started. See $PODMAN_PLUGIN_LOG for details." \
|
||||
"alert"
|
||||
podman_log_error "preflight: $FAILURES check(s) failed, aborting start"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
podman_log "preflight: all checks passed"
|
||||
exit 0
|
||||
Executable
+180
@@ -0,0 +1,180 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# plugin/sbin/podman-storage.sh
|
||||
#
|
||||
# "Container Storage erstellen" — creates, mounts, and unmounts the
|
||||
# podman.img loopback filesystem that backs Podman's overlay storage graph
|
||||
# (images, containers, named volumes). See docs/ARCHITECTURE.md section 4.3
|
||||
# for why this needs to be a real mounted filesystem (XFS with ftype=1, on a
|
||||
# cache pool or dedicated disk) rather than a directory under /mnt/user —
|
||||
# the FUSE (shfs) layer behind /mnt/user does not reliably support the
|
||||
# overlay storage driver's filesystem requirements (d_type, etc).
|
||||
#
|
||||
# Usage:
|
||||
# podman-storage.sh create # create podman.img if it doesn't exist yet
|
||||
# podman-storage.sh mount # mount it at $PODMAN_GRAPHROOT (idempotent)
|
||||
# podman-storage.sh unmount # cleanly unmount
|
||||
# podman-storage.sh status # report existence/mount/usage
|
||||
# =============================================================================
|
||||
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=./podman-common.sh
|
||||
. "$SCRIPT_DIR/podman-common.sh"
|
||||
|
||||
podman_load_cfg
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# cmd_create
|
||||
#
|
||||
# Creates podman.img at the configured size if it doesn't already exist,
|
||||
# and formats it XFS with ftype=1 (required for the overlay storage driver
|
||||
# to see correct directory entry types — without it, podman's overlay
|
||||
# backend silently falls back to a slower/less-capable mode or fails
|
||||
# outright, depending on version). Does nothing (and exits 0) if the image
|
||||
# already exists — this script is meant to be safe to call on every boot.
|
||||
# -----------------------------------------------------------------------------
|
||||
cmd_create() {
|
||||
if ! podman_storage_path_is_safe "$STORAGE_PATH"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -f "$PODMAN_STORAGE_IMAGE" ]; then
|
||||
podman_log "storage: $PODMAN_STORAGE_IMAGE already exists, not recreating"
|
||||
return 0
|
||||
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
|
||||
fi
|
||||
|
||||
# Free space check: refuse to create an image bigger than what's actually
|
||||
# available, with a small safety margin, rather than letting truncate
|
||||
# silently create a sparse file that will fail unpredictably later once
|
||||
# it's actually written to (see docs/ARCHITECTURE.md section 16.2).
|
||||
local available_kb required_kb
|
||||
available_kb=$(df --output=avail -k "$STORAGE_PATH" | tail -n1 | tr -d '[:space:]')
|
||||
required_kb=$((STORAGE_IMAGE_SIZE_GB * 1024 * 1024))
|
||||
if [ "$available_kb" -lt "$required_kb" ]; then
|
||||
podman_log_error "storage: not enough free space on $STORAGE_PATH (need ${STORAGE_IMAGE_SIZE_GB}G, have $((available_kb / 1024 / 1024))G)"
|
||||
podman_notify "Podman storage creation failed" \
|
||||
"Not enough free space on $STORAGE_PATH for a ${STORAGE_IMAGE_SIZE_GB}G podman.img." \
|
||||
"alert"
|
||||
return 1
|
||||
fi
|
||||
|
||||
podman_log "storage: creating ${STORAGE_IMAGE_SIZE_GB}G image at $PODMAN_STORAGE_IMAGE"
|
||||
mkdir -p "$STORAGE_PATH"
|
||||
|
||||
# Sparse file: only actually consumes disk space as data is written,
|
||||
# matching Docker-for-Unraid's docker.img behavior that users already
|
||||
# understand.
|
||||
truncate -s "${STORAGE_IMAGE_SIZE_GB}G" "$PODMAN_STORAGE_IMAGE"
|
||||
|
||||
podman_require_command mkfs.xfs
|
||||
# -n ftype=1 is the whole reason this has to be a purpose-made image
|
||||
# rather than a plain directory — see file header comment.
|
||||
mkfs.xfs -n ftype=1 -q "$PODMAN_STORAGE_IMAGE"
|
||||
|
||||
podman_log "storage: created and formatted $PODMAN_STORAGE_IMAGE"
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# cmd_mount
|
||||
#
|
||||
# Idempotent: does nothing if $PODMAN_GRAPHROOT is already mounted from
|
||||
# podman.img. Runs a read-only integrity check (xfs_repair -n) before
|
||||
# mounting and refuses to proceed if it reports corruption — per
|
||||
# docs/ARCHITECTURE.md section 16.1, this project does not auto-repair
|
||||
# storage without the user's explicit action, to avoid silent data loss.
|
||||
# -----------------------------------------------------------------------------
|
||||
cmd_mount() {
|
||||
if mountpoint -q "$PODMAN_GRAPHROOT" 2> /dev/null; then
|
||||
podman_log "storage: $PODMAN_GRAPHROOT already mounted"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ ! -f "$PODMAN_STORAGE_IMAGE" ]; then
|
||||
podman_log_error "storage: $PODMAN_STORAGE_IMAGE does not exist — run 'podman-storage.sh create' first"
|
||||
return 1
|
||||
fi
|
||||
|
||||
podman_require_command xfs_repair
|
||||
podman_log "storage: checking filesystem integrity of $PODMAN_STORAGE_IMAGE"
|
||||
if ! xfs_repair -n "$PODMAN_STORAGE_IMAGE" > /tmp/podman-xfs-repair.log 2>&1; then
|
||||
podman_log_error "storage: filesystem check failed for $PODMAN_STORAGE_IMAGE — refusing to mount."
|
||||
podman_log_error "storage: see /tmp/podman-xfs-repair.log. Run 'xfs_repair $PODMAN_STORAGE_IMAGE' manually to attempt repair, or restore from backup."
|
||||
podman_notify "Podman storage corruption detected" \
|
||||
"$PODMAN_STORAGE_IMAGE failed an integrity check and was not mounted. Manual recovery required — see plugin.log." \
|
||||
"alert"
|
||||
return 1
|
||||
fi
|
||||
|
||||
mkdir -p "$PODMAN_GRAPHROOT"
|
||||
podman_log "storage: mounting $PODMAN_STORAGE_IMAGE at $PODMAN_GRAPHROOT"
|
||||
mount -o loop "$PODMAN_STORAGE_IMAGE" "$PODMAN_GRAPHROOT"
|
||||
|
||||
# Runtime state (runroot) is tmpfs-backed and fine to live on RAM-root —
|
||||
# only the persistent graphroot needs the loopback filesystem.
|
||||
mkdir -p /var/run/containers/storage
|
||||
|
||||
podman_log "storage: mounted"
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# cmd_unmount
|
||||
#
|
||||
# Unmounts cleanly. Retries briefly if the mount is momentarily busy (a
|
||||
# container process exiting can hold a reference for a few hundred ms),
|
||||
# rather than immediately failing rc.podman's stop sequence.
|
||||
# -----------------------------------------------------------------------------
|
||||
cmd_unmount() {
|
||||
if ! mountpoint -q "$PODMAN_GRAPHROOT" 2> /dev/null; then
|
||||
podman_log "storage: $PODMAN_GRAPHROOT not mounted, nothing to do"
|
||||
return 0
|
||||
fi
|
||||
|
||||
podman_log "storage: unmounting $PODMAN_GRAPHROOT"
|
||||
local attempt
|
||||
for attempt in 1 2 3 4 5; do
|
||||
if umount "$PODMAN_GRAPHROOT" 2> /dev/null; then
|
||||
podman_log "storage: unmounted"
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
podman_log_error "storage: failed to unmount $PODMAN_GRAPHROOT after 5 attempts (still busy?)"
|
||||
return 1
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
echo "storage image: $PODMAN_STORAGE_IMAGE"
|
||||
if [ -f "$PODMAN_STORAGE_IMAGE" ]; then
|
||||
echo " exists: yes ($(du -h "$PODMAN_STORAGE_IMAGE" | cut -f1) allocated / ${STORAGE_IMAGE_SIZE_GB}G nominal)"
|
||||
else
|
||||
echo " exists: no"
|
||||
fi
|
||||
|
||||
echo "graphroot: $PODMAN_GRAPHROOT"
|
||||
if mountpoint -q "$PODMAN_GRAPHROOT" 2> /dev/null; then
|
||||
echo " mounted: yes"
|
||||
df -h "$PODMAN_GRAPHROOT" | tail -n1 | awk '{print " usage: " $3 " used / " $2 " total (" $5 " full)"}'
|
||||
else
|
||||
echo " mounted: no"
|
||||
fi
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
create) cmd_create ;;
|
||||
mount) cmd_mount ;;
|
||||
unmount) cmd_unmount ;;
|
||||
status) cmd_status ;;
|
||||
*)
|
||||
echo "usage: $0 {create|mount|unmount|status}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Executable
+94
@@ -0,0 +1,94 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# plugin/sbin/podman-uninstall-cleanup.sh
|
||||
#
|
||||
# "Beim Entfernen sauber aufräumen" — run by plugin/podman.plg's <FILE
|
||||
# Run="removepkg"> block when the plugin is uninstalled via Unraid's Plugins
|
||||
# page. Two-tier cleanup, matching docs/ARCHITECTURE.md section 3.2 /
|
||||
# section 17:
|
||||
#
|
||||
# ALWAYS removed (installed software, safe to regenerate):
|
||||
# - running containers/service stopped cleanly
|
||||
# - runtime config under /etc/containers, /var/run/podman
|
||||
# - staged helper scripts, templates, WebUI files, event hooks
|
||||
#
|
||||
# NOT applicable: this project has no /boot/config/go entry to clean up
|
||||
# in the first place — it hooks the official Unraid plugin event
|
||||
# mechanism (/usr/local/emhttp/plugins/podman/event/*, see
|
||||
# plugin/event/disks_mounted's header comment) instead of editing go,
|
||||
# so removepkg-ing the unraid-podman package (done by podman.plg right
|
||||
# after this script runs) already removes those hooks along with
|
||||
# everything else under /usr/local/emhttp/plugins/podman/.
|
||||
#
|
||||
# PRESERVED BY DEFAULT (user data — see docs/INSTALL.md "Uninstallation"):
|
||||
# - /boot/config/plugins/podman/ (settings, autostart list, backups)
|
||||
# - $STORAGE_PATH (podman.img — every image/container/volume the user
|
||||
# has)
|
||||
#
|
||||
# Full data removal is opt-in only, via:
|
||||
# podman-uninstall-cleanup.sh --purge-data
|
||||
# never the default, and never triggered automatically — deleting a user's
|
||||
# containers/images without an explicit, separate confirmation is exactly
|
||||
# the kind of destructive-by-surprise behavior this project avoids (see
|
||||
# docs/ARCHITECTURE.md "Nicht-Ziele" around data safety).
|
||||
# =============================================================================
|
||||
|
||||
set -u
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=./podman-common.sh
|
||||
. "$SCRIPT_DIR/podman-common.sh"
|
||||
|
||||
PURGE_DATA=0
|
||||
[ "${1:-}" = "--purge-data" ] && PURGE_DATA=1
|
||||
|
||||
podman_log "uninstall: starting cleanup (purge-data=$PURGE_DATA)"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 1. Stop podman cleanly if it's running, so containers shut down properly
|
||||
# and the storage loopback is unmounted before we remove anything else.
|
||||
# -----------------------------------------------------------------------------
|
||||
if [ -x /etc/rc.d/rc.podman ]; then
|
||||
podman_log "uninstall: stopping podman"
|
||||
/etc/rc.d/rc.podman stop || podman_log_error "uninstall: rc.podman stop reported an error (continuing cleanup anyway)"
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 2. Remove runtime state (RAM-root anyway, but tidy up now rather than
|
||||
# waiting for the next reboot to implicitly clear it).
|
||||
# -----------------------------------------------------------------------------
|
||||
podman_log "uninstall: removing runtime state"
|
||||
rm -rf "$PODMAN_ETC_DIR" "$PODMAN_RUN_DIR"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 3. Remove staged files this plugin installed outside of package-managed
|
||||
# paths. This is technically redundant with podman.plg's removepkg of
|
||||
# the unraid-podman package that immediately follows this script (see
|
||||
# that package's manifest — it owns exactly these paths), but doing it
|
||||
# explicitly here too means this script is also safe to run manually as
|
||||
# a standalone repair tool without relying on removepkg bookkeeping.
|
||||
# -----------------------------------------------------------------------------
|
||||
podman_log "uninstall: removing plugin scaffolding"
|
||||
rm -rf /usr/local/share/unraid-podman
|
||||
rm -rf /usr/local/emhttp/plugins/podman
|
||||
rm -f /etc/rc.d/rc.podman
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 4. User data — preserved unless --purge-data was explicitly passed.
|
||||
# -----------------------------------------------------------------------------
|
||||
if [ "$PURGE_DATA" -eq 1 ]; then
|
||||
podman_load_cfg
|
||||
podman_log "uninstall: --purge-data given, removing user data"
|
||||
|
||||
if [ -n "${STORAGE_PATH:-}" ] && [ -d "$STORAGE_PATH" ]; then
|
||||
podman_log "uninstall: removing $STORAGE_PATH (images, containers, volumes, logs)"
|
||||
rm -rf "${STORAGE_PATH:?}"
|
||||
fi
|
||||
|
||||
podman_log "uninstall: removing $PODMAN_BOOT_DIR (settings, autostart, backups)"
|
||||
rm -rf "${PODMAN_BOOT_DIR:?}"
|
||||
else
|
||||
podman_log "uninstall: preserving $PODMAN_BOOT_DIR and podman's storage path (pass --purge-data to remove them too)"
|
||||
fi
|
||||
|
||||
podman_log "uninstall: cleanup complete"
|
||||
Executable
+134
@@ -0,0 +1,134 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# plugin/sbin/podman-update-packages.sh
|
||||
#
|
||||
# "Pakete aktualisieren" — reconciles installed packages with what the
|
||||
# currently installed plugin version expects, per
|
||||
# /usr/local/share/unraid-podman/installed-versions.env (written by
|
||||
# plugin/podman.plg's postinstall — see podman-verify-packages.sh for the
|
||||
# same manifest used the other direction, to *check* rather than *fix*).
|
||||
#
|
||||
# This is intentionally idempotent and safe to run any time, not just
|
||||
# during a plugin update: if every package already matches, it's a no-op.
|
||||
# It exists as a separate script (rather than being inline in podman.plg)
|
||||
# for two reasons:
|
||||
# 1. plugin/podman.plg's own <FILE Run="upgradepkg"> blocks handle the
|
||||
# *normal* update path (new .txz already downloaded by the plg,
|
||||
# installed as part of the same transaction) — this script is the
|
||||
# *repair* path for when that didn't fully complete (e.g. Unraid was
|
||||
# rebooted mid-update), and can be re-run safely from the command line
|
||||
# or from a future WebUI "check for package issues" action.
|
||||
# 2. It calls podman-backup.sh snapshot-config first, since any package
|
||||
# swap is exactly the moment a config schema migration might be
|
||||
# needed — see docs/ARCHITECTURE.md section 13.1.
|
||||
#
|
||||
# Usage:
|
||||
# podman-update-packages.sh # reconcile all 7 packages
|
||||
# podman-update-packages.sh podman # reconcile a single package
|
||||
# =============================================================================
|
||||
|
||||
set -u
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=./podman-common.sh
|
||||
. "$SCRIPT_DIR/podman-common.sh"
|
||||
|
||||
INSTALLED_VERSIONS_FILE="/usr/local/share/unraid-podman/installed-versions.env"
|
||||
ALL_PACKAGES="podman conmon crun netavark aardvark-dns passt fuse-overlayfs unraid-podman"
|
||||
|
||||
if [ ! -f "$INSTALLED_VERSIONS_FILE" ]; then
|
||||
podman_log_error "update-packages: $INSTALLED_VERSIONS_FILE missing — plugin install metadata not found"
|
||||
exit 1
|
||||
fi
|
||||
# shellcheck source=/dev/null
|
||||
. "$INSTALLED_VERSIONS_FILE"
|
||||
|
||||
if [ -z "${PLUGIN_VERSION:-}" ]; then
|
||||
podman_log_error "update-packages: PLUGIN_VERSION not recorded in $INSTALLED_VERSIONS_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
targets="${*:-$ALL_PACKAGES}"
|
||||
|
||||
podman_require_command upgradepkg
|
||||
podman_require_command installpkg
|
||||
|
||||
updated=0
|
||||
already_current=0
|
||||
errors=0
|
||||
|
||||
for name in $targets; do
|
||||
entity_prefix=$(echo "$name" | tr '[:lower:]-' '[:upper:]_')
|
||||
expected_version_var="${entity_prefix}_INSTALLED_VERSION"
|
||||
expected_version="${!expected_version_var:-}"
|
||||
|
||||
if [ -z "$expected_version" ]; then
|
||||
podman_log_error "update-packages: no expected version recorded for '$name', skipping"
|
||||
errors=$((errors + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
installed_record=$(find /var/log/packages -maxdepth 1 -name "${name}-*" -print 2> /dev/null | head -n1)
|
||||
installed_basename=$(basename "${installed_record:-__none__}")
|
||||
|
||||
case "$installed_basename" in
|
||||
"${name}-${expected_version}-"*)
|
||||
podman_log "update-packages: $name already at expected version $expected_version"
|
||||
already_current=$((already_current + 1))
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
|
||||
# The package for the currently expected version was downloaded straight
|
||||
# into its backup slot by podman.plg (grouped by PLUGIN_VERSION, since a
|
||||
# rollback targets "this plugin release" — see podman-backup.sh's header
|
||||
# comment for why that's also where we install FROM.
|
||||
txz=$(find "$PODMAN_BACKUP_DIR/packages/$PLUGIN_VERSION" -maxdepth 1 -name "${name}-*.txz" 2> /dev/null | head -n1)
|
||||
if [ -z "$txz" ]; then
|
||||
podman_log_error "update-packages: no package file found for $name (expected version $expected_version) in $PODMAN_BACKUP_DIR/packages/$PLUGIN_VERSION/"
|
||||
errors=$((errors + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
podman_log "update-packages: installing $name -> $expected_version ($txz)"
|
||||
|
||||
# Snapshot config once, before the first actual package change — a
|
||||
# version bump is exactly when a schema migration might be needed (see
|
||||
# file header). Only do this once per run, not once per package.
|
||||
if [ "$updated" -eq 0 ]; then
|
||||
"$SCRIPT_DIR/podman-backup.sh" snapshot-config > /dev/null
|
||||
fi
|
||||
|
||||
if [ -n "$installed_record" ]; then
|
||||
if upgradepkg --install-new "$txz"; then
|
||||
updated=$((updated + 1))
|
||||
else
|
||||
podman_log_error "update-packages: upgradepkg failed for $name ($txz)"
|
||||
errors=$((errors + 1))
|
||||
fi
|
||||
else
|
||||
if installpkg "$txz"; then
|
||||
updated=$((updated + 1))
|
||||
else
|
||||
podman_log_error "update-packages: installpkg failed for $name ($txz)"
|
||||
errors=$((errors + 1))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
podman_log "update-packages: done (updated=$updated already-current=$already_current errors=$errors)"
|
||||
|
||||
if [ "$errors" -gt 0 ]; then
|
||||
podman_notify "Podman package update had errors" \
|
||||
"$errors package(s) failed to update — see $PODMAN_PLUGIN_LOG." \
|
||||
"alert"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$updated" -gt 0 ]; then
|
||||
podman_notify "Podman packages updated" \
|
||||
"$updated package(s) updated. Run 'rc.podman restart' to apply." \
|
||||
"normal"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
Executable
+127
@@ -0,0 +1,127 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# plugin/sbin/podman-verify-packages.sh
|
||||
#
|
||||
# "Pakete prüfen" — verifies the seven packages this plugin ships are
|
||||
# actually installed, at the version the plugin expects, and that the
|
||||
# backed-up .txz copies (see podman-backup.sh) haven't bit-rotted on disk.
|
||||
#
|
||||
# Three checks per package:
|
||||
# 1. Installed: Slackware records every installed package under
|
||||
# /var/log/packages/<name>-<version>-<arch>-<build><tag> — its mere
|
||||
# existence IS the install record (standard Slackware pkgtools
|
||||
# convention, nothing custom here).
|
||||
# 2. Expected version: compared against
|
||||
# /usr/local/share/unraid-podman/installed-versions.env, written by
|
||||
# plugin/podman.plg's postinstall step at install/update time.
|
||||
# 3. Backup integrity: if a backed-up .txz exists for the installed
|
||||
# version (see podman-backup.sh), its current SHA256 is re-checked
|
||||
# against the .sha256 sidecar recorded at build time — catches flash
|
||||
# storage corruption on the backup copy before it's needed for a
|
||||
# rollback.
|
||||
#
|
||||
# Usage:
|
||||
# podman-verify-packages.sh # human-readable report
|
||||
# podman-verify-packages.sh --quiet # exit code only, minimal output
|
||||
#
|
||||
# Exit code: 0 if everything checks out, 1 if any package has a problem.
|
||||
# =============================================================================
|
||||
|
||||
set -u
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=./podman-common.sh
|
||||
. "$SCRIPT_DIR/podman-common.sh"
|
||||
|
||||
INSTALLED_VERSIONS_FILE="/usr/local/share/unraid-podman/installed-versions.env"
|
||||
PACKAGES="podman conmon crun netavark aardvark-dns passt fuse-overlayfs unraid-podman"
|
||||
|
||||
QUIET=0
|
||||
[ "${1:-}" = "--quiet" ] && QUIET=1
|
||||
|
||||
report() {
|
||||
[ "$QUIET" -eq 0 ] && echo "$1"
|
||||
}
|
||||
|
||||
if [ ! -f "$INSTALLED_VERSIONS_FILE" ]; then
|
||||
podman_log_error "verify: $INSTALLED_VERSIONS_FILE missing — plugin install metadata not found"
|
||||
exit 1
|
||||
fi
|
||||
# shellcheck source=/dev/null
|
||||
. "$INSTALLED_VERSIONS_FILE"
|
||||
|
||||
if [ -z "${PLUGIN_VERSION:-}" ]; then
|
||||
podman_log_error "verify: PLUGIN_VERSION not recorded in $INSTALLED_VERSIONS_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PROBLEMS=0
|
||||
|
||||
for name in $PACKAGES; do
|
||||
entity_prefix=$(echo "$name" | tr '[:lower:]-' '[:upper:]_')
|
||||
expected_version_var="${entity_prefix}_INSTALLED_VERSION"
|
||||
expected_version="${!expected_version_var:-}"
|
||||
|
||||
report "== $name =="
|
||||
|
||||
if [ -z "$expected_version" ]; then
|
||||
report " expected version: UNKNOWN (not recorded in $INSTALLED_VERSIONS_FILE)"
|
||||
PROBLEMS=$((PROBLEMS + 1))
|
||||
continue
|
||||
fi
|
||||
report " expected version: $expected_version"
|
||||
|
||||
# --- Check 1: installed -----------------------------------------------
|
||||
installed_record=$(find /var/log/packages -maxdepth 1 -name "${name}-*" -print 2> /dev/null | head -n1)
|
||||
if [ -z "$installed_record" ]; then
|
||||
report " installed: NO"
|
||||
podman_log_error "verify: $name is not installed (no /var/log/packages/$name-* record)"
|
||||
PROBLEMS=$((PROBLEMS + 1))
|
||||
continue
|
||||
fi
|
||||
installed_basename=$(basename "$installed_record")
|
||||
report " installed package: $installed_basename"
|
||||
|
||||
# --- Check 2: version matches expectation ------------------------------
|
||||
case "$installed_basename" in
|
||||
"${name}-${expected_version}-"*)
|
||||
report " version match: OK"
|
||||
;;
|
||||
*)
|
||||
report " version match: MISMATCH (installed record does not match expected $expected_version)"
|
||||
podman_log_error "verify: $name installed record '$installed_basename' does not match expected version $expected_version"
|
||||
PROBLEMS=$((PROBLEMS + 1))
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- Check 3: backup artifact integrity, if present --------------------
|
||||
# Packages of all 7 components released together as one plugin version
|
||||
# are grouped under a single PLUGIN_VERSION directory (not per-component
|
||||
# version) — a rollback targets "go back to plugin release X", matching
|
||||
# podman-backup.sh's restore-packages <plugin-version>.
|
||||
backup_dir="$PODMAN_BACKUP_DIR/packages/$PLUGIN_VERSION"
|
||||
backup_txz=$(find "$backup_dir" -maxdepth 1 -name "${name}-*.txz" 2> /dev/null | head -n1)
|
||||
if [ -n "$backup_txz" ] && [ -f "$backup_txz.sha256" ]; then
|
||||
if ( cd "$(dirname "$backup_txz")" && sha256sum -c "$(basename "$backup_txz").sha256" > /dev/null 2>&1 ); then
|
||||
report " backup integrity: OK ($backup_txz)"
|
||||
else
|
||||
report " backup integrity: CORRUPT ($backup_txz)"
|
||||
podman_log_error "verify: backup artifact $backup_txz failed checksum verification"
|
||||
PROBLEMS=$((PROBLEMS + 1))
|
||||
fi
|
||||
else
|
||||
report " backup integrity: no backup artifact on file (nothing to verify)"
|
||||
fi
|
||||
done
|
||||
|
||||
report ""
|
||||
if [ "$PROBLEMS" -gt 0 ]; then
|
||||
report "$PROBLEMS problem(s) found."
|
||||
podman_notify "Podman package verification found problems" \
|
||||
"$PROBLEMS issue(s) found — see $PODMAN_PLUGIN_LOG for details." \
|
||||
"warning"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
report "All packages verified OK."
|
||||
exit 0
|
||||
Reference in New Issue
Block a user