Reconcile dynamic CAPsMAN bridge ports and verify failed changes
This commit is contained in:
1 parent
3244d4414b
commit
eb05748821
3 files changed
+46
-21
No files matched your search
@@ -3,9 +3,10 @@
|
||||
This service applies bridge PVIDs on `wifi-qcom-ac` CAPs. CAPsMAN supplies the
|
||||
SSID and MAC address for each Wi-Fi interface. The local CAP supplies the Wi-Fi
|
||||
interface and bridge port. The reconciler matches by MAC address, chooses the
|
||||
single enabled local interface when a stale disabled duplicate exists, disables
|
||||
the CAPsMAN interface before changing a wrong bridge port, writes the PVID,
|
||||
reads the port back, and only then enables CAPsMAN again. It exports distinct
|
||||
single enabled local interface when a stale disabled duplicate exists, writes
|
||||
the local bridge PVID, and reads the port back. Static CAPsMAN interfaces are
|
||||
disabled during a repair and enabled after verification; RouterOS cannot toggle
|
||||
dynamic CAPsMAN interfaces, so their bridge ports are updated directly. It exports distinct
|
||||
Prometheus metrics for API write failures and VLANs still wrong after readback.
|
||||
|
||||
`chart/` is the portable Kubernetes contract. The deployed cluster chooses its
|
||||
|
||||
+21
-18
@@ -94,27 +94,30 @@ def reconcile_cap(cap, controller_wifi, api, controller_api, vlans):
|
||||
continue
|
||||
|
||||
if not desired_port(port, vlan):
|
||||
# Prevent traffic from using the wrong VLAN while repairing the port.
|
||||
try:
|
||||
if remote.get("disabled") != "true":
|
||||
# Dynamic CAPsMAN interfaces cannot be disabled through RouterOS.
|
||||
# The local bridge port is still writable on wifi-qcom-ac CAPs.
|
||||
can_write = True
|
||||
if remote.get("dynamic") != "true" and remote.get("disabled") != "true":
|
||||
try:
|
||||
controller_resource.call("disable", {"numbers": remote["id"]})
|
||||
disabled_now = [w for w in controller_resource.get() if w.get("id") == remote["id"]]
|
||||
if len(disabled_now) != 1 or disabled_now[0].get("disabled") != "true":
|
||||
raise RuntimeError("CAPsMAN interface remained enabled after disable")
|
||||
except Exception as exc:
|
||||
LOG.warning("%s %s: disable failed: %s", cap, name, exc)
|
||||
results.append(event("apply_failure", cap, name, ssid, vlan, port.get("pvid", "missing") if port else "missing", "disable", exc))
|
||||
continue
|
||||
try:
|
||||
fields = {"bridge": "bridgeLocal", "pvid": str(vlan),
|
||||
"ingress-filtering": "yes", "frame-types": "admit-only-untagged-and-priority-tagged"}
|
||||
if port is None:
|
||||
local_ports.add(interface=local_name, **fields)
|
||||
else:
|
||||
local_ports.set(id=port["id"], **fields)
|
||||
except Exception as exc:
|
||||
LOG.warning("%s %s: bridge port write failed: %s", cap, name, exc)
|
||||
results.append(event("apply_failure", cap, name, ssid, vlan, port.get("pvid", "missing") if port else "missing", "bridge_port", exc))
|
||||
except Exception as exc:
|
||||
LOG.warning("%s %s: disable failed: %s", cap, name, exc)
|
||||
results.append(event("apply_failure", cap, name, ssid, vlan, port.get("pvid", "missing") if port else "missing", "disable", exc))
|
||||
can_write = False
|
||||
if can_write:
|
||||
try:
|
||||
fields = {"bridge": "bridgeLocal", "pvid": str(vlan),
|
||||
"ingress-filtering": "yes", "frame-types": "admit-only-untagged-and-priority-tagged"}
|
||||
if port is None:
|
||||
local_ports.add(interface=local_name, **fields)
|
||||
else:
|
||||
local_ports.set(id=port["id"], **fields)
|
||||
except Exception as exc:
|
||||
LOG.warning("%s %s: bridge port write failed: %s", cap, name, exc)
|
||||
results.append(event("apply_failure", cap, name, ssid, vlan, port.get("pvid", "missing") if port else "missing", "bridge_port", exc))
|
||||
|
||||
verified_ports = [p for p in local_ports.get() if p.get("interface") == local_name]
|
||||
verified = verified_ports[0] if len(verified_ports) == 1 else None
|
||||
@@ -130,7 +133,7 @@ def reconcile_cap(cap, controller_wifi, api, controller_api, vlans):
|
||||
if len(current) != 1:
|
||||
results.append(event("audit_failure", cap, name, ssid, vlan, "controller interface missing"))
|
||||
continue
|
||||
if current[0].get("disabled") == "true":
|
||||
if current[0].get("disabled") == "true" and current[0].get("dynamic") != "true":
|
||||
try:
|
||||
controller_resource.call("enable", {"numbers": remote["id"]})
|
||||
except Exception as exc:
|
||||
|
||||
@@ -30,6 +30,8 @@ class Resource:
|
||||
|
||||
def call(self, command, arguments):
|
||||
self.calls.append((command, arguments))
|
||||
if self.fail == command:
|
||||
raise RuntimeError("can not change interface")
|
||||
row = next(row for row in self.rows if row["id"] == arguments["numbers"])
|
||||
row["disabled"] = "true" if command == "disable" else "false"
|
||||
|
||||
@@ -74,6 +76,25 @@ class ReconcileTest(unittest.TestCase):
|
||||
self.assertEqual([], self.ports.calls)
|
||||
self.assertEqual([], self.remote.calls)
|
||||
|
||||
def test_dynamic_controller_interface_repairs_local_port_without_toggle(self):
|
||||
self.remote.rows[0]["dynamic"] = "true"
|
||||
self.assertEqual([], self.reconcile())
|
||||
self.assertEqual("6", self.ports.rows[0]["pvid"])
|
||||
self.assertEqual([], self.remote.calls)
|
||||
|
||||
def test_dynamic_port_write_failure_reports_failure_and_mismatch(self):
|
||||
self.remote.rows[0]["dynamic"] = "true"
|
||||
self.ports.fail = "set"
|
||||
self.assertEqual(["apply_failure", "mismatch"],
|
||||
[entry["kind"] for entry in self.reconcile()])
|
||||
self.assertEqual([], self.remote.calls)
|
||||
|
||||
def test_disable_failure_still_reports_mismatch(self):
|
||||
self.remote.fail = "disable"
|
||||
self.assertEqual(["apply_failure", "mismatch"],
|
||||
[entry["kind"] for entry in self.reconcile()])
|
||||
self.assertEqual([], self.ports.calls)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user