mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-10-06 21:46:44 +00:00
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>
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from oci_ui import translate
|
||||
|
||||
|
||||
def command_output(*command: str) -> str:
|
||||
result = subprocess.run(command, text=True, capture_output=True, check=False)
|
||||
return result.stdout
|
||||
|
||||
|
||||
def existing_bridges() -> set[str]:
|
||||
bridges: set[str] = set()
|
||||
for line in command_output("ip", "-o", "link", "show").splitlines():
|
||||
match = re.match(r"^\d+: ([^:@]+)", line)
|
||||
if match:
|
||||
bridges.add(match.group(1))
|
||||
for path in Path("/etc/pve/lxc").glob("*.conf"):
|
||||
text = path.read_text(encoding="utf-8", errors="ignore")
|
||||
bridges.update(re.findall(r"(?:^|,)bridge=([^,\s]+)", text, re.MULTILINE))
|
||||
interface_paths = [Path("/etc/network/interfaces")]
|
||||
interface_paths.extend(Path("/etc/network/interfaces.d").glob("*"))
|
||||
for path in interface_paths:
|
||||
if not path.is_file():
|
||||
continue
|
||||
text = path.read_text(encoding="utf-8", errors="ignore")
|
||||
bridges.update(
|
||||
re.findall(r"^(?:auto|iface)\s+(vmbr\d+)\b", text, re.MULTILINE)
|
||||
)
|
||||
return bridges
|
||||
|
||||
|
||||
def existing_networks() -> list[ipaddress.IPv4Network]:
|
||||
networks: list[ipaddress.IPv4Network] = []
|
||||
for line in command_output("ip", "-4", "route", "show").splitlines():
|
||||
token = line.split(maxsplit=1)[0]
|
||||
if token == "default":
|
||||
continue
|
||||
try:
|
||||
networks.append(ipaddress.ip_network(token, strict=False))
|
||||
except ValueError:
|
||||
pass
|
||||
for path in Path("/etc/pve/lxc").glob("*.conf"):
|
||||
text = path.read_text(encoding="utf-8", errors="ignore")
|
||||
for address in re.findall(r"(?:^|,)ip=(\d+\.\d+\.\d+\.\d+/\d+)", text, re.MULTILINE):
|
||||
try:
|
||||
networks.append(ipaddress.ip_interface(address).network)
|
||||
except ValueError:
|
||||
pass
|
||||
return networks
|
||||
|
||||
|
||||
def allocate_network(
|
||||
deployment: dict,
|
||||
bridges: set[str],
|
||||
occupied: list[ipaddress.IPv4Network],
|
||||
) -> tuple[str, ipaddress.IPv4Network] | None:
|
||||
network = deployment.get("network", {})
|
||||
if network.get("private_allocation") != "automatic":
|
||||
return None
|
||||
|
||||
original = ipaddress.ip_network(network["private_subnet"], strict=True)
|
||||
if original.prefixlen != 24:
|
||||
raise ValueError(translate("Automatic private network allocation requires a /24 subnet"))
|
||||
|
||||
selected: tuple[str, ipaddress.IPv4Network] | None = None
|
||||
for index in range(0, 178):
|
||||
bridge = f"vmbr{10 + index}"
|
||||
candidate = ipaddress.ip_network(f"10.77.{index}.0/24")
|
||||
if bridge in bridges or any(candidate.overlaps(item) for item in occupied):
|
||||
continue
|
||||
selected = bridge, candidate
|
||||
break
|
||||
if selected is None:
|
||||
raise SystemExit(translate("No free ProxMenux private /24 network is available"))
|
||||
|
||||
bridge, candidate = selected
|
||||
for key, value in list(network.items()):
|
||||
if not key.endswith("_address") or not isinstance(value, str):
|
||||
continue
|
||||
interface = ipaddress.ip_interface(value)
|
||||
if interface.ip not in original:
|
||||
continue
|
||||
host_offset = int(interface.ip) - int(original.network_address)
|
||||
network[key] = f"{candidate.network_address + host_offset}/{candidate.prefixlen}"
|
||||
network["private_bridge"] = bridge
|
||||
network["private_subnet"] = str(candidate)
|
||||
network["private_allocation"] = "allocated"
|
||||
return bridge, candidate
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) != 2:
|
||||
raise SystemExit("usage: allocate_private_network.py DEPLOYMENT.json")
|
||||
path = Path(sys.argv[1])
|
||||
deployment = json.loads(path.read_text(encoding="utf-8"))
|
||||
try:
|
||||
selected = allocate_network(deployment, existing_bridges(), existing_networks())
|
||||
except ValueError as exc:
|
||||
raise SystemExit(str(exc)) from exc
|
||||
if selected is None:
|
||||
return 0
|
||||
|
||||
bridge, candidate = selected
|
||||
path.write_text(json.dumps(deployment, indent=2, ensure_ascii=True) + "\n", encoding="utf-8")
|
||||
print(f"{translate('Private network assigned automatically:')} {bridge} ({candidate})")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user