diff --git a/Makefile b/Makefile index 08701b3..2eb9814 100644 --- a/Makefile +++ b/Makefile @@ -12,21 +12,34 @@ CONF_DIR := /etc/fenris DATA_DIR := /var/lib/fenris # Placement manifest -MANIFEST := manifest.txt +MANIFEST := $(DATA_DIR)/manifest.txt -.PHONY: help install upgrade uninstall purge update-deps test lint +# Legacy history path (IN-4) +LEGACY_HISTORY := ./data/history.jsonl + +.PHONY: help install upgrade uninstall purge update-deps test lint check-python check-smartctl import-legacy help: @echo "Fenris NVMe endurance monitor" @echo "" @echo "Targets:" - @echo " install - Install Fenris (builds wheel, installs to /opt/fenris)" - @echo " upgrade - Upgrade Fenris (reinstall wheel, sync units)" - @echo " uninstall - Uninstall Fenris (preserves config and store)" - @echo " purge - Remove everything including config and store" - @echo " test - Run tests" - @echo " lint - Run linter" - @echo " update-deps - Update dependency pins" + @echo " install - Install Fenris (builds wheel, installs to /opt/fenris)" + @echo " upgrade - Upgrade Fenris (reinstall wheel, sync units)" + @echo " uninstall - Uninstall Fenris (preserves config and store)" + @echo " purge - Remove everything including config and store" + @echo " test - Run tests" + @echo " lint - Run linter" + @echo " update-deps - Update dependency pins" + +# ─── Pre-install gates ────────────────────────────────────────────────────── + +check-python: + @echo "=== Verifying Python ≥ 3.9 ===" + @$(PYTHON) -c "import sys; v=sys.version_info; exit(0 if (v>=(3,9)) else 1)" || { echo "Error: Python 3.9+ required (found $$($(PYTHON) --version 2>&1))"; exit 1; } + +check-smartctl: + @echo "=== Verifying smartctl ===" + @smartctl --version 2>/dev/null | head -1 || { echo "Error: smartctl not found (install smartmontools)"; exit 1; } # ─── Build ────────────────────────────────────────────────────────────────── @@ -36,25 +49,21 @@ dist/fenris-*.whl: pyproject.toml src/fenris/*.py # ─── Install ──────────────────────────────────────────────────────────────── -install: dist/fenris-*.whl - @echo "=== Verifying prerequisites ===" - @$(PYTHON) --version 2>/dev/null || (echo "Error: python3 not found"; exit 1) - @smartctl --version 2>/dev/null | head -1 || (echo "Error: smartctl not found"; exit 1) - +install: check-python check-smartctl dist/fenris-*.whl @echo "=== Creating directories ===" @sudo mkdir -p $(LIBEXEC_DIR) @sudo mkdir -p $(CONF_DIR) - @sudo mkdir -p $(DATA_DIR) @sudo mkdir -p $(POLKIT_DIR) - @echo "=== Creating data directory ===" - @sudo install -d -o root -g fenris -m 2750 $(DATA_DIR) 2>/dev/null || sudo groupadd -f fenris && sudo install -d -o root -g fenris -m 2750 $(DATA_DIR) + @echo "=== Creating data directory (root-written, group-read) ===" + @sudo groupadd -f fenris + @sudo install -d -o root -g fenris -m 2750 $(DATA_DIR) - @echo "=== Installing venv ===" + @echo "=== Installing venv with pinned dependencies ===" @sudo rm -rf $(VENV_DIR) @sudo $(PYTHON) -m venv $(VENV_DIR) - @sudo $(VENV_DIR)/bin/pip install --upgrade pip - @sudo $(VENV_DIR)/bin/pip install dist/fenris-*.whl + @sudo $(VENV_DIR)/bin/pip install --upgrade pip --quiet + @sudo $(VENV_DIR)/bin/pip install dist/fenris-*.whl --quiet @echo "=== Installing wrapper ===" @sudo install -m 0755 scripts/fenris $(BIN_DIR)/fenris @@ -63,7 +72,7 @@ install: dist/fenris-*.whl @sudo install -m 0755 src/fenris/monitor.py $(LIBEXEC_DIR)/fenris-monitor @sudo install -m 0755 src/fenris/collect.py $(LIBEXEC_DIR)/fenris-collect - @echo "=== Installing systemd units ===" + @echo "=== Installing systemd units (dormant — not enabled/started) ===" @sudo install -m 0644 units/fenris-collect.timer $(UNIT_DIR)/ @sudo install -m 0644 units/fenris-collect.service $(UNIT_DIR)/ @sudo systemctl daemon-reload @@ -71,28 +80,47 @@ install: dist/fenris-*.whl @echo "=== Installing polkit policy ===" @sudo install -m 0644 polkit/com.bongbetic.fenris.monitor.policy $(POLKIT_DIR)/ - @echo "=== Creating manifest ===" - @echo "# Fenris placement manifest - do not edit" > $(MANIFEST) - @echo "# Generated by install target" >> $(MANIFEST) - @echo "$(BIN_DIR)/fenris" >> $(MANIFEST) - @echo "$(LIBEXEC_DIR)/fenris-monitor" >> $(MANIFEST) - @echo "$(LIBEXEC_DIR)/fenris-collect" >> $(MANIFEST) - @echo "$(UNIT_DIR)/fenris-collect.timer" >> $(MANIFEST) - @echo "$(UNIT_DIR)/fenris-collect.service" >> $(MANIFEST) - @echo "$(POLKIT_DIR)/com.bongbetic.fenris.monitor.policy" >> $(MANIFEST) - @echo "$(VENV_DIR)" >> $(MANIFEST) - @echo "$(DATA_DIR)" >> $(MANIFEST) + @echo "=== Recording manifest (IN-2, IN-10) ===" + @echo "# Fenris placement manifest — do not edit" | sudo tee $(MANIFEST) > /dev/null + @echo "# Generated by: sudo make install" | sudo tee -a $(MANIFEST) > /dev/null + @echo "# Timestamp: $$(date -u +%%Y-%%m-%%dT%%H:%%M:%%SZ)" | sudo tee -a $(MANIFEST) > /dev/null + @echo "$(BIN_DIR)/fenris" | sudo tee -a $(MANIFEST) > /dev/null + @echo "$(LIBEXEC_DIR)/fenris-monitor" | sudo tee -a $(MANIFEST) > /dev/null + @echo "$(LIBEXEC_DIR)/fenris-collect" | sudo tee -a $(MANIFEST) > /dev/null + @echo "$(UNIT_DIR)/fenris-collect.timer" | sudo tee -a $(MANIFEST) > /dev/null + @echo "$(UNIT_DIR)/fenris-collect.service" | sudo tee -a $(MANIFEST) > /dev/null + @echo "$(POLKIT_DIR)/com.bongbetic.fenris.monitor.policy" | sudo tee -a $(MANIFEST) > /dev/null + @echo "$(VENV_DIR)" | sudo tee -a $(MANIFEST) > /dev/null + @echo "$(DATA_DIR)" | sudo tee -a $(MANIFEST) > /dev/null + @echo "$(CONF_DIR)" | sudo tee -a $(MANIFEST) > /dev/null + @echo "$(MANIFEST)" | sudo tee -a $(MANIFEST) > /dev/null @echo "=== Install complete ===" - @echo "Units installed but NOT enabled or started." + @echo "Units installed but NOT enabled or started (dormant — IN-3)." @echo "To start monitoring: fenris monitor resume" + + @$(MAKE) --no-print-directory import-legacy + +# ─── Legacy import (IN-4, ST-7) ──────────────────────────────────────────── + +import-legacy: + @if [ -f "$(LEGACY_HISTORY)" ]; then \ + echo "=== Detected legacy history: $(LEGACY_HISTORY) ==="; \ + 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)"; \ + else \ + echo "=== No legacy history found at $(LEGACY_HISTORY) ==="; \ + fi # ─── Upgrade ──────────────────────────────────────────────────────────────── upgrade: dist/fenris-*.whl @echo "=== Upgrading Fenris ===" - @echo "=== Installing new wheel ===" - @sudo $(VENV_DIR)/bin/pip install dist/fenris-*.whl + @echo "=== Snapshotting database (IN-6) ===" + @sudo cp $(DATA_DIR)/observations.db $(DATA_DIR)/observations.db.bak 2>/dev/null || true + + @echo "=== Installing new wheel with pinned dependencies ===" + @sudo $(VENV_DIR)/bin/pip install dist/fenris-*.whl --quiet @echo "=== Syncing units ===" @sudo install -m 0644 units/fenris-collect.timer $(UNIT_DIR)/ @@ -103,27 +131,44 @@ upgrade: dist/fenris-*.whl @sudo install -m 0755 src/fenris/collect.py $(LIBEXEC_DIR)/fenris-collect @sudo systemctl daemon-reload - # Restart timer only if active and contents changed - @if systemctl is-active --quiet fenris-collect.timer; then echo "=== Restarting timer (active) ==="; sudo systemctl restart fenris-collect.timer; fi + @echo "=== Updating manifest ===" + @echo "# Fenris placement manifest — do not edit" | sudo tee $(MANIFEST) > /dev/null + @echo "# Generated by: sudo make upgrade" | sudo tee -a $(MANIFEST) > /dev/null + @echo "# Timestamp: $$(date -u +%%Y-%%m-%%dT%%H:%%M:%%SZ)" | sudo tee -a $(MANIFEST) > /dev/null + @echo "$(BIN_DIR)/fenris" | sudo tee -a $(MANIFEST) > /dev/null + @echo "$(LIBEXEC_DIR)/fenris-monitor" | sudo tee -a $(MANIFEST) > /dev/null + @echo "$(LIBEXEC_DIR)/fenris-collect" | sudo tee -a $(MANIFEST) > /dev/null + @echo "$(UNIT_DIR)/fenris-collect.timer" | sudo tee -a $(MANIFEST) > /dev/null + @echo "$(UNIT_DIR)/fenris-collect.service" | sudo tee -a $(MANIFEST) > /dev/null + @echo "$(POLKIT_DIR)/com.bongbetic.fenris.monitor.policy" | sudo tee -a $(MANIFEST) > /dev/null + @echo "$(VENV_DIR)" | sudo tee -a $(MANIFEST) > /dev/null + @echo "$(DATA_DIR)" | sudo tee -a $(MANIFEST) > /dev/null + @echo "$(CONF_DIR)" | sudo tee -a $(MANIFEST) > /dev/null + @echo "$(MANIFEST)" | sudo tee -a $(MANIFEST) > /dev/null + + @if systemctl is-active --quiet fenris-collect.timer; then \ + echo "=== Restarting timer (active) ==="; \ + sudo systemctl restart fenris-collect.timer; \ + fi @echo "=== Upgrade complete ===" -# ─── Uninstall ────────────────────────────────────────────────────────────── +# ─── Uninstall (IN-7) ────────────────────────────────────────────────────── uninstall: @echo "=== Uninstalling Fenris ===" - # Sanctioned disable first (§10.4) - @if [ -x $(LIBEXEC_DIR)/fenris-monitor ]; then echo "=== Performing sanctioned disable ==="; sudo $(LIBEXEC_DIR)/fenris-monitor disable --now || true; fi + @if [ -x $(LIBEXEC_DIR)/fenris-monitor ]; then \ + echo "=== Performing sanctioned disable (§10.4) ==="; \ + sudo $(LIBEXEC_DIR)/fenris-monitor disable --now || true; \ + fi - # Stop and disable units @echo "=== Stopping units ===" @sudo systemctl stop fenris-collect.timer 2>/dev/null || true @sudo systemctl disable fenris-collect.timer 2>/dev/null || true @sudo systemctl daemon-reload - # Remove installed files (preserving /etc/fenris and /var/lib/fenris) - @echo "=== Removing files ===" + @echo "=== Removing installed files (preserving config and store) ===" @-rm -f $(BIN_DIR)/fenris @-rm -f $(LIBEXEC_DIR)/fenris-monitor @-rm -f $(LIBEXEC_DIR)/fenris-collect @@ -132,7 +177,7 @@ uninstall: @-rm -f $(UNIT_DIR)/fenris-collect.service @-rm -f $(POLKIT_DIR)/com.bongbetic.fenris.monitor.policy @sudo rm -rf $(VENV_DIR) - @-rm -f $(MANIFEST) + @-sudo rm -f $(MANIFEST) @echo "=== Uninstall complete ===" @echo "Config preserved at $(CONF_DIR)" @@ -156,7 +201,7 @@ test: lint: $(PYTHON) -m ruff check src/ tests/ -# ─── Dependencies ─────────────────────────────────────────────────────────── +# ─── Dependencies (IN-8) ─────────────────────────────────────────────────── update-deps: $(PYTHON) -m pip compile pyproject.toml -o requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..68dc4d4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,11 @@ +# Fenris dependency lockfile +# Exact pins for reproducible installs (IN-8) +# Refresh with: make update-deps +textual==8.2.8 +rich==15.0.0 +markdown-it-py==4.2.0 +mdit-py-plugins==0.6.1 +mdurl==0.1.2 +platformdirs==4.11.7 +Pygments==2.21.0 +linkify-it-py==2.2.0