fix: derive VLANs from VM net tags and fix update warning consistency
get_vlans() previously parsed 'bridge vlan show' which trunks all 4094 VIDs by default on VLAN-aware bridges, producing phantom VLAN entries. Now derives real VLAN assignments from VM/container netN bridge=,tag= config. get_device_warnings() now reports updates_available with severity "warning" and a full package list/title, matching the format used when the warning is refreshed via the updates API (previously alternated between "info" and "warning" for the same content).
This commit is contained in:
+74
-31
@@ -705,45 +705,83 @@ class ProxmoxDriver(HypervisorDriver):
|
|||||||
vlan_entry["untagged"] = list(access_by_vlan.get(vid, []))
|
vlan_entry["untagged"] = list(access_by_vlan.get(vid, []))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# --- Linux bridge fallback (bridge vlan show) ---
|
# --- Linux bridge fallback ---
|
||||||
# Convert result entries to use tagged/untagged keys
|
# Convert result entries to use tagged/untagged keys
|
||||||
for entry in result.values():
|
for entry in result.values():
|
||||||
entry.setdefault("tagged", [])
|
entry.setdefault("tagged", [])
|
||||||
entry.setdefault("untagged", [])
|
entry.setdefault("untagged", [])
|
||||||
|
|
||||||
raw = self._exec_ssh_command("bridge vlan show 2>/dev/null || true")
|
# `bridge vlan show` reports every VID (1-4094) as trunked on each port
|
||||||
current_iface = ""
|
# of a VLAN-aware bridge by default — that's Proxmox's standard
|
||||||
for line in raw.splitlines():
|
# configuration and not meaningful as a VLAN table. Instead, derive the
|
||||||
line = line.strip()
|
# VLANs actually in use from the per-VM/container net config (tag=N on
|
||||||
if not line:
|
# a bridge), which reflects real assignments.
|
||||||
continue
|
for tag, bridges in self._get_vm_vlan_tags().items():
|
||||||
m = re.match(r"^(\S+)\s+(\d+)", line)
|
entry = result.setdefault(tag, {"name": "", "tagged": [], "untagged": []})
|
||||||
if m:
|
for bridge in bridges:
|
||||||
current_iface = m.group(1)
|
if bridge not in entry["untagged"]:
|
||||||
vid = m.group(2)
|
entry["untagged"].append(bridge)
|
||||||
else:
|
|
||||||
m2 = re.match(r"^\s*(\d+)", line)
|
|
||||||
if m2:
|
|
||||||
vid = m2.group(1)
|
|
||||||
else:
|
|
||||||
continue
|
|
||||||
if current_iface and vid:
|
|
||||||
entry = result.setdefault(vid, {"name": "", "tagged": [], "untagged": []})
|
|
||||||
if "Untagged" in line or "PVID" in line:
|
|
||||||
if current_iface not in entry["untagged"]:
|
|
||||||
entry["untagged"].append(current_iface)
|
|
||||||
else:
|
|
||||||
if current_iface not in entry["tagged"]:
|
|
||||||
entry["tagged"].append(current_iface)
|
|
||||||
|
|
||||||
# Only return VLANs that are actually assigned to at least one interface.
|
|
||||||
# Linux bridge vlan show reports all 4094 possible VIDs per port —
|
|
||||||
# filtering here avoids polluting the VLAN table with phantom entries.
|
|
||||||
return {
|
return {
|
||||||
vid: entry for vid, entry in result.items()
|
vid: entry for vid, entry in result.items()
|
||||||
if entry.get("tagged") or entry.get("untagged")
|
if entry.get("tagged") or entry.get("untagged")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _get_vm_vlan_tags(self) -> dict[str, set[str]]:
|
||||||
|
"""Return ``{vlan_tag: {bridge_names}}`` derived from VM/container net configs.
|
||||||
|
|
||||||
|
Scans every QEMU VM and LXC container on this node for ``netN`` config
|
||||||
|
entries of the form ``bridge=vmbrX,tag=N,...`` and groups the bridges
|
||||||
|
each VLAN tag is used on.
|
||||||
|
"""
|
||||||
|
tags: dict[str, set[str]] = {}
|
||||||
|
net_re = re.compile(r"^net\d+$")
|
||||||
|
|
||||||
|
def _collect(vmid: int, config: _JsonDict) -> None:
|
||||||
|
for key, val in config.items():
|
||||||
|
if not net_re.match(key):
|
||||||
|
continue
|
||||||
|
bridge = ""
|
||||||
|
tag: int | None = None
|
||||||
|
for part in str(val).split(","):
|
||||||
|
if "=" not in part:
|
||||||
|
continue
|
||||||
|
k, v = part.split("=", 1)
|
||||||
|
k = k.strip().lower()
|
||||||
|
if k == "bridge":
|
||||||
|
bridge = v.strip()
|
||||||
|
elif k == "tag":
|
||||||
|
try:
|
||||||
|
tag = int(v.strip())
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if tag is not None and bridge:
|
||||||
|
tags.setdefault(str(tag), set()).add(bridge)
|
||||||
|
|
||||||
|
try:
|
||||||
|
for vm in (self._node_api().qemu.get() or []):
|
||||||
|
vmid = int(vm.get("vmid", 0))
|
||||||
|
try:
|
||||||
|
config = self._node_api().qemu(vmid).config.get() or {}
|
||||||
|
_collect(vmid, config)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug("_get_vm_vlan_tags: QEMU %s config failed: %s", vmid, exc)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("_get_vm_vlan_tags: failed to list QEMU VMs: %s", exc)
|
||||||
|
|
||||||
|
try:
|
||||||
|
for ct in (self._node_api().lxc.get() or []):
|
||||||
|
vmid = int(ct.get("vmid", 0))
|
||||||
|
try:
|
||||||
|
config = self._node_api().lxc(vmid).config.get() or {}
|
||||||
|
_collect(vmid, config)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug("_get_vm_vlan_tags: LXC %s config failed: %s", vmid, exc)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("_get_vm_vlan_tags: failed to list LXC containers: %s", exc)
|
||||||
|
|
||||||
|
return tags
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# get_network_instances (SDN Zones as VRF-like instances)
|
# get_network_instances (SDN Zones as VRF-like instances)
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
@@ -1783,15 +1821,20 @@ class ProxmoxDriver(HypervisorDriver):
|
|||||||
|
|
||||||
# 1. Available package updates
|
# 1. Available package updates
|
||||||
try:
|
try:
|
||||||
updates = self._api.nodes(self._node_name).apt.update.get()
|
updates = self.get_available_updates()
|
||||||
if updates:
|
if updates:
|
||||||
warnings.append({
|
warnings.append({
|
||||||
"code": "updates_available",
|
"code": "updates_available",
|
||||||
"severity": "info",
|
"severity": "warning",
|
||||||
|
"title": (
|
||||||
|
f"{len(updates)} package update"
|
||||||
|
f"{'s' if len(updates) != 1 else ''} available"
|
||||||
|
),
|
||||||
|
"message": None,
|
||||||
"action": None,
|
"action": None,
|
||||||
"meta": {
|
"meta": {
|
||||||
"count": len(updates),
|
"count": len(updates),
|
||||||
"packages": [u.get("Package", "") for u in updates[:10]],
|
"packages": [u["name"] for u in updates],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
Reference in New Issue
Block a user