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.""" The post-install makes the sync remove that file from the disk copy."""
from pathlib import Path from pathlib import Path
import re
import shutil import shutil
import stat import stat
import subprocess import subprocess
@@ -86,7 +87,18 @@ class Log2ramJournalSyncTests(unittest.TestCase):
# An existing installation is told that it needs a reboot. # 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) 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. # 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__": if __name__ == "__main__":
+2 -1
View File
@@ -54,9 +54,10 @@ function taskState(task: Task): keyof typeof badgeTone {
return "error" return "error"
} }
// PBS reports "OK", "WARNINGS: n" or the error text of the last run.
function jobState(state?: string): keyof typeof badgeTone { function jobState(state?: string): keyof typeof badgeTone {
if (!state) return "muted" if (!state) return "muted"
if (state === "ok") return "ok" if (state.toLowerCase() === "ok") return "ok"
if (state.toLowerCase().startsWith("warn")) return "warn" if (state.toLowerCase().startsWith("warn")) return "warn"
return "error" return "error"
} }
+2 -14
View File
@@ -12242,18 +12242,6 @@ def _compute_node_metrics_payload(timeframe: str) -> dict:
deadline = time.monotonic() + _NODE_METRICS_TIMEOUT deadline = time.monotonic() + _NODE_METRICS_TIMEOUT
local_node = get_proxmox_node_name() 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: try:
rrd_result = subprocess.run( rrd_result = subprocess.run(
_node_rrd_command(local_node, timeframe), _node_rrd_command(local_node, timeframe),
@@ -12322,11 +12310,11 @@ def _compute_node_metrics_payload(timeframe: str) -> dict:
'code': 'metrics_invalid_data', '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: for item in rrd_data:
if 'arcsize' in item: if 'arcsize' in item:
item['zfsarc'] = item['arcsize'] 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): def _values_from(items, field_key, scale=1.0):
return [item[field_key] * scale for item in items 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 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(): def _local_datastores():
stores = _cli("datastore", "list") stores = _cli("datastore", "list")
result = [] result = []
@@ -325,9 +339,9 @@ def get_status(identifier):
services = _local_services() services = _local_services()
operations = {"datastores": _local_datastores, operations = {"datastores": _local_datastores,
"tasks": lambda: _cli("task", "list", "--all", "true", "--limit", "20"), "tasks": lambda: _cli("task", "list", "--all", "true", "--limit", "20"),
"sync": lambda: _cli("sync-job", "list"), "sync": lambda: _local_api("/admin/sync"),
"verify": lambda: _cli("verify-job", "list"), "verify": lambda: _local_api("/admin/verify"),
"prune": lambda: _cli("prune-job", "list")} "prune": lambda: _local_api("/admin/prune")}
else: else:
with _lock: with _lock:
server = next((row for row in _load_servers() if row["id"] == identifier), None) 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 test_local_status_reads_pbs_commands_and_filesystem_capacity(self):
def run(command, **kwargs): def run(command, **kwargs):
self.assertNotIn("shell", 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"]) self.assertEqual(command[-2:], ["--output-format", "json"])
data = [{"name": "backup", "path": str(self.root)}] if command[1:3] == ["datastore", "list"] else [] 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) return SimpleNamespace(stdout=json.dumps(data), returncode=0)
with mock.patch.object(pbs, "get_platform", return_value={"product": "pbs", "version": "4.2.5"}), \ 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}), \ 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["errors"], {})
self.assertEqual(result["datastores"][0]["used"], 30) self.assertEqual(result["datastores"][0]["used"], 30)
self.assertEqual(set(result["services"]), set(pbs.PBS_SERVICES)) 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 test_pbs_installed_next_to_pve_is_read_locally_with_its_own_version(self):
def run(command, **kwargs): def run(command, **kwargs):
+2 -5
View File
@@ -392,7 +392,7 @@ EOF
# ========================================================== # ==========================================================
optimize_logrotate() { optimize_logrotate() {
local FUNC_VERSION="1.1" local FUNC_VERSION="1.2"
pmx_journal_context "optimize_logrotate" "$FUNC_VERSION" pmx_journal_context "optimize_logrotate" "$FUNC_VERSION"
# description: Replace logrotate.conf with a Log2RAM-friendly profile (daily rotation, copytruncate). # description: Replace logrotate.conf with a Log2RAM-friendly profile (daily rotation, copytruncate).
msg_info "$(translate "Optimizing logrotate configuration...")" msg_info "$(translate "Optimizing logrotate configuration...")"
@@ -408,7 +408,6 @@ su root adm
rotate 7 rotate 7
size 10M size 10M
compress compress
delaycompress
missingok missingok
notifempty notifempty
create 0640 root adm create 0640 root adm
@@ -941,7 +940,6 @@ _update_existing_log2ram_auto() {
size 20M size 20M
rotate 3 rotate 3
compress compress
delaycompress
missingok missingok
notifempty notifempty
copytruncate copytruncate
@@ -1041,7 +1039,7 @@ EOF
install_log2ram_auto() { install_log2ram_auto() {
local FUNC_VERSION="1.7" local FUNC_VERSION="1.8"
local existing_log2ram_bin="" local existing_log2ram_bin=""
pmx_journal_context "install_log2ram_auto" "$FUNC_VERSION" pmx_journal_context "install_log2ram_auto" "$FUNC_VERSION"
@@ -1174,7 +1172,6 @@ install_log2ram_auto() {
size 20M size 20M
rotate 3 rotate 3
compress compress
delaycompress
missingok missingok
notifempty notifempty
copytruncate copytruncate
@@ -2348,7 +2348,7 @@ setup_motd() {
optimize_logrotate() { optimize_logrotate() {
local FUNC_VERSION="1.1" local FUNC_VERSION="1.2"
# description: Replace logrotate.conf with a Log2RAM-friendly profile (daily rotation, copytruncate). # description: Replace logrotate.conf with a Log2RAM-friendly profile (daily rotation, copytruncate).
msg_info2 "$(translate "Optimizing logrotate configuration...")" msg_info2 "$(translate "Optimizing logrotate configuration...")"
@@ -2369,7 +2369,6 @@ su root adm
rotate 7 rotate 7
size 10M size 10M
compress compress
delaycompress
missingok missingok
notifempty notifempty
create 0640 root adm create 0640 root adm
@@ -3264,7 +3263,6 @@ _update_existing_log2ram_custom() {
size 20M size 20M
rotate 3 rotate 3
compress compress
delaycompress
missingok missingok
notifempty notifempty
copytruncate copytruncate
@@ -3349,7 +3347,7 @@ EOF
configure_log2ram() { configure_log2ram() {
local FUNC_VERSION="1.7" local FUNC_VERSION="1.8"
local existing_log2ram_bin="" local existing_log2ram_bin=""
pmx_journal_context "configure_log2ram" "$FUNC_VERSION" 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. # 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 size 20M
rotate 3 rotate 3
compress compress
delaycompress
missingok missingok
notifempty notifempty
copytruncate copytruncate