mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-02 19:02:31 +00:00
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:
@@ -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__":
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user