diff options
| author | Christian Breunig <christian@breunig.cc> | 2026-03-31 15:35:33 +0200 |
|---|---|---|
| committer | Christian Breunig <christian@breunig.cc> | 2026-04-09 20:54:06 +0200 |
| commit | 51922535b79529d603c3b7d52cde9da54c069d42 (patch) | |
| tree | 2bcaca4b3877a598a5f2f0229ac0329cb87f3ab6 | |
| parent | d2ad852ab4393862e9f27b2bc3bd7d382feb4c5a (diff) | |
| download | vyos-1x-51922535b79529d603c3b7d52cde9da54c069d42.tar.gz vyos-1x-51922535b79529d603c3b7d52cde9da54c069d42.zip | |
serial: T8375: use boot activation script to define a serial console on the CLI
Required during first-boot of a system. We have images form amd64 and arm64
CPUs which also tend to have different serial interfaces (ttyS vs. ttyAMA).
The images which are installed have the correct serial setting for GRUB (ttyS0
or ttyAMA0) and the activation script will probe the Kernel command-line. If a
serial interface is defined, we will include it in the VyOS CLI configuration.
| -rw-r--r-- | data/config.boot.default | 5 | ||||
| -rw-r--r-- | python/vyos/flavor.py | 68 | ||||
| -rw-r--r-- | python/vyos/system/compat.py | 6 | ||||
| -rw-r--r-- | python/vyos/system/grub.py | 12 | ||||
| -rw-r--r-- | python/vyos/system/grub_util.py | 20 | ||||
| -rw-r--r-- | python/vyos/utils/serial.py | 5 | ||||
| -rwxr-xr-x | src/activation-scripts/05-serial_console.py | 57 | ||||
| -rwxr-xr-x | src/conf_mode/system_console.py | 26 | ||||
| -rwxr-xr-x | src/op_mode/image_installer.py | 23 |
9 files changed, 188 insertions, 34 deletions
diff --git a/data/config.boot.default b/data/config.boot.default index cee350bad..4145515f1 100644 --- a/data/config.boot.default +++ b/data/config.boot.default @@ -26,11 +26,6 @@ system { config-management { commit-revisions "100" } - console { - device ttyS0 { - speed "115200" - } - } host-name "vyos" login { operator-group default { diff --git a/python/vyos/flavor.py b/python/vyos/flavor.py new file mode 100644 index 000000000..46c1775f5 --- /dev/null +++ b/python/vyos/flavor.py @@ -0,0 +1,68 @@ +# Copyright (C) VyOS Inc. +# +# 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/>. + +""" +VyOS flavor data access library. + +VyOS stores its flavor specific data in a JSON file. This module provides a +convenient interface to reading it. + +Example of the version data dict:: + { + 'console_type': 'ttyS0', + 'console_speed': '115200' + } +""" + +import os +import vyos.defaults + +from vyos.utils.file import read_json + +flavor_file = os.path.join(vyos.defaults.directories['data'], 'flavor.json') + +def get_flavor_data(fname=flavor_file): + """ + Get complete flavor data + + Args: + file (str): path to the flavor file + + Returns: + dict: flavor data, if it can not be found and empty dict + + The optional ``file`` argument comes in handy in upgrade scripts + that need to retrieve information from images other than the running image. + It should not be used on a running system since the location of that file + is an implementation detail and may change in the future, while the interface + of this module will stay the same. + """ + return read_json(flavor_file, {}) + +def get_image_serial_console(fname=flavor_file): + """ + Get serial console parameters baked into the image flavor. + + Args: + file (str): path to the flavor file + + Returns: + dict: serial interface data baked into the image flavor. Example: + {"console_type": "ttyS", "console_speed": "115200", "console_num":"0"} + """ + console_type = get_flavor_data(fname=fname).get('console_type', '') + console_num = get_flavor_data(fname=fname).get('console_num', '') + console_speed = get_flavor_data(fname=fname).get('console_speed', '') + return (console_type, console_num, console_speed) diff --git a/python/vyos/system/compat.py b/python/vyos/system/compat.py index 24cad7041..40b38b366 100644 --- a/python/vyos/system/compat.py +++ b/python/vyos/system/compat.py @@ -134,11 +134,15 @@ def parse_entry(entry: tuple) -> dict: entry_dict['bootmode'] = 'pw_reset' else: entry_dict['bootmode'] = 'normal' + (_, _, default_speed) = get_image_serial_console() # find console type and number regex_filter = compile(REGEX_CONSOLE) entry_dict.update(regex_filter.match(entry[1]).groupdict()) + # Set new or default console speed - this line must always be present to + # keep backward compatibility. It is needed to boot into old images and + # use the serial console speed speed = entry_dict.get('console_speed', None) - entry_dict['console_speed'] = speed if speed is not None else '115200' + entry_dict['console_speed'] = speed if speed is not None else default_speed entry_dict['boot_opts'] = sanitize_boot_opts(entry[1]) return entry_dict diff --git a/python/vyos/system/grub.py b/python/vyos/system/grub.py index 50651d20e..1bae0d5fb 100644 --- a/python/vyos/system/grub.py +++ b/python/vyos/system/grub.py @@ -23,6 +23,7 @@ from uuid import uuid5 from uuid import NAMESPACE_URL from uuid import UUID +from vyos.flavor import get_image_serial_console from vyos.system import disk from vyos.template import render from vyos.utils.process import cmd @@ -401,10 +402,13 @@ def set_console_type(console_type: str, root_dir: str = '') -> None: vars_current['console_type'] = str(console_type) vars_write(vars_file, vars_current) -def set_console_speed(console_speed: str, root_dir: str = '') -> None: +def set_serial_console(console_type: str, console_num: str, + console_speed: str, root_dir: str = '') -> None: """Write default console speed to GRUB configuration Args: + console_type (str): console device, e.g. 'ttyS' or 'ttyAMA' + console_num (str): console instance, e.g. '0' console_speed (str): default console speed root_dir (str, optional): an optional path to the root directory. Defaults to empty. @@ -412,9 +416,13 @@ def set_console_speed(console_speed: str, root_dir: str = '') -> None: if not root_dir: root_dir = disk.find_persistence() + (default_type, default_num, default_speed) = get_image_serial_console() + vars_file: str = f'{root_dir}/{CFG_VYOS_VARS}' vars_current: dict[str, str] = vars_read(vars_file) - vars_current['console_speed'] = str(console_speed) + vars_current['console_type'] = console_type if console_type else default_type + vars_current['console_num'] = console_num if console_num else default_num + vars_current['console_speed'] = console_speed if console_speed else default_speed vars_write(vars_file, vars_current) def set_kernel_cmdline_options(cmdline_options: str, version_name: str, diff --git a/python/vyos/system/grub_util.py b/python/vyos/system/grub_util.py index 344569168..edaea8ad3 100644 --- a/python/vyos/system/grub_util.py +++ b/python/vyos/system/grub_util.py @@ -19,7 +19,8 @@ from vyos.system import image from vyos.system import compat @compat.grub_cfg_update -def set_console_speed(console_speed: str, root_dir: str = '') -> None: +def set_serial_console(console_type: str, console_num: str, + console_speed: str, root_dir: str = '') -> None: """Write default console speed to GRUB configuration Args: @@ -30,10 +31,11 @@ def set_console_speed(console_speed: str, root_dir: str = '') -> None: if not root_dir: root_dir = disk.find_persistence() - grub.set_console_speed(console_speed, root_dir) + grub.set_serial_console(console_type, console_num, console_speed, root_dir) @image.if_not_live_boot -def update_console_speed(console_speed: str, root_dir: str = '') -> None: +def update_serial_console(console_type: str, console_num: str, + console_speed: str, root_dir: str = '') -> None: """Update console_speed if different from current value""" if not root_dir: @@ -41,9 +43,15 @@ def update_console_speed(console_speed: str, root_dir: str = '') -> None: vars_file: str = f'{root_dir}/{grub.CFG_VYOS_VARS}' vars_current: dict[str, str] = grub.vars_read(vars_file) - console_speed_current = vars_current.get('console_speed', None) - if console_speed != console_speed_current: - set_console_speed(console_speed, root_dir) + + console_type_current = vars_current.get('console_type') + console_num_current = vars_current.get('console_num') + console_speed_current = vars_current.get('console_speed') + + if console_type != console_type_current or \ + console_num != console_num_current or \ + console_speed != console_speed_current: + set_serial_console(console_type, console_num, console_speed, root_dir) @compat.grub_cfg_update def set_kernel_cmdline_options(cmdline_options: str, version: str = '', diff --git a/python/vyos/utils/serial.py b/python/vyos/utils/serial.py index 5d276fb17..fcb7a21c9 100644 --- a/python/vyos/utils/serial.py +++ b/python/vyos/utils/serial.py @@ -119,7 +119,7 @@ def restart_login_consoles(prompt_user=False, quiet=True, devices: List[str]=[]) return True -def is_tty(name: str) -> bool: +def is_tty(name: str, warning=False) -> bool: """ Check if a given device file (e.g. /dev/ttyS0) is a TTY (teletypewriter) device in Linux """ @@ -130,4 +130,7 @@ def is_tty(name: str) -> bool: fd = f.fileno() # True if filename is a TTY return os.isatty(fd) + elif warning: + from vyos.base import Warning + Warning(f'Device "{name}" does not exist!') return False diff --git a/src/activation-scripts/05-serial_console.py b/src/activation-scripts/05-serial_console.py new file mode 100755 index 000000000..93e381d17 --- /dev/null +++ b/src/activation-scripts/05-serial_console.py @@ -0,0 +1,57 @@ +# Copyright (C) VyOS Inc. +# +# 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 re + +from typing import Optional +from typing import Tuple + +from vyos.configtree import ConfigTree +from vyos.utils.file import read_file +from vyos.system.image import is_live_boot + +base = ['system', 'console', 'device'] + +def get_kernel_serial_console() -> Tuple[Optional[str], Optional[str]]: + """ + Extract the serial console device and speed setting from the kernel + command line. + """ + device = speed = None + CMDLINE_CONSOLE_RE = re.compile( + r'(?:^|\s)console=(?P<device>tty(?:S|AMA)\d+),(?P<speed>\d+)(?=\s|$)' + ) + kernel_cmdline = read_file('/proc/cmdline') + if m := CMDLINE_CONSOLE_RE.search(kernel_cmdline): + device = m.group("device") # "ttyS0" + speed = m.group("speed") # Baud rate/speed, e.g. "115200" + + return (device, speed) + +def activate(config: ConfigTree): + # Configure the kernel serial console only once during live boot. + # During installation, the user can define the console. If this is not + # limited to live boot, the serial interface will always be re-added during + # system boot, even if it was removed from config.boot. + if not is_live_boot(): + return + + (device, speed) = get_kernel_serial_console() + if device and speed: + # The kernel was booted with a configured serial console, but no + # console is configured in the CLI; align/fix the CLI configuration. + if not config.exists(base + [device]): + config.set(base + [device, 'speed'], value=speed) + config.set_tag(base) diff --git a/src/conf_mode/system_console.py b/src/conf_mode/system_console.py index 4411257e3..5d89eeba7 100755 --- a/src/conf_mode/system_console.py +++ b/src/conf_mode/system_console.py @@ -68,7 +68,7 @@ def verify(console): # and it can not be used as a serial interface if not os.path.isdir(by_bus_dir) or not os.path.exists(by_bus_device): raise ConfigError(f'Device {device} does not support being used as tty') - if not is_tty(device): + if not is_tty(device, warning=True): Warning(f'Device "{device}" used for console is not a TTY!') return None @@ -82,6 +82,7 @@ def generate(console): os.unlink(os.path.join(root, basename)) if not console or 'device' not in console: + grub_util.update_serial_console('', '', '') return None # replace keys in the config for ttyUSB items to use them in `apply()` later @@ -112,14 +113,17 @@ def generate(console): render(config_file, 'getty/serial-getty.service.j2', device_config) os.symlink(config_file, getty_wants_symlink) - # GRUB - # For existing serial line change speed (if necessary) - # Only applies to ttyS0 - if 'ttyS0' not in console['device']: - return None + # GRUB - use first defined serial console to populate Kernel console= parameter + device = None + if 'device' in console and len(console['device']) > 0: + # We use the first device for the Kernel console + console_device = next(iter(console['device'])) + console_speed = console['device'][console_device]['speed'] - speed = console['device']['ttyS0']['speed'] - grub_util.update_console_speed(speed) + # get console type ("ttyS" or "ttyAMA") from console_device (e.g. "ttyS0") + console_type = console_device.translate(str.maketrans('', '', '0123456789')) + console_num = ''.join(ch for ch in console_device if ch.isdigit()) + grub_util.update_serial_console(console_type, console_num, console_speed) return None @@ -127,9 +131,9 @@ def apply(console): # Reset screen blanking call('/usr/bin/setterm -blank 0 -powersave off -powerdown 0 -term linux </dev/tty1 >/dev/tty1 2>&1') - # 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. + # 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: diff --git a/src/op_mode/image_installer.py b/src/op_mode/image_installer.py index db9647782..fb3cdda9e 100755 --- a/src/op_mode/image_installer.py +++ b/src/op_mode/image_installer.py @@ -42,8 +42,10 @@ from psutil import disk_partitions from vyos.base import Warning from vyos.configtree import ConfigTree +from vyos.config_mgmt import unsaved_commits from vyos.defaults import base_dir from vyos.defaults import directories +from vyos.flavor import get_image_serial_console from vyos.remote import download from vyos.system import disk from vyos.system import grub @@ -69,7 +71,6 @@ from vyos.utils.process import cmd from vyos.utils.process import run from vyos.utils.process import rc_cmd from vyos.version import get_version_data -from vyos.config_mgmt import unsaved_commits # define text messages MSG_ERR_NOT_LIVE: str = 'The system is already installed. Please use "add system image" instead.' @@ -143,16 +144,17 @@ ISO_DOWNLOAD_PATH: str = '' external_download_script: str = f'{base_dir}/simple-download.py' external_latest_image_url_script: str = f'{base_dir}/latest-image-url.py' +(flavor_sercon_type, flavor_sercon_num, flavor_sercon_speed) = get_image_serial_console() + # default boot variables DEFAULT_BOOT_VARS: dict[str, str] = { 'timeout': '5', 'console_type': 'tty', - 'console_num': '0', - 'console_speed': '115200', + 'console_num': flavor_sercon_num, + 'console_speed': flavor_sercon_speed, 'bootmode': 'normal' } - def bytes_to_gb(size: int) -> float: """Convert Bytes to GBytes, rounded to 1 decimal number @@ -767,12 +769,13 @@ def console_hint() -> str: path = '/dev/tty' name = Path(path).name - if name in ['ttyS0', 'ttyAMA0']: + if name in ['ttyS0']: return 'S' + elif name in ['ttyAMA0']: + return 'A' else: return 'K' - def cleanup(mounts: list[str] = [], remove_items: list[str] = []) -> None: """Clean up after installation @@ -921,10 +924,14 @@ def install_image() -> None: print(MSG_WARN_PASSWORD_CONFIRM) # ask for default console + console_dict: dict[str, str] = {'K': 'tty'} + if flavor_sercon_type: + tmp: str = flavor_sercon_type[-1] # get "S" from "ttyS" and "A" from "ttyAMA" + console_dict.update({tmp: flavor_sercon_type}) + console_type: str = ask_input(MSG_INPUT_CONSOLE_TYPE, default=console_hint(), - valid_responses=['K', 'S']) - console_dict: dict[str, str] = {'K': 'tty', 'S': 'ttyS'} + valid_responses=console_dict.keys()) config_boot_list = [f'{DIR_CONFIG}/config.boot', '/opt/vyatta/etc/config.boot.default'] |
