fix: keep PBS logs and maintenance jobs readable

- Draw ZFS ARC only from recorded history, leaving it out on PBS
- Read the maintenance jobs of a local PBS from its API, with their last state
- Show the OK state of PBS jobs in green whatever its case
- Compress rotated logs at once in the Log2RAM and logrotate profiles
- Offer the Log2RAM 1.8 and logrotate 1.2 update to existing installations
This commit is contained in:
MacRimi
2026-10-20 00:53:21 +02:00
parent d8741a8c5d
commit 0a7a598210
7 changed files with 43 additions and 30 deletions
@@ -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__":
+2 -1
View File
@@ -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"
}
+2 -14
View File
@@ -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
+17 -3
View File
@@ -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)
+5 -1
View File
@@ -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):
+2 -5
View File
@@ -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
@@ -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