diff options
| author | Christian Breunig <christian@breunig.cc> | 2026-09-28 21:18:12 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-28 21:18:12 +0200 |
| commit | 504c97e7c661de8ea771a8daa7a3c7d6c8e5d86b (patch) | |
| tree | d7d3d0120fa74465e34ce2948c55f8fc249d6165 /src | |
| parent | 39528a2d74fd1f18433cc4cbec301f869181e5ec (diff) | |
| parent | 9b5110a868ef413fdc1303f834220d99af8ceead (diff) | |
| download | vyos-1x-504c97e7c661de8ea771a8daa7a3c7d6c8e5d86b.tar.gz vyos-1x-504c97e7c661de8ea771a8daa7a3c7d6c8e5d86b.zip | |
Merge pull request #5503 from c-po/T9354
op-mode: T9354: replace "reset connection" with "reconnect interface"
Diffstat (limited to 'src')
| -rwxr-xr-x | src/op_mode/connect_disconnect.py | 60 | ||||
| -rw-r--r-- | src/services/api/rest/models.py | 14 | ||||
| -rw-r--r-- | src/services/api/rest/routers.py | 23 |
3 files changed, 95 insertions, 2 deletions
diff --git a/src/op_mode/connect_disconnect.py b/src/op_mode/connect_disconnect.py index 17cf9d7e0..e30f512a0 100755 --- a/src/op_mode/connect_disconnect.py +++ b/src/op_mode/connect_disconnect.py @@ -15,15 +15,22 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os +import re import argparse +from argparse import SUPPRESS from psutil import process_iter from vyos.utils.process import call +from vyos.utils.process import rc_cmd from vyos.utils.commit import commit_in_progress from vyos.utils.network import is_wwan_connected from vyos.utils.process import DEVNULL +# Dialer interfaces we support - also sanitizes the name before it becomes +# part of a systemd-run(1) command line +dialer_interface_re = r'(pppoe|sstpc|wwan)\d+' + def check_ppp_interface(interface): if not os.path.isfile(f'/etc/ppp/peers/{interface}'): print(f'Interface {interface} does not exist!') @@ -80,6 +87,51 @@ def disconnect(interface): else: print(f'Unknown interface {interface}, cannot disconnect. Aborting!') +def reconnect(interface): + """ Reconnect dialer interface """ + + if interface.startswith('pppoe') or interface.startswith('sstpc'): + check_ppp_interface(interface) + # Restarting the service hangs up and dials again in one operation, + # leaving no window in which we could be interrupted with the link down + print(f'Interface {interface}: reconnecting...') + call(f'systemctl restart ppp@{interface}.service') + elif interface.startswith('wwan'): + # The modem must be disconnected before it can be dialed again + disconnect(interface) + connect(interface) + else: + print(f'Unknown interface {interface}, cannot reconnect. Aborting!') + +def reconnect_detached(interface): + """ Reconnect dialer interface from a transient systemd unit. + + A reconnect is usually issued via the very interface being reconnected. + Once the link drops, the calling session dies and this process would be + killed by SIGHUP - or SIGPIPE on the next write to the dead terminal - + before the interface is dialed again, leaving the system unreachable. """ + + if not re.fullmatch(dialer_interface_re, interface): + print(f'Unknown interface {interface}, cannot reconnect. Aborting!') + exit(1) + + unit = f'vyos-reconnect-{interface}' + command = ( + f'systemd-run --quiet --collect --unit={unit} ' + f'{os.path.abspath(__file__)} --reconnect --detached ' + f'--interface {interface}' + ) + + rc, output = rc_cmd(command) + if rc != 0: + print(f'Interface {interface}: cannot reconnect: {output}') + exit(1) + + print( + f'Interface {interface}: reconnecting in the background, this session ' + f'may be disconnected. Progress is logged to journal unit {unit}.' + ) + def main(): parser = argparse.ArgumentParser() group = parser.add_mutually_exclusive_group() @@ -87,6 +139,8 @@ def main(): group.add_argument("--disconnect", help="Take down connection-oriented network interface", action="store_true") group.add_argument("--reconnect", help="Reconnect connection-oriented network interface", action="store_true") parser.add_argument("--interface", help="Interface name", action="store", required=True) + # Set only when we re-launch ourself detached from the calling session + parser.add_argument('--detached', action='store_true', help=SUPPRESS) args = parser.parse_args() # Disallow connecting interfaces while their configuration might be changing @@ -100,8 +154,10 @@ def main(): elif args.disconnect: disconnect(args.interface) elif args.reconnect: - disconnect(args.interface) - connect(args.interface) + if args.detached: + reconnect(args.interface) + else: + reconnect_detached(args.interface) else: parser.print_help() diff --git a/src/services/api/rest/models.py b/src/services/api/rest/models.py index 8321ad2e3..8762c3bff 100644 --- a/src/services/api/rest/models.py +++ b/src/services/api/rest/models.py @@ -291,6 +291,20 @@ class ResetModel(ApiModel): } +class ReconnectModel(ApiModel): + op: StrictStr + path: List[StrictStr] + + class Config: + json_schema_extra = { + 'example': { + 'key': 'id_key', + 'op': 'reconnect', + 'path': ['op', 'mode', 'path'], + } + } + + class PoweroffModel(ApiModel): op: StrictStr path: List[StrictStr] diff --git a/src/services/api/rest/routers.py b/src/services/api/rest/routers.py index 1808e5a5b..c2aa033bc 100644 --- a/src/services/api/rest/routers.py +++ b/src/services/api/rest/routers.py @@ -74,6 +74,7 @@ from .models import GenerateModel from .models import ShowModel from .models import RebootModel from .models import ResetModel +from .models import ReconnectModel from .models import RenewModel from .models import ImportPkiModel from .models import PingModel @@ -934,6 +935,28 @@ def reset_op(data: ResetModel): return success(res) +@router.post('/reconnect') +def reconnect_op(data: ReconnectModel): + state = SessionState() + session = state.session + + op = data.op + path = data.path + + try: + if op == 'reconnect': + res = session.reconnect(path) + else: + return error(400, f"'{op}' is not a valid operation") + except ConfigSessionError as e: + return error(400, str(e)) + except Exception: + LOG.critical(traceback.format_exc()) + return error(500, 'An internal error occurred. Check the logs for details.') + + return success(res) + + @router.post('/import-pki') def import_pki(data: ImportPkiModel): # pylint: disable=consider-using-with |
