mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-01 10:22:08 +00:00
Monitor: forget the App tab of a destroyed guest
This commit is contained in:
@@ -2302,6 +2302,27 @@ def _handle_guest_lifecycle(vmid: str, vm_type: str, action: str) -> None:
|
||||
return
|
||||
if action == 'stop':
|
||||
_publish_guest_modal_cache_revision(guest_id)
|
||||
if action == 'destroy' and not _oci_operation_running(guest_id):
|
||||
# The VMID can be reused by a new guest that must not inherit the
|
||||
# applications and scheduled updates of this one. An OCI update
|
||||
# destroys and recreates its own container: it keeps them.
|
||||
try:
|
||||
import lxc_apps
|
||||
lxc_apps.forget_guest(guest_id)
|
||||
_vm_cache_invalidate(guest_id, _vm_schedule_cache)
|
||||
except Exception as exc:
|
||||
print(f'[ProxMenux] could not forget the applications of {guest_type} {guest_id}: {exc}', flush=True)
|
||||
_publish_guest_modal_cache_revision(guest_id)
|
||||
|
||||
|
||||
def _oci_operation_running(guest_id: int) -> bool:
|
||||
"""An OCI operation on the guest is running or ended moments ago."""
|
||||
try:
|
||||
with open(f'/run/proxmenux/oci-operations/{int(guest_id)}', encoding='utf-8') as handle:
|
||||
mark = json.load(handle)
|
||||
return mark.get('ended') is None or time.time() - float(mark['ended']) < 120
|
||||
except (OSError, ValueError, TypeError, KeyError):
|
||||
return False
|
||||
|
||||
# Filesystem types that never count towards VM disk usage:
|
||||
# read-only image / CD formats, ram-backed pseudo-fs, kernel virtual
|
||||
@@ -12424,6 +12445,11 @@ def _vm_modal_prewarmer_pass():
|
||||
disappears until something genuinely expires."""
|
||||
from flask import g as _flask_g
|
||||
warmed = 0
|
||||
try:
|
||||
import lxc_apps
|
||||
lxc_apps.forget_missing_guests()
|
||||
except Exception as exc:
|
||||
print(f'[ProxMenux] could not check the applications of removed guests: {exc}', flush=True)
|
||||
resources = get_cached_pvesh_cluster_resources_vm() or []
|
||||
for r in resources:
|
||||
vmid = r.get('vmid')
|
||||
|
||||
@@ -28,6 +28,7 @@ from __future__ import annotations
|
||||
import datetime
|
||||
import copy
|
||||
import concurrent.futures
|
||||
import glob
|
||||
import hashlib
|
||||
import ipaddress
|
||||
import json
|
||||
@@ -48,6 +49,7 @@ from typing import Any, Optional
|
||||
import local_app_icons
|
||||
|
||||
_APPS_DIR = "/etc/proxmenux/apps"
|
||||
_PVE_NODES = "/etc/pve/nodes"
|
||||
_PCT_BIN = "/usr/sbin/pct"
|
||||
_PROBE_TIMEOUT_SEC = 15
|
||||
_GITHUB_TIMEOUT_SEC = 15
|
||||
@@ -3517,6 +3519,49 @@ def delete_app(vmid, app_id: str) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def forget_guest(vmid) -> None:
|
||||
"""A destroyed guest takes its registrations with it, scheduled updates
|
||||
included, so a new guest that reuses the VMID starts with nothing."""
|
||||
key = str(int(vmid))
|
||||
path = _sidecar_path(vmid)
|
||||
with _cache_lock:
|
||||
try:
|
||||
os.unlink(path)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except OSError as exc:
|
||||
print(f"[ProxMenux] lxc_apps: could not forget CT {key}: {exc}")
|
||||
with _sidecar_cache_lock:
|
||||
_sidecar_cache.pop(path, None)
|
||||
dismissed = _oci_dismissed()
|
||||
if dismissed.pop(key, None) is not None:
|
||||
_save_oci_dismissed(dismissed)
|
||||
with _docker_inventory_lock:
|
||||
_docker_inventory_cache.pop(key, None)
|
||||
for cache in (_detected_apps_cache, _port_probe_cache, _docker_web_links_cache):
|
||||
cache.pop(key, None)
|
||||
for probe in [probe for probe in list(_file_probe_cache) if probe[0] == key]:
|
||||
_file_probe_cache.pop(probe, None)
|
||||
|
||||
|
||||
def forget_missing_guests() -> None:
|
||||
"""Registrations of guests that no node of the cluster has any more,
|
||||
removed while the Monitor was not running."""
|
||||
if not os.path.isdir(_PVE_NODES):
|
||||
return
|
||||
try:
|
||||
names = os.listdir(_APPS_DIR)
|
||||
except OSError:
|
||||
return
|
||||
for name in names:
|
||||
vmid = name[:-5] if name.endswith(".json") else ""
|
||||
if not vmid.isdigit():
|
||||
continue
|
||||
if not (glob.glob(f"{_PVE_NODES}/*/lxc/{vmid}.conf")
|
||||
or glob.glob(f"{_PVE_NODES}/*/qemu-server/{vmid}.conf")):
|
||||
forget_guest(vmid)
|
||||
|
||||
|
||||
def delete_all(vmid) -> bool:
|
||||
sidecar = _read_sidecar(vmid) or {}
|
||||
if any(a.get("installed_via") == "oci_image" for a in sidecar.get("apps") or []):
|
||||
@@ -5654,6 +5699,10 @@ def _dismiss_oci_registration(vmid) -> None:
|
||||
return
|
||||
data = _oci_dismissed()
|
||||
data[str(int(vmid))] = record["installation_id"]
|
||||
_save_oci_dismissed(data)
|
||||
|
||||
|
||||
def _save_oci_dismissed(data) -> None:
|
||||
_ensure_dir()
|
||||
tmp = f"{_OCI_DISMISSED_FILE}.tmp.{os.getpid()}"
|
||||
try:
|
||||
|
||||
@@ -2423,6 +2423,18 @@ class TaskWatcher:
|
||||
# Get VM/CT name
|
||||
vmname = self._get_vm_name(vmid) if vmid else ''
|
||||
|
||||
# A destroyed guest frees its VMID. It has no notification of its
|
||||
# own, but the cache listener must forget what belonged to it.
|
||||
if task_type in ('vzdestroy', 'qmdestroy'):
|
||||
if status == 'OK' and self._guest_lifecycle_callback:
|
||||
guest_type = 'lxc' if task_type == 'vzdestroy' else 'qemu'
|
||||
try:
|
||||
self._guest_lifecycle_callback(vmid, guest_type, 'destroy')
|
||||
except Exception as exc:
|
||||
print(f'[TaskWatcher] guest lifecycle callback failed for '
|
||||
f'{guest_type} {vmid}: {exc}', flush=True)
|
||||
return
|
||||
|
||||
# Map to event type
|
||||
event_info = self.TASK_MAP.get(task_type)
|
||||
if not event_info:
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"""A destroyed guest takes its App registrations with it, so a new guest
|
||||
that reuses the VMID does not inherit them."""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
SCRIPTS_DIR = Path(__file__).resolve().parents[1]
|
||||
if str(SCRIPTS_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(SCRIPTS_DIR))
|
||||
|
||||
import lxc_apps
|
||||
|
||||
|
||||
class ForgetDestroyedGuestTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
folder = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(folder.cleanup)
|
||||
self.root = Path(folder.name)
|
||||
self.apps = self.root / "apps"
|
||||
self.nodes = self.root / "nodes"
|
||||
self.apps.mkdir()
|
||||
for target, value in (("_APPS_DIR", str(self.apps)), ("_PVE_NODES", str(self.nodes)),
|
||||
("_OCI_DISMISSED_FILE", str(self.apps / ".oci-dismissed.json"))):
|
||||
patcher = mock.patch.object(lxc_apps, target, value)
|
||||
patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
def register(self, vmid):
|
||||
(self.apps / f"{vmid}.json").write_text(json.dumps({
|
||||
"vmid": vmid, "apps": [{"id": "portainer", "name": "Portainer"}],
|
||||
"schedule": {"enabled": True, "target": "both"}}))
|
||||
|
||||
def guest(self, vmid, kind="lxc", node="pve1"):
|
||||
folder = self.nodes / node / kind
|
||||
folder.mkdir(parents=True, exist_ok=True)
|
||||
(folder / f"{vmid}.conf").write_text("arch: amd64\n")
|
||||
|
||||
def test_a_destroyed_guest_loses_its_applications_and_schedule(self):
|
||||
self.register(112)
|
||||
self.assertIsNotNone(lxc_apps._read_sidecar(112))
|
||||
lxc_apps._docker_inventory_cache["112"] = {"containers": ["portainer"]}
|
||||
lxc_apps.forget_guest(112)
|
||||
self.assertFalse((self.apps / "112.json").exists())
|
||||
self.assertIsNone(lxc_apps._read_sidecar(112))
|
||||
self.assertNotIn("112", lxc_apps._docker_inventory_cache)
|
||||
|
||||
def test_its_oci_dismissal_is_dropped_and_others_are_kept(self):
|
||||
(self.apps / ".oci-dismissed.json").write_text(json.dumps({"112": "old-id", "130": "other-id"}))
|
||||
lxc_apps.forget_guest(112)
|
||||
self.assertEqual(json.loads((self.apps / ".oci-dismissed.json").read_text()), {"130": "other-id"})
|
||||
|
||||
def test_guests_removed_while_the_monitor_was_stopped_are_forgotten(self):
|
||||
for vmid in (112, 113, 200):
|
||||
self.register(vmid)
|
||||
self.guest(113)
|
||||
self.guest(200, kind="qemu-server", node="pve2")
|
||||
lxc_apps.forget_missing_guests()
|
||||
self.assertEqual(sorted(p.name for p in self.apps.glob("*.json")), ["113.json", "200.json"])
|
||||
|
||||
def test_nothing_is_forgotten_without_the_cluster_filesystem(self):
|
||||
self.register(112)
|
||||
lxc_apps.forget_missing_guests()
|
||||
self.assertTrue((self.apps / "112.json").exists())
|
||||
|
||||
def test_the_task_watcher_publishes_destroy_tasks(self):
|
||||
source = (SCRIPTS_DIR / "notification_events.py").read_text()
|
||||
self.assertIn("if task_type in ('vzdestroy', 'qmdestroy'):", source)
|
||||
self.assertLess(source.index("if task_type in ('vzdestroy', 'qmdestroy'):"),
|
||||
source.index('event_info = self.TASK_MAP.get(task_type)'))
|
||||
|
||||
def test_the_monitor_keeps_the_applications_of_an_oci_operation(self):
|
||||
source = (SCRIPTS_DIR / "flask_server.py").read_text()
|
||||
self.assertIn("if action == 'destroy' and not _oci_operation_running(guest_id):", source)
|
||||
self.assertIn("/run/proxmenux/oci-operations/", source)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -336,6 +336,7 @@ Thank you to [@byGarcia](https://github.com/byGarcia) for the implementation, ve
|
||||
- **The web terminals send Ctrl+D from their Ctrl menu**, which ends a text pasted in the terminal, such as a Compose file or an oci-compose in OCI manager Apps, also from a phone or a tablet. A single Ctrl+D ends the paste, also right after its last line.
|
||||
- **A VM or LXC edited from Proxmox shows its current settings** the next time its modal opens: start at boot, tags, cores, memory, network and mount points.
|
||||
- **An LXC changed outside the Monitor shows its current data** ([PR #441](https://github.com/MacRimi/ProxMenux/pull/441), contributed by [@Vaso73](https://github.com/Vaso73)): a new DHCP address, a setting edited in Proxmox while its modal is open, and the operating system of a restored or reinstalled container. The Updates tab marks a result older than 48 hours as outdated, and the card keeps showing the number of pending updates.
|
||||
- **A guest that is destroyed takes its App tab with it**: its registered applications and scheduled updates are forgotten, also when it was removed while the Monitor was stopped, so a new container that reuses the VMID starts empty. An OCI update, which recreates its own container, keeps them.
|
||||
- **Updating a stopped LXC keeps its result** ([PR #447](https://github.com/MacRimi/ProxMenux/pull/447), contributed by [@Vaso73](https://github.com/Vaso73)): its packages and applications are read before it is stopped again, so the Updates tab shows what was installed and what is still pending. A run that never got to start the container keeps the previous result.
|
||||
- **An LXC update keeps running when the browser session ends** ([PR #527](https://github.com/MacRimi/ProxMenux/pull/527), contributed by [@Vaso73](https://github.com/Vaso73)): the update runs as a job of the Monitor, so closing or reloading the page leaves it running, and after signing in again its output is shown from where it was. Ctrl+C still cancels it.
|
||||
- **Checking and installing LXC packages are separate steps** ([PR #448](https://github.com/MacRimi/ProxMenux/pull/448), contributed by [@Vaso73](https://github.com/Vaso73)): Check system packages refreshes the package indexes of a running container and counts what is pending without installing anything, and installing needs a recent successful check. A stopped container is asked to be started first and keeps its last result with its date.
|
||||
|
||||
@@ -335,6 +335,7 @@ Gracias a [@byGarcia](https://github.com/byGarcia) por la implementación, los d
|
||||
- **Las terminales web envían Ctrl+D desde su menú Ctrl**, lo que termina un texto pegado en la terminal, como un archivo Compose o un oci-compose en OCI manager Apps, también desde un móvil o una tableta. Un solo Ctrl+D termina el pegado, también justo después de su última línea.
|
||||
- **Una VM o un LXC editados desde Proxmox muestran sus ajustes actuales** la siguiente vez que se abre su modal: inicio al arrancar, etiquetas, núcleos, memoria, red y puntos de montaje.
|
||||
- **Un LXC cambiado fuera del Monitor muestra sus datos actuales** ([PR #441](https://github.com/MacRimi/ProxMenux/pull/441), aportado por [@Vaso73](https://github.com/Vaso73)): una dirección DHCP nueva, un ajuste editado en Proxmox mientras su modal está abierto y el sistema operativo de un contenedor restaurado o reinstalado. La pestaña Actualizaciones marca como desactualizado un resultado de hace más de 48 horas, y la tarjeta sigue mostrando el número de actualizaciones pendientes.
|
||||
- **Un contenedor o una VM que se eliminan se llevan su pestaña App**: se olvidan sus aplicaciones registradas y sus actualizaciones programadas, también si se eliminó con el Monitor parado, así que un contenedor nuevo que reutiliza el VMID empieza vacío. Una actualización de OCI, que recrea su propio contenedor, las conserva.
|
||||
- **Actualizar un LXC detenido conserva su resultado** ([PR #447](https://github.com/MacRimi/ProxMenux/pull/447), aportado por [@Vaso73](https://github.com/Vaso73)): sus paquetes y aplicaciones se leen antes de volver a detenerlo, así que la pestaña Actualizaciones muestra lo que se instaló y lo que sigue pendiente. Una ejecución que no llegó a arrancar el contenedor conserva el resultado anterior.
|
||||
- **La actualización de un LXC sigue en marcha aunque termine la sesión del navegador** ([PR #527](https://github.com/MacRimi/ProxMenux/pull/527), aportado por [@Vaso73](https://github.com/Vaso73)): la actualización se ejecuta como un trabajo del Monitor, así que cerrar o recargar la página no la detiene, y al volver a iniciar sesión se muestra su salida desde donde iba. Ctrl+C sigue cancelándola.
|
||||
- **Comprobar e instalar paquetes de un LXC son pasos separados** ([PR #448](https://github.com/MacRimi/ProxMenux/pull/448), aportado por [@Vaso73](https://github.com/Vaso73)): Comprobar paquetes del sistema actualiza los índices de paquetes de un contenedor en marcha y cuenta lo que está pendiente sin instalar nada, e instalar requiere una comprobación correcta reciente. A un contenedor detenido se le pide arrancarlo primero, y conserva su último resultado con su fecha.
|
||||
|
||||
Reference in New Issue
Block a user