mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-14 18:56:52 +00:00
Merge pull request #336 from Vaso73/fix/lxc-mount-target-normalization
fix: recognize LXC mount targets with trailing slashes
This commit is contained in:
@@ -606,6 +606,9 @@ function MountPointCard({ mp }: { mp: LxcMountPoint }) {
|
|||||||
})
|
})
|
||||||
const flags = optsEntries.filter((o) => o.value === null).map((o) => o.key)
|
const flags = optsEntries.filter((o) => o.value === null).map((o) => o.key)
|
||||||
const keyValues = optsEntries.filter((o) => o.value !== null) as Array<{ key: string; value: string }>
|
const keyValues = optsEntries.filter((o) => o.value !== null) as Array<{ key: string; value: string }>
|
||||||
|
const runtimeError = mp.runtime_error === "configured but not mounted"
|
||||||
|
? t("vmLxc.details.mountErrors.configuredButNotMounted")
|
||||||
|
: mp.runtime_error
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`rounded-lg p-4 ${cardClasses}`}>
|
<div className={`rounded-lg p-4 ${cardClasses}`}>
|
||||||
@@ -774,13 +777,13 @@ function MountPointCard({ mp }: { mp: LxcMountPoint }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Error / divergence note. */}
|
{/* Error / divergence note. */}
|
||||||
{mp.runtime_error && (
|
{runtimeError && (
|
||||||
<p
|
<p
|
||||||
className={`mt-3 text-sm ${
|
className={`mt-3 text-sm ${
|
||||||
isStale ? "text-red-400" : "text-amber-400"
|
isStale ? "text-red-400" : "text-amber-400"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{mp.runtime_error}
|
{runtimeError}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1044,6 +1044,9 @@
|
|||||||
"stopped": "stopped",
|
"stopped": "stopped",
|
||||||
"mounted": "mounted"
|
"mounted": "mounted"
|
||||||
},
|
},
|
||||||
|
"mountErrors": {
|
||||||
|
"configuredButNotMounted": "Configured but not mounted"
|
||||||
|
},
|
||||||
"startOnBoot": "Start at boot",
|
"startOnBoot": "Start at boot",
|
||||||
"tags": "Tags",
|
"tags": "Tags",
|
||||||
"tagsPlaceholder": "Add tag…",
|
"tagsPlaceholder": "Add tag…",
|
||||||
|
|||||||
@@ -1044,6 +1044,9 @@
|
|||||||
"stopped": "vypnuté",
|
"stopped": "vypnuté",
|
||||||
"mounted": "pripojené"
|
"mounted": "pripojené"
|
||||||
},
|
},
|
||||||
|
"mountErrors": {
|
||||||
|
"configuredButNotMounted": "Nastavené, ale nepripojené"
|
||||||
|
},
|
||||||
"startOnBoot": "Spúšťať pri štarte",
|
"startOnBoot": "Spúšťať pri štarte",
|
||||||
"tags": "Značky",
|
"tags": "Značky",
|
||||||
"tagsPlaceholder": "Pridať značku…",
|
"tagsPlaceholder": "Pridať značku…",
|
||||||
|
|||||||
@@ -123,6 +123,20 @@ def _read_lxc_config(vmid: str) -> list[dict[str, Any]]:
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _mount_target_key(target: str) -> str:
|
||||||
|
"""Return a stable comparison key for a CT-side mount target.
|
||||||
|
|
||||||
|
Proxmox config accepts a trailing slash in ``mp=/path/`` while
|
||||||
|
``/proc/<pid>/mounts`` reports the realised target as ``/path``.
|
||||||
|
They name the same mount point, so comparisons must not treat the
|
||||||
|
spelling difference as a runtime divergence. Keep the root path
|
||||||
|
intact: stripping its only slash would turn it into an empty key.
|
||||||
|
"""
|
||||||
|
if target == "/":
|
||||||
|
return target
|
||||||
|
return target.rstrip("/")
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Type classification + source resolution
|
# Type classification + source resolution
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -605,14 +619,14 @@ def get_lxc_mount_points_static(vmid: str) -> dict[str, Any]:
|
|||||||
if running and host_pid:
|
if running and host_pid:
|
||||||
try:
|
try:
|
||||||
config_targets = {
|
config_targets = {
|
||||||
entry.get("target", "")
|
_mount_target_key(entry.get("target", ""))
|
||||||
for entry in config_entries
|
for entry in config_entries
|
||||||
if entry.get("target")
|
if entry.get("target")
|
||||||
}
|
}
|
||||||
for rt in _read_ct_proc_mounts(host_pid):
|
for rt in _read_ct_proc_mounts(host_pid):
|
||||||
if not _REMOTE_FS_RE.match(rt.get("rt_fstype", "")):
|
if not _REMOTE_FS_RE.match(rt.get("rt_fstype", "")):
|
||||||
continue
|
continue
|
||||||
if rt.get("rt_target") in config_targets:
|
if _mount_target_key(rt.get("rt_target", "")) in config_targets:
|
||||||
continue
|
continue
|
||||||
ad_hoc_hint_count += 1
|
ad_hoc_hint_count += 1
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -658,7 +672,9 @@ def get_lxc_mount_points_runtime(vmid: str) -> dict[str, Any]:
|
|||||||
# mount point are I/O-bound. Serialised, a CT with 5+ binds
|
# mount point are I/O-bound. Serialised, a CT with 5+ binds
|
||||||
# tripped Caddy's 3s reverse-proxy timeout.
|
# tripped Caddy's 3s reverse-proxy timeout.
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
rt_by_target: dict[str, dict[str, Any]] = {m["rt_target"]: m for m in rt_mounts}
|
rt_by_target: dict[str, dict[str, Any]] = {
|
||||||
|
_mount_target_key(m["rt_target"]): m for m in rt_mounts
|
||||||
|
}
|
||||||
|
|
||||||
runtime_by_target: dict[str, dict[str, Any]] = {}
|
runtime_by_target: dict[str, dict[str, Any]] = {}
|
||||||
matched_targets: set[str] = set()
|
matched_targets: set[str] = set()
|
||||||
@@ -673,9 +689,10 @@ def get_lxc_mount_points_runtime(vmid: str) -> dict[str, Any]:
|
|||||||
host_pid=host_pid if running else "",
|
host_pid=host_pid if running else "",
|
||||||
target=tgt,
|
target=tgt,
|
||||||
)
|
)
|
||||||
live_target = bool(running and tgt and tgt in rt_by_target)
|
target_key = _mount_target_key(tgt)
|
||||||
|
live_target = bool(running and tgt and target_key in rt_by_target)
|
||||||
health = _stat_via_host(host_pid, tgt) if live_target else None
|
health = _stat_via_host(host_pid, tgt) if live_target else None
|
||||||
return entry, capacity, live_target, health
|
return entry, capacity, target_key, live_target, health
|
||||||
|
|
||||||
if config_entries:
|
if config_entries:
|
||||||
max_workers = max(2, min(8, len(config_entries)))
|
max_workers = max(2, min(8, len(config_entries)))
|
||||||
@@ -684,11 +701,11 @@ def get_lxc_mount_points_runtime(vmid: str) -> dict[str, Any]:
|
|||||||
else:
|
else:
|
||||||
gathered = []
|
gathered = []
|
||||||
|
|
||||||
for entry, cap, live_target, health in gathered:
|
for entry, cap, target_key, live_target, health in gathered:
|
||||||
target = entry.get("target", "")
|
target = entry.get("target", "")
|
||||||
rt_item: dict[str, Any] = {**cap}
|
rt_item: dict[str, Any] = {**cap}
|
||||||
if live_target:
|
if live_target:
|
||||||
rt = rt_by_target[target]
|
rt = rt_by_target[target_key]
|
||||||
rt_item.update({
|
rt_item.update({
|
||||||
"runtime_mounted": True,
|
"runtime_mounted": True,
|
||||||
"runtime_source": rt["rt_source"],
|
"runtime_source": rt["rt_source"],
|
||||||
@@ -698,7 +715,7 @@ def get_lxc_mount_points_runtime(vmid: str) -> dict[str, Any]:
|
|||||||
"runtime_reachable": health["reachable"],
|
"runtime_reachable": health["reachable"],
|
||||||
"runtime_error": health["error"],
|
"runtime_error": health["error"],
|
||||||
})
|
})
|
||||||
matched_targets.add(target)
|
matched_targets.add(target_key)
|
||||||
elif running:
|
elif running:
|
||||||
rt_item["runtime_mounted"] = False
|
rt_item["runtime_mounted"] = False
|
||||||
rt_item["runtime_error"] = "configured but not mounted"
|
rt_item["runtime_error"] = "configured but not mounted"
|
||||||
@@ -712,7 +729,7 @@ def get_lxc_mount_points_runtime(vmid: str) -> dict[str, Any]:
|
|||||||
if running:
|
if running:
|
||||||
ad_hoc_candidates = [
|
ad_hoc_candidates = [
|
||||||
rt for rt in rt_mounts
|
rt for rt in rt_mounts
|
||||||
if rt["rt_target"] not in matched_targets
|
if _mount_target_key(rt["rt_target"]) not in matched_targets
|
||||||
and _REMOTE_FS_RE.match(rt["rt_fstype"])
|
and _REMOTE_FS_RE.match(rt["rt_fstype"])
|
||||||
]
|
]
|
||||||
if ad_hoc_candidates:
|
if ad_hoc_candidates:
|
||||||
@@ -776,7 +793,9 @@ def get_lxc_mount_points(vmid: str) -> dict[str, Any]:
|
|||||||
|
|
||||||
# Index runtime mounts by their CT-side target path so we can
|
# Index runtime mounts by their CT-side target path so we can
|
||||||
# match a config entry to its current realised state in O(1).
|
# match a config entry to its current realised state in O(1).
|
||||||
rt_by_target: dict[str, dict[str, Any]] = {m["rt_target"]: m for m in rt_mounts}
|
rt_by_target: dict[str, dict[str, Any]] = {
|
||||||
|
_mount_target_key(m["rt_target"]): m for m in rt_mounts
|
||||||
|
}
|
||||||
|
|
||||||
out: list[dict[str, Any]] = []
|
out: list[dict[str, Any]] = []
|
||||||
matched_targets: set[str] = set()
|
matched_targets: set[str] = set()
|
||||||
@@ -801,15 +820,16 @@ def get_lxc_mount_points(vmid: str) -> dict[str, Any]:
|
|||||||
target=tgt,
|
target=tgt,
|
||||||
)
|
)
|
||||||
host_src = _host_source_state(src)
|
host_src = _host_source_state(src)
|
||||||
live_target = bool(running and tgt and tgt in rt_by_target)
|
target_key = _mount_target_key(tgt)
|
||||||
|
live_target = bool(running and tgt and target_key in rt_by_target)
|
||||||
health = _stat_via_host(host_pid, tgt) if live_target else None
|
health = _stat_via_host(host_pid, tgt) if live_target else None
|
||||||
return entry, classification, capacity, host_src, live_target, health
|
return entry, classification, capacity, host_src, target_key, live_target, health
|
||||||
|
|
||||||
max_workers = max(2, min(8, len(config_entries) or 1))
|
max_workers = max(2, min(8, len(config_entries) or 1))
|
||||||
with ThreadPoolExecutor(max_workers=max_workers) as pool:
|
with ThreadPoolExecutor(max_workers=max_workers) as pool:
|
||||||
gathered = list(pool.map(_gather_one, config_entries))
|
gathered = list(pool.map(_gather_one, config_entries))
|
||||||
|
|
||||||
for entry, cls, cap, host_src, live_target, health in gathered:
|
for entry, cls, cap, host_src, target_key, live_target, health in gathered:
|
||||||
source = entry.get("source", "")
|
source = entry.get("source", "")
|
||||||
target = entry.get("target", "")
|
target = entry.get("target", "")
|
||||||
|
|
||||||
@@ -830,7 +850,7 @@ def get_lxc_mount_points(vmid: str) -> dict[str, Any]:
|
|||||||
|
|
||||||
# Runtime enrichment when CT is up.
|
# Runtime enrichment when CT is up.
|
||||||
if live_target:
|
if live_target:
|
||||||
rt = rt_by_target[target]
|
rt = rt_by_target[target_key]
|
||||||
item.update({
|
item.update({
|
||||||
"runtime_mounted": True,
|
"runtime_mounted": True,
|
||||||
"runtime_source": rt["rt_source"],
|
"runtime_source": rt["rt_source"],
|
||||||
@@ -840,7 +860,7 @@ def get_lxc_mount_points(vmid: str) -> dict[str, Any]:
|
|||||||
"runtime_reachable": health["reachable"],
|
"runtime_reachable": health["reachable"],
|
||||||
"runtime_error": health["error"],
|
"runtime_error": health["error"],
|
||||||
})
|
})
|
||||||
matched_targets.add(target)
|
matched_targets.add(target_key)
|
||||||
elif running:
|
elif running:
|
||||||
# CT is running but the configured mount isn't in
|
# CT is running but the configured mount isn't in
|
||||||
# /proc/<pid>/mounts — divergence. Could be a startup
|
# /proc/<pid>/mounts — divergence. Could be a startup
|
||||||
@@ -860,7 +880,7 @@ def get_lxc_mount_points(vmid: str) -> dict[str, Any]:
|
|||||||
if running:
|
if running:
|
||||||
ad_hoc_candidates = [
|
ad_hoc_candidates = [
|
||||||
rt for rt in rt_mounts
|
rt for rt in rt_mounts
|
||||||
if rt["rt_target"] not in matched_targets
|
if _mount_target_key(rt["rt_target"]) not in matched_targets
|
||||||
and _REMOTE_FS_RE.match(rt["rt_fstype"])
|
and _REMOTE_FS_RE.match(rt["rt_fstype"])
|
||||||
]
|
]
|
||||||
# Same parallelisation as the configured-mp loop: stat'ing
|
# Same parallelisation as the configured-mp loop: stat'ing
|
||||||
|
|||||||
Reference in New Issue
Block a user