Merge pull request #450 from phyrexia/feature/zram-memory-profile

feat: add optional ZRAM swap profile and live metrics
This commit is contained in:
MacRimi
2026-10-16 17:28:11 +02:00
committed by GitHub
19 changed files with 1322 additions and 3 deletions
+36 -1
View File
@@ -12,7 +12,7 @@ import { ProcessDetailModal } from "./process-detail-modal"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
import { fetchApi } from "../lib/api-config" import { fetchApi } from "../lib/api-config"
import { formatNetworkTraffic, getNetworkUnit } from "../lib/format-network" import { formatNetworkTraffic, getNetworkUnit } from "../lib/format-network"
import { formatStorage } from "../lib/utils" import { formatBytes as formatByteSize, formatStorage } from "../lib/utils"
import { getCountFormKey, useI18n, useT } from "../lib/i18n/provider" import { getCountFormKey, useI18n, useT } from "../lib/i18n/provider"
import { Area, AreaChart, ResponsiveContainer } from "recharts" import { Area, AreaChart, ResponsiveContainer } from "recharts"
@@ -21,6 +21,19 @@ interface TempDataPoint {
value: number value: number
} }
interface ZramDevice {
name: string
active: boolean
capacity_bytes: number
swap_used_bytes: number
original_bytes: number
compressed_bytes: number
memory_used_bytes: number
algorithm: string | null
compression_ratio: number | null
saved_bytes: number
}
interface SystemData { interface SystemData {
cpu_usage: number cpu_usage: number
cpu_user?: number // preview restyle cpu_user?: number // preview restyle
@@ -29,6 +42,7 @@ interface SystemData {
memory_total: number memory_total: number
memory_used: number memory_used: number
memory_cached?: number // preview restyle memory_cached?: number // preview restyle
zram?: { available: boolean; devices: ZramDevice[] }
temperature: number temperature: number
temperature_sparkline?: TempDataPoint[] temperature_sparkline?: TempDataPoint[]
uptime: string uptime: string
@@ -489,6 +503,27 @@ export function SystemOverview() {
</div> </div>
</div> </div>
</div> </div>
{systemData.zram?.available && systemData.zram.devices.length > 0 && (
<div className="mt-4 border-t border-border pt-3 space-y-3">
{systemData.zram.devices.map((device) => (
<div key={device.name} className="text-xs">
<div className="font-medium mb-2">{t("overview.zram")} <span className="font-mono text-muted-foreground">{device.name}</span></div>
<div className="grid grid-cols-2 gap-x-3 gap-y-1">
<span className="text-muted-foreground">{t("overview.zramCapacity")}</span>
<span className="text-right font-mono">{formatByteSize(device.capacity_bytes)}</span>
<span className="text-muted-foreground">{t("overview.zramSwapUsed")}</span>
<span className="text-right font-mono">{formatByteSize(device.swap_used_bytes)}</span>
<span className="text-muted-foreground">{t("overview.zramAlgorithm")}</span>
<span className="text-right font-mono">{device.algorithm ?? "—"}</span>
<span className="text-muted-foreground">{t("overview.zramCompressionRatio")}</span>
<span className="text-right font-mono">{device.compression_ratio === null ? "—" : `${device.compression_ratio.toFixed(2)}:1`}</span>
<span className="text-muted-foreground">{t("overview.zramPhysicalMemory")}</span>
<span className="text-right font-mono">{formatByteSize(device.memory_used_bytes)}</span>
</div>
</div>
))}
</div>
)}
</CardContent> </CardContent>
</Card> </Card>
+6
View File
@@ -92,6 +92,12 @@
"loadAxis": "Laden", "loadAxis": "Laden",
"memory": "Arbeitsspeicher", "memory": "Arbeitsspeicher",
"memoryUsage": "Arbeitsspeichernutzung", "memoryUsage": "Arbeitsspeichernutzung",
"zram": "ZRAM",
"zramCapacity": "Kapazität",
"zramSwapUsed": "Verwendeter Swap",
"zramAlgorithm": "Algorithmus",
"zramCompressionRatio": "Kompressionsverhältnis",
"zramPhysicalMemory": "Verwendeter Arbeitsspeicher",
"user": "Benutzer", "user": "Benutzer",
"system": "System", "system": "System",
"cores": "Kerne", "cores": "Kerne",
+6
View File
@@ -91,6 +91,12 @@
"loadAxis": "Load", "loadAxis": "Load",
"memory": "Memory", "memory": "Memory",
"memoryUsage": "Memory Usage", "memoryUsage": "Memory Usage",
"zram": "ZRAM",
"zramCapacity": "Capacity",
"zramSwapUsed": "Swap used",
"zramAlgorithm": "Algorithm",
"zramCompressionRatio": "Compression ratio",
"zramPhysicalMemory": "Physical memory used",
"user": "User", "user": "User",
"system": "System", "system": "System",
"cores": "Cores", "cores": "Cores",
+6
View File
@@ -92,6 +92,12 @@
"loadAxis": "Carga", "loadAxis": "Carga",
"memory": "Memoria", "memory": "Memoria",
"memoryUsage": "Uso de la memoria", "memoryUsage": "Uso de la memoria",
"zram": "ZRAM",
"zramCapacity": "Capacidad",
"zramSwapUsed": "Swap usado",
"zramAlgorithm": "Algoritmo",
"zramCompressionRatio": "Relación de compresión",
"zramPhysicalMemory": "Memoria física usada",
"user": "Usuario", "user": "Usuario",
"system": "Sistema", "system": "Sistema",
"cores": "Núcleos", "cores": "Núcleos",
+6
View File
@@ -92,6 +92,12 @@
"loadAxis": "Charger", "loadAxis": "Charger",
"memory": "Mémoire", "memory": "Mémoire",
"memoryUsage": "Utilisation de la mémoire", "memoryUsage": "Utilisation de la mémoire",
"zram": "ZRAM",
"zramCapacity": "Capacité",
"zramSwapUsed": "Swap utilisé",
"zramAlgorithm": "Algorithme",
"zramCompressionRatio": "Taux de compression",
"zramPhysicalMemory": "Mémoire physique utilisée",
"user": "Utilisateur", "user": "Utilisateur",
"system": "Système", "system": "Système",
"cores": "Noyaux", "cores": "Noyaux",
+6
View File
@@ -92,6 +92,12 @@
"loadAxis": "Carico", "loadAxis": "Carico",
"memory": "Memoria", "memory": "Memoria",
"memoryUsage": "Utilizzo memoria", "memoryUsage": "Utilizzo memoria",
"zram": "ZRAM",
"zramCapacity": "Capacità",
"zramSwapUsed": "Swap utilizzato",
"zramAlgorithm": "Algoritmo",
"zramCompressionRatio": "Rapporto di compressione",
"zramPhysicalMemory": "Memoria fisica utilizzata",
"user": "Utente", "user": "Utente",
"system": "Sistema", "system": "Sistema",
"cores": "Core", "cores": "Core",
+6
View File
@@ -92,6 +92,12 @@
"loadAxis": "Carregar", "loadAxis": "Carregar",
"memory": "Memória", "memory": "Memória",
"memoryUsage": "Uso de memória", "memoryUsage": "Uso de memória",
"zram": "ZRAM",
"zramCapacity": "Capacidade",
"zramSwapUsed": "Swap usado",
"zramAlgorithm": "Algoritmo",
"zramCompressionRatio": "Taxa de compressão",
"zramPhysicalMemory": "Memória física usada",
"user": "Usuário", "user": "Usuário",
"system": "Sistema", "system": "Sistema",
"cores": "Núcleos", "cores": "Núcleos",
+6
View File
@@ -91,6 +91,12 @@
"loadAxis": "Záťaž", "loadAxis": "Záťaž",
"memory": "Pamäť", "memory": "Pamäť",
"memoryUsage": "Využitie pamäte", "memoryUsage": "Využitie pamäte",
"zram": "ZRAM",
"zramCapacity": "Kapacita",
"zramSwapUsed": "Použitý swap",
"zramAlgorithm": "Algoritmus",
"zramCompressionRatio": "Pomer kompresie",
"zramPhysicalMemory": "Použitá fyzická pamäť",
"user": "Používateľ", "user": "Používateľ",
"system": "Systém", "system": "Systém",
"cores": "Jadrá", "cores": "Jadrá",
+6
View File
@@ -92,6 +92,12 @@
"loadAxis": "Belastning", "loadAxis": "Belastning",
"memory": "Minne", "memory": "Minne",
"memoryUsage": "Minnesanvändning", "memoryUsage": "Minnesanvändning",
"zram": "ZRAM",
"zramCapacity": "Kapacitet",
"zramSwapUsed": "Använt swaputrymme",
"zramAlgorithm": "Algoritm",
"zramCompressionRatio": "Komprimeringsgrad",
"zramPhysicalMemory": "Använt fysiskt minne",
"user": "Användare", "user": "Användare",
"system": "System", "system": "System",
"cores": "Kärnor", "cores": "Kärnor",
+1
View File
@@ -126,6 +126,7 @@ cd "$SCRIPT_DIR"
# Copy Flask server # Copy Flask server
echo "📋 Copying Flask server..." echo "📋 Copying Flask server..."
cp "$SCRIPT_DIR/flask_server.py" "$APP_DIR/usr/bin/" cp "$SCRIPT_DIR/flask_server.py" "$APP_DIR/usr/bin/"
cp "$SCRIPT_DIR/zram_metrics.py" "$APP_DIR/usr/bin/"
cp "$SCRIPT_DIR/flask_auth_routes.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ flask_auth_routes.py not found" cp "$SCRIPT_DIR/flask_auth_routes.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ flask_auth_routes.py not found"
cp "$SCRIPT_DIR/auth_manager.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ auth_manager.py not found" cp "$SCRIPT_DIR/auth_manager.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ auth_manager.py not found"
cp "$SCRIPT_DIR/jwt_middleware.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ jwt_middleware.py not found" cp "$SCRIPT_DIR/jwt_middleware.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ jwt_middleware.py not found"
@@ -26,6 +26,7 @@ TOOL_METADATA = {
'system_limits': {'name': 'System Limits Increase', 'function': 'increase_system_limits', 'version': '1.1'}, 'system_limits': {'name': 'System Limits Increase', 'function': 'increase_system_limits', 'version': '1.1'},
# entropy removed — modern kernels 5.6+ have built-in entropy generation, haveged no longer needed # entropy removed — modern kernels 5.6+ have built-in entropy generation, haveged no longer needed
'memory_settings': {'name': 'Memory Settings Optimization', 'function': 'optimize_memory_settings', 'version': '1.1'}, 'memory_settings': {'name': 'Memory Settings Optimization', 'function': 'optimize_memory_settings', 'version': '1.1'},
'zram_swap': {'name': 'ZRAM Swap Profile', 'function': 'configure_zram_swap', 'version': '1.0'},
'kernel_panic': {'name': 'Kernel Panic Configuration', 'function': 'configure_kernel_panic', 'version': '1.0'}, 'kernel_panic': {'name': 'Kernel Panic Configuration', 'function': 'configure_kernel_panic', 'version': '1.0'},
'apt_ipv4': {'name': 'APT IPv4 Force', 'function': 'force_apt_ipv4', 'version': '1.0'}, 'apt_ipv4': {'name': 'APT IPv4 Force', 'function': 'force_apt_ipv4', 'version': '1.0'},
'kexec': {'name': 'kexec for quick reboots', 'function': 'enable_kexec', 'version': '1.0'}, 'kexec': {'name': 'kexec for quick reboots', 'function': 'enable_kexec', 'version': '1.0'},
+7
View File
@@ -78,6 +78,7 @@ from smartctl_resolver import ( # noqa: E402
smartctl_type_args, smartctl_type_args,
) )
from temperature_sensor_resolver import get_storage_temperatures # noqa: E402 from temperature_sensor_resolver import get_storage_temperatures # noqa: E402
from zram_metrics import read_zram_metrics # noqa: E402
from flask_script_runner import script_runner from flask_script_runner import script_runner
import threading import threading
from proxmox_storage_monitor import classify_storage_state, proxmox_storage_monitor from proxmox_storage_monitor import classify_storage_state, proxmox_storage_monitor
@@ -9837,6 +9838,11 @@ def api_system():
# Get temperature sparkline (last 1h) for overview mini chart # Get temperature sparkline (last 1h) for overview mini chart
temp_sparkline = get_temperature_sparkline(60) temp_sparkline = get_temperature_sparkline(60)
try:
zram = read_zram_metrics()
except Exception:
zram = {'available': False, 'devices': []}
return jsonify({ return jsonify({
'cpu_usage': round(cpu_usage, 1), 'cpu_usage': round(cpu_usage, 1),
'cpu_user': cpu_user_pct, 'cpu_user': cpu_user_pct,
@@ -9845,6 +9851,7 @@ def api_system():
'memory_total': round(memory_total_gb, 1), 'memory_total': round(memory_total_gb, 1),
'memory_used': round(memory_used_gb, 1), 'memory_used': round(memory_used_gb, 1),
'memory_cached': memory_cached_gb, 'memory_cached': memory_cached_gb,
'zram': zram,
'temperature': temp, 'temperature': temp,
'temperature_sparkline': temp_sparkline, 'temperature_sparkline': temp_sparkline,
'uptime': uptime, 'uptime': uptime,
+110
View File
@@ -0,0 +1,110 @@
"""Tests for reading zram metrics from injectable proc and sys paths."""
import importlib
import importlib.util
import sys
import tempfile
import unittest
from pathlib import Path
SCRIPTS_DIR = Path(__file__).resolve().parents[1]
if str(SCRIPTS_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPTS_DIR))
class ZramMetricsTests(unittest.TestCase):
def test_reader_module_is_available(self):
module_spec = importlib.util.find_spec("zram_metrics")
self.assertIsNotNone(module_spec)
module = importlib.import_module("zram_metrics")
self.assertTrue(callable(getattr(module, "read_zram_metrics", None)))
def setUp(self):
self.temporary = tempfile.TemporaryDirectory()
self.addCleanup(self.temporary.cleanup)
self.root = Path(self.temporary.name)
self.block_root = self.root / "sys" / "block"
self.block_root.mkdir(parents=True)
self.swaps_path = self.root / "proc" / "swaps"
self.swaps_path.parent.mkdir(parents=True)
self.swaps_path.write_text(
"Filename\tType\tSize\tUsed\tPriority\n", encoding="utf-8"
)
def add_device(self, name="zram0", mm_stat="2097152 1048576 1179648 0"):
device = self.block_root / name
device.mkdir()
(device / "disksize").write_text("1073741824\n", encoding="utf-8")
(device / "mm_stat").write_text(mm_stat + "\n", encoding="utf-8")
(device / "comp_algorithm").write_text(
"lzo lzo-rle [zstd] lz4\n", encoding="utf-8"
)
return device
def read_metrics(self):
from zram_metrics import read_zram_metrics
return read_zram_metrics(self.block_root, self.swaps_path)
def test_no_zram_devices_reports_unavailable(self):
(self.block_root / "sda").mkdir()
self.assertEqual(self.read_metrics(), {"available": False, "devices": []})
def test_reads_capacity_swap_usage_and_compression_counters(self):
self.add_device()
self.swaps_path.write_text(
"Filename\tType\tSize\tUsed\tPriority\n"
"/dev/zram0\tpartition\t1048576\t1024\t100\n",
encoding="utf-8",
)
self.assertEqual(
self.read_metrics(),
{
"available": True,
"devices": [
{
"name": "zram0",
"active": True,
"capacity_bytes": 1073741824,
"swap_used_bytes": 1048576,
"original_bytes": 2097152,
"compressed_bytes": 1048576,
"memory_used_bytes": 1179648,
"algorithm": "zstd",
"compression_ratio": 2.0,
"saved_bytes": 917504,
}
],
},
)
def test_zero_compressed_bytes_has_null_ratio(self):
self.add_device(mm_stat="2097152 0 0 0")
result = self.read_metrics()
self.assertTrue(result["available"])
self.assertEqual(len(result["devices"]), 1)
self.assertFalse(result["devices"][0]["active"])
self.assertEqual(result["devices"][0]["swap_used_bytes"], 0)
self.assertIsNone(result["devices"][0]["compression_ratio"])
def test_malformed_or_short_mm_stat_omits_device(self):
for mm_stat in ("not-a-number 1048576 1179648", "2097152 1048576"):
with self.subTest(mm_stat=mm_stat):
device = self.block_root / "zram0"
if device.exists():
(device / "mm_stat").write_text(mm_stat + "\n", encoding="utf-8")
else:
self.add_device(mm_stat=mm_stat)
result = self.read_metrics()
self.assertFalse(result["available"])
self.assertEqual(result["devices"], [])
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,356 @@
"""Contract tests for the opt-in ZRAM profile's pure Bash helpers."""
import os
import subprocess
import tempfile
import unittest
from pathlib import Path
HELPERS = Path(__file__).resolve().parents[3] / "scripts/post_install/zram_profile_helpers.sh"
SWAPS_HEADER = "Filename\tType\tSize\tUsed\tPriority\n"
class HelperAvailabilityTests(unittest.TestCase):
def test_helper_file_is_available(self):
self.assertTrue(HELPERS.is_file(), f"ZRAM helper is missing: {HELPERS}")
def test_helper_can_be_sourced_twice(self):
result = subprocess.run(
["bash", "-c", 'source "$1" && source "$1"', "bash", str(HELPERS)],
capture_output=True,
text=True,
check=False,
)
self.assertEqual(result.returncode, 0, result.stderr)
def call_helper(function, *arguments):
return subprocess.run(
[
"bash",
"-c",
'source "$1" || exit; shift; "$@"',
"bash",
str(HELPERS),
function,
*(str(argument) for argument in arguments),
],
capture_output=True,
text=True,
check=False,
)
class ZramGeneratorConfigTests(unittest.TestCase):
def assert_config(self, supports_algorithm_params, expected_algorithm):
result = call_helper("zram_render_generator_config", supports_algorithm_params)
self.assertEqual(result.returncode, 0, result.stderr)
settings = {
key.strip(): value.strip()
for line in result.stdout.splitlines()
if "=" in line
for key, value in [line.split("=", 1)]
}
self.assertIn("[zram0]", result.stdout.splitlines())
self.assertEqual(settings.get("zram-size", "").strip(), "ram / 10")
self.assertEqual(settings.get("swap-priority", "").strip(), "32767")
self.assertEqual(settings.get("compression-algorithm", "").strip(), expected_algorithm)
self.assertNotIn("writeback", result.stdout.lower())
def test_generator_1_1_2_2_uses_plain_zstd(self):
self.assert_config("false", "zstd")
def test_generator_1_2_1_2_uses_zstd_level_nine(self):
self.assert_config("true", "zstd(level=9)")
class ForeignActiveZramSwapTests(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory()
self.addCleanup(self.temporary.cleanup)
directory = Path(self.temporary.name)
self.root = directory
self.swaps_file = directory / "swaps"
def detect(self, swaps, owned_device=""):
self.swaps_file.write_text(swaps, encoding="utf-8")
return call_helper(
"zram_list_foreign_active_zram_swap", self.swaps_file, owned_device
)
def test_active_disk_swap_is_allowed(self):
result = self.detect(
SWAPS_HEADER + "/dev/nvme0n1p3\tpartition\t1048576\t0\t-2\n",
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout, "")
def test_active_unowned_zram0_is_detected(self):
result = self.detect(SWAPS_HEADER + "/dev/zram0\tpartition\t1048576\t0\t100\n")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout, "active zram: /dev/zram0\n")
def test_active_owned_zram0_is_allowed_alongside_disk_swap(self):
result = self.detect(
SWAPS_HEADER
+ "/dev/nvme0n1p3\tpartition\t1048576\t0\t-2\n"
+ "/dev/zram0\tpartition\t1048576\t0\t100\n",
"/dev/zram0",
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout, "")
def test_another_active_zram_device_is_rejected(self):
result = self.detect(
SWAPS_HEADER
+ "/dev/zram0\tpartition\t1048576\t0\t100\n"
+ "/dev/zram1\tpartition\t1048576\t0\t90\n",
"/dev/zram0",
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout, "active zram: /dev/zram1\n")
def test_swap_table_read_failure_is_reported(self):
fake_bin = self.root / "bin"
fake_bin.mkdir()
fake_awk = fake_bin / "awk"
fake_awk.write_text("#!/bin/sh\nexit 2\n", encoding="utf-8")
fake_awk.chmod(0o755)
env = os.environ.copy()
env["PATH"] = f"{fake_bin}:{env.get('PATH', '')}"
result = subprocess.run(
[
"bash",
"-c",
'source "$1" || exit; shift; "$@"',
"bash",
str(HELPERS),
"zram_list_foreign_active_zram_swap",
str(self.swaps_file),
],
capture_output=True,
text=True,
check=False,
env=env,
)
self.assertEqual(result.returncode, 2, result.stderr)
class ForeignFstabZramSwapTests(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory()
self.addCleanup(self.temporary.cleanup)
self.fstab_file = Path(self.temporary.name) / "fstab"
def detect(self, text):
self.fstab_file.write_text(text, encoding="utf-8")
return call_helper("zram_list_foreign_fstab_zram_swap", self.fstab_file)
def test_disk_swap_entries_are_allowed(self):
result = self.detect(
"UUID=swap-partition none swap defaults 0 0\n"
"/swapfile none swap sw 0 0\n"
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout, "")
def test_zram_swap_entry_is_reported(self):
result = self.detect(
"UUID=swap-partition none swap defaults 0 0\n"
"/dev/zram1 none swap pri=100 0 0\n"
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout, "fstab zram: /dev/zram1\n")
class ForeignZramConfigTests(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory()
self.addCleanup(self.temporary.cleanup)
self.root = Path(self.temporary.name)
self.etc = self.root / "etc/systemd"
(self.etc / "zram-generator.conf.d").mkdir(parents=True)
self.owned = self.etc / "zram-generator.conf.d/90-proxmenux.conf"
def foreign_configs(self):
return call_helper(
"zram_foreign_generator_config", self.owned, self.root
).stdout.splitlines()
def test_empty_packaged_default_does_not_block_profile(self):
(self.etc / "zram-generator.conf").write_text(
"# package default\n[zram0]\n; no active directives\n",
encoding="utf-8",
)
self.assertEqual(self.foreign_configs(), [])
def test_active_foreign_directive_is_reported(self):
foreign = self.etc / "zram-generator.conf"
foreign.write_text("[zram1]\nzram-size = ram / 4\n", encoding="utf-8")
self.assertEqual(self.foreign_configs(), [str(foreign)])
def test_owned_drop_in_is_not_reported(self):
self.owned.write_text(
"# ProxMenux ZRAM profile\n[zram0]\nzram-size = ram / 10\n",
encoding="utf-8",
)
self.assertEqual(self.foreign_configs(), [])
class ForeignSystemdZramUnitTests(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory()
self.addCleanup(self.temporary.cleanup)
self.root = Path(self.temporary.name)
self.system_dir = self.root / "etc/systemd/system"
self.system_dir.mkdir(parents=True)
def test_disk_swap_unit_is_allowed_and_zram_units_are_reported(self):
(self.system_dir / "pve-swap.swap").write_text(
"[Swap]\nWhat=/dev/mapper/pve-swap\n", encoding="utf-8"
)
(self.system_dir / "dev-zram0.swap").write_text(
"[Swap]\nWhat=/dev/zram0\n", encoding="utf-8"
)
external = self.system_dir / "external.swap"
external.write_text("[Swap]\nWhat=/dev/zram1\n", encoding="utf-8")
vendor_dir = self.root / "usr/lib/systemd/system"
vendor_dir.mkdir(parents=True)
vendor = vendor_dir / "vendor-zram.swap"
vendor.write_text("[Swap]\nWhat=/dev/zram2\n", encoding="utf-8")
result = call_helper("zram_list_foreign_systemd_zram_units", self.root)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(
set(result.stdout.splitlines()),
{
f"systemd zram unit: {self.system_dir / 'dev-zram0.swap'}",
f"systemd zram unit: {external}",
f"systemd zram unit: {vendor}",
},
)
def test_unrelated_custom_swap_unit_is_allowed(self):
(self.system_dir / "custom.swap").write_text(
"[Swap]\nWhat=/swapfile\n", encoding="utf-8"
)
result = call_helper("zram_list_foreign_systemd_zram_units", self.root)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout, "")
def test_reserved_zram0_unit_masked_with_dev_null_is_reported(self):
(self.system_dir / "dev-zram0.swap").symlink_to("/dev/null")
result = call_helper("zram_list_foreign_systemd_zram_units", self.root)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(
result.stdout,
f"systemd zram unit masked: {self.system_dir / 'dev-zram0.swap'}\n",
)
def test_zram_swap_drop_in_is_reported(self):
dropins = self.system_dir / "custom.swap.d"
dropins.mkdir()
dropin = dropins / "10-zram.conf"
dropin.write_text("[Swap]\nWhat=/dev/zram1\n", encoding="utf-8")
result = call_helper("zram_list_foreign_systemd_zram_units", self.root)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout, f"systemd zram unit: {dropin}\n")
def test_generator_wants_symlink_is_not_a_foreign_unit_file(self):
generator = self.root / "run/systemd/generator"
generator.mkdir(parents=True)
(generator / "dev-zram0.swap").write_text(
"[Swap]\nWhat=/dev/zram0\n", encoding="utf-8"
)
wants = self.root / "run/systemd/system/swap.target.wants"
wants.mkdir(parents=True)
(wants / "dev-zram0.swap").symlink_to("../../generator/dev-zram0.swap")
result = call_helper("zram_list_foreign_systemd_zram_units", self.root)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout, "")
class ZramProfileActivationVerifierTests(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory()
self.addCleanup(self.temporary.cleanup)
self.root = Path(self.temporary.name)
self.block_root = self.root / "sys/block"
zram0 = self.block_root / "zram0"
zram0.mkdir(parents=True)
(zram0 / "disksize").write_text("3435973836\n", encoding="utf-8")
(zram0 / "comp_algorithm").write_text(
"lzo [zstd] lz4\n", encoding="utf-8"
)
self.meminfo = self.root / "proc/meminfo"
self.meminfo.parent.mkdir(parents=True)
self.meminfo.write_text("MemTotal: 33554432 kB\n", encoding="utf-8")
self.swaps = self.root / "proc/swaps"
self.swaps.write_text(SWAPS_HEADER, encoding="utf-8")
def verify(self):
return call_helper(
"zram_profile_verify_active",
"zstd",
"32767",
self.block_root,
self.meminfo,
self.swaps,
)
def test_profile_is_valid_with_lower_priority_disk_swap_active(self):
self.swaps.write_text(
SWAPS_HEADER
+ "/dev/zram0\tpartition\t3355443\t0\t32767\n"
+ "/dev/nvme0n1p3\tpartition\t1048576\t32\t-2\n",
encoding="utf-8",
)
result = self.verify()
self.assertEqual(result.returncode, 0, result.stderr)
def test_profile_rejects_a_second_active_zram_device(self):
self.swaps.write_text(
SWAPS_HEADER
+ "/dev/zram0\tpartition\t3355443\t0\t32767\n"
+ "/dev/zram1\tpartition\t1048576\t0\t90\n",
encoding="utf-8",
)
result = self.verify()
self.assertEqual(result.returncode, 1, result.stderr)
def test_profile_rejects_zram_device_without_requested_priority(self):
self.swaps.write_text(
SWAPS_HEADER
+ "/dev/zram0\tpartition\t3355443\t0\t100\n",
encoding="utf-8",
)
result = self.verify()
self.assertEqual(result.returncode, 1, result.stderr)
if __name__ == "__main__":
unittest.main()
+68
View File
@@ -0,0 +1,68 @@
"""Read zram swap metrics from procfs and sysfs."""
import re
from pathlib import Path
def _swap_usage(swaps_path):
usage = {}
try:
lines = Path(swaps_path).read_text(encoding="utf-8").splitlines()
except OSError:
return usage
for line in lines[1:]:
fields = line.split()
if len(fields) < 4:
continue
name = Path(fields[0]).name
if not re.fullmatch(r"zram\d+", name):
continue
try:
usage[name] = int(fields[3]) * 1024
except ValueError:
continue
return usage
def read_zram_metrics(sys_block_root="/sys/block", swaps_path="/proc/swaps"):
"""Return zram device counters with byte units and active swap usage."""
root = Path(sys_block_root)
try:
device_paths = sorted(
path for path in root.iterdir()
if path.is_dir() and re.fullmatch(r"zram\d+", path.name)
)
except OSError:
device_paths = []
swap_usage = _swap_usage(swaps_path)
devices = []
for path in device_paths:
try:
capacity = int((path / "disksize").read_text(encoding="utf-8").strip())
counters = (path / "mm_stat").read_text(encoding="utf-8").split()
if len(counters) < 3:
continue
original, compressed, memory_used = map(int, counters[:3])
if min(capacity, original, compressed, memory_used) < 0:
continue
algorithms = (path / "comp_algorithm").read_text(encoding="utf-8")
except (OSError, ValueError):
continue
selected = re.search(r"\[([^\]]+)\]", algorithms)
devices.append({
"name": path.name,
"active": path.name in swap_usage,
"capacity_bytes": capacity,
"swap_used_bytes": swap_usage.get(path.name, 0),
"original_bytes": original,
"compressed_bytes": compressed,
"memory_used_bytes": memory_used,
"algorithm": selected.group(1) if selected else None,
"compression_ratio": original / compressed if compressed else None,
"saved_bytes": original - memory_used,
})
return {"available": bool(devices), "devices": devices}
@@ -63,6 +63,9 @@ fi
if [[ -f "$LOCAL_SCRIPTS/global/pmx_journal.sh" ]]; then if [[ -f "$LOCAL_SCRIPTS/global/pmx_journal.sh" ]]; then
source "$LOCAL_SCRIPTS/global/pmx_journal.sh" source "$LOCAL_SCRIPTS/global/pmx_journal.sh"
fi fi
if [[ -f "$LOCAL_SCRIPTS/post_install/zram_profile_helpers.sh" ]]; then
source "$LOCAL_SCRIPTS/post_install/zram_profile_helpers.sh"
fi
# ========================================================== # ==========================================================
@@ -2448,8 +2451,182 @@ EOF
} }
configure_zram_swap() {
local FUNC_VERSION="1.0"
# description: Enable an opt-in ZRAM swap profile sized to one tenth of RAM.
pmx_journal_context "configure_zram_swap" "$FUNC_VERSION"
local config=/etc/systemd/zram-generator.conf.d/90-proxmenux.conf
local marker="$BASE_DIR/zram-swap.state"
local package_added=false package_owned=false owned=false previous_active=false state_mutated=false
local previous_config="" previous_marker="" desired="" conflicts="" fstab_conflicts="" systemd_conflicts="" version="" algorithm=""
local supports_params=false failure="" old_umask allowed_zram=""
if ! declare -f zram_render_generator_config >/dev/null ||
! declare -f zram_list_foreign_active_zram_swap >/dev/null ||
! declare -f zram_list_foreign_fstab_zram_swap >/dev/null ||
! declare -f zram_list_foreign_systemd_zram_units >/dev/null ||
! declare -f zram_foreign_generator_config >/dev/null ||
! declare -f zram_profile_verify_active >/dev/null; then
msg_error "$(translate "ZRAM profile helpers are missing")"
return 1
fi
if [[ ! -r /proc/swaps || ! -r /etc/fstab ]]; then
msg_error "$(translate "Cannot inspect current swap configuration")"
return 1
fi
# Validate ownership before allowing the existing /dev/zram0 device.
if [[ -e "$marker" ]]; then
if [[ ! -f "$config" || -L "$config" || -L "$marker" ]] ||
! grep -qx '# ProxMenux ZRAM profile' "$config" ||
! grep -Eq '^installed_by_proxmenux=(true|false)$' "$marker"; then
msg_error "$(translate "ZRAM ownership state is incomplete; inspect it before retrying")"
return 1
fi
owned=true
previous_config="$(cat "$config")"
previous_marker="$(cat "$marker")"
grep -qx 'installed_by_proxmenux=true' "$marker" && package_owned=true
allowed_zram=/dev/zram0
fi
if ! conflicts="$(zram_list_foreign_active_zram_swap /proc/swaps "$allowed_zram")" ||
! fstab_conflicts="$(zram_list_foreign_fstab_zram_swap /etc/fstab)" ||
! systemd_conflicts="$(zram_list_foreign_systemd_zram_units)"; then
msg_error "$(translate "Cannot inspect existing ZRAM configurations")"
return 1
fi
if [[ -n "$fstab_conflicts" ]]; then
[[ -n "$conflicts" ]] && conflicts+=$'\n'
conflicts+="$fstab_conflicts"
fi
if [[ -n "$systemd_conflicts" ]]; then
[[ -n "$conflicts" ]] && conflicts+=$'\n'
conflicts+="$systemd_conflicts"
fi
if [[ -n "$conflicts" ]]; then
msg_error "$(translate "Another ZRAM swap setup exists; refusing to replace it:") $conflicts"
return 1
fi
if ! conflicts="$(zram_foreign_generator_config "$config")"; then
msg_error "$(translate "Cannot inspect existing ZRAM generator configuration")"
return 1
fi
if [[ -n "$conflicts" ]]; then
msg_error "$(translate "Another ZRAM generator configuration exists; refusing to overwrite it:") $conflicts"
return 1
fi
if [[ "$owned" == false && ( -e "$config" || -L "$config" ) ]]; then
msg_error "$(translate "Existing zram0 configuration is not owned by ProxMenux")"
return 1
fi
if awk 'NR > 1 && $1 == "/dev/zram0" { found=1 } END { exit !found }' /proc/swaps; then
previous_active=true
fi
if ! dpkg-query -W -f='${Status}' systemd-zram-generator 2>/dev/null | grep -q 'install ok installed'; then
if ! pmx_install_pkg systemd-zram-generator; then
if [[ "$owned" == false ]] &&
dpkg-query -W -f='${Status}' systemd-zram-generator 2>/dev/null | grep -q 'install ok installed' &&
[[ -z "$(zram_foreign_generator_config "$config")" ]]; then
if DEBIAN_FRONTEND=noninteractive apt-get purge -y systemd-zram-generator >/dev/null 2>&1; then
pmx_record_uninstall "systemd-zram-generator" "$FUNC_VERSION"
fi
fi
msg_error "$(translate "Could not install systemd-zram-generator")"
return 1
fi
package_added=true
package_owned=true
fi
version="$(dpkg-query -W -f='${Version}' systemd-zram-generator 2>/dev/null)"
if [[ -z "$version" ]]; then
failure="Cannot determine installed systemd-zram-generator version"
else
if dpkg --compare-versions "$version" ge '1.2.0'; then
supports_params=true
fi
desired="$(zram_render_generator_config "$supports_params")"
algorithm=zstd
fi
if [[ -z "$failure" && "$owned" == true && "$previous_config" == "$desired" && "$previous_active" == true ]]; then
if zram_profile_verify_active "$algorithm"; then
register_tool "zram_swap" true "$FUNC_VERSION"
msg_success "$(translate "ZRAM profile is already active")"
return 0
fi
failure="Existing ZRAM profile is active but does not match its configuration"
fi
if [[ -z "$failure" && "$previous_active" == true ]]; then
if ! swapoff /dev/zram0; then
msg_error "$(translate "Could not release ZRAM pages; existing profile remains active")"
return 1
fi
state_mutated=true
if ! systemctl stop dev-zram0.swap >/dev/null 2>&1; then
failure="Could not stop existing dev-zram0.swap"
fi
fi
if [[ -z "$failure" ]]; then
state_mutated=true
if ! install -d -m 755 "$(dirname "$config")"; then
failure="Could not create ZRAM generator directory"
elif ! printf '%s\n' "$desired" | pmx_write_file "$config"; then
failure="Could not write ZRAM generator configuration"
fi
fi
if [[ -z "$failure" ]]; then
old_umask="$(umask)"
umask 077
if ! printf 'installed_by_proxmenux=%s\n' "$package_owned" | pmx_write_file "$marker"; then
failure="Could not write ZRAM package ownership state"
fi
umask "$old_umask"
[[ -f "$marker" ]] && chmod 600 "$marker"
fi
if [[ -z "$failure" ]]; then
if ! systemctl daemon-reload || ! systemctl start dev-zram0.swap; then
failure="Could not activate dev-zram0.swap"
elif ! zram_profile_verify_active "$algorithm"; then
failure="Active ZRAM algorithm, capacity or swap sources did not match the profile"
fi
fi
if [[ -z "$failure" ]]; then
register_tool "zram_swap" true "$FUNC_VERSION"
msg_success "$(translate "ZRAM swap profile enabled")"
return 0
fi
msg_error "$(translate "ZRAM profile activation failed; details:") $failure"
if [[ "$state_mutated" == true ]] && awk 'NR > 1 && $1 == "/dev/zram0" { found=1 } END { exit !found }' /proc/swaps; then
if ! swapoff /dev/zram0; then
# Keep the drop-in and marker so a later uninstall can retry safely.
register_tool "zram_swap" true "$FUNC_VERSION"
msg_error "$(translate "ZRAM still holds pages; configuration was preserved")"
return 1
fi
fi
if [[ "$state_mutated" == true ]]; then
systemctl stop dev-zram0.swap >/dev/null 2>&1 || true
if [[ "$owned" == true ]]; then
printf '%s\n' "$previous_config" | pmx_write_file "$config"
printf '%s\n' "$previous_marker" | pmx_write_file "$marker"
chmod 600 "$marker"
else
pmx_remove_file "$config"
pmx_remove_file "$marker"
fi
systemctl daemon-reload >/dev/null 2>&1 || true
[[ "$previous_active" == true ]] && systemctl start dev-zram0.swap >/dev/null 2>&1
fi
if [[ "$package_added" == true && "$owned" == false && ! -e "$config" && ! -e "$marker" && -z "$(zram_foreign_generator_config "$config")" ]]; then
if DEBIAN_FRONTEND=noninteractive apt-get purge -y systemd-zram-generator >/dev/null 2>&1; then
pmx_record_uninstall "systemd-zram-generator" "$FUNC_VERSION"
fi
fi
return 1
}
# ========================================================== # ==========================================================
@@ -3674,6 +3851,7 @@ custom_post_description_label() {
"Enable High Availability services") translate "Enable High Availability services" ;; "Enable High Availability services") translate "Enable High Availability services" ;;
"Install Figurine") translate "Install Figurine" ;; "Install Figurine") translate "Install Figurine" ;;
"Install and configure Log2RAM") translate "Install and configure Log2RAM" ;; "Install and configure Log2RAM") translate "Install and configure Log2RAM" ;;
"Enable ZRAM swap profile") translate "Enable ZRAM swap profile" ;;
*) echo "$1" ;; *) echo "$1" ;;
esac esac
} }
@@ -3769,6 +3947,7 @@ main_menu() {
"System|Increase various system limits|LIMITS" "System|Increase various system limits|LIMITS"
# Entropy (haveged) removed — modern kernels 5.6+ have built-in entropy generation # Entropy (haveged) removed — modern kernels 5.6+ have built-in entropy generation
"System|Optimize Memory|MEMORYFIXES" "System|Optimize Memory|MEMORYFIXES"
"Optional|Enable ZRAM swap profile|ZRAM_SWAP"
"System|Enable fast reboots|KEXEC" "System|Enable fast reboots|KEXEC"
"System|Enable restart on kernel panic|KERNELPANIC" "System|Enable restart on kernel panic|KERNELPANIC"
"Optional|Apply AMD CPU fixes|AMDFIXES" "Optional|Apply AMD CPU fixes|AMDFIXES"
@@ -3912,6 +4091,7 @@ done
LIMITS) increase_system_limits ;; LIMITS) increase_system_limits ;;
# ENTROPY removed — modern kernels 5.6+ have built-in entropy # ENTROPY removed — modern kernels 5.6+ have built-in entropy
MEMORYFIXES) optimize_memory_settings ;; MEMORYFIXES) optimize_memory_settings ;;
ZRAM_SWAP) configure_zram_swap ;;
KEXEC) enable_kexec ;; KEXEC) enable_kexec ;;
KERNELPANIC) configure_kernel_panic ;; KERNELPANIC) configure_kernel_panic ;;
AMDFIXES) apply_amd_fixes ;; AMDFIXES) apply_amd_fixes ;;
@@ -0,0 +1,286 @@
#!/bin/bash
set -euo pipefail
SCRIPT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
REPO_ROOT="$(cd "$SCRIPT_ROOT/../.." && pwd)"
TEST_ROOT="$(mktemp -d)"
trap 'rm -rf "$TEST_ROOT"' EXIT
CONFIG="$TEST_ROOT/etc/systemd/zram-generator.conf.d/90-proxmenux.conf"
MARKER="$TEST_ROOT/state/zram-swap.state"
SWAPS="$TEST_ROOT/proc/swaps"
FSTAB="$TEST_ROOT/etc/fstab"
EXPECTED_FSTAB="$TEST_ROOT/fstab.expected"
VERIFY_MODE=0
START_COUNT=0
PURGE_COUNT=0
fail() {
printf 'FAIL: %s\n' "$*" >&2
exit 1
}
load_language() { :; }
initialize_cache() { :; }
translate() { printf '%s' "$1"; }
msg_info() { :; }
msg_info2() { :; }
msg_ok() { :; }
msg_warn() { :; }
msg_error() { printf '%s\n' "$*" >&2; }
msg_success() { :; }
show_proxmenux_logo() { :; }
clear() { :; }
pmx_install_pkg() { return 1; }
pmx_record_uninstall() { :; }
apt-get() {
if [[ "$*" == "purge -y systemd-zram-generator" ]]; then
PURGE_COUNT=$((PURGE_COUNT + 1))
fi
return 0
}
pmx_journal_context() { :; }
dpkg-query() {
case "$*" in
*'${Status}'*) printf 'install ok installed\n' ;;
*'${Version}'*) printf '1.2.0-1\n' ;;
*) return 1 ;;
esac
}
dpkg() {
[[ "$1" == "--compare-versions" && "$3" == "ge" && "$2" == "1.2.0-1" && "$4" == "1.2.0" ]]
}
systemctl() {
case "$1" in
daemon-reload|stop) return 0 ;;
start)
START_COUNT=$((START_COUNT + 1))
if [[ "${2:-}" == "dev-zram0.swap" ]] &&
! awk 'NR > 1 && $1 == "/dev/zram0" { found=1 } END { exit !found }' "$SWAPS"; then
printf '/dev/zram0\tpartition\t3355443\t0\t32767\n' >> "$SWAPS"
fi
return 0
;;
is-active) return 1 ;;
*) return 0 ;;
esac
}
swapoff() {
local target="${1:-}" next="$TEST_ROOT/swaps.next"
[[ -n "$target" ]] || fail "swapoff was called without a target"
awk -v target="$target" 'NR == 1 || $1 != target' "$SWAPS" > "$next"
mv "$next" "$SWAPS"
}
PMX_JOURNAL_ROOT="$TEST_ROOT/journal"
export PMX_JOURNAL_ROOT
source "$REPO_ROOT/scripts/global/pmx_journal.sh"
mkdir -p "$TEST_ROOT/host/scripts/post_install"
cp "$SCRIPT_ROOT/zram_profile_helpers.sh" "$TEST_ROOT/host/scripts/post_install/"
# Isolate only host paths; source the real installer function and helper library.
python3 - "$SCRIPT_ROOT/customizable_post_install.sh" "$TEST_ROOT/customizable.sh" "$TEST_ROOT" <<'PY'
from pathlib import Path
import sys
source_path, output_path, root = map(Path, sys.argv[1:])
text = source_path.read_text(encoding="utf-8")
def replace_exact(old, new, expected=1):
global text
count = text.count(old)
if count != expected:
raise SystemExit(f"expected {expected} path substitutions, found {count}: {old}")
text = text.replace(old, new)
replace_exact(
'LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"',
f'LOCAL_SCRIPTS="{root}/host/scripts"',
)
replace_exact(
'BASE_DIR="/usr/local/share/proxmenux"',
f'BASE_DIR="{root}/state"',
)
replace_exact(
'UTILS_FILE="$BASE_DIR/utils.sh"',
f'UTILS_FILE="{root}/missing-utils.sh"',
)
replace_exact(
'TOOLS_JSON="/usr/local/share/proxmenux/installed_tools.json"',
f'TOOLS_JSON="{root}/state/installed_tools.json"',
)
replace_exact(
'local config=/etc/systemd/zram-generator.conf.d/90-proxmenux.conf',
f'local config={root}/etc/systemd/zram-generator.conf.d/90-proxmenux.conf',
)
replace_exact(
'if [[ ! -r /proc/swaps || ! -r /etc/fstab ]]; then',
f'if [[ ! -r "{root}/proc/swaps" || ! -r "{root}/etc/fstab" ]]; then',
)
replace_exact(
'zram_list_foreign_active_zram_swap /proc/swaps "$allowed_zram"',
f'zram_list_foreign_active_zram_swap "{root}/proc/swaps" "$allowed_zram"',
)
replace_exact(
'zram_list_foreign_fstab_zram_swap /etc/fstab',
f'zram_list_foreign_fstab_zram_swap "{root}/etc/fstab"',
)
replace_exact(
'zram_list_foreign_systemd_zram_units)',
'zram_list_foreign_systemd_zram_units "$TEST_ROOT")',
)
replace_exact(
'zram_foreign_generator_config "$config"',
'zram_foreign_generator_config "$config" "$TEST_ROOT"',
expected=3,
)
replace_exact(
"awk 'NR > 1 && $1 == \"/dev/zram0\" { found=1 } END { exit !found }' /proc/swaps",
f"awk 'NR > 1 && $1 == \"/dev/zram0\" {{ found=1 }} END {{ exit !found }}' \"{root}/proc/swaps\"",
expected=2,
)
output_path.write_text(text, encoding="utf-8")
PY
# Isolate the real uninstall function and redirect only the paths it inspects.
python3 - "$SCRIPT_ROOT/uninstall-tools.sh" "$TEST_ROOT/uninstall-zram.sh" "$TEST_ROOT" <<'PY'
from pathlib import Path
import sys
source_path, output_path, root = map(Path, sys.argv[1:])
text = source_path.read_text(encoding="utf-8")
start = text.index("uninstall_zram_swap() {")
end = text.index("\n}\n", start) + 3
function = text[start:end]
replacements = {
"local config=/etc/systemd/zram-generator.conf.d/90-proxmenux.conf":
f"local config={root}/etc/systemd/zram-generator.conf.d/90-proxmenux.conf",
'awk \'NR > 1 && $1 == "/dev/zram0" { found=1 } END { exit !found }\' /proc/swaps':
f'awk \'NR > 1 && $1 == "/dev/zram0" {{ found=1 }} END {{ exit !found }}\' "{root}/proc/swaps"',
'zram_foreign_generator_config "$config"':
'zram_foreign_generator_config "$config" "$TEST_ROOT"',
'zram_list_foreign_active_zram_swap /proc/swaps /dev/zram0':
f'zram_list_foreign_active_zram_swap "{root}/proc/swaps" /dev/zram0',
'zram_list_foreign_fstab_zram_swap /etc/fstab':
f'zram_list_foreign_fstab_zram_swap "{root}/etc/fstab"',
'zram_list_foreign_systemd_zram_units)':
'zram_list_foreign_systemd_zram_units "$TEST_ROOT")',
}
for old, new in replacements.items():
if function.count(old) != 1:
raise SystemExit(f"expected one uninstall path to rewrite: {old}")
function = function.replace(old, new)
output_path.write_text(function, encoding="utf-8")
PY
source "$TEST_ROOT/customizable.sh"
source "$TEST_ROOT/uninstall-zram.sh"
BASE_DIR="$TEST_ROOT/state"
TOOLS_JSON="$TEST_ROOT/state/installed_tools.json"
mkdir -p "$BASE_DIR"
register_tool() { printf '%s|%s|%s\n' "$1" "$2" "${3:-}" >> "$TEST_ROOT/registry.log"; }
zram_profile_verify_active() { return "$VERIFY_MODE"; }
reset_fixture() {
rm -rf "$TEST_ROOT/etc" "$TEST_ROOT/run" "$TEST_ROOT/proc" "$BASE_DIR" "$TEST_ROOT/journal"
mkdir -p "$TEST_ROOT/etc/systemd/system" "$TEST_ROOT/proc" "$BASE_DIR" "$TEST_ROOT/run/systemd/system"
printf 'Filename\tType\tSize\tUsed\tPriority\n/dev/nvme0n1p3\tpartition\t1048576\t8\t-2\n' > "$SWAPS"
printf '/dev/nvme0n1p3 none swap defaults 0 0\n/swapfile none swap sw 0 0\n' > "$FSTAB"
cp "$FSTAB" "$EXPECTED_FSTAB"
printf '[Swap]\nWhat=/dev/mapper/pve-swap\n' > "$TEST_ROOT/etc/systemd/system/pve-swap.swap"
PMX_JOURNAL_ROOT="$TEST_ROOT/journal"
START_COUNT=0
PURGE_COUNT=0
VERIFY_MODE=0
: > "$TEST_ROOT/registry.log"
}
assert_disk_swap_unchanged() {
cmp -s "$FSTAB" "$EXPECTED_FSTAB" || fail "installer changed fstab"
grep -Fq '/dev/nvme0n1p3' "$SWAPS" || fail "active disk swap was removed"
}
reset_fixture
configure_zram_swap || fail "disk swap prevented ZRAM installation"
grep -Fq 'swap-priority = 32767' "$CONFIG" || fail "ZRAM priority is not maximal"
grep -Fq 'installed_by_proxmenux=false' "$MARKER" || fail "package ownership was not preserved"
grep -Fq '/dev/zram0' "$SWAPS" || fail "ZRAM did not become active"
assert_disk_swap_unchanged
configure_zram_swap || fail "owned ZRAM profile could not be re-applied with disk swap"
[[ "$START_COUNT" -eq 1 ]] || fail "matching owned profile was not idempotent"
assert_disk_swap_unchanged
printf 'PASS: disk swap coexists and owned profile is idempotent\n'
reset_fixture
printf '/dev/zram1 none swap defaults 0 0\n' >> "$FSTAB"
cp "$FSTAB" "$EXPECTED_FSTAB"
if configure_zram_swap; then
fail "foreign fstab ZRAM entry was accepted"
fi
[[ ! -e "$CONFIG" && ! -e "$MARKER" ]] || fail "foreign fstab ZRAM entry left partial profile state"
assert_disk_swap_unchanged
printf 'PASS: foreign fstab ZRAM setup is refused without changing disk swap\n'
reset_fixture
printf '%s\n' '[Swap]' 'What=/dev/zram1' > "$TEST_ROOT/etc/systemd/system/external-zram.swap"
if configure_zram_swap; then
fail "foreign systemd ZRAM unit was accepted"
fi
[[ ! -e "$CONFIG" && ! -e "$MARKER" ]] || fail "foreign systemd ZRAM unit left partial profile state"
assert_disk_swap_unchanged
printf 'PASS: foreign systemd ZRAM unit is refused without changing disk swap\n'
reset_fixture
VERIFY_MODE=1
if configure_zram_swap; then
fail "activation verification failure was reported as success"
fi
[[ ! -e "$CONFIG" && ! -e "$MARKER" ]] || fail "failed activation left partial profile state"
if awk 'NR > 1 && $1 == "/dev/zram0" { found=1 } END { exit !found }' "$SWAPS"; then
fail "rollback left the failed ZRAM device active"
fi
assert_disk_swap_unchanged
printf 'PASS: verification failure rolls back only the ProxMenux ZRAM state\n'
reset_fixture
mkdir -p "$(dirname "$CONFIG")"
printf '# ProxMenux ZRAM profile\n[zram0]\nzram-size = ram / 10\n' > "$CONFIG"
printf 'installed_by_proxmenux=true\n' > "$MARKER"
printf '/dev/zram1 none swap defaults 0 0\n' >> "$FSTAB"
printf '[Swap]\nWhat=/dev/zram2\n' > "$TEST_ROOT/etc/systemd/system/external-zram.swap"
cp "$FSTAB" "$EXPECTED_FSTAB"
uninstall_zram_swap || fail "profile uninstall failed with foreign ZRAM configuration"
[[ "$PURGE_COUNT" -eq 0 ]] || fail "uninstaller purged generator while foreign ZRAM config exists"
[[ ! -e "$CONFIG" && ! -e "$MARKER" ]] || fail "uninstaller left ProxMenux profile files behind"
assert_disk_swap_unchanged
printf 'PASS: uninstall retains generator when foreign ZRAM config exists\n'
reset_fixture
mkdir -p "$(dirname "$CONFIG")"
printf '# ProxMenux ZRAM profile\n[zram0]\nzram-size = ram / 10\n' > "$CONFIG"
printf 'installed_by_proxmenux=true\n' > "$MARKER"
printf '%s\n' '[Swap]' 'What=/dev/zram2' > "$TEST_ROOT/etc/systemd/system/external-zram.swap"
uninstall_zram_swap || fail "profile uninstall failed with foreign systemd ZRAM unit"
[[ "$PURGE_COUNT" -eq 0 ]] || fail "uninstaller purged generator while foreign systemd ZRAM unit exists"
assert_disk_swap_unchanged
printf 'PASS: uninstall retains generator for foreign systemd ZRAM unit\n'
reset_fixture
mkdir -p "$(dirname "$CONFIG")"
printf '# ProxMenux ZRAM profile\n[zram0]\nzram-size = ram / 10\n' > "$CONFIG"
printf 'installed_by_proxmenux=true\n' > "$MARKER"
printf '/dev/zram0\tpartition\t3355443\t8\t32767\n' >> "$SWAPS"
uninstall_zram_swap || fail "owned profile could not be uninstalled"
[[ "$PURGE_COUNT" -eq 1 ]] || fail "owned generator package was not purged"
if awk 'NR > 1 && $1 == "/dev/zram0" { found=1 } END { exit !found }' "$SWAPS"; then
fail "uninstaller left ProxMenux ZRAM active"
fi
assert_disk_swap_unchanged
printf 'PASS: uninstall removes owned ZRAM and preserves disk swap\n'
+95 -2
View File
@@ -37,6 +37,9 @@ fi
if [[ -f "$LOCAL_SCRIPTS/global/pmx_journal.sh" ]]; then if [[ -f "$LOCAL_SCRIPTS/global/pmx_journal.sh" ]]; then
source "$LOCAL_SCRIPTS/global/pmx_journal.sh" source "$LOCAL_SCRIPTS/global/pmx_journal.sh"
fi fi
if [[ -f "$LOCAL_SCRIPTS/post_install/zram_profile_helpers.sh" ]]; then
source "$LOCAL_SCRIPTS/post_install/zram_profile_helpers.sh"
fi
load_language load_language
initialize_cache initialize_cache
@@ -55,6 +58,83 @@ register_tool() {
################################################################ ################################################################
uninstall_zram_swap() {
local FUNC_VERSION="1.0"
pmx_journal_context "uninstall_zram_swap" "$FUNC_VERSION"
local config=/etc/systemd/zram-generator.conf.d/90-proxmenux.conf
local marker="$BASE_DIR/zram-swap.state"
local package_owned=false foreign="" other_active="" fstab_zram="" systemd_zram="" inspection_failed=false old_active=false
local old_config="" old_marker=""
if ! declare -f zram_foreign_generator_config >/dev/null ||
! declare -f zram_list_foreign_active_zram_swap >/dev/null ||
! declare -f zram_list_foreign_fstab_zram_swap >/dev/null ||
! declare -f zram_list_foreign_systemd_zram_units >/dev/null; then
msg_error "$(translate "ZRAM profile helpers are missing")"
return 1
fi
if [[ ! -f "$config" || ! -f "$marker" || -L "$config" || -L "$marker" ]] ||
! grep -qx '# ProxMenux ZRAM profile' "$config" ||
! grep -Eq '^installed_by_proxmenux=(true|false)$' "$marker"; then
msg_error "$(translate "ZRAM ownership state is incomplete; no files were removed")"
return 1
fi
old_config="$(cat "$config")"
old_marker="$(cat "$marker")"
[[ "$old_marker" == installed_by_proxmenux=true ]] && package_owned=true
if awk 'NR > 1 && $1 == "/dev/zram0" { found=1 } END { exit !found }' /proc/swaps; then
old_active=true
if ! swapoff /dev/zram0; then
msg_error "$(translate "ZRAM still holds pages; profile remains active")"
return 1
fi
fi
if systemctl is-active --quiet dev-zram0.swap && ! systemctl stop dev-zram0.swap; then
if [[ "$old_active" == true ]]; then
systemctl start dev-zram0.swap >/dev/null 2>&1 || true
fi
msg_error "$(translate "Could not stop ZRAM swap; profile configuration remains")"
return 1
fi
if ! pmx_remove_file "$config" || ! pmx_remove_file "$marker" || ! systemctl daemon-reload; then
printf '%s\n' "$old_config" | pmx_write_file "$config"
printf '%s\n' "$old_marker" | pmx_write_file "$marker"
chmod 600 "$marker"
systemctl daemon-reload >/dev/null 2>&1 || true
if [[ "$old_active" == true ]] && ! systemctl start dev-zram0.swap >/dev/null 2>&1; then
msg_error "$(translate "Could not remove ZRAM profile; configuration was restored but swap could not be restarted")"
else
msg_error "$(translate "Could not remove ZRAM profile; configuration was restored")"
fi
return 1
fi
rmdir "$(dirname "$config")" 2>/dev/null || true
if [[ "$package_owned" == true ]]; then
if ! foreign="$(zram_foreign_generator_config "$config")" ||
! other_active="$(zram_list_foreign_active_zram_swap /proc/swaps /dev/zram0)" ||
! fstab_zram="$(zram_list_foreign_fstab_zram_swap /etc/fstab)" ||
! systemd_zram="$(zram_list_foreign_systemd_zram_units)"; then
inspection_failed=true
fi
if [[ "$inspection_failed" == false && -z "$foreign" && -z "$other_active" && -z "$fstab_zram" && -z "$systemd_zram" ]]; then
if DEBIAN_FRONTEND=noninteractive apt-get purge -y systemd-zram-generator >/dev/null 2>&1; then
pmx_record_uninstall "systemd-zram-generator" "$FUNC_VERSION"
else
msg_warn "$(translate "ZRAM profile removed, but package purge failed")"
fi
else
msg_warn "$(translate "Another ZRAM configuration exists or could not be inspected; systemd-zram-generator was retained")"
fi
fi
register_tool "zram_swap" false
msg_success "$(translate "ZRAM swap profile removed")"
return 0
}
################################################################
uninstall_fastfetch() { uninstall_fastfetch() {
local FUNC_VERSION="1.0" local FUNC_VERSION="1.0"
pmx_journal_context "uninstall_fastfetch" "$FUNC_VERSION" pmx_journal_context "uninstall_fastfetch" "$FUNC_VERSION"
@@ -1270,6 +1350,7 @@ show_uninstall_menu() {
system_limits) desc="System Limits Increase";; system_limits) desc="System Limits Increase";;
entropy) desc="Entropy Generation (haveged)";; entropy) desc="Entropy Generation (haveged)";;
memory_settings) desc="Memory Settings Optimization";; memory_settings) desc="Memory Settings Optimization";;
zram_swap) desc="ZRAM swap profile";;
kernel_panic) desc="Kernel Panic Configuration";; kernel_panic) desc="Kernel Panic Configuration";;
apt_ipv4) desc="APT IPv4 Force";; apt_ipv4) desc="APT IPv4 Force";;
kexec) desc="kexec for quick reboots";; kexec) desc="kexec for quick reboots";;
@@ -1315,18 +1396,30 @@ show_uninstall_menu() {
fi fi
# Execute uninstallations # Execute uninstallations
local failed=0
for tool in $selected_tools; do for tool in $selected_tools; do
tool=$(echo "$tool" | tr -d '"') tool=$(echo "$tool" | tr -d '"')
if declare -f "uninstall_$tool" > /dev/null 2>&1; then if declare -f "uninstall_$tool" > /dev/null 2>&1; then
clear clear
show_proxmenux_logo show_proxmenux_logo
"uninstall_$tool" # Preserve legacy return handling for existing uninstallers.
# Only ZRAM uses a failure status to signal that its active swap
# profile could not be safely removed.
if [[ "$tool" == "zram_swap" ]]; then
uninstall_zram_swap || failed=1
else
"uninstall_$tool" || true
fi
else else
msg_warn "$(translate "No uninstaller found for:") $tool" msg_warn "$(translate "No uninstaller found for:") $tool"
fi fi
done done
msg_success "$(translate "Selected optimizations have been uninstalled.")" if [[ "$failed" -eq 0 ]]; then
msg_success "$(translate "Selected optimizations have been uninstalled.")"
else
msg_error "$(translate "Some optimizations could not be uninstalled; their configuration was preserved.")"
fi
msg_warn "$(translate "A system reboot is recommended to ensure all changes take effect.")" msg_warn "$(translate "A system reboot is recommended to ensure all changes take effect.")"
echo -e echo -e
msg_success "$(translate "Press Enter to continue...")" msg_success "$(translate "Press Enter to continue...")"
@@ -0,0 +1,134 @@
#!/usr/bin/env bash
# Pure helpers shared by the ZRAM installer and its fixture tests.
# The kernel's maximum priority puts ZRAM ahead of standard Proxmox swap.
# A disk swap explicitly configured at this same maximum cannot be outranked.
if ! declare -p ZRAM_SWAP_PRIORITY >/dev/null 2>&1; then
readonly ZRAM_SWAP_PRIORITY=32767
fi
zram_render_generator_config() {
local algorithm=zstd
[[ "${1:-false}" == true ]] && algorithm='zstd(level=9)'
cat <<EOF
# ProxMenux ZRAM profile
[zram0]
zram-size = ram / 10
compression-algorithm = $algorithm
swap-priority = $ZRAM_SWAP_PRIORITY
EOF
}
zram_list_foreign_active_zram_swap() {
local swaps_file="$1" owned_device="${2:-}"
local swap_paths swap_path real_device
[[ -r "$swaps_file" ]] || return 2
swap_paths="$(awk 'NR > 1 { print $1 }' "$swaps_file")" || return 2
while IFS= read -r swap_path; do
[[ -z "$swap_path" ]] && continue
real_device="$(readlink -f -- "$swap_path" 2>/dev/null)" || real_device="$swap_path"
if [[ "$real_device" =~ ^/dev/zram[0-9]+$ && "$real_device" != "$owned_device" ]]; then
printf 'active zram: %s\n' "$swap_path"
fi
done <<< "$swap_paths"
}
zram_list_foreign_fstab_zram_swap() {
local fstab_file="$1"
[[ -r "$fstab_file" ]] || return 2
awk '
$0 !~ /^[[:space:]]*#/ && NF >= 3 && $3 == "swap" && $1 ~ /^\/dev\/zram[0-9]+$/ {
print "fstab zram: " $1
}
' "$fstab_file"
}
# Print generator configuration outside this feature's dedicated drop-in.
zram_foreign_generator_config() {
local owned_file="$1" fixture_root="${2:-}" root file
local roots=(/etc/systemd /run/systemd /usr/local/lib/systemd /usr/lib/systemd /lib/systemd)
if [[ -n "$fixture_root" ]]; then
roots=("$fixture_root/etc/systemd" "$fixture_root/run/systemd" "$fixture_root/usr/local/lib/systemd" "$fixture_root/usr/lib/systemd" "$fixture_root/lib/systemd")
fi
for root in "${roots[@]}"; do
for file in "$root/zram-generator.conf" "$root"/zram-generator.conf.d/*.conf; do
[[ -e "$file" || -L "$file" ]] || continue
[[ "$file" == "$owned_file" ]] && continue
# Ignore package defaults containing only comments, whitespace and
# section headers. Any directive or malformed active line is foreign.
local active
active="$(awk '
{
line=$0
sub(/^[[:space:]]+/, "", line)
sub(/[[:space:]]+$/, "", line)
if (line == "" || line ~ /^[#;]/ || line ~ /^\[[^]]+\]$/) next
found=1
}
END { if (found) print "active" }
' "$file")" || return 2
if [[ -n "$active" ]]; then
printf '%s\n' "$file"
fi
done
done
}
zram_list_foreign_systemd_zram_units() {
local fixture_root="${1:-}" root file unit what unit_files
local roots=(/etc/systemd/system /run/systemd/system /usr/local/lib/systemd/system /usr/lib/systemd/system /lib/systemd/system)
if [[ -n "$fixture_root" ]]; then
roots=("$fixture_root/etc/systemd/system" "$fixture_root/run/systemd/system" "$fixture_root/usr/local/lib/systemd/system" "$fixture_root/usr/lib/systemd/system" "$fixture_root/lib/systemd/system")
fi
for root in "${roots[@]}"; do
[[ -d "$root" ]] || continue
# Skip activation links; systemd-generator creates links under
# *.wants and they must not look like foreign unit definitions.
unit_files="$(find "$root" \( -type d \( -name '*.wants' -o -name '*.requires' \) -prune \) -o \( \( -name '*.swap' -o -path '*.swap.d/*.conf' \) \( -type f -o -type l \) -print \) 2>/dev/null)" || return 2
while IFS= read -r file; do
[[ -z "$file" ]] && continue
unit="$(basename "$file")"
if [[ -L "$file" && "$(readlink -f -- "$file" 2>/dev/null || true)" == /dev/null ]]; then
if [[ "$unit" == "dev-zram0.swap" ]]; then
printf 'systemd zram unit masked: %s\n' "$file"
fi
continue
fi
# A persistent unit named for a zram device could shadow the
# generated unit, even if it currently points at another device.
what="$(awk '
/^[[:space:]]*\[Swap\][[:space:]]*$/ { in_swap=1; next }
/^[[:space:]]*\[/ { in_swap=0 }
in_swap && /^[[:space:]]*What[[:space:]]*=[[:space:]]*\/dev\/zram[0-9]+([[:space:]]|$)/ {
found=1
}
END { if (found) print "zram" }
' "$file")" || return 2
if [[ "$unit" =~ ^dev-zram[0-9]+\.swap$ || -n "$what" ]]; then
printf 'systemd zram unit: %s\n' "$file"
fi
done <<< "$unit_files"
done
}
zram_profile_verify_active() {
local expected_algorithm="$1" expected_priority="${2:-$ZRAM_SWAP_PRIORITY}"
local sys_block_root="${3:-/sys/block}" meminfo_path="${4:-/proc/meminfo}" swaps_path="${5:-/proc/swaps}"
local zram0="$sys_block_root/zram0" expected_bytes actual_bytes active_algorithm
[[ -r "$zram0/disksize" && -r "$zram0/comp_algorithm" && -r "$meminfo_path" && -r "$swaps_path" ]] || return 1
expected_bytes="$(awk '/^MemTotal:/ { print int($2 * 1024 / 10) }' "$meminfo_path")"
actual_bytes="$(cat "$zram0/disksize")"
[[ "$expected_bytes" =~ ^[0-9]+$ && "$actual_bytes" =~ ^[0-9]+$ ]] || return 1
(( actual_bytes >= expected_bytes - 1048576 && actual_bytes <= expected_bytes + 1048576 )) || return 1
active_algorithm="$(grep -oE '\[[^]]+\]' "$zram0/comp_algorithm" | tr -d '[]')"
[[ "$active_algorithm" == "$expected_algorithm" ]] || return 1
awk -v priority="$expected_priority" '
NR > 1 && $1 == "/dev/zram0" {
found=1
if ($5 + 0 != priority + 0) bad=1
}
NR > 1 && $1 ~ /^\/dev\/zram[0-9]+$/ && $1 != "/dev/zram0" { bad=1 }
END { exit !(found && !bad) }
' "$swaps_path"
}