From 3fa2548c408499c8521e9235185886632cb5597c Mon Sep 17 00:00:00 2001 From: Andrew Topp Date: Fri, 21 Jun 2024 17:53:47 +1000 Subject: [PATCH] system: op-mode: T3334: allow delayed getty restart when configuring serial ports * Relocated service control to op-mode command "restart serial-console" * Checking for logged-in serial sessions that may be affected by getty reconfig * Warning the user when changes are committed and serial sessions are active, otherwise restart services as normal. No prompts issued during commit, all config gen/commit steps still occur except for the service restarts (everything remains consistent) * To apply committed changes, user will need to run "restart serial-console" to complete the process or reboot the whole router --- op-mode-definitions/restart-serial.xml.in | 13 ++++ src/conf_mode/system_console.py | 9 +-- src/op_mode/restart_serial.py | 83 +++++++++++++++++++++++ 3 files changed, 98 insertions(+), 7 deletions(-) create mode 100644 op-mode-definitions/restart-serial.xml.in create mode 100644 src/op_mode/restart_serial.py diff --git a/op-mode-definitions/restart-serial.xml.in b/op-mode-definitions/restart-serial.xml.in new file mode 100644 index 00000000000..6ae58f17000 --- /dev/null +++ b/op-mode-definitions/restart-serial.xml.in @@ -0,0 +1,13 @@ + + + + + + + Restart serial console service + + ${vyos_op_scripts_dir}/restart_serial.py + + + + diff --git a/src/conf_mode/system_console.py b/src/conf_mode/system_console.py index 19bbb887580..142cf7d7526 100755 --- a/src/conf_mode/system_console.py +++ b/src/conf_mode/system_console.py @@ -74,7 +74,6 @@ def generate(console): for root, dirs, files in os.walk(base_dir): for basename in files: if 'serial-getty' in basename: - call(f'systemctl stop {basename}') os.unlink(os.path.join(root, basename)) if not console or 'device' not in console: @@ -129,12 +128,8 @@ def apply(console): # Configure screen blank powersaving on VGA console call('/usr/bin/setterm -blank 15 -powersave powerdown -powerdown 60 -term linux /dev/tty1 2>&1') - # Start getty process on configured serial interfaces - for device in console['device']: - # Only start console if it exists on the running system. If a user - # detaches a USB serial console and reboots - it should not fail! - if os.path.exists(f'/dev/{device}'): - call(f'systemctl restart serial-getty@{device}.service') + # Service control moved to op_mode/restart_serial.py to unify checks and prompts. + call('/usr/libexec/vyos/op_mode/restart_serial.py --quiet') return None diff --git a/src/op_mode/restart_serial.py b/src/op_mode/restart_serial.py new file mode 100644 index 00000000000..ec3c441fc92 --- /dev/null +++ b/src/op_mode/restart_serial.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import sys, os, re, json + +from vyos.base import Warning +from vyos.utils.io import ask_yes_no +from vyos.utils.process import cmd + +GLOB_GETTY_UNITS = 'serial-getty@*.service' +RE_GETTY_DEVICES = re.compile(r'.+@(.+).service$') + +SD_UNIT_PATH = '/run/systemd/system' +UTMP_PATH = '/run/utmp' + +def get_serial_units(): + # Since we cannot depend on the current config for decommissioned ports, + # we just grab everything that systemd knows about. + tmp = cmd(f'sudo systemctl list-units {GLOB_GETTY_UNITS} --all --output json --no-pager') + getty_units = json.loads(tmp) + + for sdunit in getty_units: + m = RE_GETTY_DEVICES.search(sdunit['unit']) + if m is None: + Warning(f'Serial console unit name "{sdunit["unit"]}" is malformed and cannot be checked for activity!') + else: + sdunit['device'] = m.group(1) + + return getty_units + +def get_connected_ports(units): + connected = [] + + ports = [ x['device'] for x in units if 'device' in x ] + for line in cmd(f'utmpdump {UTMP_PATH}').splitlines(): + row = line.split('] [') + user_name = row[3].strip() + user_term = row[4].strip() + if user_name and user_name != 'LOGIN' and user_term in ports: + connected.append(user_term) + + return connected + +if __name__ == '__main__': + no_prompt = '--quiet' in sys.argv # don't need a full argparse for this. + units = get_serial_units() + connected = get_connected_ports(units) + + if connected: + print('There are user sessions connected via serial console that will be terminated\n' \ + 'when serial console settings are changed.\n') # extra newline is deliberate. + + if no_prompt: + # This flag is used by conf_mode/system_console.py to reset things, if there's + # a problem, the user should issue a manual restart for serial-getty. + print('Please ensure all settings are committed and saved before issuing a\n' \ + '"restart serial-console" command to apply new configuration.') + sys.exit(0) + + if not ask_yes_no('Any uncommitted changes from these sessions will be lost and in-progress actions\n' \ + 'may be left in an inconsistent state. Continue?'): + sys.exit(1) + + for unit in units: + unit_name = unit['unit'] + if os.path.exists(os.path.join(SD_UNIT_PATH, unit_name)): + cmd(f'sudo systemctl restart {unit_name}') + else: + # Deleted stubs don't need to be restarted, just shut them down. + cmd(f'sudo systemctl stop {unit_name}')