mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-10-09 06:56:37 +00:00
fix(monitor): refresh the VM/LXC modal after edits made outside the Monitor
This commit is contained in:
@@ -3967,11 +3967,11 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
|||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="text-sm text-green-500 flex items-center gap-1">
|
<div className="text-sm text-green-500 flex items-center gap-1">
|
||||||
<span>↓</span>
|
<span>↓</span>
|
||||||
<span>{((selectedVM.diskread || 0) / 1024 ** 2).toFixed(2)} MB</span>
|
<span>{formatBytes(selectedVM.diskread, false)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-blue-500 flex items-center gap-1">
|
<div className="text-sm text-blue-500 flex items-center gap-1">
|
||||||
<span>↑</span>
|
<span>↑</span>
|
||||||
<span>{((selectedVM.diskwrite || 0) / 1024 ** 2).toFixed(2)} MB</span>
|
<span>{formatBytes(selectedVM.diskwrite, false)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1913,6 +1913,35 @@ def _vm_cache_put(cache: dict, vmid: int, value) -> None:
|
|||||||
with _vm_modal_cache_lock:
|
with _vm_modal_cache_lock:
|
||||||
cache[vmid] = (time.time(), value)
|
cache[vmid] = (time.time(), value)
|
||||||
|
|
||||||
|
# The details and mount-point caches come from the guest's .conf, and an edit
|
||||||
|
# made from the Proxmox interface or `pct/qm set` raises no event the Monitor
|
||||||
|
# sees. Each entry remembers the .conf it was built from and is rebuilt when
|
||||||
|
# the file changes.
|
||||||
|
_vm_conf_built: dict = {} # (cache name, vmid) -> .conf signature
|
||||||
|
|
||||||
|
def _vm_conf_signature(vmid: int):
|
||||||
|
for path in (f'/etc/pve/lxc/{int(vmid)}.conf', f'/etc/pve/qemu-server/{int(vmid)}.conf'):
|
||||||
|
try:
|
||||||
|
info = os.stat(path)
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
return (info.st_mtime_ns, info.st_size)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _vm_conf_cache_get(cache: dict, name: str, vmid: int, ttl: int):
|
||||||
|
"""Cached payload for vmid only while its .conf is unchanged."""
|
||||||
|
cached = _vm_cache_get(cache, vmid, ttl)
|
||||||
|
if cached is None:
|
||||||
|
return None
|
||||||
|
with _vm_modal_cache_lock:
|
||||||
|
built = _vm_conf_built.get((name, vmid))
|
||||||
|
return cached if built == _vm_conf_signature(vmid) else None
|
||||||
|
|
||||||
|
def _vm_conf_cache_put(cache: dict, name: str, vmid: int, value, signature) -> None:
|
||||||
|
_vm_cache_put(cache, vmid, value)
|
||||||
|
with _vm_modal_cache_lock:
|
||||||
|
_vm_conf_built[(name, vmid)] = signature
|
||||||
|
|
||||||
def _vm_cache_invalidate(vmid: int, *caches) -> None:
|
def _vm_cache_invalidate(vmid: int, *caches) -> None:
|
||||||
"""Drop this vmid's entries from the given caches. With no argument
|
"""Drop this vmid's entries from the given caches. With no argument
|
||||||
hits every per-VM modal cache — used by write actions that could
|
hits every per-VM modal cache — used by write actions that could
|
||||||
@@ -15214,7 +15243,7 @@ def api_vms_modal_cache_all():
|
|||||||
entry = {
|
entry = {
|
||||||
'vmid': vmid,
|
'vmid': vmid,
|
||||||
'type': vm_type,
|
'type': vm_type,
|
||||||
'details': _vm_cache_get(_vm_details_cache, vmid, _VM_DETAILS_TTL),
|
'details': _vm_conf_cache_get(_vm_details_cache, 'details', vmid, _VM_DETAILS_TTL),
|
||||||
'backups': _vm_cache_get(_vm_backups_cache, vmid, _VM_BACKUPS_TTL),
|
'backups': _vm_cache_get(_vm_backups_cache, vmid, _VM_BACKUPS_TTL),
|
||||||
}
|
}
|
||||||
if vm_type == 'lxc':
|
if vm_type == 'lxc':
|
||||||
@@ -15225,7 +15254,7 @@ def api_vms_modal_cache_all():
|
|||||||
_VM_APP_SUGGESTIONS_TTL,
|
_VM_APP_SUGGESTIONS_TTL,
|
||||||
)
|
)
|
||||||
entry['schedule'] = _vm_cache_get(_vm_schedule_cache, vmid, _VM_SCHEDULE_TTL)
|
entry['schedule'] = _vm_cache_get(_vm_schedule_cache, vmid, _VM_SCHEDULE_TTL)
|
||||||
entry['mount_points'] = _vm_cache_get(_vm_mounts_cache, vmid, _VM_MOUNTS_TTL)
|
entry['mount_points'] = _vm_conf_cache_get(_vm_mounts_cache, 'mounts', vmid, _VM_MOUNTS_TTL)
|
||||||
guests.append(entry)
|
guests.append(entry)
|
||||||
return jsonify({'guests': guests, 'ts': int(time.time())})
|
return jsonify({'guests': guests, 'ts': int(time.time())})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -15238,7 +15267,9 @@ def api_vms_modal_cache_all():
|
|||||||
def get_vm_config(vmid):
|
def get_vm_config(vmid):
|
||||||
"""Get detailed configuration for a specific VM/LXC"""
|
"""Get detailed configuration for a specific VM/LXC"""
|
||||||
try:
|
try:
|
||||||
cached = _vm_cache_get(_vm_details_cache, vmid, _VM_DETAILS_TTL)
|
# Taken before reading the .conf, so an edit during the build is seen next time.
|
||||||
|
signature = _vm_conf_signature(vmid)
|
||||||
|
cached = _vm_conf_cache_get(_vm_details_cache, 'details', vmid, _VM_DETAILS_TTL)
|
||||||
if cached is not None:
|
if cached is not None:
|
||||||
return jsonify(cached)
|
return jsonify(cached)
|
||||||
|
|
||||||
@@ -15308,7 +15339,7 @@ def get_vm_config(vmid):
|
|||||||
if hardware_info:
|
if hardware_info:
|
||||||
response_data['hardware_info'] = hardware_info
|
response_data['hardware_info'] = hardware_info
|
||||||
|
|
||||||
_vm_cache_put(_vm_details_cache, vmid, response_data)
|
_vm_conf_cache_put(_vm_details_cache, 'details', vmid, response_data, signature)
|
||||||
return jsonify(response_data)
|
return jsonify(response_data)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -15328,7 +15359,8 @@ def api_lxc_mount_points(vmid):
|
|||||||
start/stop of the guest, since config-visible fields normally
|
start/stop of the guest, since config-visible fields normally
|
||||||
only change through a guest reboot). The runtime endpoint is
|
only change through a guest reboot). The runtime endpoint is
|
||||||
NEVER cached — it must reflect the live state at click time."""
|
NEVER cached — it must reflect the live state at click time."""
|
||||||
cached = _vm_cache_get(_vm_mounts_cache, vmid, _VM_MOUNTS_TTL)
|
signature = _vm_conf_signature(vmid)
|
||||||
|
cached = _vm_conf_cache_get(_vm_mounts_cache, 'mounts', vmid, _VM_MOUNTS_TTL)
|
||||||
if cached is not None:
|
if cached is not None:
|
||||||
return jsonify(cached)
|
return jsonify(cached)
|
||||||
try:
|
try:
|
||||||
@@ -15339,7 +15371,7 @@ def api_lxc_mount_points(vmid):
|
|||||||
result = lxc_mount_points.get_lxc_mount_points_static(str(vmid))
|
result = lxc_mount_points.get_lxc_mount_points_static(str(vmid))
|
||||||
if not result.get("ok"):
|
if not result.get("ok"):
|
||||||
return jsonify(result), 400
|
return jsonify(result), 400
|
||||||
_vm_cache_put(_vm_mounts_cache, vmid, result)
|
_vm_conf_cache_put(_vm_mounts_cache, 'mounts', vmid, result, signature)
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"ok": False, "error": str(e)}), 500
|
return jsonify({"ok": False, "error": str(e)}), 500
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
"""The VM/LXC modal caches notice a .conf edited outside the Monitor."""
|
||||||
|
import ast
|
||||||
|
from pathlib import Path
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from types import SimpleNamespace
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
SERVER = Path(__file__).resolve().parents[1] / 'flask_server.py'
|
||||||
|
NAMES = ('_vm_cache_get', '_vm_cache_put', '_vm_conf_signature', '_vm_conf_cache_get', '_vm_conf_cache_put')
|
||||||
|
|
||||||
|
|
||||||
|
class ConfAwareCacheTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
tree = ast.parse(SERVER.read_text(encoding='utf-8'))
|
||||||
|
nodes = [n for n in tree.body if isinstance(n, ast.FunctionDef) and n.name in NAMES]
|
||||||
|
self.assertEqual(sorted(n.name for n in nodes), sorted(NAMES))
|
||||||
|
self.files = {'/etc/pve/lxc/111.conf': (1, 300)}
|
||||||
|
|
||||||
|
def stat(path):
|
||||||
|
if path not in self.files:
|
||||||
|
raise OSError(path)
|
||||||
|
mtime, size = self.files[path]
|
||||||
|
return SimpleNamespace(st_mtime_ns=mtime, st_size=size)
|
||||||
|
|
||||||
|
self.scope = {'os': SimpleNamespace(stat=stat), 'time': time, '_vm_modal_cache_lock': threading.Lock(),
|
||||||
|
'_vm_conf_built': {}}
|
||||||
|
exec(compile(ast.Module(body=nodes, type_ignores=[]), str(SERVER), 'exec'), self.scope)
|
||||||
|
self.cache = {}
|
||||||
|
|
||||||
|
def build(self, value):
|
||||||
|
signature = self.scope['_vm_conf_signature'](111)
|
||||||
|
self.scope['_vm_conf_cache_put'](self.cache, 'details', 111, value, signature)
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
return self.scope['_vm_conf_cache_get'](self.cache, 'details', 111, 10 ** 9)
|
||||||
|
|
||||||
|
def test_unchanged_conf_serves_the_cache(self):
|
||||||
|
self.build({'onboot': 1})
|
||||||
|
self.assertEqual(self.get(), {'onboot': 1})
|
||||||
|
|
||||||
|
def test_conf_edited_outside_the_monitor_drops_the_entry(self):
|
||||||
|
self.build({'onboot': 1})
|
||||||
|
self.files['/etc/pve/lxc/111.conf'] = (2, 300) # `pct set 111 --onboot 0`
|
||||||
|
self.assertIsNone(self.get())
|
||||||
|
self.build({'onboot': 0})
|
||||||
|
self.assertEqual(self.get(), {'onboot': 0})
|
||||||
|
|
||||||
|
def test_entry_without_a_recorded_conf_is_rebuilt(self):
|
||||||
|
self.scope['_vm_cache_put'](self.cache, 111, {'old': True})
|
||||||
|
self.assertIsNone(self.get())
|
||||||
|
|
||||||
|
def test_vm_conf_path_and_missing_guest(self):
|
||||||
|
self.files = {'/etc/pve/qemu-server/200.conf': (5, 10)}
|
||||||
|
self.assertEqual(self.scope['_vm_conf_signature'](200), (5, 10))
|
||||||
|
self.assertIsNone(self.scope['_vm_conf_signature'](999))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user