diff --git a/data/templates/grub/grub_common.j2 b/data/templates/grub/grub_common.j2 index 78df3f48c..278ffbf2c 100644 --- a/data/templates/grub/grub_common.j2 +++ b/data/templates/grub/grub_common.j2 @@ -1,22 +1,23 @@ # load EFI video modules if [ "${grub_platform}" == "efi" ]; then insmod efi_gop insmod efi_uga fi # create and activate serial console function setup_serial { # initialize the first serial port by default if [ "${console_type}" == "ttyS" ]; then serial --unit=${console_num} else serial --unit=0 fi terminal_output --append serial console terminal_input --append serial console } setup_serial -# find root device -#search --no-floppy --fs-uuid --set=root ${root_uuid} +{% if search_root %} +{{ search_root }} +{% endif %} diff --git a/data/templates/grub/grub_compat.j2 b/data/templates/grub/grub_compat.j2 index 935172005..887d5d0bd 100644 --- a/data/templates/grub/grub_compat.j2 +++ b/data/templates/grub/grub_compat.j2 @@ -1,58 +1,63 @@ {# j2lint: disable=S6 #} ### Generated by VyOS image-tools v.{{ tools_version }} ### {% macro menu_name(mode) -%} {% if mode == 'normal' -%} VyOS {%- elif mode == 'pw_reset' -%} Lost password change {%- else -%} Unknown {%- endif %} {%- endmacro %} {% macro console_name(type) -%} {% if type == 'tty' -%} KVM {%- elif type == 'ttyS' -%} Serial {%- elif type == 'ttyUSB' -%} USB {%- else -%} Unknown {%- endif %} {%- endmacro %} {% macro console_opts(type) -%} {% if type == 'tty' -%} console=ttyS0,115200 console=tty0 {%- elif type == 'ttyS' -%} console=tty0 console=ttyS0,115200 {%- elif type == 'ttyUSB' -%} console=tty0 console=ttyUSB0,115200 {%- else -%} console=tty0 console=ttyS0,115200 {%- endif %} {%- endmacro %} {% macro passwd_opts(mode) -%} {% if mode == 'pw_reset' -%} init=/opt/vyatta/sbin/standalone_root_pw_reset {%- endif %} {%- endmacro %} set default={{ default }} set timeout={{ timeout }} {% if console_type == 'ttyS' %} serial --unit={{ console_num }} --speed=115200 {% else %} serial --unit=0 --speed=115200 {% endif %} terminal_output --append serial terminal_input serial console -{% if efi %} -insmod efi_gop -insmod efi_uga +{% for mod in modules %} +insmod {{ mod }} +{% endfor %} +{% if root %} +set root={{ root }} +{% endif %} +{% if search_root %} +{{ search_root }} {% endif %} {% for v in versions %} menuentry "{{ menu_name(v.bootmode) }} {{ v.version }} ({{ console_name(v.console_type) }} console)" { linux /boot/{{ v.version }}/vmlinuz {{ v.boot_opts }} {{ console_opts(v.console_type) }} {{ passwd_opts(v.bootmode) }} initrd /boot/{{ v.version }}/initrd.img } {% endfor %} diff --git a/python/vyos/system/compat.py b/python/vyos/system/compat.py index aa9b0b4b5..319c3dabf 100644 --- a/python/vyos/system/compat.py +++ b/python/vyos/system/compat.py @@ -1,307 +1,316 @@ # Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io> # # 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/>. from pathlib import Path from re import compile, MULTILINE, DOTALL from functools import wraps from copy import deepcopy from typing import Union from vyos.system import disk, grub, image, SYSTEM_CFG_VER from vyos.template import render TMPL_GRUB_COMPAT: str = 'grub/grub_compat.j2' # define regexes and variables REGEX_VERSION = r'^menuentry "[^\n]*{\n[^}]*\s+linux /boot/(?P<version>\S+)/[^}]*}' REGEX_MENUENTRY = r'^menuentry "[^\n]*{\n[^}]*\s+linux /boot/(?P<version>\S+)/vmlinuz (?P<options>[^\n]+)\n[^}]*}' REGEX_CONSOLE = r'^.*console=(?P<console_type>[^\s\d]+)(?P<console_num>[\d]+).*$' REGEX_SANIT_CONSOLE = r'\ ?console=[^\s\d]+[\d]+(,\d+)?\ ?' REGEX_SANIT_INIT = r'\ ?init=\S*\ ?' REGEX_SANIT_QUIET = r'\ ?quiet\ ?' PW_RESET_OPTION = 'init=/opt/vyatta/sbin/standalone_root_pw_reset' class DowngradingImageTools(Exception): """Raised when attempting to add an image with an earlier version of image-tools than the current system, as indicated by the value of SYSTEM_CFG_VER or absence thereof.""" pass def mode(): if grub.get_cfg_ver() >= SYSTEM_CFG_VER: return False return True def find_versions(menu_entries: list) -> list: """Find unique VyOS versions from menu entries Args: menu_entries (list): a list with menu entries Returns: list: List of installed versions """ versions = [] for vyos_ver in menu_entries: versions.append(vyos_ver.get('version')) # remove duplicates versions = list(set(versions)) return versions def filter_unparsed(grub_path: str) -> str: """Find currently installed VyOS version Args: grub_path (str): a path to the grub.cfg file Returns: str: unparsed grub.cfg items """ config_text = Path(grub_path).read_text() regex_filter = compile(REGEX_VERSION, MULTILINE | DOTALL) filtered = regex_filter.sub('', config_text) regex_filter = compile(grub.REGEX_GRUB_VARS, MULTILINE) filtered = regex_filter.sub('', filtered) regex_filter = compile(grub.REGEX_GRUB_MODULES, MULTILINE) filtered = regex_filter.sub('', filtered) # strip extra new lines filtered = filtered.strip() return filtered +def get_search_root(unparsed: str) -> str: + unparsed_lines = unparsed.splitlines() + search_root = next((x for x in unparsed_lines if 'search' in x), '') + return search_root + + def sanitize_boot_opts(boot_opts: str) -> str: """Sanitize boot options from console and init Args: boot_opts (str): boot options Returns: str: sanitized boot options """ regex_filter = compile(REGEX_SANIT_CONSOLE) boot_opts = regex_filter.sub('', boot_opts) regex_filter = compile(REGEX_SANIT_INIT) boot_opts = regex_filter.sub('', boot_opts) # legacy tools add 'quiet' on add system image; this is not desired regex_filter = compile(REGEX_SANIT_QUIET) boot_opts = regex_filter.sub(' ', boot_opts) return boot_opts def parse_entry(entry: tuple) -> dict: """Parse GRUB menuentry Args: entry (tuple): tuple of (version, options) Returns: dict: dictionary with parsed options """ # save version to dict entry_dict = {'version': entry[0]} # detect boot mode type if PW_RESET_OPTION in entry[1]: entry_dict['bootmode'] = 'pw_reset' else: entry_dict['bootmode'] = 'normal' # find console type and number regex_filter = compile(REGEX_CONSOLE) entry_dict.update(regex_filter.match(entry[1]).groupdict()) entry_dict['boot_opts'] = sanitize_boot_opts(entry[1]) return entry_dict def parse_menuentries(grub_path: str) -> list: """Parse all GRUB menuentries Args: grub_path (str): a path to GRUB config file Returns: list: list with menu items (each item is a dict) """ menuentries = [] # read configuration file config_text = Path(grub_path).read_text() # parse menuentries to tuples (version, options) regex_filter = compile(REGEX_MENUENTRY, MULTILINE) filter_results = regex_filter.findall(config_text) # parse each entry for entry in filter_results: menuentries.append(parse_entry(entry)) return menuentries def prune_vyos_versions(root_dir: str = '') -> None: """Delete vyos-versions files of registered images subsequently deleted or renamed by legacy image-tools Args: root_dir (str): an optional path to the root directory """ if not root_dir: root_dir = disk.find_persistence() for version in grub.version_list(): if not Path(f'{root_dir}/boot/{version}').is_dir(): grub.version_del(version) def update_cfg_ver(root_dir:str = '') -> int: """Get minumum version of image-tools across all installed images Args: root_dir (str): an optional path to the root directory Returns: int: minimum version of image-tools """ if not root_dir: root_dir = disk.find_persistence() prune_vyos_versions(root_dir) images_details = image.get_images_details() cfg_version = min(d['tools_version'] for d in images_details) return cfg_version def get_default(menu_entries: list, root_dir: str = '') -> Union[int, None]: """Translate default version to menuentry index Args: menu_entries (list): list of dicts of installed version boot data root_dir (str): an optional path to the root directory Returns: int: index of default version in menu_entries or None """ if not root_dir: root_dir = disk.find_persistence() grub_cfg_main = f'{root_dir}/{grub.GRUB_CFG_MAIN}' image_name = image.get_default_image() sublist = list(filter(lambda x: x.get('version') == image_name, menu_entries)) if sublist: return menu_entries.index(sublist[0]) return None def update_version_list(root_dir: str = '') -> list[dict]: """Update list of dicts of installed version boot data Args: root_dir (str): an optional path to the root directory Returns: list: list of dicts of installed version boot data """ if not root_dir: root_dir = disk.find_persistence() grub_cfg_main = f'{root_dir}/{grub.GRUB_CFG_MAIN}' # get list of versions in menuentries menu_entries = parse_menuentries(grub_cfg_main) menu_versions = find_versions(menu_entries) # get list of versions added/removed by image-tools current_versions = grub.version_list(root_dir) remove = list(set(menu_versions) - set(current_versions)) for ver in remove: menu_entries = list(filter(lambda x: x.get('version') != ver, menu_entries)) add = list(set(current_versions) - set(menu_versions)) for ver in add: last = menu_entries[0].get('version') new = deepcopy(list(filter(lambda x: x.get('version') == last, menu_entries))) for e in new: boot_opts = e.get('boot_opts').replace(last, ver) e.update({'version': ver, 'boot_opts': boot_opts}) menu_entries = new + menu_entries return menu_entries def grub_cfg_fields(root_dir: str = '') -> dict: """Gather fields for rendering grub.cfg Args: root_dir (str): an optional path to the root directory Returns: dict: dictionary for rendering TMPL_GRUB_COMPAT """ if not root_dir: root_dir = disk.find_persistence() grub_cfg_main = f'{root_dir}/{grub.GRUB_CFG_MAIN}' fields = {'default': 0, 'timeout': 5} # 'default' and 'timeout' from legacy grub.cfg fields |= grub.vars_read(grub_cfg_main) + fields['tools_version'] = SYSTEM_CFG_VER menu_entries = update_version_list(root_dir) fields['versions'] = menu_entries + default = get_default(menu_entries, root_dir) if default is not None: fields['default'] = default - p = Path('/sys/firmware/efi') - if p.is_dir(): - fields['efi'] = True - else: - fields['efi'] = False + modules = grub.modules_read(grub_cfg_main) + fields['modules'] = modules + + unparsed = filter_unparsed(grub_cfg_main).splitlines() + search_root = next((x for x in unparsed if 'search' in x), '') + fields['search_root'] = search_root return fields def render_grub_cfg(root_dir: str = '') -> None: """Render grub.cfg for legacy compatibility""" if not root_dir: root_dir = disk.find_persistence() grub_cfg_main = f'{root_dir}/{grub.GRUB_CFG_MAIN}' fields = grub_cfg_fields(root_dir) render(grub_cfg_main, TMPL_GRUB_COMPAT, fields) def grub_cfg_update(func): """Decorator to update grub.cfg after function call""" @wraps(func) def wrapper(*args, **kwargs): ret = func(*args, **kwargs) if mode(): render_grub_cfg() return ret return wrapper diff --git a/src/system/grub_update.py b/src/system/grub_update.py index da1986e9d..366a85344 100644 --- a/src/system/grub_update.py +++ b/src/system/grub_update.py @@ -1,107 +1,104 @@ #!/usr/bin/env python3 # # Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io> # # This file is part of VyOS. # # VyOS is free software: you can redistribute it and/or modify it under the # terms of the GNU General Public License as published by the Free Software # Foundation, either version 3 of the License, or (at your option) any later # version. # # VyOS 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 # VyOS. If not, see <https://www.gnu.org/licenses/>. from pathlib import Path from sys import exit from vyos.system import disk, grub, image, compat, SYSTEM_CFG_VER from vyos.template import render def cfg_check_update() -> bool: """Check if GRUB structure update is required Returns: bool: False if not required, True if required """ current_ver = grub.get_cfg_ver() if current_ver and current_ver >= SYSTEM_CFG_VER: return False return True if __name__ == '__main__': if image.is_live_boot(): exit(0) # Skip everything if update is not required if not cfg_check_update(): exit(0) # find root directory of persistent storage root_dir = disk.find_persistence() # read current GRUB config grub_cfg_main = f'{root_dir}/{grub.GRUB_CFG_MAIN}' vars = grub.vars_read(grub_cfg_main) modules = grub.modules_read(grub_cfg_main) vyos_menuentries = compat.parse_menuentries(grub_cfg_main) vyos_versions = compat.find_versions(vyos_menuentries) unparsed_items = compat.filter_unparsed(grub_cfg_main) - + # compatibilty for raid installs + search_root = compat.get_search_root(unparsed_items) + common_dict = {} + common_dict['search_root'] = search_root # find default values default_entry = vyos_menuentries[int(vars['default'])] default_settings = { 'default': grub.gen_version_uuid(default_entry['version']), 'bootmode': default_entry['bootmode'], 'console_type': default_entry['console_type'], 'console_num': default_entry['console_num'] } vars.update(default_settings) - # print(f'vars: {vars}') - # print(f'modules: {modules}') - # print(f'vyos_menuentries: {vyos_menuentries}') - # print(f'unparsed_items: {unparsed_items}') - # create new files grub_cfg_vars = f'{root_dir}/{grub.CFG_VYOS_VARS}' grub_cfg_modules = f'{root_dir}/{grub.CFG_VYOS_MODULES}' grub_cfg_platform = f'{root_dir}/{grub.CFG_VYOS_PLATFORM}' grub_cfg_menu = f'{root_dir}/{grub.CFG_VYOS_MENU}' grub_cfg_options = f'{root_dir}/{grub.CFG_VYOS_OPTIONS}' Path(image.GRUB_DIR_VYOS).mkdir(exist_ok=True) grub.vars_write(grub_cfg_vars, vars) grub.modules_write(grub_cfg_modules, modules) - # Path(grub_cfg_platform).write_text(unparsed_items) - grub.common_write() + grub.common_write(grub_common=common_dict) render(grub_cfg_menu, grub.TMPL_GRUB_MENU, {}) render(grub_cfg_options, grub.TMPL_GRUB_OPTS, {}) # create menu entries for vyos_ver in vyos_versions: boot_opts = None for entry in vyos_menuentries: if entry.get('version') == vyos_ver and entry.get( 'bootmode') == 'normal': boot_opts = entry.get('boot_opts') grub.version_add(vyos_ver, root_dir, boot_opts) # update structure version cfg_ver = compat.update_cfg_ver(root_dir) grub.write_cfg_ver(cfg_ver, root_dir) if compat.mode(): compat.render_grub_cfg(root_dir) else: render(grub_cfg_main, grub.TMPL_GRUB_MAIN, {}) exit(0)