mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-29 10:06:41 +00:00
Adds the OCI manager: an engine that turns a Docker Compose file into an LXC definition, a catalog of 365 applications drawn from LinuxServer.io and other container image sources, and a per-instance registry recording what each container was built from. Reachable from the main menu. Catalog text is translated like every other string in the project: the taglines go through translate() and land in lang/*.json, so the entries read in all eight languages instead of only English. Translation cache builder: - a failed translation leaves the key absent rather than writing English, which previously made the string count as translated forever - a result identical to a 3+ word source is rejected, catching a provider that silently returns the text it was given - strings that are nothing but glossary terms keep their source spelling instead of being discarded as failures - no backoff between attempts when the provider is deterministic - application names are protected so "HAOS One" survives translation - argos joins the provider list, and the workflow reads the OCI sources Audit & Report: - findings that moved in the wrong direction between runs are reported alongside the ones that improved - an accepted risk can carry a review date and is flagged when it falls due - backup checks explain in plain language what they looked at and what to do next Monitor: - disks can be excluded from periodic reads, and an idle disk says so instead of showing a stale temperature - per-disk identity survives a controller or enclosure change - scheduled Borg backups resolve their SSH key from the repository entry - PVE upgrades log the package list and the resulting dpkg changes The web build no longer copies scripts/ into public/: the documentation links to GitHub, so nothing read that folder. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
77 lines
2.6 KiB
Python
77 lines
2.6 KiB
Python
"""
|
|
Physical disks and a stable identity for each, read without touching them.
|
|
|
|
Everything here comes from udev's database and /sys through lsblk, which
|
|
reads these columns without opening the block device. A drive that is
|
|
asleep, or idle and about to be, is not disturbed by being listed.
|
|
|
|
The identity matters because a kernel name is not one: a USB drive can be
|
|
sda on one boot and sdb on the next, so anything the user attaches to a
|
|
disk has to follow the disk, not the letter it happened to get.
|
|
"""
|
|
|
|
import re
|
|
import subprocess
|
|
from typing import Any, Dict, List
|
|
|
|
_LSBLK_TIMEOUT = 5
|
|
_FIELD_RE = re.compile(r'(\w+)="([^"]*)"')
|
|
_SKIP_PREFIXES = ("loop", "zd", "nbd", "ram", "sr")
|
|
|
|
|
|
def disk_key(serial: str, wwn: str, name: str) -> str:
|
|
"""Stable identity: the serial where udev knows one, then the WWN,
|
|
and only as a last resort the kernel name."""
|
|
serial = (serial or "").strip()
|
|
wwn = (wwn or "").strip()
|
|
if serial:
|
|
return f"serial:{serial}"
|
|
if wwn:
|
|
return f"wwn:{wwn}"
|
|
return f"name:{name}"
|
|
|
|
|
|
def list_physical_disks() -> List[Dict[str, Any]]:
|
|
"""Every physical disk with its identity and the facts the interface
|
|
shows about it. Returns an empty list if lsblk cannot be read."""
|
|
try:
|
|
proc = subprocess.run(
|
|
["lsblk", "-d", "-n", "-P", "-b", "-o",
|
|
"NAME,TYPE,MODEL,SERIAL,WWN,SIZE,TRAN,ROTA"],
|
|
capture_output=True, text=True, timeout=_LSBLK_TIMEOUT,
|
|
)
|
|
except (subprocess.TimeoutExpired, OSError):
|
|
return []
|
|
if proc.returncode != 0:
|
|
return []
|
|
|
|
disks: List[Dict[str, Any]] = []
|
|
for line in proc.stdout.splitlines():
|
|
fields = dict(_FIELD_RE.findall(line))
|
|
name = fields.get("NAME", "")
|
|
if fields.get("TYPE") != "disk" or not name or name.startswith(_SKIP_PREFIXES):
|
|
continue
|
|
serial = fields.get("SERIAL", "").strip()
|
|
wwn = fields.get("WWN", "").strip()
|
|
try:
|
|
size = int(fields.get("SIZE") or 0)
|
|
except ValueError:
|
|
size = 0
|
|
disks.append({
|
|
"name": name,
|
|
"key": disk_key(serial, wwn, name),
|
|
"model": fields.get("MODEL", "").strip(),
|
|
"serial": serial,
|
|
"size_bytes": size,
|
|
"transport": fields.get("TRAN", "").strip(),
|
|
"rotational": fields.get("ROTA", "").strip() == "1",
|
|
})
|
|
return disks
|
|
|
|
|
|
def names_for_keys(keys) -> set:
|
|
"""Current kernel names of the disks whose identity is in ``keys``."""
|
|
if not keys:
|
|
return set()
|
|
return {d["name"] for d in list_physical_disks() if d["key"] in keys}
|