diff --git a/debian/control b/debian/control index 12b2889317..561e6e1511 100644 --- a/debian/control +++ b/debian/control @@ -110,6 +110,7 @@ Depends: iproute2 (>= 6.0.0), linux-cpupower, hwloc, + kexec-tools, # ipaddrcheck is widely used in IP value validators ipaddrcheck, ethtool (>= 6.10), diff --git a/src/conf_mode/system_option.py b/src/conf_mode/system_option.py index 80b9f96bdb..fed298cf58 100755 --- a/src/conf_mode/system_option.py +++ b/src/conf_mode/system_option.py @@ -16,19 +16,22 @@ import os import psutil +import re from sys import exit from time import sleep - +from vyos.base import Warning from vyos.config import Config from vyos.configverify import verify_source_interface from vyos.configverify import verify_interface_exists from vyos.system import grub_util from vyos.template import render +from vyos.utils.boot import boot_configuration_complete from vyos.utils.cpu import get_cpus from vyos.utils.dict import dict_search from vyos.utils.file import write_file +from vyos.utils.file import read_file from vyos.utils.kernel import check_kmod from vyos.utils.process import cmd from vyos.utils.process import is_systemd_service_running @@ -58,6 +61,103 @@ 'virtual-host': 'virtual-host', } +MANAGED_PARAMS = { + 'hugepages1g': { + 'parse': r'hugepagesz=1[Gg]\s+hugepages=(?P\d+)', + 'clean': r'hugepagesz=1[Gg](?:\s+hugepages=\d+)?', + 'build': lambda v: f'hugepagesz=1G hugepages={v}', + 'type': int, + }, + 'hugepages2m': { + 'parse': r'hugepagesz=2[Mm]\s+hugepages=(?P\d+)', + 'clean': r'hugepagesz=2[Mm](?:\s+hugepages=\d+)?', + 'build': lambda v: f'hugepagesz=2M hugepages={v}', + 'type': int, + }, + 'default_hugepagesz': { + 'parse': r'default_hugepagesz=(?P\S+)', + 'clean': r'default_hugepagesz=\S+', + 'type': str, + }, + 'mitigations': { + 'parse': r'mitigations=(?P\S+)', + 'clean': r'mitigations=\S+', + 'type': str, + }, + 'intel_idle.max_cstate': { + 'parse': r'intel_idle\.max_cstate=(?P\d+)', + 'clean': r'intel_idle\.max_cstate=\d+', + 'build': lambda v: f'intel_idle.max_cstate={v}', + 'type': int, + }, + 'processor.max_cstate': { + 'parse': r'processor\.max_cstate=(?P\d+)', + 'clean': r'processor\.max_cstate=\d+', + 'build': lambda v: f'processor.max_cstate={v}', + 'type': int, + }, + 'initcall_blacklist': { + 'parse': r'initcall_blacklist=(?P\S+)', + 'clean': r'initcall_blacklist=\S+', + 'type': str, + }, + 'amd_pstate': { + 'parse': r'amd_pstate=(?P\S+)', + 'clean': r'amd_pstate=\S+', + 'type': str, + }, + 'quiet': { + 'parse': r'(?P\bquiet\b)', + 'clean': r'\bquiet\b', + 'type': bool, + }, + 'nosoftlockup': { + 'parse': r'(?P\bnosoftlockup\b)', + 'clean': r'\bnosoftlockup\b', + 'type': bool, + }, + 'panic': { + 'parse': r'panic=(?P\d+)', + 'clean': r'panic=\d+', + 'type': int, + }, + 'mce': { + 'parse': r'mce=(?P\S+)', + 'clean': r'mce=\S+', + 'type': str, + }, + 'hpet': { + 'parse': r'hpet=(?P\S+)', + 'clean': r'hpet=\S+', + 'type': str, + }, + 'nmi_watchdog': { + 'parse': r'nmi_watchdog=(?P\d+)', + 'clean': r'nmi_watchdog=\d+', + 'type': int, + }, + 'isolcpus': { + 'parse': r'isolcpus=(?P\S+)', + 'clean': r'isolcpus=\S+', + 'type': str, + }, + 'nohz_full': { + 'parse': r'nohz_full=(?P\S+)', + 'clean': r'nohz_full=\S+', + 'type': str, + }, + 'rcu_nocbs': { + 'parse': r'rcu_nocbs=(?P\S+)', + 'clean': r'rcu_nocbs=\S+', + 'type': str, + }, + 'numa_balancing': { + 'parse': r'numa_balancing=(?P\S+)', + 'clean': r'numa_balancing=\S+', + 'type': str, + }, +} + def _get_total_hugepages_and_memory(config): unit_map = {'M': 1 << 20, 'G': 1 << 30} @@ -112,10 +212,10 @@ def verify(options): if 'source_address' in config: address = config['source_address'] if not is_addr_assigned(config['source_address']): - raise ConfigError('No interface with address "{address}" configured!') + raise ConfigError(f'No interface with address "{address}" configured!') if 'source_interface' in config: - # verify_source_interface reuires key 'ifname' + # verify_source_interface requires key 'ifname' config['ifname'] = config['source_interface'] verify_source_interface(config) if 'source_address' in config: @@ -242,12 +342,123 @@ def generate(options): if count: cmdline_options.append(f'hugepages={count}') - grub_util.update_kernel_cmdline_options(' '.join(cmdline_options)) + cmdline_options_str = ' '.join(cmdline_options) + + grub_util.update_kernel_cmdline_options(cmdline_options_str) + + options['cmdline_options'] = cmdline_options_str return None +def parse_cmdline(cmdline): + """ + Parse command line parameters into a dictionary. + + Args: + cmdline: The command line string (e.g., from /proc/cmdline) + + Returns: + Dictionary with parsed parameters + """ + # Produce a complete template of all managed parameters with + # consistent default values before scanning the actual kernel cmdline. + result = { + k: (False if v['type'] is bool else None) for k, v in MANAGED_PARAMS.items() + } + + # Mapping from regex group names to real parameter keys + group_to_key = { + 'intel_idle_max_cstate': 'intel_idle.max_cstate', + 'processor_max_cstate': 'processor.max_cstate', + } + + # Compiled pattern + parse_pattern = re.compile('|'.join(v['parse'] for v in MANAGED_PARAMS.values())) + + # Find all matches and populate result + for match in parse_pattern.finditer(cmdline): + for group_name, value in match.groupdict().items(): + if value is None: + continue + + key = group_to_key.get(group_name, group_name) + entry = MANAGED_PARAMS[key] + + if entry['type'] is bool: + result[key] = True + elif entry['type'] is int: + result[key] = int(value) + else: + result[key] = value + + return result + + +def generate_cmdline_for_kexec(options): + """ + Build an updated kernel cmdline string based on desired options and the + currently running /proc/cmdline. + + Returns: + (kexec_required: bool, new_cmdline: str) + - kexec_required is True if kernel options were added, removed, + or modified. + """ + # Read current cmdline and parse it + current_cmdline = read_file('/proc/cmdline').strip() + current_parsed = parse_cmdline(current_cmdline) + + # Parse desired options from options['cmdline_options'] + desired_options = options.get('cmdline_options', []) + desired_parsed = parse_cmdline(desired_options) + + # Compare dicts to define if kexec is needed + kexec_required = current_parsed != desired_parsed + if not kexec_required: + return kexec_required, current_cmdline + + # Clean managed params + for entry in MANAGED_PARAMS.values(): + current_cmdline = re.sub(entry['clean'], '', current_cmdline) + cleaned = re.sub(r'\s+', ' ', current_cmdline).strip() + + # Build new cmdline + parts = [] + for key, entry in MANAGED_PARAMS.items(): + val = desired_parsed[key] + if val is None or val is False: + continue + + if 'build' in entry: + parts.append(entry['build'](val)) + elif entry['type'] is bool: + parts.append(key) + else: + parts.append(f'{key}={val}') + + rebuilt = ' '.join(parts) + + new_cmdline = (cleaned + ' ' + rebuilt).strip() if cleaned else rebuilt + + return kexec_required, new_cmdline + + def apply(options): + kexec_required, cmdline_new = generate_cmdline_for_kexec(options) + if kexec_required: + if not boot_configuration_complete() and os.environ.get('VYOS_CONFIGD'): + cmd( + f'sudo kexec -l /boot/vmlinuz --initrd=/boot/initrd.img --command-line="{cmdline_new}"' + ) + os.sync() + cmd('sudo systemctl kexec') + elif boot_configuration_complete(): + Warning( + 'Kernel configuration options have changed. ' + 'To apply these changes, you must save the configuration and reboot the system!' + ) + # System bootup beep beep_service = 'vyos-beep.service' if 'startup_beep' in options: