mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-02 02:42:08 +00:00
fix: revalidate externally changed LXC state
This commit is contained in:
@@ -580,35 +580,34 @@ def parse_lxc_hardware_config(vmid, node):
|
||||
|
||||
|
||||
def _get_lxc_ip_info_cached(vmid):
|
||||
"""Return an LXC's complete IP classification from the lifecycle cache.
|
||||
|
||||
First read per CT spawns one `lxc-info` subprocess; subsequent reads are
|
||||
free until the CT's lifecycle event drops the entry via
|
||||
`_invalidate_lxc_ip`. Keeping the complete result (rather than only the
|
||||
primary address) lets `/api/vms` expose every address without changing
|
||||
the cache's lifecycle semantics.
|
||||
"""
|
||||
"""Probe at most once per TTL, also noticing DHCP changes without events."""
|
||||
try:
|
||||
vmid_int = int(vmid)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
if vmid_int in _lxc_ip_cache:
|
||||
return _lxc_ip_cache[vmid_int]
|
||||
info = get_lxc_ip_from_lxc_info(vmid_int)
|
||||
_remember_lxc_ip(vmid_int, info)
|
||||
return info
|
||||
# Serialize cold/expired reads so concurrent list polls share one probe.
|
||||
with _lxc_ip_cache_lock:
|
||||
hit = _lxc_ip_cache.get(vmid_int)
|
||||
if hit is not None:
|
||||
info = hit[1]
|
||||
ttl = (_LXC_IP_TTL if info and any(':' not in ip for ip in info.get('real_ips') or [])
|
||||
else _LXC_IP_RETRY_TTL)
|
||||
if time.monotonic() - hit[0] < ttl:
|
||||
return info
|
||||
_lxc_ip_cache.pop(vmid_int, None)
|
||||
info = get_lxc_ip_from_lxc_info(vmid_int)
|
||||
_remember_lxc_ip(vmid_int, info)
|
||||
return info
|
||||
|
||||
|
||||
def _remember_lxc_ip(vmid, info):
|
||||
"""Keep the addresses of a container until its next lifecycle event.
|
||||
"""Keep the addresses for a short TTL or until its next lifecycle event.
|
||||
|
||||
A container that shows addresses but no IPv4 of its own is still waiting
|
||||
for its lease, or lost it when it rebooted from inside: that answer is not
|
||||
kept, so the next read looks again instead of serving an address of its
|
||||
inner bridges until the container is restarted.
|
||||
Missing leases and failed probes use a shorter retry TTL. Concurrent
|
||||
polling still shares the result instead of spawning a probe per request.
|
||||
"""
|
||||
if info is None or any(':' not in ip for ip in info.get('real_ips') or []):
|
||||
_lxc_ip_cache[vmid] = info
|
||||
with _lxc_ip_cache_lock:
|
||||
_lxc_ip_cache[int(vmid)] = (time.monotonic(), info)
|
||||
|
||||
|
||||
def _get_lxc_primary_ip_cached(vmid):
|
||||
@@ -619,6 +618,41 @@ def _get_lxc_primary_ip_cached(vmid):
|
||||
return info.get('primary_ip') or (info.get('real_ips') or [None])[0]
|
||||
|
||||
|
||||
def _lxc_runtime_details(payload):
|
||||
"""Keep live addresses and OS separate from the .conf-versioned payload."""
|
||||
if payload.get('vm_type') != 'lxc':
|
||||
return payload
|
||||
result = dict(payload)
|
||||
result.pop('lxc_ip_info', None)
|
||||
result.pop('os_info', None)
|
||||
vmid = payload['vmid']
|
||||
if _fast_guest_status(vmid, 'lxc') != 'running':
|
||||
return result
|
||||
info = _get_lxc_ip_info_cached(vmid)
|
||||
if info:
|
||||
result['lxc_ip_info'] = info
|
||||
os_info = _vm_cache_get(_lxc_os_info_cache, vmid, _LXC_OS_INFO_TTL)
|
||||
if os_info is None:
|
||||
os_info = {}
|
||||
try:
|
||||
probe = subprocess.run(
|
||||
['pct', 'exec', str(vmid), '--', 'cat', '/etc/os-release'],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
if probe.returncode == 0:
|
||||
keys = {'ID': 'id', 'VERSION_ID': 'version_id',
|
||||
'NAME': 'name', 'PRETTY_NAME': 'pretty_name'}
|
||||
for line in probe.stdout.splitlines():
|
||||
key, sep, value = line.partition('=')
|
||||
if sep and key.strip() in keys:
|
||||
os_info[keys[key.strip()]] = value.strip().strip('\"').strip("'")
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
pass
|
||||
_vm_cache_put(_lxc_os_info_cache, vmid, os_info)
|
||||
if os_info:
|
||||
result['os_info'] = os_info
|
||||
return result
|
||||
|
||||
|
||||
def _normalise_lxc_ips(ip_info):
|
||||
"""Return a stable, JSON-safe ordered list of LXC addresses.
|
||||
|
||||
@@ -646,7 +680,8 @@ def _invalidate_lxc_ip(vmid):
|
||||
"""Drop the cached IP so the next request re-probes `lxc-info`.
|
||||
Fired from the guest lifecycle handler on start/stop/reboot."""
|
||||
try:
|
||||
_lxc_ip_cache.pop(int(vmid), None)
|
||||
with _lxc_ip_cache_lock:
|
||||
_lxc_ip_cache.pop(int(vmid), None)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
@@ -675,9 +710,8 @@ def _restore_lxc_dhcp_clients():
|
||||
def _warmup_lxc_ip_cache() -> int:
|
||||
"""Populate the LXC IP cache for every running CT at Monitor
|
||||
startup. After this runs, /api/vms serves the IPs from memory
|
||||
without spawning `lxc-info` on the request path — the cache only
|
||||
changes when a CT's lifecycle event (start/stop/reboot) fires the
|
||||
invalidator. Returns the count for the startup log line.
|
||||
until the short TTL expires or a lifecycle event invalidates it.
|
||||
Returns the count for the startup log line.
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
@@ -1815,20 +1849,21 @@ _vm_mounts_cache: dict = {} # vmid -> (ts, payload) — LXC only
|
||||
_VM_LIST_SEARCH_NOTES_TTL = 30
|
||||
_vm_list_search_notes_cache: dict = {"ts": 0.0, "signature": (), "notes": {}}
|
||||
_vm_list_search_notes_lock = threading.Lock()
|
||||
# LXC IP classification cache — populated on first read, held indefinitely.
|
||||
# A running CT's IP doesn't change; the cache is invalidated only when
|
||||
# the CT's lifecycle event fires (start/stop/reboot), so no periodic
|
||||
# polling is needed. See `_handle_guest_lifecycle`.
|
||||
# DHCP addresses can change without a lifecycle event.
|
||||
_LXC_IP_TTL = 60
|
||||
_LXC_IP_RETRY_TTL = 5
|
||||
_lxc_ip_cache: dict = {}
|
||||
_lxc_ip_cache_lock = threading.RLock()
|
||||
_lxc_os_info_cache: dict = {}
|
||||
_LXC_OS_INFO_TTL = 60
|
||||
# Effective TTL is "indefinite": these caches are refreshed only
|
||||
# by explicit event-based invalidation (`_vm_cache_invalidate` calls
|
||||
# on start/stop/reboot, add/edit/delete app, apply update, edit
|
||||
# schedule, create backup). No periodic poll — the prewarmer runs
|
||||
# once at startup and then stays quiet. The rationale is that all
|
||||
# of these payloads are backed by files (.conf / sidecar JSON /
|
||||
# storage inventory) that only change through actions the Monitor
|
||||
# either performs itself (invalidates in-line) or that require a
|
||||
# guest restart (start/stop invalidates too). Runtime data that DOES
|
||||
# storage inventory). Details and mounts additionally validate the .conf
|
||||
# signature on each read, including edits made outside Monitor. Runtime data that DOES
|
||||
# change without an invalidation event lives in a different path:
|
||||
# - Live CPU/mem/disk/network per guest → served by /api/vms (SWR
|
||||
# poll every 2.5 s from the client, no cache here).
|
||||
@@ -1923,12 +1958,16 @@ def _vm_cache_put(cache: dict, vmid: int, value) -> None:
|
||||
_vm_conf_built: dict = {} # (cache name, vmid) -> .conf signature
|
||||
|
||||
def _vm_conf_signature(vmid: int):
|
||||
import hashlib
|
||||
for path in (f'/etc/pve/lxc/{int(vmid)}.conf', f'/etc/pve/qemu-server/{int(vmid)}.conf'):
|
||||
try:
|
||||
info = os.stat(path)
|
||||
with open(path, 'rb') as handle:
|
||||
digest = hashlib.sha256(handle.read()).digest()
|
||||
except OSError:
|
||||
continue
|
||||
return (info.st_mtime_ns, info.st_size)
|
||||
# Same-second, same-size changes must also invalidate the payload.
|
||||
return (path, info.st_mtime_ns, info.st_size, digest)
|
||||
return None
|
||||
|
||||
def _vm_conf_cache_get(cache: dict, name: str, vmid: int, ttl: int):
|
||||
@@ -1938,11 +1977,11 @@ def _vm_conf_cache_get(cache: dict, name: str, vmid: int, ttl: int):
|
||||
return None
|
||||
with _vm_modal_cache_lock:
|
||||
built = _vm_conf_built.get((name, vmid))
|
||||
return cached if built == _vm_conf_signature(vmid) else None
|
||||
return cached if built is not None and 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:
|
||||
cache[vmid] = (time.time(), value)
|
||||
_vm_conf_built[(name, vmid)] = signature
|
||||
|
||||
def _vm_cache_invalidate(vmid: int, *caches) -> None:
|
||||
@@ -1953,6 +1992,7 @@ def _vm_cache_invalidate(vmid: int, *caches) -> None:
|
||||
targets = caches or (
|
||||
_vm_details_cache, _vm_backups_cache,
|
||||
_vm_app_suggestions_cache, _vm_schedule_cache, _vm_mounts_cache,
|
||||
_lxc_os_info_cache,
|
||||
)
|
||||
with _vm_modal_cache_lock:
|
||||
for c in targets:
|
||||
@@ -6635,6 +6675,7 @@ def _get_lxc_update_status_map() -> dict:
|
||||
continue
|
||||
update = it.get('update_check') or {}
|
||||
out[str(vmid)] = {
|
||||
**managed_installs.lxc_update_freshness(update),
|
||||
'available': bool(update.get('available')),
|
||||
'count': int(update.get('_count') or 0),
|
||||
'security_count': int(update.get('_security_count') or 0),
|
||||
@@ -13594,6 +13635,46 @@ def api_managed_installs_refresh():
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
_lxc_update_check_lock = threading.Lock()
|
||||
_lxc_update_check_attempts: dict = {}
|
||||
_lxc_update_check_inflight: set = set()
|
||||
_LXC_UPDATE_CHECK_INTERVAL = 60
|
||||
|
||||
|
||||
@app.route('/api/vms/<int:vmid>/updates/check', methods=['POST'])
|
||||
@require_auth
|
||||
def api_lxc_updates_check(vmid):
|
||||
"""Explicit read of OS and locally known packages, with no index refresh."""
|
||||
import managed_installs
|
||||
if not managed_installs._lxc_updates_detection_enabled():
|
||||
return jsonify({'error': 'LXC update detection is disabled'}), 403
|
||||
if _detect_vm_type_local(vmid) != 'lxc':
|
||||
return jsonify({'error': 'LXC not found'}), 404
|
||||
if _fast_guest_status(vmid, 'lxc') != 'running':
|
||||
return jsonify({'error': 'LXC is not running'}), 409
|
||||
now = time.monotonic()
|
||||
with _lxc_update_check_lock:
|
||||
last = _lxc_update_check_attempts.get(vmid)
|
||||
if vmid in _lxc_update_check_inflight or (
|
||||
last is not None and now - last < _LXC_UPDATE_CHECK_INTERVAL):
|
||||
retry = max(1, int(_LXC_UPDATE_CHECK_INTERVAL - (now - (last if last is not None else now))) + 1)
|
||||
return jsonify({'error': 'Check already running or recently requested',
|
||||
'retry_after': retry}), 429, {'Retry-After': str(retry)}
|
||||
_lxc_update_check_attempts[vmid] = now
|
||||
_lxc_update_check_inflight.add(vmid)
|
||||
try:
|
||||
item = managed_installs.refresh_lxc(vmid, refresh_package_cache=False)
|
||||
if item is None:
|
||||
return jsonify({'error': 'No supported running LXC detected'}), 409
|
||||
_vm_cache_invalidate(vmid, _lxc_os_info_cache)
|
||||
return jsonify({'vmid': vmid, 'update_check': _get_lxc_update_status_map().get(str(vmid))})
|
||||
except Exception as exc:
|
||||
return jsonify({'error': str(exc)}), 500
|
||||
finally:
|
||||
with _lxc_update_check_lock:
|
||||
_lxc_update_check_inflight.discard(vmid)
|
||||
|
||||
|
||||
# ─── LXC Update Detection toggle ────────────────────────────────────────────
|
||||
# Dedicated toggle so the operator can opt out of the per-CT `pct exec apt
|
||||
# list --upgradable` scan entirely. The Notifications section keeps its own
|
||||
@@ -15269,12 +15350,12 @@ def api_vms_modal_cache_all():
|
||||
def get_vm_config(vmid):
|
||||
"""Get detailed configuration for a specific VM/LXC"""
|
||||
try:
|
||||
# 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:
|
||||
return jsonify(cached)
|
||||
return jsonify(_lxc_runtime_details(cached))
|
||||
|
||||
# Taken before reading the .conf, so an edit during the build is seen next time.
|
||||
signature = _vm_conf_signature(vmid)
|
||||
node = get_proxmox_node_name()
|
||||
|
||||
# Fast local path — read .conf directly and derive status from
|
||||
@@ -15294,8 +15375,6 @@ def get_vm_config(vmid):
|
||||
if not config:
|
||||
return jsonify({'error': 'VM/LXC not found'}), 404
|
||||
|
||||
status = _fast_guest_status(vmid, vm_type)
|
||||
|
||||
response_data = {
|
||||
'vmid': vmid,
|
||||
'config': config,
|
||||
@@ -15303,46 +15382,17 @@ def get_vm_config(vmid):
|
||||
'vm_type': vm_type,
|
||||
}
|
||||
|
||||
# For LXC, try to get IP from lxc-info if running
|
||||
if vm_type == 'lxc' and status == 'running':
|
||||
lxc_ip_info = get_lxc_ip_from_lxc_info(vmid)
|
||||
if lxc_ip_info:
|
||||
response_data['lxc_ip_info'] = lxc_ip_info
|
||||
|
||||
# Get OS information for LXC
|
||||
os_info = {}
|
||||
if vm_type == 'lxc' and status == 'running':
|
||||
try:
|
||||
os_release_result = subprocess.run(
|
||||
['pct', 'exec', str(vmid), '--', 'cat', '/etc/os-release'],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
if os_release_result.returncode == 0:
|
||||
for line in os_release_result.stdout.split('\n'):
|
||||
line = line.strip()
|
||||
if line.startswith('ID='):
|
||||
os_info['id'] = line.split('=', 1)[1].strip('"').strip("'")
|
||||
elif line.startswith('VERSION_ID='):
|
||||
os_info['version_id'] = line.split('=', 1)[1].strip('"').strip("'")
|
||||
elif line.startswith('NAME='):
|
||||
os_info['name'] = line.split('=', 1)[1].strip('"').strip("'")
|
||||
elif line.startswith('PRETTY_NAME='):
|
||||
os_info['pretty_name'] = line.split('=', 1)[1].strip('"').strip("'")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Get hardware information for LXC — already reads the .conf
|
||||
# directly (no pvesh), so it's cheap.
|
||||
hardware_info = {}
|
||||
if vm_type == 'lxc':
|
||||
hardware_info = parse_lxc_hardware_config(vmid, node)
|
||||
|
||||
if os_info:
|
||||
response_data['os_info'] = os_info
|
||||
if hardware_info:
|
||||
response_data['hardware_info'] = hardware_info
|
||||
|
||||
_vm_conf_cache_put(_vm_details_cache, 'details', vmid, response_data, signature)
|
||||
return jsonify(response_data)
|
||||
return jsonify(_lxc_runtime_details(response_data))
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@@ -846,9 +846,8 @@ def _probe_lxc_os(vmid: str) -> Optional[str]:
|
||||
speak — those CTs are skipped in detection so the framework
|
||||
doesn't keep retrying a checker we can't run.
|
||||
|
||||
Cached per CT in the registry — re-probed only when the entry has
|
||||
no ``_os_family`` yet, since the OS rarely changes for the life of
|
||||
a CT.
|
||||
Detection may reuse the registry hint; each explicit or scheduled update
|
||||
check re-probes this file before selecting the package manager.
|
||||
"""
|
||||
try:
|
||||
r = subprocess.run(
|
||||
@@ -860,13 +859,20 @@ def _probe_lxc_os(vmid: str) -> Optional[str]:
|
||||
return None
|
||||
if r.returncode != 0:
|
||||
return None
|
||||
text = r.stdout.lower()
|
||||
if "id=ubuntu" in text:
|
||||
release = {}
|
||||
for line in r.stdout.splitlines():
|
||||
key, sep, value = line.partition("=")
|
||||
if sep:
|
||||
release[key.strip()] = value.strip().strip('\"').strip("'").lower()
|
||||
identity = release.get("ID", "")
|
||||
if identity == "ubuntu":
|
||||
return "ubuntu"
|
||||
if "id=debian" in text or "id_like=debian" in text:
|
||||
if identity == "debian":
|
||||
return "debian"
|
||||
if "id=alpine" in text:
|
||||
if identity == "alpine":
|
||||
return "alpine"
|
||||
if "debian" in release.get("ID_LIKE", "").split():
|
||||
return "debian"
|
||||
# Future Phase 1.5: CentOS/Rocky/Alma (dnf check-update), Arch
|
||||
# (checkupdates), openSUSE (zypper list-updates). Each needs a
|
||||
# parser similar to apt/apk — skip silently for now.
|
||||
@@ -877,10 +883,9 @@ def _detect_lxc_containers(only_vmid: Optional[int] = None) -> list[dict]:
|
||||
"""Enumerate running Debian/Ubuntu CTs as registry entries.
|
||||
|
||||
OS detection is cached in the registry entry (`_os_family`), so the
|
||||
expensive ``pct exec cat /etc/os-release`` only runs the first time
|
||||
a CT is seen. CT reinstalls with a different OS will keep the old
|
||||
family cached until the user resets the registry — acceptable
|
||||
trade-off vs paying the probe cost every 24h cycle.
|
||||
expensive ``pct exec cat /etc/os-release`` only runs during detection
|
||||
when a CT is first seen. The checker independently re-reads it on each
|
||||
check, including after a restore or external reinstall.
|
||||
|
||||
Detection respects the dedicated `lxc_updates.detection_enabled`
|
||||
toggle (Settings → LXC Update Detection). When OFF, this returns []
|
||||
@@ -1442,12 +1447,33 @@ def _refresh_lxc_pkg_cache_if_stale(vmid: str, family: str) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _check_lxc_updates(entry: dict) -> dict:
|
||||
LXC_UPDATE_STALE_AFTER_SECONDS = 24 * 3600
|
||||
|
||||
|
||||
def lxc_update_freshness(update: dict) -> dict:
|
||||
"""Describe a stored observation, without running any guest command."""
|
||||
age = None
|
||||
try:
|
||||
checked = datetime.datetime.fromisoformat(update.get("last_check", "").replace("Z", "+00:00"))
|
||||
if checked.tzinfo is None:
|
||||
checked = checked.replace(tzinfo=datetime.timezone.utc)
|
||||
seconds = (datetime.datetime.now(datetime.timezone.utc) - checked).total_seconds()
|
||||
if seconds >= 0:
|
||||
age = int(seconds)
|
||||
except (ValueError, TypeError, AttributeError):
|
||||
pass
|
||||
stale = age is None or age >= LXC_UPDATE_STALE_AFTER_SECONDS
|
||||
return {"stale": stale, "age_seconds": age,
|
||||
"stale_after_seconds": LXC_UPDATE_STALE_AFTER_SECONDS}
|
||||
|
||||
|
||||
def _check_lxc_updates(entry: dict, *, refresh_package_cache: bool = True) -> dict:
|
||||
"""Inspect pending package updates inside the LXC and report them.
|
||||
|
||||
Dispatches to the right package-manager parser based on the cached
|
||||
``_os_family``. If the CT's local apt/apk metadata cache is older
|
||||
than 24h, runs a best-effort refresh first via
|
||||
Re-read /etc/os-release on every explicit/scheduled check, so a restore
|
||||
or external reinstall cannot keep the old registry family. If the CT's
|
||||
local apt/apk metadata cache is older than 24h, the existing collector
|
||||
runs a best-effort refresh first via
|
||||
``_refresh_lxc_pkg_cache_if_stale`` — without this, CTs that no
|
||||
one ever runs ``apt update`` in (long-running appliances) report
|
||||
0 pending updates even when upstream has hundreds queued.
|
||||
@@ -1458,13 +1484,15 @@ def _check_lxc_updates(entry: dict) -> dict:
|
||||
update set does.
|
||||
"""
|
||||
vmid = entry.get("_vmid")
|
||||
family = (entry.get("_os_family") or "").lower()
|
||||
if not vmid:
|
||||
return {
|
||||
"available": False, "latest": None,
|
||||
"last_check": _now_iso(), "error": "no vmid in entry",
|
||||
}
|
||||
|
||||
family = _probe_lxc_os(str(vmid))
|
||||
entry["_os_family"] = family
|
||||
|
||||
# OCI-image CTs are immutable by design — apt/apk upgrade inside
|
||||
# them is the wrong workflow (update = rebuild from a newer image
|
||||
# tag). Skip the package-manager probe entirely so the UI doesn't
|
||||
@@ -1483,7 +1511,15 @@ def _check_lxc_updates(entry: dict) -> dict:
|
||||
"_count": 0, "_security_count": 0, "_packages": [],
|
||||
}
|
||||
|
||||
refresh_diag = _refresh_lxc_pkg_cache_if_stale(vmid, family)
|
||||
if family not in _SUPPORTED_OS_FAMILIES:
|
||||
return {
|
||||
"available": False, "latest": None, "last_check": _now_iso(),
|
||||
"error": "current OS could not be determined or is unsupported",
|
||||
}
|
||||
|
||||
# The manual read-only action must never run apt-get/apk update.
|
||||
refresh_diag = (_refresh_lxc_pkg_cache_if_stale(vmid, family)
|
||||
if refresh_package_cache else {})
|
||||
|
||||
if family in ("debian", "ubuntu"):
|
||||
ok, stdout, err = _run_pct_pkg_listing(
|
||||
@@ -1783,12 +1819,12 @@ def check_for_updates(force: bool = False) -> list[dict]:
|
||||
return updates_available
|
||||
|
||||
|
||||
def refresh_lxc(vmid: int) -> Optional[dict]:
|
||||
def refresh_lxc(vmid: int, *, refresh_package_cache: bool = True) -> Optional[dict]:
|
||||
"""Detect and refresh exactly one running LXC.
|
||||
|
||||
This is the lifecycle counterpart of the daily collector. It is called
|
||||
after a stopped container starts and deliberately leaves every other
|
||||
guest's registry row untouched.
|
||||
Called by lifecycle/apply finalizers and the explicit read-only action.
|
||||
The latter passes refresh_package_cache=False to forbid index writes.
|
||||
Every other guest's registry row is left untouched.
|
||||
"""
|
||||
try:
|
||||
target_vmid = int(vmid)
|
||||
@@ -1813,7 +1849,8 @@ def refresh_lxc(vmid: int) -> Optional[dict]:
|
||||
_merge_detected_entry(target, entry, now)
|
||||
|
||||
try:
|
||||
result = _check_lxc_updates(target)
|
||||
result = (_check_lxc_updates(target) if refresh_package_cache else
|
||||
_check_lxc_updates(target, refresh_package_cache=False))
|
||||
except Exception as exc:
|
||||
result = {
|
||||
"available": False,
|
||||
@@ -1825,5 +1862,6 @@ def refresh_lxc(vmid: int) -> Optional[dict]:
|
||||
reg["items"] = items
|
||||
reg["version"] = _SCHEMA_VERSION
|
||||
reg["last_targeted_refresh"] = now
|
||||
_write_registry(reg)
|
||||
if not _write_registry(reg):
|
||||
raise OSError("could not persist LXC update check")
|
||||
return dict(target)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import ast
|
||||
import threading
|
||||
from unittest.mock import Mock
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
@@ -109,6 +110,10 @@ class LxcPrimaryAddressTests(unittest.TestCase):
|
||||
import types
|
||||
self.server.update({
|
||||
"_lxc_ip_cache": {},
|
||||
"_lxc_ip_cache_lock": threading.RLock(),
|
||||
"_LXC_IP_TTL": 60,
|
||||
"_LXC_IP_RETRY_TTL": 5,
|
||||
"time": Mock(monotonic=Mock(return_value=0)),
|
||||
"subprocess": types.SimpleNamespace(run=run),
|
||||
"_lxc_isolated_ips": lambda _vmid: set(),
|
||||
"_lxc_shares_host_network": lambda _vmid: False,
|
||||
@@ -126,7 +131,8 @@ class LxcPrimaryAddressTests(unittest.TestCase):
|
||||
|
||||
def test_an_answer_without_ipv4_of_its_own_is_read_again(self):
|
||||
self.read("172.30.232.1\n172.30.32.1\nfd0c:ac1e:2100::1\n")
|
||||
self.assertNotIn(121, self.server["_lxc_ip_cache"])
|
||||
self.assertIn(121, self.server["_lxc_ip_cache"])
|
||||
self.server["time"].monotonic.return_value = 5
|
||||
self.assertEqual(self.read("192.168.0.100\n172.30.32.1\n")["primary_ip"], "192.168.0.100")
|
||||
self.assertIn(121, self.server["_lxc_ip_cache"])
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""The VM/LXC modal caches notice a .conf edited outside the Monitor."""
|
||||
import ast
|
||||
import io
|
||||
from pathlib import Path
|
||||
import threading
|
||||
import time
|
||||
@@ -16,6 +17,7 @@ class ConfAwareCacheTests(unittest.TestCase):
|
||||
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)}
|
||||
self.contents = {}
|
||||
|
||||
def stat(path):
|
||||
if path not in self.files:
|
||||
@@ -24,7 +26,7 @@ class ConfAwareCacheTests(unittest.TestCase):
|
||||
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': {}}
|
||||
'_vm_conf_built': {}, 'open': lambda path, _: io.BytesIO(self.contents.get(path, b'onboot: 0'))}
|
||||
exec(compile(ast.Module(body=nodes, type_ignores=[]), str(SERVER), 'exec'), self.scope)
|
||||
self.cache = {}
|
||||
|
||||
@@ -50,9 +52,19 @@ class ConfAwareCacheTests(unittest.TestCase):
|
||||
self.scope['_vm_cache_put'](self.cache, 111, {'old': True})
|
||||
self.assertIsNone(self.get())
|
||||
|
||||
def test_same_size_same_mtime_content_edit_is_detected(self):
|
||||
self.build({'onboot': 0})
|
||||
self.contents['/etc/pve/lxc/111.conf'] = b'onboot: 1'
|
||||
self.assertIsNone(self.get())
|
||||
|
||||
def test_deleted_configuration_is_never_served_from_cache(self):
|
||||
self.build({'onboot': 0})
|
||||
self.files.clear()
|
||||
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.assertEqual(self.scope['_vm_conf_signature'](200)[1:3], (5, 10))
|
||||
self.assertIsNone(self.scope['_vm_conf_signature'](999))
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user