diff --git a/.github/scripts/tests/test_log2ram_journal_sync.py b/.github/scripts/tests/test_log2ram_journal_sync.py index eb9a9522..bd5e8b61 100644 --- a/.github/scripts/tests/test_log2ram_journal_sync.py +++ b/.github/scripts/tests/test_log2ram_journal_sync.py @@ -4,6 +4,7 @@ journald then continues an old sequence and deletes its newest archives first. The post-install makes the sync remove that file from the disk copy.""" from pathlib import Path +import re import shutil import stat import subprocess @@ -86,7 +87,18 @@ class Log2ramJournalSyncTests(unittest.TestCase): # An existing installation is told that it needs a reboot. self.assertIn('if pmx_log2ram_drop_stale_journal "$log2ram_bin"; then\n NECESSARY_REBOOT=1', text) # The version is what offers the update to existing installations. - self.assertIn(f'{function}() {{\n local FUNC_VERSION="1.7"', text) + version = re.search(rf'{function}\(\) {{\n local FUNC_VERSION="([0-9.]+)"', text) + self.assertIsNotNone(version) + self.assertGreaterEqual(tuple(map(int, version.group(1).split("."))), (1, 7)) + + def test_rotated_logs_are_compressed_at_once_on_a_tmpfs_var_log(self): + # A rotated log kept uncompressed filled the 128M Log2RAM of a PBS. + for name, function in FLOWS.items(): + with self.subTest(flow=name): + text = (ROOT / "scripts/post_install" / name).read_text() + self.assertNotIn("delaycompress", text) + version = re.search(rf'{function}\(\) {{\n local FUNC_VERSION="([0-9.]+)"', text) + self.assertGreaterEqual(tuple(map(int, version.group(1).split("."))), (1, 8)) if __name__ == "__main__": diff --git a/AppImage/components/pbs-panel.tsx b/AppImage/components/pbs-panel.tsx index cb5a112a..5ea6856d 100644 --- a/AppImage/components/pbs-panel.tsx +++ b/AppImage/components/pbs-panel.tsx @@ -54,9 +54,10 @@ function taskState(task: Task): keyof typeof badgeTone { return "error" } +// PBS reports "OK", "WARNINGS: n" or the error text of the last run. function jobState(state?: string): keyof typeof badgeTone { if (!state) return "muted" - if (state === "ok") return "ok" + if (state.toLowerCase() === "ok") return "ok" if (state.toLowerCase().startsWith("warn")) return "warn" return "error" } diff --git a/AppImage/scripts/flask_server.py b/AppImage/scripts/flask_server.py index 854e76a7..2250d230 100644 --- a/AppImage/scripts/flask_server.py +++ b/AppImage/scripts/flask_server.py @@ -12242,18 +12242,6 @@ def _compute_node_metrics_payload(timeframe: str) -> dict: deadline = time.monotonic() + _NODE_METRICS_TIMEOUT local_node = get_proxmox_node_name() - zfs_arc_size = 0 - try: - with open('/proc/spl/kstat/zfs/arcstats', 'r') as f: - for line in f: - if line.startswith('size'): - parts = line.split() - if len(parts) >= 3: - zfs_arc_size = int(parts[2]) - break - except (FileNotFoundError, PermissionError, ValueError): - pass - try: rrd_result = subprocess.run( _node_rrd_command(local_node, timeframe), @@ -12322,11 +12310,11 @@ def _compute_node_metrics_payload(timeframe: str) -> dict: 'code': 'metrics_invalid_data', }) + # Only the history the node recorded is drawn. A PBS RRD has no ARC field, + # and the current size repeated over the period is not its history. for item in rrd_data: if 'arcsize' in item: item['zfsarc'] = item['arcsize'] - elif zfs_arc_size > 0 and ('zfsarc' not in item or item.get('zfsarc', 0) == 0): - item['zfsarc'] = zfs_arc_size def _values_from(items, field_key, scale=1.0): return [item[field_key] * scale for item in items diff --git a/AppImage/scripts/pbs_manager.py b/AppImage/scripts/pbs_manager.py index 7c93e6a1..7ec910d1 100644 --- a/AppImage/scripts/pbs_manager.py +++ b/AppImage/scripts/pbs_manager.py @@ -279,6 +279,20 @@ def _cli(*arguments): raise PBSError("Could not read PBS " + " ".join(arguments[:2]) + ". Check its service and permissions.") from exc +def _local_api(path): + """The local PBS API, which reports what the job configuration lacks: the + state of the last run and the next one.""" + try: + result = subprocess.run(["proxmox-backup-debug", "api", "get", path, "--output-format", "json"], + check=True, text=True, capture_output=True, timeout=8) + data = json.loads(result.stdout) + if not isinstance(data, list) or not all(isinstance(row, dict) for row in data): + raise ValueError() + return data + except (OSError, subprocess.SubprocessError, ValueError) as exc: + raise PBSError("Could not read PBS " + path + ". Check its service and permissions.") from exc + + def _local_datastores(): stores = _cli("datastore", "list") result = [] @@ -325,9 +339,9 @@ def get_status(identifier): services = _local_services() operations = {"datastores": _local_datastores, "tasks": lambda: _cli("task", "list", "--all", "true", "--limit", "20"), - "sync": lambda: _cli("sync-job", "list"), - "verify": lambda: _cli("verify-job", "list"), - "prune": lambda: _cli("prune-job", "list")} + "sync": lambda: _local_api("/admin/sync"), + "verify": lambda: _local_api("/admin/verify"), + "prune": lambda: _local_api("/admin/prune")} else: with _lock: server = next((row for row in _load_servers() if row["id"] == identifier), None) diff --git a/AppImage/scripts/tests/test_pbs.py b/AppImage/scripts/tests/test_pbs.py index 7b60fc6e..cf932a8e 100644 --- a/AppImage/scripts/tests/test_pbs.py +++ b/AppImage/scripts/tests/test_pbs.py @@ -152,9 +152,11 @@ class PBSManagerTests(unittest.TestCase): def test_local_status_reads_pbs_commands_and_filesystem_capacity(self): def run(command, **kwargs): self.assertNotIn("shell", kwargs) - self.assertEqual(command[0], "proxmox-backup-manager") + self.assertIn(command[0], ("proxmox-backup-manager", "proxmox-backup-debug")) self.assertEqual(command[-2:], ["--output-format", "json"]) data = [{"name": "backup", "path": str(self.root)}] if command[1:3] == ["datastore", "list"] else [] + if command[:4] == ["proxmox-backup-debug", "api", "get", "/admin/verify"]: + data = [{"id": "v-1", "store": "backup", "schedule": "daily", "last-run-state": "OK"}] return SimpleNamespace(stdout=json.dumps(data), returncode=0) with mock.patch.object(pbs, "get_platform", return_value={"product": "pbs", "version": "4.2.5"}), \ mock.patch.object(pbs, "_local_services", return_value={key: "active" for key in pbs.PBS_SERVICES}), \ @@ -165,6 +167,8 @@ class PBSManagerTests(unittest.TestCase): self.assertEqual(result["errors"], {}) self.assertEqual(result["datastores"][0]["used"], 30) self.assertEqual(set(result["services"]), set(pbs.PBS_SERVICES)) + # Jobs come from the API, which carries the state of their last run. + self.assertEqual(result["verify"][0]["last-run-state"], "OK") def test_pbs_installed_next_to_pve_is_read_locally_with_its_own_version(self): def run(command, **kwargs): diff --git a/scripts/post_install/auto_post_install.sh b/scripts/post_install/auto_post_install.sh index 83f84a82..e11f131f 100644 --- a/scripts/post_install/auto_post_install.sh +++ b/scripts/post_install/auto_post_install.sh @@ -392,7 +392,7 @@ EOF # ========================================================== optimize_logrotate() { - local FUNC_VERSION="1.1" + local FUNC_VERSION="1.2" pmx_journal_context "optimize_logrotate" "$FUNC_VERSION" # description: Replace logrotate.conf with a Log2RAM-friendly profile (daily rotation, copytruncate). msg_info "$(translate "Optimizing logrotate configuration...")" @@ -408,7 +408,6 @@ su root adm rotate 7 size 10M compress -delaycompress missingok notifempty create 0640 root adm @@ -941,7 +940,6 @@ _update_existing_log2ram_auto() { size 20M rotate 3 compress - delaycompress missingok notifempty copytruncate @@ -1041,7 +1039,7 @@ EOF install_log2ram_auto() { - local FUNC_VERSION="1.7" + local FUNC_VERSION="1.8" local existing_log2ram_bin="" pmx_journal_context "install_log2ram_auto" "$FUNC_VERSION" @@ -1174,7 +1172,6 @@ install_log2ram_auto() { size 20M rotate 3 compress - delaycompress missingok notifempty copytruncate diff --git a/scripts/post_install/customizable_post_install.sh b/scripts/post_install/customizable_post_install.sh index 81601230..72e1b86d 100644 --- a/scripts/post_install/customizable_post_install.sh +++ b/scripts/post_install/customizable_post_install.sh @@ -2348,7 +2348,7 @@ setup_motd() { optimize_logrotate() { - local FUNC_VERSION="1.1" + local FUNC_VERSION="1.2" # description: Replace logrotate.conf with a Log2RAM-friendly profile (daily rotation, copytruncate). msg_info2 "$(translate "Optimizing logrotate configuration...")" @@ -2369,7 +2369,6 @@ su root adm rotate 7 size 10M compress -delaycompress missingok notifempty create 0640 root adm @@ -3264,7 +3263,6 @@ _update_existing_log2ram_custom() { size 20M rotate 3 compress - delaycompress missingok notifempty copytruncate @@ -3349,7 +3347,7 @@ EOF configure_log2ram() { - local FUNC_VERSION="1.7" + local FUNC_VERSION="1.8" local existing_log2ram_bin="" pmx_journal_context "configure_log2ram" "$FUNC_VERSION" # description: Install Log2RAM with user-chosen RAM size; prompts for size and SSD/M.2 awareness before applying. @@ -3487,7 +3485,6 @@ configure_log2ram() { size 20M rotate 3 compress - delaycompress missingok notifempty copytruncate