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

* Created op-mode command "restart serial console"
* Relocated service control to vyos.utils.serial helpers, used by conf- and
  op-mode serial console handling
  * 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
  * Added additional flags and target filtering for generic use of helpers.
  • Loading branch information
talmakion committed Jul 8, 2024
1 parent d386072 commit 598f53c
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 8 deletions.
31 changes: 31 additions & 0 deletions op-mode-definitions/restart-serial.xml.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?xml version="1.0"?>
<interfaceDefinition>
<node name="restart">
<children>
<node name="serial">
<properties>
<help>Restart services on serial ports</help>
</properties>
<children>
<node name="console">
<properties>
<help>Restart serial console service for login TTYs</help>
</properties>
<command>sudo ${vyos_op_scripts_dir}/serial.py restart_console</command>
<children>
<tagNode name="device">
<properties>
<help>Restart specific TTY device</help>
<completionHelp>
<script>${vyos_completion_dir}/list_login_ttys.py</script>
</completionHelp>
</properties>
<command>sudo ${vyos_op_scripts_dir}/serial.py restart_console --device-name "$5"</command>
</tagNode>
</children>
</node>
</children>
</node>
</children>
</node>
</interfaceDefinition>
117 changes: 117 additions & 0 deletions python/vyos/utils/serial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Copyright 2024 VyOS maintainers and contributors <[email protected]>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.

import os, re, json
from typing import List

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(include_devices=[]):
# 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!')
continue

getty_device = m.group(1)
if include_devices and getty_device not in include_devices:
continue

sdunit['device'] = getty_device

return getty_units

def get_authenticated_ports(units):
connected = []
ports = [ x['device'] for x in units if 'device' in x ]
#
# utmpdump just gives us an easily parseable dump of currently logged-in sessions, for eg:
# $ utmpdump /run/utmp
# Utmp dump of /run/utmp
# [2] [00000] [~~ ] [reboot ] [~ ] [6.6.31-amd64-vyos ] [0.0.0.0 ] [2024-06-18T13:56:53,958484+00:00]
# [1] [00051] [~~ ] [runlevel] [~ ] [6.6.31-amd64-vyos ] [0.0.0.0 ] [2024-06-18T13:57:01,790808+00:00]
# [6] [03178] [tty1] [LOGIN ] [tty1 ] [ ] [0.0.0.0 ] [2024-06-18T13:57:31,015392+00:00]
# [7] [37151] [ts/0] [vyos ] [pts/0 ] [10.9.8.7 ] [10.9.8.7 ] [2024-07-04T13:42:08,760892+00:00]
# [8] [24812] [ts/1] [ ] [pts/1 ] [10.9.8.7 ] [10.9.8.7 ] [2024-06-20T18:10:07,309365+00:00]
#
# We can safely skip blank or LOGIN sessions with valid device names.
#
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_login_consoles(prompt_user=False, quiet=True, devices: List[str]=[]):
# restart_login_consoles() is called from both conf- and op-mode scripts, including
# the warning messages and user prompts common to both.
#
# The default case, called with no arguments, is a simple serial-getty restart &
# cleanup wrapper with no output or prompts that can be used from anywhere.
#
# quiet and prompt_user args have been split from an original "no_prompt", in
# order to support the completely silent default use case. "no_prompt" would
# only suppress the user interactive prompt.
#
# quiet intentionally does not suppress a vyos.base.Warning() for malformed
# device names in _get_serial_units().
#
cmd('systemctl daemon-reload')

units = get_serial_units(devices)
connected = get_authenticated_ports(units)

if connected:
if not quiet:
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 not prompt_user:
# 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.')
if not prompt_user:
return False
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 False

for unit in units:
if 'device' not in unit:
continue # malformed or filtered.
unit_name = unit['unit']
unit_device = unit['device']
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}')

return True
25 changes: 25 additions & 0 deletions src/completion/list_login_ttys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/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/>.

from vyos.utils.serial import get_serial_units

if __name__ == '__main__':
# Autocomplete uses runtime state rather than the config tree, as a manual
# restart/cleanup may be needed for deleted devices.
tty_completions = [ '<text>' ] + [ x['device'] for x in get_serial_units() if 'device' in x ]
print(' '.join(tty_completions))


15 changes: 7 additions & 8 deletions src/conf_mode/system_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@

from vyos.config import Config
from vyos.utils.process import call
from vyos.utils.serial import restart_login_consoles
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 +76,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 +123,18 @@ def apply(console):
# Reload systemd manager configuration
call('systemctl daemon-reload')

# Service control moved to vyos.utils.serial to unify checks and prompts.
# If users are connected, we want to show an informational message on completing
# the process, but not halt configuration processing with an interactive prompt.
restart_login_consoles(prompt_user=False, quiet=False)

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
38 changes: 38 additions & 0 deletions src/op_mode/serial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/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, typing

import vyos.opmode
from vyos.utils.serial import restart_login_consoles as _restart_login_consoles

def restart_console(device_name: typing.Optional[str]):
# Service control moved to vyos.utils.serial to unify checks and prompts.
# If users are connected, we want to show an informational message and a prompt
# to continue, verifying that the user acknowledges possible interruptions.
if device_name:
_restart_login_consoles(prompt_user=True, quiet=False, devices=[device_name])
else:
_restart_login_consoles(prompt_user=True, quiet=False)

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 598f53c

Please sign in to comment.