summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
Diffstat (limited to 'python')
-rw-r--r--python/vyos/utils/network.py5
-rw-r--r--python/vyos/utils/wwan.py92
2 files changed, 89 insertions, 8 deletions
diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py
index cb5863e51..93ed05f6b 100644
--- a/python/vyos/utils/network.py
+++ b/python/vyos/utils/network.py
@@ -290,6 +290,7 @@ def is_wwan_connected(interface):
carrier network or not """
from vyos.utils.dict import dict_search
from vyos.utils.process import is_systemd_service_active
+ from vyos.utils.wwan import modem_index
if not interface.startswith('wwan'):
raise ValueError(f'Specified interface "{interface}" is not a WWAN interface')
@@ -299,7 +300,9 @@ def is_wwan_connected(interface):
if not is_systemd_service_active('ModemManager.service'):
return False
- modem = interface.lstrip('wwan')
+ modem = modem_index(interface)
+ if modem is None:
+ return False
try:
tmp = cmdl(['mmcli', '--modem', modem, '--output-json'])
diff --git a/python/vyos/utils/wwan.py b/python/vyos/utils/wwan.py
index d8da34bfa..8ee6a5a9a 100644
--- a/python/vyos/utils/wwan.py
+++ b/python/vyos/utils/wwan.py
@@ -52,11 +52,74 @@ modem_wait_poll = 0.250
# survive a reboot, and a commit re-asserts the configured state too.
admin_disconnect_dir = '/run/vyos-wwan'
-def modem_index(ifname: str) -> str:
- """ Return the ModemManager modem index backing an interface, wwan0 -> 0 """
+
+def _modem_owning_port(ifname: str):
+ """Return the ModemManager modem index whose own port list includes
+ ifname, or None if no currently-known modem owns it."""
+ from json import loads
+
+ try:
+ data = loads(cmdl(['mmcli', '--list-modems', '--output-json']))
+ except (OSError, ValueError):
+ # ValueError also catches json.JSONDecodeError - mmcli can return
+ # empty or malformed output while ModemManager is still starting
+ # (T6604), and that must retry under wait=True, not abort it.
+ return None
+
+ # Well-formed JSON of an unexpected shape is treated like no modems at all
+ modem_list = []
+ if isinstance(data, dict) and 'modem-list' in data:
+ modem_list = data['modem-list']
+ if not isinstance(modem_list, list):
+ return None
+
+ for modem_path in modem_list:
+ idx = modem_path.rsplit('/', 1)[-1]
+ try:
+ detail = loads(cmdl(['mmcli', '--modem', idx, '--output-json']))
+ except (OSError, ValueError):
+ continue
+ ports = dict_search('modem.generic.ports', detail) or []
+ if any(port.split(' ')[0] == ifname for port in ports):
+ return idx
+
+ return None
+
+
+def modem_index(ifname: str, wait: bool = False):
+ """Return the ModemManager modem index backing an interface, or None if
+ no modem currently owns it.
+
+ The kernel-assigned WWAN interface number and ModemManager's own modem
+ index are independently enumerated and are not guaranteed to match - on
+ a box with more than one modem, wwan0 does not necessarily belong to
+ modem 0 (T7487). Resolved by real port ownership instead: every modem
+ ModemManager currently knows about is listed, and whichever one
+ actually has ifname among its own ports is returned.
+
+ "No modem owns this yet" is a routine result for most callers (deleted
+ interfaces, admin-disconnect markers, vyos-netlinkd's reconcile loop), so
+ by default this checks once and returns immediately. Only
+ start_modem_manager() passes wait=True, to give a modem that is still
+ enumerating up to modem_wait_timeout to show up."""
if not ifname.startswith('wwan'):
raise ValueError(f'Specified interface "{ifname}" is not a WWAN interface')
- return ifname[len('wwan'):]
+
+ if not wait:
+ return _modem_owning_port(ifname)
+
+ from vyos.utils.misc import wait_for
+
+ index = None
+
+ def _resolve():
+ nonlocal index
+ index = _modem_owning_port(ifname)
+ return index is not None
+
+ wait_for(_resolve, interval=modem_wait_poll, timeout=modem_wait_timeout)
+
+ return index
def modem_state(index: str) -> str:
""" ModemManager state of a modem, empty if it cannot be read yet """
@@ -83,7 +146,14 @@ def start_modem_manager(ifname: str) -> None:
if not is_systemd_service_active(service_name):
cmdl(['systemctl', 'start', service_name])
- index = modem_index(ifname)
+ index = modem_index(ifname, wait=True)
+ if index is None:
+ # No modem currently owns ifname at all - nothing more this can do.
+ # Matches the pre-existing tolerance for hardware that isn't detected
+ # yet: interfaces_wwan.py's own w.exists() check right after this
+ # call already bails out the same way.
+ return None
+
counter = int(modem_wait_timeout / modem_wait_poll)
while counter > 0:
counter -= 1
@@ -150,13 +220,21 @@ def modem_disconnect(ifname: str, quiet: bool = False) -> None:
""" Disconnect every bearer of the modem backing ifname. The number of
bearers a modem can hold is limited, so we always disconnect before we
dial again. """
- call(f'mmcli --modem {modem_index(ifname)} --simple-disconnect',
- stdout=DEVNULL if quiet else None,
- stderr=DEVNULL if quiet else None)
+ modem = modem_index(ifname)
+ if modem is None:
+ return None
+ call(
+ f'mmcli --modem {modem} --simple-disconnect',
+ stdout=DEVNULL if quiet else None,
+ stderr=DEVNULL if quiet else None,
+ )
+
def modem_connect(ifname: str, options: str) -> bool:
""" Dial the modem backing ifname, returns True if the modem connected """
modem = modem_index(ifname)
+ if modem is None:
+ return False
# Some networks only ever admit a single combined IPv4+IPv6 PDN context per
# APN and reject a standalone IPv6 "Start Network" request outright (QMI