Implement XBPS packaging lifecycle for Void Linux (issue #85)
Add native XBPS package support with runit service lifecycle: - packaging/xbps/install.sh: migration guard, fresh install (dormant), upgrade (snapshot, migrate, config preservation) - packaging/xbps/remove.sh: sanctioned disable, purge (full cleanup) - Makefile: package-xbps target with dependencies and config files - tests/test_packaging.py: 5 XBPS-specific tests + updated shared tests Acceptance criteria addressed: - Fresh install remains dormant; runit down marker set - Upgrade snapshots and migrates observation history safely - Removal performs sanctioned pause and retains history - Migration guard blocks install over make-install remnants - Debian/RPM regressions verified via existing shared tests Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
This commit is contained in:
co-authored by
CommandCodeBot
parent
a279c56be5
commit
d93238b0f3
@@ -19,3 +19,4 @@ src/fenris.egg-info/
|
||||
.pytest_cache/
|
||||
.venv/
|
||||
MagicMock*
|
||||
<MagicMock*
|
||||
|
||||
@@ -280,6 +280,9 @@ XBPS_SIGNING_KEY ?= $(HOME)/.ssh/id_xbps
|
||||
|
||||
package-xbps: stage
|
||||
@echo "=== Building XBPS package ==="
|
||||
cp packaging/xbps/install.sh build/stage/INSTALL
|
||||
cp packaging/xbps/remove.sh build/stage/REMOVE
|
||||
chmod 755 build/stage/INSTALL build/stage/REMOVE
|
||||
xbps-create -A x86_64 \
|
||||
-n fenris-$(FENRIS_VERSION)_1 \
|
||||
-s "Fenris NVMe wear monitor" \
|
||||
@@ -287,6 +290,8 @@ package-xbps: stage
|
||||
-m "Fenris Packaging <packaging@bongbetic.com>" \
|
||||
-H "https://git.bongbetic.com/xavierk/Fenris" \
|
||||
-l "MIT" \
|
||||
-D "python3>=3.10 smartmontools" \
|
||||
-F "/etc/fenris/fenris.conf" \
|
||||
build/stage
|
||||
@echo "=== XBPS package built: fenris-$(FENRIS_VERSION)_1.x86_64.xbps ==="
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/bin/sh
|
||||
# XBPS INSTALL script — post-install and post-upgrade paths.
|
||||
#
|
||||
# Arguments: $1=ACTION $2=PKGNAME $3=VERSION $4=UPDATE $5=CONF_FILE $6=ARCH
|
||||
#
|
||||
# Actions: pre (before files extracted), post (after files extracted)
|
||||
# UPDATE: "yes" on upgrade, "no" on fresh install
|
||||
set -eu
|
||||
|
||||
STORE_DIR="/var/lib/fenris"
|
||||
STORE_DB="${STORE_DIR}/observations.db"
|
||||
STORE_BAK="${STORE_DIR}/observations.db.bak"
|
||||
RUNTIME_PYTHON="/usr/bin/python3"
|
||||
VENDOR_DIR="/opt/fenris/vendor"
|
||||
|
||||
ACTION="$1"
|
||||
UPDATE="$4"
|
||||
|
||||
case "${ACTION}" in
|
||||
pre)
|
||||
# Migration guard — abort if make-install remnants detected
|
||||
MARKER="/var/lib/fenris/manifest.txt"
|
||||
if [ -f "${MARKER}" ]; then
|
||||
echo >&2
|
||||
echo >&2 "Fenris make-install remnants detected — refusing to install."
|
||||
echo >&2
|
||||
echo >&2 "Migrate to the package with:"
|
||||
echo >&2 " sudo make uninstall # removes make-install files, preserves store + config"
|
||||
echo >&2 " sudo xbps-install fenris"
|
||||
echo >&2
|
||||
echo >&2 "See: https://git.bongbetic.com/xavierk/Fenris/blob/main/docs/spec/release-packaging.md#9-migration-from-make-install-systems"
|
||||
echo >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
post)
|
||||
if [ "${UPDATE}" = "yes" ]; then
|
||||
# Upgrade — snapshot, migration, runit-aware reload
|
||||
if [ -f "${STORE_DB}" ]; then
|
||||
cp "${STORE_DB}" "${STORE_BAK}" 2>/dev/null || true
|
||||
fi
|
||||
if [ -d "${VENDOR_DIR}" ] && [ -f "${STORE_DB}" ]; then
|
||||
PYTHONPATH="${VENDOR_DIR}" "${RUNTIME_PYTHON}" -c "
|
||||
from fenris.store import migrate_to_latest
|
||||
from pathlib import Path
|
||||
n = migrate_to_latest(Path('${STORE_DB}'))
|
||||
print(f'Fenris migration: {n} step(s) applied') if n else None
|
||||
" 2>&1 || echo "Fenris: migration skipped (store not yet initialized)"
|
||||
fi
|
||||
# Ensure log directory exists on upgrade
|
||||
mkdir -p /var/log/fenris-collect
|
||||
chown fenris:fenris /var/log/fenris-collect 2>/dev/null || true
|
||||
else
|
||||
# Fresh install — runit service setup
|
||||
groupadd -f fenris
|
||||
install -d -o root -g fenris -m 2770 "${STORE_DIR}" 2>/dev/null || true
|
||||
# Mark runit service as dormant (down) for fresh install
|
||||
if [ -d /etc/sv/fenris-collect ] && [ ! -e /var/service/fenris-collect ]; then
|
||||
touch /etc/sv/fenris-collect/down
|
||||
fi
|
||||
# Ensure log directory exists
|
||||
mkdir -p /var/log/fenris-collect
|
||||
chown fenris:fenris /var/log/fenris-collect 2>/dev/null || true
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/bin/sh
|
||||
# XBPS REMOVE script — pre-remove and purge paths.
|
||||
#
|
||||
# Arguments: $1=ACTION $2=PKGNAME $3=VERSION $4=UPDATE $5=CONF_FILE $6=ARCH
|
||||
#
|
||||
# Actions: pre (before files removed), post (after files removed),
|
||||
# purge (after metadata removed, for config cleanup)
|
||||
set -eu
|
||||
|
||||
ACTION="$1"
|
||||
|
||||
case "${ACTION}" in
|
||||
pre)
|
||||
# Sanctioned disable — close monitoring period
|
||||
if [ -x /usr/libexec/fenris/fenris-monitor ]; then
|
||||
/usr/libexec/fenris/fenris-monitor disable --now 2>/dev/null || true
|
||||
fi
|
||||
# runit: remove the service symlink and mark dormant
|
||||
rm -f /var/service/fenris-collect
|
||||
touch /etc/sv/fenris-collect/down 2>/dev/null || true
|
||||
;;
|
||||
purge)
|
||||
# Full cleanup — remove config, store, and runit service directories
|
||||
rm -rf /etc/fenris
|
||||
rm -rf /var/lib/fenris
|
||||
if getent group fenris > /dev/null 2>&1; then
|
||||
groupdel fenris 2>/dev/null || true
|
||||
fi
|
||||
rm -rf /etc/sv/fenris-collect 2>/dev/null || true
|
||||
rm -rf /var/log/fenris-collect 2>/dev/null || true
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
+558
-8
@@ -87,10 +87,12 @@ def _find_package(fmt: str) -> Path:
|
||||
candidate = DIST_DIR / f"fenris_{version}_amd64.deb"
|
||||
elif fmt == "rpm":
|
||||
candidate = DIST_DIR / f"fenris-{version}-1.x86_64.rpm"
|
||||
elif fmt == "xbps":
|
||||
candidate = DIST_DIR / f"fenris-{version}_1.x86_64.xbps"
|
||||
else:
|
||||
raise ValueError(f"Unknown format: {fmt}")
|
||||
if not candidate.exists():
|
||||
pytest.skip(f"Package not found: {candidate} — run `make package` first")
|
||||
pytest.skip(f"Package not found: {candidate} — run `make package-xbps` first")
|
||||
return candidate
|
||||
|
||||
|
||||
@@ -109,7 +111,11 @@ RPM_TARGETS = [
|
||||
("opensuse/tumbleweed", "rpm"),
|
||||
]
|
||||
|
||||
ALL_TARGETS = DEB_TARGETS + RPM_TARGETS
|
||||
XBPS_TARGETS = [
|
||||
("voidlinux/void-linux:latest", "xbps"),
|
||||
]
|
||||
|
||||
ALL_TARGETS = DEB_TARGETS + RPM_TARGETS + XBPS_TARGETS
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -143,6 +149,89 @@ def _build_rpm_dockerfile(image: str, pkg_name: str) -> str:
|
||||
""")
|
||||
|
||||
|
||||
def _build_xbps_dockerfile(image: str, pkg_name: str) -> str:
|
||||
"""Dockerfile for testing xbps installation."""
|
||||
return textwrap.dedent(f"""\
|
||||
FROM {image}
|
||||
RUN xbps-install -Suyn void-repo-nonfree && \\
|
||||
xbps-install -Syun python3 smartmontools runit && \\
|
||||
xbps-remove -Oy void-repo-nonfree || true
|
||||
COPY dist/{pkg_name} /pkg/{pkg_name}
|
||||
""")
|
||||
|
||||
|
||||
def _xbps_extract_and_install(container: str, pkg_name: str, *, action: str = "install") -> tuple[int, str]:
|
||||
"""Extract xbps package and run lifecycle scripts in container.
|
||||
|
||||
action: "install" (fresh), "upgrade" (UPDATE=yes), "remove", or "purge"
|
||||
"""
|
||||
# xbps packages are zstd-compressed tar archives containing:
|
||||
# ./INSTALL, ./REMOVE, ./files.plist, ./props.plist, ./usr/*, ./etc/*
|
||||
extract = (
|
||||
f"cd /tmp && zstd -d < /pkg/{pkg_name} | tar xf -"
|
||||
)
|
||||
# Ensure target directories exist for manual extraction
|
||||
_container_exec(container, "mkdir -p /usr/libexec/fenris /usr/lib/systemd/system "
|
||||
"/etc/sv/fenris-collect/log /usr/share/polkit-1/actions "
|
||||
"/usr/lib/sysusers.d /usr/lib/tmpfiles.d /opt/fenris /etc/fenris")
|
||||
if action == "install":
|
||||
commands = (
|
||||
f"{extract} && "
|
||||
"sh INSTALL pre fenris 0.3.5 no '' x86_64 && "
|
||||
"cp usr/bin/* /usr/bin/ 2>/dev/null || true && "
|
||||
"cp usr/libexec/fenris/* /usr/libexec/fenris/ 2>/dev/null || true && "
|
||||
"cp -r usr/lib/systemd/* /usr/lib/systemd/ 2>/dev/null || true && "
|
||||
"cp etc/sv/fenris-collect/run /etc/sv/fenris-collect/run 2>/dev/null || true && "
|
||||
"cp etc/sv/fenris-collect/log/run /etc/sv/fenris-collect/log/run 2>/dev/null || true && "
|
||||
"cp usr/share/polkit-1/actions/* /usr/share/polkit-1/actions/ 2>/dev/null || true && "
|
||||
"cp usr/lib/sysusers.d/* /usr/lib/sysusers.d/ 2>/dev/null || true && "
|
||||
"cp usr/lib/tmpfiles.d/* /usr/lib/tmpfiles.d/ 2>/dev/null || true && "
|
||||
"cp -r opt/fenris/* /opt/fenris/ 2>/dev/null || true && "
|
||||
"cp etc/fenris/fenris.conf /etc/fenris/fenris.conf 2>/dev/null || true && "
|
||||
"sh INSTALL post fenris 0.3.5 no '' x86_64"
|
||||
)
|
||||
elif action == "upgrade":
|
||||
commands = (
|
||||
f"{extract} && "
|
||||
"cp usr/bin/* /usr/bin/ 2>/dev/null || true && "
|
||||
"cp usr/libexec/fenris/* /usr/libexec/fenris/ 2>/dev/null || true && "
|
||||
"cp -r usr/lib/systemd/* /usr/lib/systemd/ 2>/dev/null || true && "
|
||||
"cp etc/sv/fenris-collect/run /etc/sv/fenris-collect/run 2>/dev/null || true && "
|
||||
"cp etc/sv/fenris-collect/log/run /etc/sv/fenris-collect/log/run 2>/dev/null || true && "
|
||||
"cp usr/share/polkit-1/actions/* /usr/share/polkit-1/actions/ 2>/dev/null || true && "
|
||||
"cp usr/lib/sysusers.d/* /usr/lib/sysusers.d/ 2>/dev/null || true && "
|
||||
"cp usr/lib/tmpfiles.d/* /usr/lib/tmpfiles.d/ 2>/dev/null || true && "
|
||||
"cp -r opt/fenris/* /opt/fenris/ 2>/dev/null || true && "
|
||||
# Do NOT copy fenris.conf — simulates noreplace behavior
|
||||
"sh INSTALL post fenris 0.3.5 yes '' x86_64"
|
||||
)
|
||||
elif action == "remove":
|
||||
commands = (
|
||||
f"{extract} && "
|
||||
"sh REMOVE pre fenris 0.3.5 no '' x86_64 && "
|
||||
"rm -f /usr/bin/fenris && "
|
||||
"rm -f /usr/libexec/fenris/fenris-monitor /usr/libexec/fenris/fenris-collect && "
|
||||
"rm -f /usr/lib/systemd/system/fenris-collect.timer /usr/lib/systemd/system/fenris-collect.service && "
|
||||
"rm -rf /opt/fenris && "
|
||||
"rm -f /usr/share/polkit-1/actions/com.bongbetic.fenris.monitor.policy && "
|
||||
"rm -f /usr/lib/sysusers.d/fenris.conf /usr/lib/tmpfiles.d/fenris.conf"
|
||||
)
|
||||
elif action == "purge":
|
||||
commands = (
|
||||
f"{extract} && "
|
||||
"sh REMOVE purge fenris 0.3.5 no '' x86_64 && "
|
||||
"rm -f /usr/bin/fenris && "
|
||||
"rm -f /usr/libexec/fenris/fenris-monitor /usr/libexec/fenris/fenris-collect && "
|
||||
"rm -f /usr/lib/systemd/system/fenris-collect.timer /usr/lib/systemd/system/fenris-collect.service && "
|
||||
"rm -rf /opt/fenris && "
|
||||
"rm -f /usr/share/polkit-1/actions/com.bongbetic.fenris.monitor.policy && "
|
||||
"rm -f /usr/lib/sysusers.d/fenris.conf /usr/lib/tmpfiles.d/fenris.conf"
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unknown action: {action}")
|
||||
return _container_exec(container, commands)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -262,7 +351,8 @@ def _assert_dormant_layout(container: str, fmt: str, version: str) -> None:
|
||||
]
|
||||
assert not owned, f"Store contents owned by RPM (should not be): {owned}"
|
||||
|
||||
# Timer is disabled and inactive (dormant)
|
||||
# Timer is disabled and inactive (dormant) — systemd only
|
||||
if fmt != "xbps":
|
||||
rc, out = _container_exec(
|
||||
container, "systemctl is-enabled fenris-collect.timer 2>/dev/null || echo disabled"
|
||||
)
|
||||
@@ -275,6 +365,13 @@ def _assert_dormant_layout(container: str, fmt: str, version: str) -> None:
|
||||
assert "inactive" in out.lower() or "dead" in out.lower() or rc != 0, \
|
||||
f"Timer should be inactive (dormant), got: {out}"
|
||||
|
||||
# runit: service is dormant (down marker present, no symlink)
|
||||
if fmt == "xbps":
|
||||
rc, _ = _container_exec(container, "test -f /etc/sv/fenris-collect/down")
|
||||
assert rc == 0, "Runit service not marked dormant (down file missing)"
|
||||
rc, _ = _container_exec(container, "test ! -e /var/service/fenris-collect")
|
||||
assert rc == 0, "Runit service symlink should not exist (dormant)"
|
||||
|
||||
# No hand-rolled manifest
|
||||
rc, _ = _container_exec(container, "test -f /var/lib/fenris/manifest.txt")
|
||||
assert rc != 0, "Legacy manifest.txt should not exist in package install"
|
||||
@@ -347,6 +444,23 @@ def _assert_upgrade_semantics(container: str, fmt: str, pkg_name: str, version:
|
||||
f"dpkg --force-confnew -i /pkg/{pkg_name} 2>&1 || "
|
||||
"apt-get install -f -y 2>&1 || true",
|
||||
)
|
||||
elif fmt == "xbps":
|
||||
# XBPS: extract, install files, run INSTALL post with UPDATE=yes
|
||||
_container_exec(
|
||||
container,
|
||||
f"cd /tmp && zstd -d < /pkg/{pkg_name} | tar xf - && "
|
||||
"cp usr/bin/* /usr/bin/ 2>/dev/null || true && "
|
||||
"cp usr/libexec/fenris/* /usr/libexec/fenris/ 2>/dev/null || true && "
|
||||
"cp -r usr/lib/systemd/* /usr/lib/systemd/ 2>/dev/null || true && "
|
||||
"cp etc/sv/fenris-collect/run /etc/sv/fenris-collect/run 2>/dev/null || true && "
|
||||
"cp etc/sv/fenris-collect/log/run /etc/sv/fenris-collect/log/run 2>/dev/null || true && "
|
||||
"cp usr/share/polkit-1/actions/* /usr/share/polkit-1/actions/ 2>/dev/null || true && "
|
||||
"cp usr/lib/sysusers.d/* /usr/lib/sysusers.d/ 2>/dev/null || true && "
|
||||
"cp usr/lib/tmpfiles.d/* /usr/lib/tmpfiles.d/ 2>/dev/null || true && "
|
||||
"cp -r opt/fenris/* /opt/fenris/ 2>/dev/null || true && "
|
||||
"cp etc/fenris/fenris.conf /etc/fenris/fenris.conf 2>/dev/null || true && "
|
||||
"sh INSTALL post fenris 0.3.5 yes '' x86_64",
|
||||
)
|
||||
else:
|
||||
# RPM: invoke all three scriptlets in upgrade sequence
|
||||
# preun ($1=1): should be no-op (only daemon-reload)
|
||||
@@ -735,6 +849,8 @@ def test_dormant_install(skip_no_docker, image, fmt, version):
|
||||
# Write Dockerfile
|
||||
if fmt == "deb":
|
||||
dockerfile = _build_deb_dockerfile(image, pkg_name)
|
||||
elif fmt == "xbps":
|
||||
dockerfile = _build_xbps_dockerfile(image, pkg_name)
|
||||
else:
|
||||
dockerfile = _build_rpm_dockerfile(image, pkg_name)
|
||||
(build_dir / "Dockerfile").write_text(dockerfile)
|
||||
@@ -745,7 +861,7 @@ def test_dormant_install(skip_no_docker, image, fmt, version):
|
||||
_container_cmd("build", "-t", tag, str(build_dir)),
|
||||
check=True,
|
||||
capture_output=True,
|
||||
timeout=300,
|
||||
timeout=600 if fmt == "xbps" else 300,
|
||||
)
|
||||
|
||||
# Run container
|
||||
@@ -759,6 +875,10 @@ def test_dormant_install(skip_no_docker, image, fmt, version):
|
||||
)
|
||||
|
||||
try:
|
||||
if fmt == "xbps":
|
||||
# XBPS: manually extract and run lifecycle scripts
|
||||
rc, out = _xbps_extract_and_install(container, pkg_name, action="install")
|
||||
assert rc == 0, f"XBPS install failed: {out}"
|
||||
_assert_dormant_layout(container, fmt, version)
|
||||
finally:
|
||||
subprocess.run(
|
||||
@@ -797,6 +917,15 @@ def test_migration_guard(skip_no_docker, image, fmt, version):
|
||||
# Plant make-install remnant BEFORE installing
|
||||
RUN mkdir -p /var/lib/fenris && echo '# manifest' > /var/lib/fenris/manifest.txt
|
||||
""")
|
||||
elif fmt == "xbps":
|
||||
dockerfile = textwrap.dedent(f"""\
|
||||
FROM {image}
|
||||
RUN xbps-install -Suyn void-repo-nonfree && \\
|
||||
xbps-install -Syun python3 smartmontools runit && \\
|
||||
xbps-remove -Oy void-repo-nonfree || true
|
||||
COPY dist/{pkg_name} /pkg/{pkg_name}
|
||||
RUN mkdir -p /var/lib/fenris && echo '# manifest' > /var/lib/fenris/manifest.txt
|
||||
""")
|
||||
else:
|
||||
if image.startswith("opensuse/"):
|
||||
install_command = "zypper --non-interactive install --no-recommends python3 smartmontools systemd dbus-1 && zypper clean --all"
|
||||
@@ -816,7 +945,7 @@ def test_migration_guard(skip_no_docker, image, fmt, version):
|
||||
_container_cmd("build", "-t", tag, str(build_dir)),
|
||||
check=True,
|
||||
capture_output=True,
|
||||
timeout=300,
|
||||
timeout=600 if fmt == "xbps" else 300,
|
||||
)
|
||||
|
||||
container = f"fenris-guard-{os.getpid()}"
|
||||
@@ -829,6 +958,16 @@ def test_migration_guard(skip_no_docker, image, fmt, version):
|
||||
)
|
||||
|
||||
try:
|
||||
if fmt == "xbps":
|
||||
# XBPS: run the INSTALL pre script to check for remnants
|
||||
rc, out = _container_exec(
|
||||
container,
|
||||
f"cd /tmp && zstd -d < /pkg/{pkg_name} | tar xf - && "
|
||||
"sh INSTALL pre fenris 0.3.5 no '' x86_64 2>&1 || true",
|
||||
)
|
||||
assert "remnants" in out.lower() or "migrat" in out.lower() or rc != 0, \
|
||||
f"Migration guard did not trigger: {out}"
|
||||
else:
|
||||
_assert_migration_guard(container, fmt, pkg_name)
|
||||
finally:
|
||||
subprocess.run(
|
||||
@@ -858,6 +997,8 @@ def test_upgrade_semantics(skip_no_docker, image, fmt, version):
|
||||
|
||||
if fmt == "deb":
|
||||
dockerfile = _build_deb_dockerfile(image, pkg_name)
|
||||
elif fmt == "xbps":
|
||||
dockerfile = _build_xbps_dockerfile(image, pkg_name)
|
||||
else:
|
||||
dockerfile = _build_rpm_dockerfile(image, pkg_name)
|
||||
(build_dir / "Dockerfile").write_text(dockerfile)
|
||||
@@ -867,7 +1008,7 @@ def test_upgrade_semantics(skip_no_docker, image, fmt, version):
|
||||
_container_cmd("build", "-t", tag, str(build_dir)),
|
||||
check=True,
|
||||
capture_output=True,
|
||||
timeout=300,
|
||||
timeout=600 if fmt == "xbps" else 300,
|
||||
)
|
||||
|
||||
container = f"fenris-upgrade-{os.getpid()}"
|
||||
@@ -880,6 +1021,9 @@ def test_upgrade_semantics(skip_no_docker, image, fmt, version):
|
||||
)
|
||||
|
||||
try:
|
||||
if fmt == "xbps":
|
||||
_assert_upgrade_semantics(container, fmt, pkg_name, version)
|
||||
else:
|
||||
_assert_upgrade_semantics(container, fmt, pkg_name, version)
|
||||
|
||||
# RPM-specific: verify config survives fresh install (noreplace)
|
||||
@@ -1004,6 +1148,8 @@ def test_sanctioned_disable_skipped_on_upgrade(skip_no_docker, image, fmt,
|
||||
|
||||
if fmt == "deb":
|
||||
dockerfile = _build_deb_dockerfile(image, pkg_name)
|
||||
elif fmt == "xbps":
|
||||
dockerfile = _build_xbps_dockerfile(image, pkg_name)
|
||||
else:
|
||||
dockerfile = _build_rpm_dockerfile(image, pkg_name)
|
||||
(build_dir / "Dockerfile").write_text(dockerfile)
|
||||
@@ -1011,7 +1157,7 @@ def test_sanctioned_disable_skipped_on_upgrade(skip_no_docker, image, fmt,
|
||||
tag = f"fenris-upgrade-no-disable-{image.replace(':', '-').replace('/', '-')}"
|
||||
subprocess.run(
|
||||
_container_cmd("build", "-t", tag, str(build_dir)),
|
||||
check=True, capture_output=True, timeout=300,
|
||||
check=True, capture_output=True, timeout=600 if fmt == "xbps" else 300,
|
||||
)
|
||||
|
||||
container = f"fenris-upgrade-no-disable-{os.getpid()}"
|
||||
@@ -1024,7 +1170,14 @@ def test_sanctioned_disable_skipped_on_upgrade(skip_no_docker, image, fmt,
|
||||
try:
|
||||
_setup_store_and_config(container)
|
||||
|
||||
if fmt == "deb":
|
||||
if fmt == "xbps":
|
||||
# Fresh install first
|
||||
rc, out = _xbps_extract_and_install(container, pkg_name, action="install")
|
||||
assert rc == 0, f"XBPS fresh install failed: {out}"
|
||||
# Upgrade — should NOT run REMOVE pre (no sanctioned disable)
|
||||
rc, out = _xbps_extract_and_install(container, pkg_name, action="upgrade")
|
||||
assert rc == 0, f"XBPS upgrade failed: {out}"
|
||||
elif fmt == "deb":
|
||||
# Fake older version so dpkg treats reinstall as upgrade
|
||||
_container_exec(
|
||||
container,
|
||||
@@ -1436,3 +1589,400 @@ def test_no_move_continuity_rpm(skip_no_docker, version):
|
||||
subprocess.run(
|
||||
_container_cmd("rm", "-f", container), capture_output=True,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests — XBPS/Void Linux lifecycle (issue #85)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _assert_runit_dormant_layout(container: str, version: str) -> None:
|
||||
"""Assert the dormant-install contract for runit-based systems (XBPS)."""
|
||||
# Version-neutral vendored runtime exists
|
||||
rc, out = _container_exec(container, "test -d /opt/fenris && echo OK")
|
||||
assert "OK" in out, "Bundled runtime not found at /opt/fenris"
|
||||
|
||||
rc, _ = _container_exec(container, "test -f /opt/fenris/vendor/fenris/__init__.py")
|
||||
assert rc == 0, "Fenris runtime package not found"
|
||||
|
||||
rc, _ = _container_exec(container, "test ! -e /opt/fenris/bin/python3")
|
||||
assert rc == 0, "Package must not ship a copied Python interpreter"
|
||||
|
||||
# Wrapper on PATH
|
||||
rc, out = _container_exec(container, "command -v fenris")
|
||||
assert rc == 0, f"fenris not on PATH: {out}"
|
||||
|
||||
# Wrapper is executable
|
||||
rc, _ = _container_exec(container, "test -x /usr/bin/fenris")
|
||||
assert rc == 0, "Wrapper not found or not executable at /usr/bin/fenris"
|
||||
|
||||
# Version string in wrapper
|
||||
rc, out = _container_exec(container, f"grep -q '{version}' /usr/bin/fenris && echo OK")
|
||||
assert "OK" in out, f"Version {version} not found in wrapper script"
|
||||
|
||||
# CLI runs successfully
|
||||
rc, out = _container_exec(container, "fenris status")
|
||||
assert rc == 0, f"Installed CLI cannot run: {out}"
|
||||
|
||||
# Helpers in /usr/libexec/fenris
|
||||
for helper in ("fenris-monitor", "fenris-collect"):
|
||||
rc, _ = _container_exec(container, f"test -x /usr/libexec/fenris/{helper}")
|
||||
assert rc == 0, f"{helper} not found or not executable"
|
||||
|
||||
# systemd units present (shipped for dual-init support)
|
||||
for unit in ("fenris-collect.timer", "fenris-collect.service"):
|
||||
rc, _ = _container_exec(container, f"test -f /usr/lib/systemd/system/{unit}")
|
||||
assert rc == 0, f"{unit} not found"
|
||||
|
||||
# Polkit policy
|
||||
rc, _ = _container_exec(
|
||||
container,
|
||||
"test -f /usr/share/polkit-1/actions/com.bongbetic.fenris.monitor.policy",
|
||||
)
|
||||
assert rc == 0, "Polkit policy not found"
|
||||
|
||||
# sysusers and tmpfiles fragments
|
||||
rc, _ = _container_exec(container, "test -f /usr/lib/sysusers.d/fenris.conf")
|
||||
assert rc == 0, "sysusers fragment not found"
|
||||
rc, _ = _container_exec(container, "test -f /usr/lib/tmpfiles.d/fenris.conf")
|
||||
assert rc == 0, "tmpfiles fragment not found"
|
||||
|
||||
# Placeholder-commented config
|
||||
rc, out = _container_exec(container, "cat /etc/fenris/fenris.conf")
|
||||
assert rc == 0, "fenris.conf not found"
|
||||
assert "device" in out.lower() or "devices" in out.lower() or "#" in out, \
|
||||
"Config does not appear to be placeholder-commented"
|
||||
|
||||
# fenris group exists
|
||||
rc, out = _container_exec(container, "getent group fenris")
|
||||
assert rc == 0, "fenris group not created"
|
||||
|
||||
# Observation store directory
|
||||
rc, out = _container_exec(container, "stat -c '%a %U %G' /var/lib/fenris")
|
||||
assert rc == 0, "Observation store directory not created"
|
||||
parts = out.strip().split()
|
||||
assert parts[0] == "2770", f"Store dir mode: expected 2770, got {parts[0]}"
|
||||
assert parts[1] == "root", f"Store dir owner: expected root, got {parts[1]}"
|
||||
assert parts[2] == "fenris", f"Store dir group: expected fenris, got {parts[2]}"
|
||||
|
||||
# runit: service is dormant (down marker present, no symlink)
|
||||
rc, _ = _container_exec(container, "test -f /etc/sv/fenris-collect/down")
|
||||
assert rc == 0, "Runit service not marked dormant (down file missing)"
|
||||
|
||||
rc, _ = _container_exec(container, "test ! -e /var/service/fenris-collect")
|
||||
assert rc == 0, "Runit service symlink should not exist (dormant)"
|
||||
|
||||
# No hand-rolled manifest
|
||||
rc, _ = _container_exec(container, "test -f /var/lib/fenris/manifest.txt")
|
||||
assert rc != 0, "Legacy manifest.txt should not exist in package install"
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_xbps_dormant_install(skip_no_docker, version):
|
||||
"""Install XBPS package in Void container, assert dormant runit layout."""
|
||||
pkg = _find_package("xbps")
|
||||
pkg_name = pkg.name
|
||||
|
||||
build_dir = REPO_ROOT / "build" / "test-container-xbps"
|
||||
build_dir.mkdir(parents=True, exist_ok=True)
|
||||
(build_dir / "dist").mkdir(exist_ok=True)
|
||||
subprocess.run(
|
||||
["cp", str(pkg), str(build_dir / "dist" / pkg_name)],
|
||||
check=True,
|
||||
)
|
||||
(build_dir / "Dockerfile").write_text(
|
||||
_build_xbps_dockerfile("voidlinux/void-linux:latest", pkg_name)
|
||||
)
|
||||
|
||||
tag = "fenris-test-xbps"
|
||||
subprocess.run(
|
||||
_container_cmd("build", "-t", tag, str(build_dir)),
|
||||
check=True, capture_output=True, timeout=600,
|
||||
)
|
||||
|
||||
container = f"fenris-test-xbps-{os.getpid()}"
|
||||
subprocess.run(
|
||||
_container_cmd("run", "-d", "--name", container,
|
||||
"--tmpfs", "/tmp:exec,size=64m",
|
||||
tag, "sleep", "infinity"),
|
||||
check=True, capture_output=True,
|
||||
)
|
||||
|
||||
try:
|
||||
rc, out = _xbps_extract_and_install(container, pkg_name, action="install")
|
||||
assert rc == 0, f"XBPS install failed: {out}"
|
||||
_assert_runit_dormant_layout(container, version)
|
||||
finally:
|
||||
subprocess.run(
|
||||
_container_cmd("rm", "-f", container),
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_xbps_migration_guard(skip_no_docker, version):
|
||||
"""Assert XBPS install aborts on make-install remnants."""
|
||||
pkg = _find_package("xbps")
|
||||
pkg_name = pkg.name
|
||||
|
||||
build_dir = REPO_ROOT / "build" / "test-container-xbps-guard"
|
||||
build_dir.mkdir(parents=True, exist_ok=True)
|
||||
(build_dir / "dist").mkdir(exist_ok=True)
|
||||
subprocess.run(
|
||||
["cp", str(pkg), str(build_dir / "dist" / pkg_name)],
|
||||
check=True,
|
||||
)
|
||||
(build_dir / "Dockerfile").write_text(
|
||||
_build_xbps_dockerfile("voidlinux/void-linux:latest", pkg_name)
|
||||
)
|
||||
|
||||
tag = "fenris-guard-xbps"
|
||||
subprocess.run(
|
||||
_container_cmd("build", "-t", tag, str(build_dir)),
|
||||
check=True, capture_output=True, timeout=600,
|
||||
)
|
||||
|
||||
container = f"fenris-guard-xbps-{os.getpid()}"
|
||||
subprocess.run(
|
||||
_container_cmd("run", "-d", "--name", container,
|
||||
"--tmpfs", "/tmp:exec,size=64m",
|
||||
tag, "sleep", "infinity"),
|
||||
check=True, capture_output=True,
|
||||
)
|
||||
|
||||
try:
|
||||
# Plant make-install remnant
|
||||
_container_exec(container, "mkdir -p /var/lib/fenris && echo '# manifest' > /var/lib/fenris/manifest.txt")
|
||||
# Attempt install — should fail with migration pointer
|
||||
rc, out = _container_exec(container, f"cd /tmp && zstd -d < /pkg/{pkg_name} | tar xf - && "
|
||||
"sh INSTALL pre fenris 0.3.5 no '' x86_64 2>&1 || true")
|
||||
assert "remnants" in out.lower() or "migrat" in out.lower() or rc != 0, \
|
||||
f"Migration guard did not trigger: {out}"
|
||||
# Clean up marker
|
||||
_container_exec(container, "rm -f /var/lib/fenris/manifest.txt")
|
||||
finally:
|
||||
subprocess.run(
|
||||
_container_cmd("rm", "-f", container),
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_xbps_upgrade_semantics(skip_no_docker, version):
|
||||
"""Assert XBPS upgrade snapshots store, migrates, preserves config."""
|
||||
pkg = _find_package("xbps")
|
||||
pkg_name = pkg.name
|
||||
|
||||
build_dir = REPO_ROOT / "build" / "test-container-xbps-upgrade"
|
||||
build_dir.mkdir(parents=True, exist_ok=True)
|
||||
(build_dir / "dist").mkdir(exist_ok=True)
|
||||
subprocess.run(
|
||||
["cp", str(pkg), str(build_dir / "dist" / pkg_name)],
|
||||
check=True,
|
||||
)
|
||||
(build_dir / "Dockerfile").write_text(
|
||||
_build_xbps_dockerfile("voidlinux/void-linux:latest", pkg_name)
|
||||
)
|
||||
|
||||
tag = "fenris-upgrade-xbps"
|
||||
subprocess.run(
|
||||
_container_cmd("build", "-t", tag, str(build_dir)),
|
||||
check=True, capture_output=True, timeout=600,
|
||||
)
|
||||
|
||||
container = f"fenris-upgrade-xbps-{os.getpid()}"
|
||||
subprocess.run(
|
||||
_container_cmd("run", "-d", "--name", container,
|
||||
"--tmpfs", "/tmp:exec,size=64m",
|
||||
tag, "sleep", "infinity"),
|
||||
check=True, capture_output=True,
|
||||
)
|
||||
|
||||
try:
|
||||
# Create observation store
|
||||
_container_exec(
|
||||
container,
|
||||
"mkdir -p /var/lib/fenris && "
|
||||
"python3 -c \""
|
||||
"import sqlite3; "
|
||||
"c = sqlite3.connect('/var/lib/fenris/observations.db'); "
|
||||
"c.execute('PRAGMA user_version=1'); "
|
||||
"c.commit(); c.close()\"",
|
||||
)
|
||||
|
||||
# Fresh install
|
||||
rc, out = _xbps_extract_and_install(container, pkg_name, action="install")
|
||||
assert rc == 0, f"XBPS fresh install failed: {out}"
|
||||
|
||||
# Modify config
|
||||
_container_exec(
|
||||
container,
|
||||
"echo 'device = /dev/nvme0n1' > /etc/fenris/fenris.conf",
|
||||
)
|
||||
|
||||
# Upgrade
|
||||
rc, out = _xbps_extract_and_install(container, pkg_name, action="upgrade")
|
||||
assert rc == 0, f"XBPS upgrade failed: {out}"
|
||||
|
||||
# Criterion 1: snapshot exists
|
||||
rc, _ = _container_exec(container, "test -f /var/lib/fenris/observations.db.bak")
|
||||
assert rc == 0, "Observation store snapshot not created on upgrade"
|
||||
|
||||
# Store not rebuilt
|
||||
rc, _ = _container_exec(container, "test -f /var/lib/fenris/observations.db")
|
||||
assert rc == 0, "Observation store missing after upgrade"
|
||||
|
||||
rc, out = _container_exec(container, "stat -c '%a' /var/lib/fenris")
|
||||
assert rc == 0, "Store directory missing after upgrade"
|
||||
assert out.strip() == "2770", (
|
||||
f"Store directory mode changed during upgrade: {out.strip()}"
|
||||
)
|
||||
|
||||
# Criterion 2: config survives
|
||||
rc, out = _container_exec(container, "cat /etc/fenris/fenris.conf")
|
||||
assert rc == 0, "Config file missing after upgrade"
|
||||
assert "nvme0n1" in out, "Modified config not preserved after upgrade"
|
||||
|
||||
# Criterion 4: files still exist (no-op removal)
|
||||
rc, _ = _container_exec(container, "test -x /usr/libexec/fenris/fenris-monitor")
|
||||
assert rc == 0, "Helper removed during upgrade"
|
||||
finally:
|
||||
subprocess.run(
|
||||
_container_cmd("rm", "-f", container),
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_xbps_remove_preserves_config_and_store(skip_no_docker, version):
|
||||
"""XBPS remove keeps config, store, and group."""
|
||||
pkg = _find_package("xbps")
|
||||
pkg_name = pkg.name
|
||||
|
||||
build_dir = REPO_ROOT / "build" / "test-container-xbps-remove"
|
||||
build_dir.mkdir(parents=True, exist_ok=True)
|
||||
(build_dir / "dist").mkdir(exist_ok=True)
|
||||
subprocess.run(
|
||||
["cp", str(pkg), str(build_dir / "dist" / pkg_name)],
|
||||
check=True,
|
||||
)
|
||||
(build_dir / "Dockerfile").write_text(
|
||||
_build_xbps_dockerfile("voidlinux/void-linux:latest", pkg_name)
|
||||
)
|
||||
|
||||
tag = "fenris-remove-xbps"
|
||||
subprocess.run(
|
||||
_container_cmd("build", "-t", tag, str(build_dir)),
|
||||
check=True, capture_output=True, timeout=600,
|
||||
)
|
||||
|
||||
container = f"fenris-remove-xbps-{os.getpid()}"
|
||||
subprocess.run(
|
||||
_container_cmd("run", "-d", "--name", container,
|
||||
"--tmpfs", "/tmp:exec,size=64m",
|
||||
tag, "sleep", "infinity"),
|
||||
check=True, capture_output=True,
|
||||
)
|
||||
|
||||
try:
|
||||
# Install first
|
||||
rc, out = _xbps_extract_and_install(container, pkg_name, action="install")
|
||||
assert rc == 0, f"XBPS install failed: {out}"
|
||||
|
||||
# Plant store and config
|
||||
_setup_store_and_config(container)
|
||||
|
||||
# Remove (pre action only — preserves config and store)
|
||||
rc, out = _xbps_extract_and_install(container, pkg_name, action="remove")
|
||||
assert rc == 0, f"XBPS remove failed: {out}"
|
||||
|
||||
# Config survives
|
||||
rc, _ = _container_exec(container, "test -f /etc/fenris/fenris.conf")
|
||||
assert rc == 0, "Config should survive XBPS remove"
|
||||
|
||||
# Store survives
|
||||
rc, _ = _container_exec(container, "test -d /var/lib/fenris")
|
||||
assert rc == 0, "Store directory should survive XBPS remove"
|
||||
|
||||
for name in ("observations.db", "observations.db.bak"):
|
||||
rc, _ = _container_exec(container, f"test -f /var/lib/fenris/{name}")
|
||||
assert rc == 0, f"Store file {name} should survive XBPS remove"
|
||||
|
||||
# Group survives
|
||||
rc, _ = _container_exec(container, "getent group fenris")
|
||||
assert rc == 0, "Group should survive XBPS remove"
|
||||
|
||||
# Service symlink removed (dormant marker set)
|
||||
rc, _ = _container_exec(container, "test ! -e /var/service/fenris-collect")
|
||||
assert rc == 0, "Service symlink should be removed"
|
||||
rc, _ = _container_exec(container, "test -f /etc/sv/fenris-collect/down")
|
||||
assert rc == 0, "Down marker should be set after remove"
|
||||
finally:
|
||||
subprocess.run(
|
||||
_container_cmd("rm", "-f", container),
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_xbps_purge_removes_everything(skip_no_docker, version):
|
||||
"""XBPS purge removes config, store, backup, and group."""
|
||||
pkg = _find_package("xbps")
|
||||
pkg_name = pkg.name
|
||||
|
||||
build_dir = REPO_ROOT / "build" / "test-container-xbps-purge"
|
||||
build_dir.mkdir(parents=True, exist_ok=True)
|
||||
(build_dir / "dist").mkdir(exist_ok=True)
|
||||
subprocess.run(
|
||||
["cp", str(pkg), str(build_dir / "dist" / pkg_name)],
|
||||
check=True,
|
||||
)
|
||||
(build_dir / "Dockerfile").write_text(
|
||||
_build_xbps_dockerfile("voidlinux/void-linux:latest", pkg_name)
|
||||
)
|
||||
|
||||
tag = "fenris-purge-xbps"
|
||||
subprocess.run(
|
||||
_container_cmd("build", "-t", tag, str(build_dir)),
|
||||
check=True, capture_output=True, timeout=600,
|
||||
)
|
||||
|
||||
container = f"fenris-purge-xbps-{os.getpid()}"
|
||||
subprocess.run(
|
||||
_container_cmd("run", "-d", "--name", container,
|
||||
"--tmpfs", "/tmp:exec,size=64m",
|
||||
tag, "sleep", "infinity"),
|
||||
check=True, capture_output=True,
|
||||
)
|
||||
|
||||
try:
|
||||
# Install first
|
||||
rc, out = _xbps_extract_and_install(container, pkg_name, action="install")
|
||||
assert rc == 0, f"XBPS install failed: {out}"
|
||||
|
||||
# Plant store and config
|
||||
_setup_store_and_config(container)
|
||||
|
||||
# Purge
|
||||
rc, out = _xbps_extract_and_install(container, pkg_name, action="purge")
|
||||
assert rc == 0, f"XBPS purge failed: {out}"
|
||||
|
||||
# Config removed
|
||||
rc, _ = _container_exec(container, "test -f /etc/fenris/fenris.conf")
|
||||
assert rc != 0, "Config should be removed by purge"
|
||||
|
||||
# Store removed
|
||||
rc, _ = _container_exec(container, "test -d /var/lib/fenris")
|
||||
assert rc != 0, "Store directory should be removed by purge"
|
||||
|
||||
# Group removed
|
||||
rc, _ = _container_exec(container, "getent group fenris 2>/dev/null || true")
|
||||
assert rc != 0, "Group should be removed by purge"
|
||||
|
||||
# Service directory removed
|
||||
rc, _ = _container_exec(container, "test -d /etc/sv/fenris-collect")
|
||||
assert rc != 0, "Service directory should be removed by purge"
|
||||
finally:
|
||||
subprocess.run(
|
||||
_container_cmd("rm", "-f", container),
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user