mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-10-09 23:16:41 +00:00
- New service that starts again an OCI application that stopped on its own, such as Frigate after Save & Restart. A stop or shutdown asked by the user, a backup, a migration and a ProxMenux operation are never undone. - The installer asks it in both modes and proposes what the recipe declares; it is changed from the management menu or with the Watchdog switch of the container modal in the Monitor. - Notifications when the watchdog restarts an application and when it keeps stopping. - The service is recorded in the change journal, and the feature is documented.
99 lines
3.8 KiB
Python
99 lines
3.8 KiB
Python
"""What the VM & LXC modal needs to know about an OCI instance.
|
|
|
|
Read-only view of the installation record OCI manager Apps keeps for every
|
|
container it created: whether the container is one, whether it belongs to a
|
|
multi-container application, whether it uses host directories (which its
|
|
backup does not revert), whether an operation is pending, whether it is
|
|
restarted when it stops on its own and whether it was restored from a backup
|
|
and is not registered on this host yet. Nothing here
|
|
changes the record or runs inside the container.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
import socket
|
|
|
|
import oci_console_logs
|
|
|
|
ROOT = "/usr/local/share/proxmenux/oci/instances"
|
|
|
|
|
|
def _record(vmid: int) -> dict | None:
|
|
path = os.path.join(ROOT, str(int(vmid)), "oci-compose.json")
|
|
try:
|
|
with open(path, encoding="utf-8") as handle:
|
|
record = json.load(handle)
|
|
except (OSError, ValueError):
|
|
return None
|
|
return record if isinstance(record, dict) else None
|
|
|
|
|
|
def _installation(vmid: int) -> str | None:
|
|
"""The installation the container says it belongs to: the mark OCI manager
|
|
Apps leaves in its notes, which a backup keeps."""
|
|
path = f"/etc/pve/nodes/{socket.gethostname().split('.', 1)[0]}/lxc/{int(vmid)}.conf"
|
|
try:
|
|
with open(path, encoding="utf-8", errors="ignore") as handle:
|
|
text = handle.read().split("\n[", 1)[0]
|
|
except OSError:
|
|
return None
|
|
match = re.search(r"^#.*proxmenux-instance=([0-9a-f-]{36})(?![0-9a-f-])", text, re.MULTILINE)
|
|
return match.group(1) if match else None
|
|
|
|
|
|
def _unrecoverable(vmid: int, installation: str) -> bool:
|
|
"""A restored container that carries no copy of its record: the menu
|
|
found nothing to recover it from and left it as an ordinary container."""
|
|
try:
|
|
with open(os.path.join(ROOT, ".unrecoverable.json"), encoding="utf-8") as handle:
|
|
value = json.load(handle)
|
|
except (OSError, ValueError):
|
|
return False
|
|
return isinstance(value, dict) and value.get(str(int(vmid))) == installation
|
|
|
|
|
|
def _host_dirs(record: dict) -> bool:
|
|
mounts = (record.get("deployment") or {}).get("mounts") or []
|
|
return any(isinstance(m, dict) and m.get("type") == "host-bind" for m in mounts)
|
|
|
|
|
|
def info(vmid: int) -> dict:
|
|
vmid = int(vmid)
|
|
result = {
|
|
"vmid": vmid,
|
|
"oci_instance": False,
|
|
"console_log": oci_console_logs.configured_log(vmid) is not None,
|
|
"stack": False,
|
|
"primary_vmid": vmid,
|
|
"members": [],
|
|
"host_directories": False,
|
|
"pending": False,
|
|
"restored": False,
|
|
"watchdog": False,
|
|
}
|
|
record = _record(vmid)
|
|
installation = _installation(vmid)
|
|
if installation and (record is None or record.get("installation_id") != installation):
|
|
# Restored from a backup: the record stayed on the host it came from.
|
|
result["restored"] = not _unrecoverable(vmid, installation)
|
|
return result
|
|
if record is None:
|
|
return result
|
|
primary_id = int((record.get("stack_member") or {}).get("primary_vmid") or vmid)
|
|
primary = record if primary_id == vmid else (_record(primary_id) or {})
|
|
members = [int(m["vmid"]) for m in (primary.get("stack") or {}).get("members") or []
|
|
if isinstance(m, dict) and str(m.get("vmid", "")).isdigit()]
|
|
records = [record] if not members else [r for r in (_record(m) for m in members) if r]
|
|
result.update(
|
|
oci_instance=True,
|
|
stack=bool(members) or bool(record.get("stack_member")),
|
|
primary_vmid=primary_id,
|
|
members=members,
|
|
host_directories=any(_host_dirs(r) for r in records),
|
|
pending=bool(record.get("pending_transaction") or primary.get("pending_stack_transaction")),
|
|
watchdog=all((r.get("deployment") or {}).get("watchdog") is True for r in records),
|
|
)
|
|
return result
|