fix: make RPM runtime portable on Tumbleweed
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
SHELL := /bin/bash
|
SHELL := /bin/bash
|
||||||
PYTHON := python3
|
PYTHON := python3
|
||||||
VENV_DIR := /opt/fenris
|
VENV_DIR := /opt/fenris
|
||||||
|
VENDOR_DIR := $(VENV_DIR)/vendor
|
||||||
BIN_DIR := /usr/local/bin
|
BIN_DIR := /usr/local/bin
|
||||||
LIBEXEC_DIR := /usr/libexec/fenris
|
LIBEXEC_DIR := /usr/libexec/fenris
|
||||||
UNIT_DIR := /etc/systemd/system
|
UNIT_DIR := /etc/systemd/system
|
||||||
@@ -57,7 +58,7 @@ check-smartctl:
|
|||||||
|
|
||||||
dist/fenris-*.whl: pyproject.toml src/fenris/*.py
|
dist/fenris-*.whl: pyproject.toml src/fenris/*.py
|
||||||
@mkdir -p dist
|
@mkdir -p dist
|
||||||
$(PYTHON) -m build --wheel -o dist
|
$(PYTHON) -m pip wheel --no-deps --wheel-dir dist .
|
||||||
|
|
||||||
# ─── Install ────────────────────────────────────────────────────────────────
|
# ─── Install ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -71,11 +72,10 @@ install: check-python check-smartctl dist/fenris-*.whl
|
|||||||
@sudo groupadd -f fenris
|
@sudo groupadd -f fenris
|
||||||
@sudo install -d -o root -g fenris -m 2750 $(DATA_DIR)
|
@sudo install -d -o root -g fenris -m 2750 $(DATA_DIR)
|
||||||
|
|
||||||
@echo "=== Installing venv with pinned dependencies ==="
|
@echo "=== Installing version-neutral runtime packages ==="
|
||||||
@sudo rm -rf $(VENV_DIR)
|
@sudo rm -rf $(VENV_DIR)
|
||||||
@sudo $(PYTHON) -m venv $(VENV_DIR)
|
@sudo install -d -m 0755 $(VENDOR_DIR)
|
||||||
@sudo $(VENV_DIR)/bin/pip install --upgrade pip --quiet
|
@sudo $(PYTHON) -m pip install --disable-pip-version-check --no-compile --target $(VENDOR_DIR) -r requirements.txt dist/fenris-*.whl --quiet
|
||||||
@sudo $(VENV_DIR)/bin/pip install dist/fenris-*.whl --quiet
|
|
||||||
|
|
||||||
@echo "=== Installing wrapper ==="
|
@echo "=== Installing wrapper ==="
|
||||||
@sudo install -m 0755 scripts/fenris $(BIN_DIR)/fenris
|
@sudo install -m 0755 scripts/fenris $(BIN_DIR)/fenris
|
||||||
@@ -119,7 +119,7 @@ import-legacy:
|
|||||||
@if [ -f "$(LEGACY_HISTORY)" ]; then \
|
@if [ -f "$(LEGACY_HISTORY)" ]; then \
|
||||||
echo "=== Detected legacy history: $(LEGACY_HISTORY) ==="; \
|
echo "=== Detected legacy history: $(LEGACY_HISTORY) ==="; \
|
||||||
echo "Running idempotent import..."; \
|
echo "Running idempotent import..."; \
|
||||||
$(VENV_DIR)/bin/python3 -c "import sys; sys.path.insert(0, 'src'); from fenris.legacy import import_legacy_history; from fenris.store import init_store; from pathlib import Path; conn = init_store(Path('$(DATA_DIR)/observations.db')); r = import_legacy_history(conn, Path('$(LEGACY_HISTORY)')); conn.close(); print(f' Samples imported: {r.get(\"samples_imported\", 0)}'); print(f' Hours imported: {r.get(\"hours_imported\", 0)}'); print(f' Malformed lines: {r.get(\"malformed_lines\", 0)}') if not r.get('skipped') else print(' Skipped: already imported')" || echo " Warning: import failed (non-fatal)"; \
|
PYTHONPATH=$(VENDOR_DIR) $(PYTHON) -c "from fenris.legacy import import_legacy_history; from fenris.store import init_store; from pathlib import Path; conn = init_store(Path('$(DATA_DIR)/observations.db')); r = import_legacy_history(conn, Path('$(LEGACY_HISTORY)')); conn.close(); print(f' Samples imported: {r.get(\"samples_imported\", 0)}'); print(f' Hours imported: {r.get(\"hours_imported\", 0)}'); print(f' Malformed lines: {r.get(\"malformed_lines\", 0)}') if not r.get('skipped') else print(' Skipped: already imported')" || echo " Warning: import failed (non-fatal)"; \
|
||||||
else \
|
else \
|
||||||
echo "=== No legacy history found at $(LEGACY_HISTORY) ==="; \
|
echo "=== No legacy history found at $(LEGACY_HISTORY) ==="; \
|
||||||
fi
|
fi
|
||||||
@@ -131,8 +131,10 @@ upgrade: dist/fenris-*.whl
|
|||||||
@echo "=== Snapshotting database (IN-6) ==="
|
@echo "=== Snapshotting database (IN-6) ==="
|
||||||
@sudo cp $(DATA_DIR)/observations.db $(DATA_DIR)/observations.db.bak 2>/dev/null || true
|
@sudo cp $(DATA_DIR)/observations.db $(DATA_DIR)/observations.db.bak 2>/dev/null || true
|
||||||
|
|
||||||
@echo "=== Installing new wheel with pinned dependencies ==="
|
@echo "=== Installing new version-neutral runtime packages ==="
|
||||||
@sudo $(VENV_DIR)/bin/pip install dist/fenris-*.whl --quiet
|
@sudo rm -rf $(VENDOR_DIR)
|
||||||
|
@sudo install -d -m 0755 $(VENDOR_DIR)
|
||||||
|
@sudo $(PYTHON) -m pip install --disable-pip-version-check --no-compile --target $(VENDOR_DIR) -r requirements.txt dist/fenris-*.whl --quiet
|
||||||
|
|
||||||
@echo "=== Syncing units against manifest ==="
|
@echo "=== Syncing units against manifest ==="
|
||||||
@sudo install -m 0644 units/fenris-collect.timer $(UNIT_DIR)/
|
@sudo install -m 0644 units/fenris-collect.timer $(UNIT_DIR)/
|
||||||
@@ -172,7 +174,7 @@ upgrade: dist/fenris-*.whl
|
|||||||
done
|
done
|
||||||
|
|
||||||
@echo "=== Applying forward-only schema migrations (IN-5, IN-6) ==="
|
@echo "=== Applying forward-only schema migrations (IN-5, IN-6) ==="
|
||||||
@sudo $(VENV_DIR)/bin/python3 -c "from fenris.store import migrate_to_latest; from pathlib import Path; n = migrate_to_latest(Path('$(DATA_DIR)/observations.db')); print(f' Migration steps applied: {n}') if n else print(' Schema already current')"
|
@sudo env PYTHONPATH=$(VENDOR_DIR) $(PYTHON) -c "from fenris.store import migrate_to_latest; from pathlib import Path; n = migrate_to_latest(Path('$(DATA_DIR)/observations.db')); print(f' Migration steps applied: {n}') if n else print(' Schema already current')"
|
||||||
|
|
||||||
@echo "=== Upgrade complete ==="
|
@echo "=== Upgrade complete ==="
|
||||||
|
|
||||||
@@ -237,8 +239,9 @@ FENRIS_VERSION := $(shell sed -n 's/^version = "\(.*\)"/\1/p' pyproject.toml)
|
|||||||
# GPG signing — packaging key UID (spec §4)
|
# GPG signing — packaging key UID (spec §4)
|
||||||
PACKAGING_KEY ?= packaging@bongbetic.com
|
PACKAGING_KEY ?= packaging@bongbetic.com
|
||||||
|
|
||||||
stage: dist/fenris-*.whl
|
stage:
|
||||||
@echo "=== Staging packaging tree (v$(FENRIS_VERSION)) ==="
|
@echo "=== Staging packaging tree (v$(FENRIS_VERSION)) ==="
|
||||||
|
$(PYTHON) -m pip wheel --no-deps --wheel-dir dist .
|
||||||
bash packaging/stage.sh "$(FENRIS_VERSION)"
|
bash packaging/stage.sh "$(FENRIS_VERSION)"
|
||||||
|
|
||||||
package-deb: stage
|
package-deb: stage
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ sudo apt update && sudo apt install fenris
|
|||||||
Replace `bookworm` with your distribution codename (`bookworm`, `jammy`, or
|
Replace `bookworm` with your distribution codename (`bookworm`, `jammy`, or
|
||||||
`noble`).
|
`noble`).
|
||||||
|
|
||||||
### Fedora (dnf)
|
### Fedora / openSUSE Tumbleweed (RPM)
|
||||||
|
|
||||||
Use the Fenris-owned repo file (not Gitea's auto-generated one):
|
Use the Fenris-owned repo file (not Gitea's auto-generated one):
|
||||||
|
|
||||||
@@ -53,6 +53,15 @@ sudo dnf config-manager --add-repo \
|
|||||||
sudo dnf install fenris
|
sudo dnf install fenris
|
||||||
```
|
```
|
||||||
|
|
||||||
|
On openSUSE Tumbleweed, add the same standard RPM repository file and install
|
||||||
|
with zypper:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo zypper addrepo --refresh \
|
||||||
|
https://git.bongbetic.com/xavierk/Fenris/raw/branch/main/packaging/fenris.repo fenris
|
||||||
|
sudo zypper install fenris
|
||||||
|
```
|
||||||
|
|
||||||
The repo file sets `gpgcheck=1` against the Fenris packaging key (downloaded
|
The repo file sets `gpgcheck=1` against the Fenris packaging key (downloaded
|
||||||
from the raw URL in `gpgkey`) and `repo_gpgcheck=0` (metadata check left to
|
from the raw URL in `gpgkey`) and `repo_gpgcheck=0` (metadata check left to
|
||||||
TLS).
|
TLS).
|
||||||
@@ -91,7 +100,8 @@ For contributors building from source:
|
|||||||
sudo make install
|
sudo make install
|
||||||
```
|
```
|
||||||
|
|
||||||
This builds a wheel, installs it into `/opt/fenris` with pinned dependencies,
|
This builds a wheel, installs its locked pure-Python runtime packages into
|
||||||
|
`/opt/fenris/vendor`,
|
||||||
and places helpers, units, and the polkit policy. Units are dormant by default.
|
and places helpers, units, and the polkit policy. Units are dormant by default.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -117,7 +127,7 @@ sudo make upgrade
|
|||||||
|
|
||||||
What it does:
|
What it does:
|
||||||
1. Snapshots `observations.db` to a one-generation backup (`.bak`).
|
1. Snapshots `observations.db` to a one-generation backup (`.bak`).
|
||||||
2. Installs the new wheel into the same venv with pinned dependencies.
|
2. Replaces the locked runtime packages under `/opt/fenris/vendor`.
|
||||||
3. Syncs units and polkit against the manifest; runs `daemon-reload`.
|
3. Syncs units and polkit against the manifest; runs `daemon-reload`.
|
||||||
4. Restarts the timer **only** if unit contents changed **and** it is active — a running collection run finishes on its mapped interpreter; the next run uses the new code.
|
4. Restarts the timer **only** if unit contents changed **and** it is active — a running collection run finishes on its mapped interpreter; the next run uses the new code.
|
||||||
5. Applies forward-only schema migrations (the store directory is never rebuilt; automatic downgrade does not exist).
|
5. Applies forward-only schema migrations (the store directory is never rebuilt; automatic downgrade does not exist).
|
||||||
@@ -149,7 +159,7 @@ make uninstall # removes artifacts, preserves config and observation history
|
|||||||
make purge # also removes /etc/fenris and /var/lib/fenris
|
make purge # also removes /etc/fenris and /var/lib/fenris
|
||||||
```
|
```
|
||||||
|
|
||||||
Uninstall performs the sanctioned disable first (`fenris-monitor disable --now`) — an open period closes `user_disabled` — then removes the venv, helpers, units, polkit policy, and wrapper while keeping `/etc/fenris` and the observation store. Reinstalling resumes from the preserved store.
|
Uninstall performs the sanctioned disable first (`fenris-monitor disable --now`) — an open period closes `user_disabled` — then removes the runtime packages, helpers, units, polkit policy, and wrapper while keeping `/etc/fenris` and the observation store. Reinstalling resumes from the preserved store.
|
||||||
|
|
||||||
## Cadence drop-ins
|
## Cadence drop-ins
|
||||||
|
|
||||||
@@ -212,7 +222,7 @@ Use a stable `/dev/disk/by-id/` path. Raw `/dev/nvmeX` paths are warned against.
|
|||||||
| sysusers/tmpfiles | `/usr/lib/{sysusers,tmpfiles}.d/fenris.conf` | managed by Makefile |
|
| sysusers/tmpfiles | `/usr/lib/{sysusers,tmpfiles}.d/fenris.conf` | managed by Makefile |
|
||||||
| Configuration | `/etc/fenris/fenris.conf` | `/etc/fenris/fenris.conf` |
|
| Configuration | `/etc/fenris/fenris.conf` | `/etc/fenris/fenris.conf` |
|
||||||
| Observation store | `/var/lib/fenris/observations.db` | `/var/lib/fenris/observations.db` |
|
| Observation store | `/var/lib/fenris/observations.db` | `/var/lib/fenris/observations.db` |
|
||||||
| Venv | `/opt/fenris` | `/opt/fenris` |
|
| Runtime packages | `/opt/fenris/vendor` | `/opt/fenris/vendor` |
|
||||||
| Legacy history | — | `./data/history.jsonl` (auto-imported) |
|
| Legacy history | — | `./data/history.jsonl` (auto-imported) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Accepted — resolves [Task: Compose release spec + ADR amending 0004](https://g
|
|||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
ADR 0004 fixed delivery as `sudo make install` from a source checkout: wheel into a Fenris-owned venv at `/opt/fenris`, a hand-rolled placement manifest, units in `/etc/systemd/system`. The release plan ([map](https://git.bongbetic.com/xavierk/Fenris/issues/33), decisions [Lock channel + toolchain](https://git.bongbetic.com/xavierk/Fenris/issues/38), [Signing + key policy](https://git.bongbetic.com/xavierk/Fenris/issues/39), [Package ownership](https://git.bongbetic.com/xavierk/Fenris/issues/40), [Migration path](https://git.bongbetic.com/xavierk/Fenris/issues/41), [Release cadence](https://git.bongbetic.com/xavierk/Fenris/issues/43)) now ships Fenris as native deb + rpm packages built by nfpm and published to the self-hosted Gitea 1.27.1 package registry, for Debian 12, Ubuntu 22.04/24.04, and Fedora 40+ (x86_64), with dependencies vendored in a bundled venv because every target distro ships `python3-textual` below Fenris's floor. Packages become the primary delivery; ADR 0004's delivery model demotes to a dev fallback.
|
ADR 0004 fixed delivery as `sudo make install` from a source checkout: wheel into a Fenris-owned runtime directory at `/opt/fenris`, a hand-rolled placement manifest, units in `/etc/systemd/system`. The release plan ([map](https://git.bongbetic.com/xavierk/Fenris/issues/33), decisions [Lock channel + toolchain](https://git.bongbetic.com/xavierk/Fenris/issues/38), [Signing + key policy](https://git.bongbetic.com/xavierk/Fenris/issues/39), [Package ownership](https://git.bongbetic.com/xavierk/Fenris/issues/40), [Migration path](https://git.bongbetic.com/xavierk/Fenris/issues/41), [Release cadence](https://git.bongbetic.com/xavierk/Fenris/issues/43)) now ships Fenris as native deb + rpm packages built by nfpm and published to the self-hosted Gitea 1.27.1 package registry, for Debian 12, Ubuntu 22.04/24.04, Fedora 40+, and openSUSE Tumbleweed (x86_64), with locked pure-Python dependencies vendored at `/opt/fenris/vendor`. Packages become the primary delivery; ADR 0004's delivery model demotes to a dev fallback.
|
||||||
|
|
||||||
The implementation-ready operative contracts live in the [release and packaging specification](../spec/release-packaging.md); this ADR records the decisions and their rationale.
|
The implementation-ready operative contracts live in the [release and packaging specification](../spec/release-packaging.md); this ADR records the decisions and their rationale.
|
||||||
|
|
||||||
@@ -14,12 +14,12 @@ The implementation-ready operative contracts live in the [release and packaging
|
|||||||
|
|
||||||
Amendments to ADR 0004, section by section:
|
Amendments to ADR 0004, section by section:
|
||||||
|
|
||||||
1. **Delivery (amended).** Packages are primary: one deb per codename pool (`bookworm`, `jammy`, `noble`) and one rpm (group `fenris`, Fedora 40+), built by nfpm from a single `packaging/nfpm.yaml` over a staged `--copies` venv at `/opt/fenris`, published to the Gitea Debian/RPM registry and installed with `apt`/`dnf`. `sudo make install` remains as the dev fallback for machines without packages; the two deliveries are mutually exclusive per machine. Version scheme `<pyproject-version>-1`, revision bump on rebuild.
|
1. **Delivery (amended).** Packages are primary: one deb per codename pool (`bookworm`, `jammy`, `noble`) and one rpm (group `fenris`, Fedora 40+ and openSUSE Tumbleweed), built by nfpm from a single `packaging/nfpm.yaml` over locked pure-Python runtime packages staged at `/opt/fenris/vendor`, published to the Gitea Debian/RPM registry and installed with `apt`, `dnf`, or `zypper`. `sudo make install` remains as the dev fallback for machines without packages; the two deliveries are mutually exclusive per machine. Version scheme `<pyproject-version>-1`, revision bump on rebuild.
|
||||||
2. **Layout and manifest (amended).** The hand-rolled manifest model is retired: the dpkg/rpm database **is** the manifest, and nothing like `manifest.txt` ships. Package-owned layout: units in `/usr/lib/systemd/system` (vendor placement; `/etc/systemd/system` is admin-only for drop-ins and enable state); helpers stay in `/usr/libexec/fenris` (exactly `fenris-monitor` and `fenris-collect` — no new polkit-reachable binaries); polkit policy in `/usr/share/polkit-1/actions/`; wrapper at `/usr/bin/fenris` (FHS; `/usr/local/bin` remains `make install`'s). The `fenris` group is declared in `/usr/lib/sysusers.d/fenris.conf` (`g fenris -`) and `/var/lib/fenris` in `/usr/lib/tmpfiles.d/fenris.conf` (`d /var/lib/fenris 2750 root fenris -`), both invoked from the maintainer scripts. The package owns the `/var/lib/fenris` directory only; `observations.db`, WAL sidecars, and `.bak` are never owned and never ghosted — ghost-erase would delete the store, violating 0004 §8.
|
2. **Layout and manifest (amended).** The hand-rolled manifest model is retired: the dpkg/rpm database **is** the manifest, and nothing like `manifest.txt` ships. Package-owned layout: units in `/usr/lib/systemd/system` (vendor placement; `/etc/systemd/system` is admin-only for drop-ins and enable state); helpers stay in `/usr/libexec/fenris` (exactly `fenris-monitor` and `fenris-collect` — no new polkit-reachable binaries); polkit policy in `/usr/share/polkit-1/actions/`; wrapper at `/usr/bin/fenris` (FHS; `/usr/local/bin` remains `make install`'s). The `fenris` group is declared in `/usr/lib/sysusers.d/fenris.conf` (`g fenris -`) and `/var/lib/fenris` in `/usr/lib/tmpfiles.d/fenris.conf` (`d /var/lib/fenris 2750 root fenris -`), both invoked from the maintainer scripts. The package owns the `/var/lib/fenris` directory only; `observations.db`, WAL sidecars, and `.bak` are never owned and never ghosted — ghost-erase would delete the store, violating 0004 §8.
|
||||||
3. **Privilege (unchanged).** Root acts through maintainer scripts at install/upgrade/removal time; at runtime, elevation is exclusively polkit, exactly as 0004 §3 and [ADR 0003](0003-service-lifecycle-and-sanctioned-toggle.md) §5 fix it.
|
3. **Privilege (unchanged).** Root acts through maintainer scripts at install/upgrade/removal time; at runtime, elevation is exclusively polkit, exactly as 0004 §3 and [ADR 0003](0003-service-lifecycle-and-sanctioned-toggle.md) §5 fix it.
|
||||||
4. **Dormant install (restated for packages).** A fresh package install is fully dormant: postinst/%post performs `systemctl daemon-reload` (plus `systemd-sysusers` and `systemd-tmpfiles --create`) and nothing else — never enable, never preset, never start; no preset file ships. The sanctioned toggle (`fenris monitor resume`) remains the only opt-in.
|
4. **Dormant install (restated for packages).** A fresh package install is fully dormant: postinst/%post performs `systemctl daemon-reload` (plus `systemd-sysusers` and `systemd-tmpfiles --create`) and nothing else — never enable, never preset, never start; no preset file ships. The sanctioned toggle (`fenris monitor resume`) remains the only opt-in.
|
||||||
5. **Legacy import (narrowed).** Auto-detection of `./data/history.jsonl` is scoped to `make install` only — a package install has no checkout to inspect. `fenris import <path>` remains available as the only import path from packages.
|
5. **Legacy import (narrowed).** Auto-detection of `./data/history.jsonl` is scoped to `make install` only — a package install has no checkout to inspect. `fenris import <path>` remains available as the only import path from packages.
|
||||||
6. **Upgrade (inherited, maintainer-script mechanics).** Upgrades arrive as packages from the single registry channel. postinst/%post on upgrade: snapshot `observations.db` → one-generation `.bak`, run forward-only schema migrations via inline `/opt/fenris/bin/python3 -c "…migrate_to_latest…"` (no new binaries), `daemon-reload`, and restart `fenris-collect.timer` only if unit contents changed **and** it is active. `/var/lib/fenris` is never rebuilt; a running oneshot finishes on its old interpreter.
|
6. **Upgrade (inherited, maintainer-script mechanics).** Upgrades arrive as packages from the single registry channel. postinst/%post on upgrade: snapshot `observations.db` → one-generation `.bak`, run forward-only schema migrations through the target `python3` with `/opt/fenris/vendor` on its import path (no new binaries), `daemon-reload`, and restart `fenris-collect.timer` only if unit contents changed **and** it is active. `/var/lib/fenris` is never rebuilt; a running oneshot finishes on its old interpreter.
|
||||||
7. **Rollback (unchanged, plus one hard edge).** One-generation `.bak` semantics are unchanged. Package downgrade is additionally unsupported: forward-only store-version refusal means installing an older package over a newer store fails by design; documented rollback = restore the snapshot, then install the old release.
|
7. **Rollback (unchanged, plus one hard edge).** One-generation `.bak` semantics are unchanged. Package downgrade is additionally unsupported: forward-only store-version refusal means installing an older package over a newer store fails by design; documented rollback = restore the snapshot, then install the old release.
|
||||||
8. **Removal (mapped).** deb `remove` ≈ `make uninstall` (conffile and store survive); deb `purge` ≈ `make purge` (plus `.bak` and group cleanup); rpm erase ≈ `make uninstall` (unmodified config removed, modified survives as `.rpmsave`; purge is a documented manual command). prerm/%preun performs the sanctioned disable — `fenris-monitor disable --now`, closing the period `user_disabled` — on remove/erase **only, never on upgrade** (deb prerm upgrade case is a no-op; rpm `%preun` gated on `$1 -eq 0`).
|
8. **Removal (mapped).** deb `remove` ≈ `make uninstall` (conffile and store survive); deb `purge` ≈ `make purge` (plus `.bak` and group cleanup); rpm erase ≈ `make uninstall` (unmodified config removed, modified survives as `.rpmsave`; purge is a documented manual command). prerm/%preun performs the sanctioned disable — `fenris-monitor disable --now`, closing the period `user_disabled` — on remove/erase **only, never on upgrade** (deb prerm upgrade case is a no-op; rpm `%preun` gated on `$1 -eq 0`).
|
||||||
9. **Conffile semantics (new).** `/etc/fenris/fenris.conf` ships as a placeholder-commented default with no active device selector — deb conffile, rpm `%config(noreplace)`. The device selector is entered by hand (root edits the file), as in both prior deliveries; no configuration verb is added to `fenris-monitor`, and [ADR 0003](0003-service-lifecycle-and-sanctioned-toggle.md) §3's read-and-validate-at-collection-time semantics are untouched. On upgrade, local edits survive as-is; a changed package default lands beside them as `.dpkg-new`/`.rpmnew`.
|
9. **Conffile semantics (new).** `/etc/fenris/fenris.conf` ships as a placeholder-commented default with no active device selector — deb conffile, rpm `%config(noreplace)`. The device selector is entered by hand (root edits the file), as in both prior deliveries; no configuration verb is added to `fenris-monitor`, and [ADR 0003](0003-service-lifecycle-and-sanctioned-toggle.md) §3's read-and-validate-at-collection-time semantics are untouched. On upgrade, local edits survive as-is; a changed package default lands beside them as `.dpkg-new`/`.rpmnew`.
|
||||||
|
|||||||
@@ -14,11 +14,12 @@
|
|||||||
| Ubuntu 22.04 (jammy) | — | deb | `debian/pool/jammy/main` |
|
| Ubuntu 22.04 (jammy) | — | deb | `debian/pool/jammy/main` |
|
||||||
| Ubuntu 24.04 (noble) | — | deb | `debian/pool/noble/main` |
|
| Ubuntu 24.04 (noble) | — | deb | `debian/pool/noble/main` |
|
||||||
| Fedora 40+ | every release | rpm | `rpm/fenris` group |
|
| Fedora 40+ | every release | rpm | `rpm/fenris` group |
|
||||||
|
| openSUSE Tumbleweed | rolling | rpm | `rpm/fenris` group |
|
||||||
|
|
||||||
- Architecture: **x86_64 only** (arm64 only if real ARM hardware appears — map fog).
|
- Architecture: **x86_64 only** (arm64 only if real ARM hardware appears — map fog).
|
||||||
- Dependencies are vendored in a bundled venv for every target: Debian 12 and Ubuntu 22.04/24.04 ship `python3-textual` 0.1.13, far below the floor; Fedora 40+ ships ≥ 0.48 but below the pin ([toolchain research](../research/deb-rpm-toolchain.md)). No distro `python3-textual` dependency ever enters package metadata.
|
- Dependencies are vendored as locked, pure-Python runtime packages for every target: Debian 12 and Ubuntu 22.04/24.04 ship `python3-textual` 0.1.13, far below the floor; Fedora 40+ ships ≥ 0.48 but below the pin ([toolchain research](../research/deb-rpm-toolchain.md)). No distro `python3-textual` dependency ever enters package metadata.
|
||||||
- Package metadata `depends:`/`Requires:` are exactly `python3 (>= 3.10)`, `smartmontools`, `systemd` — the Python floor is 3.10 (oldest supported distro interpreter, Ubuntu 22.04), bumping ADR 0004 §10's 3.9 gate for packages; `make install` keeps the checkout's floor.
|
- Package metadata `depends:`/`Requires:` are exactly `python3 (>= 3.10)`, `smartmontools`, `systemd` — the Python floor is 3.10 (oldest supported distro interpreter, Ubuntu 22.04), bumping ADR 0004 §10's 3.9 gate for packages; `make install` keeps the checkout's floor.
|
||||||
- The bundled venv is staged with `python3 -m venv --copies` at `/opt/fenris`: shebangs point at the fixed absolute `/opt/fenris/bin/python`, and the stdlib still comes from the host interpreter, which is why `python3 (>= 3.10)` is a hard dependency.
|
- Runtime packages are staged with `python3 -m pip --target /opt/fenris/vendor`; entry points run the target system's `python3` with that directory on the import path. No package ships a copied Python interpreter, avoiding build-host ABI paths and rolling-distribution minor-version breakage.
|
||||||
|
|
||||||
## 2. Distribution channel
|
## 2. Distribution channel
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@
|
|||||||
## 3. Build toolchain
|
## 3. Build toolchain
|
||||||
|
|
||||||
- **nfpm** for both formats from a single `packaging/nfpm.yaml` — one config, `overrides:` for per-format deltas, two invocations (`nfpm pkg -p deb`, `nfpm pkg -p rpm`). fpm is dropped entirely (CLI-flag config drifts); no hand rpm spec; dh-virtualenv is deb-only and dormant since 2020.
|
- **nfpm** for both formats from a single `packaging/nfpm.yaml` — one config, `overrides:` for per-format deltas, two invocations (`nfpm pkg -p deb`, `nfpm pkg -p rpm`). fpm is dropped entirely (CLI-flag config drifts); no hand rpm spec; dh-virtualenv is deb-only and dormant since 2020.
|
||||||
- **Single source of truth:** version injected from `pyproject.toml`; file lists generated by a staging script (venv `--copies` → `/opt/fenris` tree, plus wrapper, helpers, units, polkit policy, sysusers/tmpfiles fragments) referenced by `nfpm.yaml` as a `type: tree` content entry — no hand-maintained file lists.
|
- **Single source of truth:** version injected from `pyproject.toml`; file lists generated by a staging script (locked runtime packages → `/opt/fenris/vendor`, plus wrapper, helpers, units, polkit policy, sysusers/tmpfiles fragments) referenced by `nfpm.yaml` as a `type: tree` content entry — no hand-maintained file lists.
|
||||||
- **Entry point:** `make package` → `dist/fenris_<v>_amd64.deb` + `dist/fenris-<v>-1.x86_64.rpm`.
|
- **Entry point:** `make package` → `dist/fenris_<v>_amd64.deb` + `dist/fenris-<v>-1.x86_64.rpm`.
|
||||||
- **Version scheme:** `<pyproject-version>-1` in both formats; a rebuild of the same upstream version bumps the revision (`-2`, `-3`, …) — the same filename is never re-PUT (registry 409s duplicates).
|
- **Version scheme:** `<pyproject-version>-1` in both formats; a rebuild of the same upstream version bumps the revision (`-2`, `-3`, …) — the same filename is never re-PUT (registry 409s duplicates).
|
||||||
- **Authoring `nfpm.yaml`, the staging script, and the workflow file is execution** — deliberately not part of the decision map. The [toolchain research doc](../research/deb-rpm-toolchain.md) sketches the pipeline.
|
- **Authoring `nfpm.yaml`, the staging script, and the workflow file is execution** — deliberately not part of the decision map. The [toolchain research doc](../research/deb-rpm-toolchain.md) sketches the pipeline.
|
||||||
@@ -62,7 +63,7 @@ Per [ADR 0007](../adr/0007-package-delivery-amends-0004.md) §2 — the dpkg/rpm
|
|||||||
|
|
||||||
| Artifact | Location | Ownership |
|
| Artifact | Location | Ownership |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Bundled venv | `/opt/fenris` | package (tree) |
|
| Bundled runtime packages | `/opt/fenris/vendor` | package (tree) |
|
||||||
| Wrapper | `/usr/bin/fenris` | package |
|
| Wrapper | `/usr/bin/fenris` | package |
|
||||||
| Helpers | `/usr/libexec/fenris/{fenris-monitor,fenris-collect}` | package — exactly these two, no new polkit-reachable binaries |
|
| Helpers | `/usr/libexec/fenris/{fenris-monitor,fenris-collect}` | package — exactly these two, no new polkit-reachable binaries |
|
||||||
| Units | `/usr/lib/systemd/system/fenris-collect.{timer,service}` | package (vendor placement; `/etc/systemd/system` is admin-only) |
|
| Units | `/usr/lib/systemd/system/fenris-collect.{timer,service}` | package (vendor placement; `/etc/systemd/system` is admin-only) |
|
||||||
@@ -76,7 +77,7 @@ Per [ADR 0007](../adr/0007-package-delivery-amends-0004.md) §2 — the dpkg/rpm
|
|||||||
|
|
||||||
- **preinst / %pre:** abort with a pointer to the migration runbook (§9) if `/var/lib/fenris/manifest.txt` **or** `/etc/systemd/system/fenris-collect.timer` exists (dual marker covers pre-manifest make installs). No auto-clean — scripts never delete files outside the package DB.
|
- **preinst / %pre:** abort with a pointer to the migration runbook (§9) if `/var/lib/fenris/manifest.txt` **or** `/etc/systemd/system/fenris-collect.timer` exists (dual marker covers pre-manifest make installs). No auto-clean — scripts never delete files outside the package DB.
|
||||||
- **postinst / %post (install):** `systemd-sysusers`, `systemd-tmpfiles --create`, `systemctl daemon-reload`. Nothing else — no enable, no preset, no start; no preset file ships.
|
- **postinst / %post (install):** `systemd-sysusers`, `systemd-tmpfiles --create`, `systemctl daemon-reload`. Nothing else — no enable, no preset, no start; no preset file ships.
|
||||||
- **postinst / %post (upgrade):** snapshot `observations.db` → `.bak` (one generation) → forward-only schema migration via inline `/opt/fenris/bin/python3 -c "…migrate_to_latest…"` → `daemon-reload` → restart `fenris-collect.timer` only if unit contents changed **and** it is active. `/var/lib/fenris` is never rebuilt; an in-flight oneshot finishes on its old interpreter.
|
- **postinst / %post (upgrade):** snapshot `observations.db` → `.bak` (one generation) → forward-only schema migration via target `python3` with `/opt/fenris/vendor` on its import path → `daemon-reload` → restart `fenris-collect.timer` only if unit contents changed **and** it is active. `/var/lib/fenris` is never rebuilt; an in-flight oneshot finishes on its old interpreter.
|
||||||
- **prerm / %preun:** sanctioned disable (`fenris-monitor disable --now`, closing the monitoring period `user_disabled`) on remove/erase **only, never on upgrade** — deb prerm upgrade case is a no-op; rpm `%preun` gated on `$1 -eq 0`.
|
- **prerm / %preun:** sanctioned disable (`fenris-monitor disable --now`, closing the monitoring period `user_disabled`) on remove/erase **only, never on upgrade** — deb prerm upgrade case is a no-op; rpm `%preun` gated on `$1 -eq 0`.
|
||||||
- **Removal mapping:** deb `remove` ≈ `make uninstall` (conffile + store survive); deb `purge` ≈ `make purge` (+ `.bak`, group cleanup); rpm erase ≈ `make uninstall` (unmodified config removed, modified survives as `.rpmsave`); rpm purge = documented manual command.
|
- **Removal mapping:** deb `remove` ≈ `make uninstall` (conffile + store survive); deb `purge` ≈ `make purge` (+ `.bak`, group cleanup); rpm erase ≈ `make uninstall` (unmodified config removed, modified survives as `.rpmsave`); rpm purge = documented manual command.
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,6 @@
|
|||||||
# The device selector specifies which NVMe drive to monitor.
|
# The device selector specifies which NVMe drive to monitor.
|
||||||
# Uncomment and set exactly one device path:
|
# Uncomment and set exactly one device path:
|
||||||
#
|
#
|
||||||
# devices = /dev/disk/by-id/nvme-Samsung_SSD_980_PRO_500GB_S5PANS0T123456
|
# device = /dev/disk/by-id/nvme-Samsung_SSD_980_PRO_500GB_S5PANS0T123456
|
||||||
#
|
#
|
||||||
# See https://git.bongbetic.com/xavierk/Fenris for documentation.
|
# See https://git.bongbetic.com/xavierk/Fenris for documentation.
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@ depends:
|
|||||||
- systemd
|
- systemd
|
||||||
|
|
||||||
contents:
|
contents:
|
||||||
# Staged tree: venv, wrapper, helpers, units, polkit, sysusers, tmpfiles
|
# Staged tree: runtime packages, wrapper, helpers, units, polkit, sysusers, tmpfiles
|
||||||
- src: build/stage/
|
- src: build/stage/
|
||||||
dst: /
|
dst: /
|
||||||
type: tree
|
type: tree
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ set -eu
|
|||||||
STORE_DIR="/var/lib/fenris"
|
STORE_DIR="/var/lib/fenris"
|
||||||
STORE_DB="${STORE_DIR}/observations.db"
|
STORE_DB="${STORE_DIR}/observations.db"
|
||||||
STORE_BAK="${STORE_DIR}/observations.db.bak"
|
STORE_BAK="${STORE_DIR}/observations.db.bak"
|
||||||
VENV_PYTHON="/opt/fenris/bin/python3"
|
RUNTIME_PYTHON="/usr/bin/python3"
|
||||||
|
VENDOR_DIR="/opt/fenris/vendor"
|
||||||
|
|
||||||
case "${1:-}" in
|
case "${1:-}" in
|
||||||
configure)
|
configure)
|
||||||
@@ -18,8 +19,8 @@ case "${1:-}" in
|
|||||||
if [ -f "${STORE_DB}" ]; then
|
if [ -f "${STORE_DB}" ]; then
|
||||||
cp "${STORE_DB}" "${STORE_BAK}" 2>/dev/null || true
|
cp "${STORE_DB}" "${STORE_BAK}" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
if [ -x "${VENV_PYTHON}" ] && [ -f "${STORE_DB}" ]; then
|
if [ -d "${VENDOR_DIR}" ] && [ -f "${STORE_DB}" ]; then
|
||||||
"${VENV_PYTHON}" -c "
|
PYTHONPATH="${VENDOR_DIR}" "${RUNTIME_PYTHON}" -c "
|
||||||
from fenris.store import migrate_to_latest
|
from fenris.store import migrate_to_latest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
n = migrate_to_latest(Path('${STORE_DB}'))
|
n = migrate_to_latest(Path('${STORE_DB}'))
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ set -eu
|
|||||||
STORE_DIR="/var/lib/fenris"
|
STORE_DIR="/var/lib/fenris"
|
||||||
STORE_DB="${STORE_DIR}/observations.db"
|
STORE_DB="${STORE_DIR}/observations.db"
|
||||||
STORE_BAK="${STORE_DIR}/observations.db.bak"
|
STORE_BAK="${STORE_DIR}/observations.db.bak"
|
||||||
VENV_PYTHON="/opt/fenris/bin/python3"
|
RUNTIME_PYTHON="/usr/bin/python3"
|
||||||
|
VENDOR_DIR="/opt/fenris/vendor"
|
||||||
|
|
||||||
if [ "$1" -eq 1 ]; then
|
if [ "$1" -eq 1 ]; then
|
||||||
# Fresh install
|
# Fresh install
|
||||||
@@ -17,8 +18,8 @@ elif [ "$1" -ge 2 ]; then
|
|||||||
if [ -f "${STORE_DB}" ]; then
|
if [ -f "${STORE_DB}" ]; then
|
||||||
cp "${STORE_DB}" "${STORE_BAK}" 2>/dev/null || true
|
cp "${STORE_DB}" "${STORE_BAK}" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
if [ -x "${VENV_PYTHON}" ] && [ -f "${STORE_DB}" ]; then
|
if [ -d "${VENDOR_DIR}" ] && [ -f "${STORE_DB}" ]; then
|
||||||
"${VENV_PYTHON}" -c "
|
PYTHONPATH="${VENDOR_DIR}" "${RUNTIME_PYTHON}" -c "
|
||||||
from fenris.store import migrate_to_latest
|
from fenris.store import migrate_to_latest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
n = migrate_to_latest(Path('${STORE_DB}'))
|
n = migrate_to_latest(Path('${STORE_DB}'))
|
||||||
|
|||||||
+12
-7
@@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# VERSION defaults to the version in pyproject.toml.
|
# VERSION defaults to the version in pyproject.toml.
|
||||||
# The staged tree contains:
|
# The staged tree contains:
|
||||||
# /opt/fenris/ — bundled venv with the built wheel
|
# /opt/fenris/vendor/ — bundled pure-Python application dependencies
|
||||||
# /usr/bin/fenris — unprivileged wrapper
|
# /usr/bin/fenris — unprivileged wrapper
|
||||||
# /usr/libexec/fenris/ — fenris-monitor, fenris-collect
|
# /usr/libexec/fenris/ — fenris-monitor, fenris-collect
|
||||||
# /usr/lib/systemd/system/ — fenris-collect.{timer,service}
|
# /usr/lib/systemd/system/ — fenris-collect.{timer,service}
|
||||||
@@ -35,18 +35,23 @@ rm -rf "${STAGE_DIR}"
|
|||||||
mkdir -p "${STAGE_DIR}"
|
mkdir -p "${STAGE_DIR}"
|
||||||
|
|
||||||
# --- Use pre-built wheel from dist/ ---
|
# --- Use pre-built wheel from dist/ ---
|
||||||
WHEEL=$(ls "${REPO_ROOT}"/dist/fenris-*.whl 2>/dev/null | head -1)
|
WHEEL=$(ls "${REPO_ROOT}"/dist/fenris-"${VERSION}"-*.whl 2>/dev/null | head -1)
|
||||||
if [ -z "${WHEEL}" ]; then
|
if [ -z "${WHEEL}" ]; then
|
||||||
echo "Error: no wheel found in dist/ — run 'make dist/fenris-*.whl' first" >&2
|
echo "Error: no wheel found in dist/ — run 'make dist/fenris-*.whl' first" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo " Using wheel: $(basename "${WHEEL}")"
|
echo " Using wheel: $(basename "${WHEEL}")"
|
||||||
|
|
||||||
# --- Create venv with --copies and install wheel ---
|
# --- Vendor runtime packages without an interpreter ---
|
||||||
echo " Creating bundled venv ..."
|
# A copied Python binary contains an ABI and build-host dynamic-library path.
|
||||||
python3 -m venv --copies "${STAGE_DIR}/opt/fenris"
|
# It fails after rolling-distribution Python upgrades (for example Tumbleweed
|
||||||
"${STAGE_DIR}/opt/fenris/bin/pip" install --upgrade pip --quiet 2>&1 | tail -1
|
# 3.12 -> 3.13). Fenris and its locked dependencies are pure Python, so place
|
||||||
"${STAGE_DIR}/opt/fenris/bin/pip" install "${WHEEL}" --quiet 2>&1 | tail -1
|
# them in a version-neutral directory and execute with the target's python3.
|
||||||
|
echo " Installing version-neutral runtime packages ..."
|
||||||
|
VENDOR_DIR="${STAGE_DIR}/opt/fenris/vendor"
|
||||||
|
mkdir -p "${VENDOR_DIR}"
|
||||||
|
python3 -m pip install --disable-pip-version-check --no-compile \
|
||||||
|
--target "${VENDOR_DIR}" -r "${REPO_ROOT}/requirements.txt" "${WHEEL}"
|
||||||
|
|
||||||
# --- Inject version into wrapper from pyproject.toml ---
|
# --- Inject version into wrapper from pyproject.toml ---
|
||||||
# The wrapper has a hardcoded version string; patch it for packaging.
|
# The wrapper has a hardcoded version string; patch it for packaging.
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "fenris"
|
name = "fenris"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
description = "NVMe wear monitor with persistent TUI"
|
description = "NVMe wear monitor with persistent TUI"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
+25
-2
@@ -10,6 +10,29 @@ import argparse
|
|||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def add_runtime_packages() -> None:
|
||||||
|
"""Make the package-owned, pure-Python dependencies importable.
|
||||||
|
|
||||||
|
RPM and deb installations deliberately use the target system's Python.
|
||||||
|
Their dependencies are vendored without a copied interpreter so a distro
|
||||||
|
Python minor-version update cannot leave Fenris linked to a removed ABI.
|
||||||
|
The legacy development install keeps its venv fallback.
|
||||||
|
"""
|
||||||
|
runtime_dir = Path("/opt/fenris")
|
||||||
|
vendor_dir = runtime_dir / "vendor"
|
||||||
|
if vendor_dir.is_dir():
|
||||||
|
sys.path.insert(0, str(vendor_dir))
|
||||||
|
return
|
||||||
|
|
||||||
|
site_packages = next((runtime_dir / "lib").glob("python*/site-packages"), None)
|
||||||
|
if site_packages:
|
||||||
|
sys.path.insert(0, str(site_packages))
|
||||||
|
|
||||||
|
|
||||||
|
add_runtime_packages()
|
||||||
|
|
||||||
|
|
||||||
def is_root() -> bool:
|
def is_root() -> bool:
|
||||||
@@ -51,8 +74,8 @@ def cmd_tui(args: argparse.Namespace) -> None:
|
|||||||
|
|
||||||
def cmd_status(args: argparse.Namespace) -> None:
|
def cmd_status(args: argparse.Namespace) -> None:
|
||||||
"""Show status."""
|
"""Show status."""
|
||||||
from fenris.status import print_status
|
from fenris.status import render_status
|
||||||
print_status()
|
print(render_status())
|
||||||
|
|
||||||
|
|
||||||
def cmd_sample(args: argparse.Namespace) -> None:
|
def cmd_sample(args: argparse.Namespace) -> None:
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
"""Fenris: NVMe wear monitor with persistent TUI."""
|
"""Fenris: NVMe wear monitor with persistent TUI."""
|
||||||
__version__ = "0.3.0"
|
__version__ = "0.3.1"
|
||||||
|
|||||||
@@ -15,12 +15,17 @@ import sys
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Add the venv to path if running from the installed location
|
# Package dependencies are vendored independently of the host Python minor
|
||||||
|
# version. Keep the venv fallback for the legacy development install.
|
||||||
VENV_DIR = Path("/opt/fenris")
|
VENV_DIR = Path("/opt/fenris")
|
||||||
if VENV_DIR.exists():
|
if VENV_DIR.exists():
|
||||||
site_packages = next((VENV_DIR / "lib").glob("python*/site-packages"), None)
|
vendor_dir = VENV_DIR / "vendor"
|
||||||
if site_packages:
|
if vendor_dir.is_dir():
|
||||||
sys.path.insert(0, str(site_packages))
|
sys.path.insert(0, str(vendor_dir))
|
||||||
|
else:
|
||||||
|
site_packages = next((VENV_DIR / "lib").glob("python*/site-packages"), None)
|
||||||
|
if site_packages:
|
||||||
|
sys.path.insert(0, str(site_packages))
|
||||||
|
|
||||||
from fenris.store import init_store, get_store_path
|
from fenris.store import init_store, get_store_path
|
||||||
from fenris.collector import run_collection
|
from fenris.collector import run_collection
|
||||||
|
|||||||
@@ -21,12 +21,17 @@ import sqlite3
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Add the venv to path if running from the installed location
|
# Package dependencies are vendored independently of the host Python minor
|
||||||
|
# version. Keep the venv fallback for the legacy development install.
|
||||||
VENV_DIR = Path("/opt/fenris")
|
VENV_DIR = Path("/opt/fenris")
|
||||||
if VENV_DIR.exists():
|
if VENV_DIR.exists():
|
||||||
site_packages = next((VENV_DIR / "lib").glob("python*/site-packages"), None)
|
vendor_dir = VENV_DIR / "vendor"
|
||||||
if site_packages:
|
if vendor_dir.is_dir():
|
||||||
sys.path.insert(0, str(site_packages))
|
sys.path.insert(0, str(vendor_dir))
|
||||||
|
else:
|
||||||
|
site_packages = next((VENV_DIR / "lib").glob("python*/site-packages"), None)
|
||||||
|
if site_packages:
|
||||||
|
sys.path.insert(0, str(site_packages))
|
||||||
|
|
||||||
from fenris.store import init_store, get_store_path
|
from fenris.store import init_store, get_store_path
|
||||||
from fenris.monitoring_periods import (
|
from fenris.monitoring_periods import (
|
||||||
@@ -53,10 +58,6 @@ def cmd_enable(args: argparse.Namespace) -> None:
|
|||||||
- Resume with no open period: opens a new row
|
- Resume with no open period: opens a new row
|
||||||
"""
|
"""
|
||||||
store_path = getattr(args, 'store_path', DEFAULT_STORE_PATH)
|
store_path = getattr(args, 'store_path', DEFAULT_STORE_PATH)
|
||||||
if not store_path.exists():
|
|
||||||
print("Error: Observation store not found at", store_path, file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
conn = init_store(store_path)
|
conn = init_store(store_path)
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,21 @@ class TestIsRoot:
|
|||||||
class TestEnableIdempotentMatrix:
|
class TestEnableIdempotentMatrix:
|
||||||
"""§8.6: Period-row idempotent matrix."""
|
"""§8.6: Period-row idempotent matrix."""
|
||||||
|
|
||||||
|
def test_first_enable_creates_missing_store(self, store_path):
|
||||||
|
"""A fresh package install has a store directory but no database yet."""
|
||||||
|
args = MagicMock(now=False, store_path=store_path)
|
||||||
|
|
||||||
|
with patch("fenris.monitor.subprocess") as mock_sub:
|
||||||
|
mock_sub.run.return_value = MagicMock(returncode=0)
|
||||||
|
cmd_enable(args)
|
||||||
|
|
||||||
|
conn = init_store(store_path)
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT ended_at FROM monitoring_periods WHERE ended_at IS NULL"
|
||||||
|
).fetchone()
|
||||||
|
conn.close()
|
||||||
|
assert row is not None
|
||||||
|
|
||||||
def test_first_opens_period(self, store_path):
|
def test_first_opens_period(self, store_path):
|
||||||
"""First-ever enable opens a period at the enable moment."""
|
"""First-ever enable opens a period at the enable moment."""
|
||||||
# Initialize store
|
# Initialize store
|
||||||
|
|||||||
+27
-17
@@ -78,6 +78,7 @@ DEB_TARGETS = [
|
|||||||
|
|
||||||
RPM_TARGETS = [
|
RPM_TARGETS = [
|
||||||
("fedora:40", "rpm"),
|
("fedora:40", "rpm"),
|
||||||
|
("opensuse/tumbleweed", "rpm"),
|
||||||
]
|
]
|
||||||
|
|
||||||
ALL_TARGETS = DEB_TARGETS + RPM_TARGETS
|
ALL_TARGETS = DEB_TARGETS + RPM_TARGETS
|
||||||
@@ -101,11 +102,14 @@ def _build_deb_dockerfile(image: str, pkg_name: str) -> str:
|
|||||||
|
|
||||||
def _build_rpm_dockerfile(image: str, pkg_name: str) -> str:
|
def _build_rpm_dockerfile(image: str, pkg_name: str) -> str:
|
||||||
"""Dockerfile for testing rpm installation."""
|
"""Dockerfile for testing rpm installation."""
|
||||||
|
install_command = (
|
||||||
|
"zypper --non-interactive install --no-recommends python3 smartmontools systemd dbus-1 && zypper clean --all"
|
||||||
|
if image.startswith("opensuse/")
|
||||||
|
else "dnf install -y --setopt=install_weak_deps=False python3 smartmontools systemd dbus && dnf clean all"
|
||||||
|
)
|
||||||
return textwrap.dedent(f"""\
|
return textwrap.dedent(f"""\
|
||||||
FROM {image}
|
FROM {image}
|
||||||
RUN dnf install -y --setopt=install_weak_deps=False \
|
RUN {install_command}
|
||||||
python3 smartmontools systemd dbus && \
|
|
||||||
dnf clean all
|
|
||||||
COPY dist/{pkg_name} /pkg/{pkg_name}
|
COPY dist/{pkg_name} /pkg/{pkg_name}
|
||||||
RUN rpm -ivh /pkg/{pkg_name}
|
RUN rpm -ivh /pkg/{pkg_name}
|
||||||
""")
|
""")
|
||||||
@@ -132,13 +136,16 @@ def skip_no_docker():
|
|||||||
|
|
||||||
def _assert_dormant_layout(container: str, fmt: str, version: str) -> None:
|
def _assert_dormant_layout(container: str, fmt: str, version: str) -> None:
|
||||||
"""Assert the dormant-install contract (spec §6, §7)."""
|
"""Assert the dormant-install contract (spec §6, §7)."""
|
||||||
# Venv directory exists with expected structure
|
# Version-neutral vendored runtime exists. It must not contain a copied
|
||||||
|
# Python binary tied to the package build host.
|
||||||
rc, out = _container_exec(container, "test -d /opt/fenris && echo OK")
|
rc, out = _container_exec(container, "test -d /opt/fenris && echo OK")
|
||||||
assert "OK" in out, "Bundled venv not found at /opt/fenris"
|
assert "OK" in out, "Bundled runtime not found at /opt/fenris"
|
||||||
|
|
||||||
# Venv Python binary exists (may not execute if shared libs differ)
|
rc, _ = _container_exec(container, "test -f /opt/fenris/vendor/fenris/__init__.py")
|
||||||
rc, _ = _container_exec(container, "test -f /opt/fenris/bin/python3")
|
assert rc == 0, "Fenris runtime package not found"
|
||||||
assert rc == 0, "Venv Python binary 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
|
# Wrapper on PATH
|
||||||
rc, out = _container_exec(container, "command -v fenris")
|
rc, out = _container_exec(container, "command -v fenris")
|
||||||
@@ -152,6 +159,11 @@ def _assert_dormant_layout(container: str, fmt: str, version: str) -> None:
|
|||||||
rc, out = _container_exec(container, f"grep -q '{version}' /usr/bin/fenris && echo OK")
|
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"
|
assert "OK" in out, f"Version {version} not found in wrapper script"
|
||||||
|
|
||||||
|
# This catches launcher import-path errors and copied-interpreter ABI
|
||||||
|
# breakage. Status is read-only and succeeds before monitoring setup.
|
||||||
|
rc, out = _container_exec(container, "fenris status")
|
||||||
|
assert rc == 0, f"Installed CLI cannot run: {out}"
|
||||||
|
|
||||||
# Helpers in /usr/libexec/fenris
|
# Helpers in /usr/libexec/fenris
|
||||||
for helper in ("fenris-monitor", "fenris-collect"):
|
for helper in ("fenris-monitor", "fenris-collect"):
|
||||||
rc, _ = _container_exec(container, f"test -x /usr/libexec/fenris/{helper}")
|
rc, _ = _container_exec(container, f"test -x /usr/libexec/fenris/{helper}")
|
||||||
@@ -284,8 +296,7 @@ def _assert_upgrade_semantics(container: str, fmt: str, pkg_name: str, version:
|
|||||||
4. Removal scripts are no-ops during upgrade
|
4. Removal scripts are no-ops during upgrade
|
||||||
5. Downgrade refusal via forward-only version check (tested at Python level)
|
5. Downgrade refusal via forward-only version check (tested at Python level)
|
||||||
"""
|
"""
|
||||||
# Create a fake observation store using system Python
|
# Create a fake observation store using the target system Python.
|
||||||
# (venv Python may not execute if host shared libs differ)
|
|
||||||
_container_exec(
|
_container_exec(
|
||||||
container,
|
container,
|
||||||
"mkdir -p /var/lib/fenris && "
|
"mkdir -p /var/lib/fenris && "
|
||||||
@@ -391,7 +402,7 @@ def _assert_rpm_upgrade_full(container: str, pkg_name: str) -> None:
|
|||||||
# Modify the config file (simulate hand-edited device selector)
|
# Modify the config file (simulate hand-edited device selector)
|
||||||
_container_exec(
|
_container_exec(
|
||||||
container,
|
container,
|
||||||
"echo 'devices = /dev/nvme0n1' > /etc/fenris/fenris.conf",
|
"echo 'device = /dev/nvme0n1' > /etc/fenris/fenris.conf",
|
||||||
)
|
)
|
||||||
# Record original config hash
|
# Record original config hash
|
||||||
rc, original_hash = _container_exec(
|
rc, original_hash = _container_exec(
|
||||||
@@ -447,8 +458,8 @@ def _assert_package_files_removed(container: str) -> None:
|
|||||||
)
|
)
|
||||||
assert rc != 0, "Helper should be removed"
|
assert rc != 0, "Helper should be removed"
|
||||||
|
|
||||||
rc, _ = _container_exec(container, "test -f /opt/fenris/bin/python3")
|
rc, _ = _container_exec(container, "test -d /opt/fenris/vendor")
|
||||||
assert rc != 0, "Venv Python should be removed"
|
assert rc != 0, "Vendored runtime packages should be removed"
|
||||||
|
|
||||||
|
|
||||||
def _assert_deb_remove_preserves(container: str) -> None:
|
def _assert_deb_remove_preserves(container: str) -> None:
|
||||||
@@ -587,10 +598,9 @@ def test_python_floor(skip_no_docker, version):
|
|||||||
assert "python3" in out, f"python3 dependency not declared: {out}"
|
assert "python3" in out, f"python3 dependency not declared: {out}"
|
||||||
assert "3.10" in out, f"Python 3.10 floor not in dependency: {out}"
|
assert "3.10" in out, f"Python 3.10 floor not in dependency: {out}"
|
||||||
|
|
||||||
# Verify the bundled venv exists (binary may not execute on the host
|
# Verify the version-neutral bundled runtime exists.
|
||||||
# interpreter — that's tested separately by the dormant-install test)
|
rc, _ = _container_exec(container, "test -f /opt/fenris/vendor/fenris/__init__.py")
|
||||||
rc, _ = _container_exec(container, "test -f /opt/fenris/bin/python3")
|
assert rc == 0, "Bundled runtime package not found"
|
||||||
assert rc == 0, "Bundled venv Python binary not found"
|
|
||||||
|
|
||||||
# fenris on PATH
|
# fenris on PATH
|
||||||
rc, _ = _container_exec(container, "command -v fenris")
|
rc, _ = _container_exec(container, "command -v fenris")
|
||||||
|
|||||||
Reference in New Issue
Block a user