failover: resolve MBIM/QMI modems to their wwan netdev
NM reports a gsm connection's device as the control port (cdc-wdm0), but IP + routes live on the wwan netdev (wwu1u2i4 on the Quectel EC25-AF), so probes and route enforcement silently saw nothing (cellular stuck "down", no metric). Map connection-keyed WANs through GENERAL.IP-IFACE, and let the gateway fallback query the connection since the netdev isn't an NM device. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
7c36757d35
commit
fe9e6d3b30
+24
-9
@@ -68,7 +68,19 @@ def resolve(wan, actives):
|
|||||||
return dev, conn
|
return dev, conn
|
||||||
conn = wan.get("connection")
|
conn = wan.get("connection")
|
||||||
dev = next((a["device"] for a in actives if a["name"] == conn), None)
|
dev = next((a["device"] for a in actives if a["name"] == conn), None)
|
||||||
return dev, conn
|
return ip_iface(dev), conn
|
||||||
|
|
||||||
|
|
||||||
|
def ip_iface(dev):
|
||||||
|
"""The routed netdev for an NM device. For MBIM/QMI modems NM's device is the
|
||||||
|
control port (cdc-wdm0) while IP/routes live on the wwan netdev — probing and
|
||||||
|
`ip route` must use the latter."""
|
||||||
|
if not dev:
|
||||||
|
return dev
|
||||||
|
r = sh(["nmcli", "-g", "GENERAL.IP-IFACE", "device", "show", dev])
|
||||||
|
if r and r.returncode == 0 and r.stdout.strip():
|
||||||
|
return r.stdout.strip()
|
||||||
|
return dev
|
||||||
|
|
||||||
|
|
||||||
def probe_one(dev, url, timeout):
|
def probe_one(dev, url, timeout):
|
||||||
@@ -105,7 +117,7 @@ def routes_on(dev):
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def enforce_route(dev, metric):
|
def enforce_route(dev, metric, conn=None):
|
||||||
"""Ensure exactly one default route on dev at the desired metric, via `ip route`.
|
"""Ensure exactly one default route on dev at the desired metric, via `ip route`.
|
||||||
NEVER use `nmcli device reapply` — it resets r8152 USB-ethernet carriers and causes
|
NEVER use `nmcli device reapply` — it resets r8152 USB-ethernet carriers and causes
|
||||||
a failover flap. This is a pure routing change (carrier-safe, verified)."""
|
a failover flap. This is a pure routing change (carrier-safe, verified)."""
|
||||||
@@ -120,7 +132,7 @@ def enforce_route(dev, metric):
|
|||||||
return
|
return
|
||||||
# Prefer the gw from an existing default route; fall back to NM's known gateway so we can
|
# Prefer the gw from an existing default route; fall back to NM's known gateway so we can
|
||||||
# also *restore* a route that went missing while the carrier is still up (not just rebase one).
|
# also *restore* a route that went missing while the carrier is still up (not just rebase one).
|
||||||
gw = next((r["gw"] for r in rts if r["gw"]), None) or device_gateway(dev)
|
gw = next((r["gw"] for r in rts if r["gw"]), None) or nm_gateway(dev, conn)
|
||||||
if not gw:
|
if not gw:
|
||||||
return # no gateway known (carrier down); profile metric still set
|
return # no gateway known (carrier down); profile metric still set
|
||||||
r = sh(["ip", "route", "add", "default", "via", gw, "dev", dev, "metric", str(metric), "proto", "static"])
|
r = sh(["ip", "route", "add", "default", "via", gw, "dev", dev, "metric", str(metric), "proto", "static"])
|
||||||
@@ -134,11 +146,14 @@ def enforce_route(dev, metric):
|
|||||||
sh(["ip", "route", "del", "default", "via", old["gw"], "dev", dev, "metric", str(old["metric"])])
|
sh(["ip", "route", "del", "default", "via", old["gw"], "dev", dev, "metric", str(old["metric"])])
|
||||||
|
|
||||||
|
|
||||||
def device_gateway(dev):
|
def nm_gateway(dev, conn):
|
||||||
"""NM's gateway for a device — available even when its default route is missing."""
|
"""NM's gateway — available even when the default route is missing. Try the device,
|
||||||
r = sh(["nmcli", "-g", "IP4.GATEWAY", "device", "show", dev])
|
then the connection (a wwan netdev is not an NM device, but its connection is active)."""
|
||||||
if r and r.returncode == 0:
|
for kind, name in (("device", dev), ("connection", conn)):
|
||||||
return r.stdout.strip() or None
|
if name:
|
||||||
|
r = sh(["nmcli", "-g", "IP4.GATEWAY", kind, "show", name])
|
||||||
|
if r and r.returncode == 0 and r.stdout.strip():
|
||||||
|
return r.stdout.strip()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -224,7 +239,7 @@ def main():
|
|||||||
desired = base if s["up"] else base + PENALTY
|
desired = base if s["up"] else base + PENALTY
|
||||||
# Enforce the live route every loop (carrier-safe, corrects any NM drift);
|
# Enforce the live route every loop (carrier-safe, corrects any NM drift);
|
||||||
# update the NM profile only on an actual state change.
|
# update the NM profile only on an actual state change.
|
||||||
enforce_route(dev, desired)
|
enforce_route(dev, desired, conn)
|
||||||
if s["applied"] != desired:
|
if s["applied"] != desired:
|
||||||
print(f"{name}: {'UP' if s['up'] else 'DOWN'} -> metric {desired}", flush=True)
|
print(f"{name}: {'UP' if s['up'] else 'DOWN'} -> metric {desired}", flush=True)
|
||||||
set_profile_metric(conn, desired)
|
set_profile_metric(conn, desired)
|
||||||
|
|||||||
Reference in New Issue
Block a user