Skip to content

Commit

Permalink
system: op-mode: T3334: allow delayed getty restart when configuring …
Browse files Browse the repository at this point in the history
…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
* Migrated to new-style vyos.opmode script as requested, renamed script
  to be more generic.
  • Loading branch information
talmakion committed Jul 2, 2024
1 parent e270712 commit 31b613b
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 8 deletions.
13 changes: 13 additions & 0 deletions op-mode-definitions/restart-serial.xml.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0"?>
<interfaceDefinition>
<node name="restart">
<children>
<node name="serial-console">
<properties>
<help>Restart serial console service</help>
</properties>
<command>sudo ${vyos_op_scripts_dir}/serial.py restart_getty</command>
</node>
</children>
</node>
</interfaceDefinition>
13 changes: 5 additions & 8 deletions src/conf_mode/system_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from vyos.utils.process import call
from vyos.system import grub_util
from vyos.template import render
from vyos.defaults import directories
from vyos import ConfigError
from vyos import airbag
airbag.enable()
Expand Down Expand Up @@ -74,7 +75,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:
Expand Down Expand Up @@ -122,20 +122,17 @@ def apply(console):
# Reload systemd manager configuration
call('systemctl daemon-reload')

# Service control moved to op_mode/restart_serial.py to unify checks and prompts.
opmode_dir = directories['op_mode']
call(f'{opmode_dir}/serial.py restart_getty --no-prompt')

if not console:
return None

if 'powersave' in console.keys():
# Configure screen blank powersaving on VGA console
call('/usr/bin/setterm -blank 15 -powersave powerdown -powerdown 60 -term linux </dev/tty1 >/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')

return None

if __name__ == '__main__':
Expand Down
99 changes: 99 additions & 0 deletions src/op_mode/serial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#!/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 <http://www.gnu.org/licenses/>.

import sys, os, re, json

import vyos.opmode
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'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

def restart_getty(no_prompt: bool):
cmd('systemctl daemon-reload')

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.')
return

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?'):
return

for unit in units:
unit_name = unit['unit']
if os.path.exists(os.path.join(SD_UNIT_PATH, unit_name)):
cmd(f'systemctl restart {unit_name}')
else:
# Deleted stubs don't need to be restarted, just shut them down.
cmd(f'systemctl stop {unit_name}')

num = len(units)
if num > 1:
return f'Reset {num} serial consoles'
elif num > 0:
return f'Reset 1 serial console'

if __name__ == '__main__':
try:
res = vyos.opmode.run(sys.modules[__name__])
if res:
print(res)
except (ValueError, vyos.opmode.Error) as e:
print(e)
sys.exit(1)

0 comments on commit 31b613b

Please sign in to comment.