Files
ProxMenux/AppImage/scripts/disk_identity.py
T
MacRimiandClaude Opus 5 bcabcb618c feat(oci): run official container images as native LXC containers
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>
2026-09-22 18:24:59 +02:00

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}