|
16 | 16 |
|
17 | 17 | import os |
18 | 18 | import psutil |
| 19 | +import shlex |
19 | 20 |
|
20 | 21 | from sys import exit |
21 | 22 | from time import sleep |
22 | 23 |
|
23 | | - |
| 24 | +from vyos.base import Warning |
24 | 25 | from vyos.config import Config |
25 | 26 | from vyos.configverify import verify_source_interface |
26 | 27 | from vyos.configverify import verify_interface_exists |
27 | 28 | from vyos.system import grub_util |
28 | 29 | from vyos.template import render |
| 30 | +from vyos.utils.boot import boot_configuration_complete |
29 | 31 | from vyos.utils.cpu import get_cpus |
30 | 32 | from vyos.utils.dict import dict_search |
31 | 33 | from vyos.utils.file import write_file |
| 34 | +from vyos.utils.file import read_file |
32 | 35 | from vyos.utils.kernel import check_kmod |
33 | 36 | from vyos.utils.process import cmd |
34 | 37 | from vyos.utils.process import is_systemd_service_running |
|
58 | 61 | 'virtual-host': 'virtual-host', |
59 | 62 | } |
60 | 63 |
|
| 64 | +# Managed keys |
| 65 | +MANAGED_SINGLE = { |
| 66 | + 'mitigations', |
| 67 | + 'intel_idle.max_cstate', |
| 68 | + 'processor.max_cstate', |
| 69 | + 'initcall_blacklist', |
| 70 | + 'amd_pstate', |
| 71 | + 'quiet', |
| 72 | + 'panic', |
| 73 | + 'hpet', |
| 74 | + 'mce', |
| 75 | + 'nosoftlockup', |
| 76 | + 'isolcpus', |
| 77 | + 'nohz_full', |
| 78 | + 'rcu_nocbs', |
| 79 | + 'nmi_watchdog', |
| 80 | + 'numa_balancing', |
| 81 | + 'default_hugepagesz', |
| 82 | +} |
| 83 | + |
| 84 | +MANAGED_HUGEPAGES = {'hugepagesz', 'hugepages'} |
| 85 | + |
61 | 86 |
|
62 | 87 | def _get_total_hugepages_and_memory(config): |
63 | 88 | unit_map = {'M': 1 << 20, 'G': 1 << 30} |
@@ -112,10 +137,10 @@ def verify(options): |
112 | 137 | if 'source_address' in config: |
113 | 138 | address = config['source_address'] |
114 | 139 | if not is_addr_assigned(config['source_address']): |
115 | | - raise ConfigError('No interface with address "{address}" configured!') |
| 140 | + raise ConfigError(f'No interface with address "{address}" configured!') |
116 | 141 |
|
117 | 142 | if 'source_interface' in config: |
118 | | - # verify_source_interface reuires key 'ifname' |
| 143 | + # verify_source_interface requires key 'ifname' |
119 | 144 | config['ifname'] = config['source_interface'] |
120 | 145 | verify_source_interface(config) |
121 | 146 | if 'source_address' in config: |
@@ -244,10 +269,127 @@ def generate(options): |
244 | 269 |
|
245 | 270 | grub_util.update_kernel_cmdline_options(' '.join(cmdline_options)) |
246 | 271 |
|
| 272 | + options['cmdline_options'] = cmdline_options |
| 273 | + |
247 | 274 | return None |
248 | 275 |
|
249 | 276 |
|
| 277 | +def generate_cmdline_for_kexec(options): |
| 278 | + """ |
| 279 | + Build an updated kernel cmdline string based on desired options and the |
| 280 | + currently running /proc/cmdline. |
| 281 | +
|
| 282 | + Returns: |
| 283 | + (kexec_required: bool, new_cmdline: str) |
| 284 | + - kexec_required is True if kernel options were added, removed, |
| 285 | + or modified. |
| 286 | + """ |
| 287 | + |
| 288 | + def parse_token(tok): |
| 289 | + return tok.split('=', 1) if '=' in tok else [tok, None] |
| 290 | + |
| 291 | + # Read current cmdline |
| 292 | + raw_cmdline = read_file('/proc/cmdline').strip() |
| 293 | + tokens = shlex.split(raw_cmdline) |
| 294 | + |
| 295 | + # Separate normal keys vs hugepages |
| 296 | + current_single_opts = {} |
| 297 | + hugepages_current = [] |
| 298 | + |
| 299 | + # Split existing cmdline into normal keys and hugepages pairs |
| 300 | + i = 0 |
| 301 | + while i < len(tokens): |
| 302 | + key, val = parse_token(tokens[i]) |
| 303 | + if key == 'hugepagesz': |
| 304 | + size = val |
| 305 | + count = None |
| 306 | + if i + 1 < len(tokens): |
| 307 | + nk, nv = parse_token(tokens[i + 1]) |
| 308 | + if nk == 'hugepages': |
| 309 | + count = nv |
| 310 | + i += 1 |
| 311 | + hugepages_current.append([size, count]) |
| 312 | + else: |
| 313 | + current_single_opts[key] = val |
| 314 | + i += 1 |
| 315 | + |
| 316 | + kexec_required = False |
| 317 | + |
| 318 | + # Extract desired single-value options from options['cmdline_options'] |
| 319 | + desired_single_opts = {} |
| 320 | + for entry in options.get("cmdline_options", []): |
| 321 | + for tok in entry.split(): |
| 322 | + k, v = parse_token(tok) |
| 323 | + if k not in MANAGED_HUGEPAGES: |
| 324 | + desired_single_opts[k] = v |
| 325 | + |
| 326 | + # Remove obsolete managed options |
| 327 | + for k in list(current_single_opts.keys()): |
| 328 | + if k in MANAGED_SINGLE and k not in desired_single_opts: |
| 329 | + kexec_required = True |
| 330 | + current_single_opts.pop(k) |
| 331 | + |
| 332 | + # Add/update managed single-value options |
| 333 | + for k, v in desired_single_opts.items(): |
| 334 | + if current_single_opts.get(k, '') != v: |
| 335 | + kexec_required = True |
| 336 | + current_single_opts[k] = v |
| 337 | + |
| 338 | + # Build desired hugepages dictionary |
| 339 | + hp_cfg = options.get('kernel', {}).get('memory', {}).get('hugepage_size', {}) |
| 340 | + desired_hp = {size: cfg.get('hugepage_count') for size, cfg in hp_cfg.items()} |
| 341 | + |
| 342 | + new_hugepages = [] |
| 343 | + seen_sizes = set() |
| 344 | + |
| 345 | + # Update existing hugepages_pairs and remove obsolete |
| 346 | + for size, count in hugepages_current: |
| 347 | + if size in desired_hp: |
| 348 | + if count != desired_hp[size]: |
| 349 | + kexec_required = True |
| 350 | + count = desired_hp[size] |
| 351 | + new_hugepages.append([size, count]) |
| 352 | + seen_sizes.add(size) |
| 353 | + else: |
| 354 | + # obsolete hugepages -> remove |
| 355 | + kexec_required = True |
| 356 | + |
| 357 | + # Add new hugepages that do not exist in current cmdline |
| 358 | + for size, count in desired_hp.items(): |
| 359 | + if size not in seen_sizes: |
| 360 | + new_hugepages.append([size, count]) |
| 361 | + kexec_required = True |
| 362 | + |
| 363 | + # Reconstruct final cmdline |
| 364 | + new_tokens = [] |
| 365 | + for key, value in current_single_opts.items(): |
| 366 | + new_tokens.append(f'{key}={value}' if value is not None else key) |
| 367 | + |
| 368 | + # Append hugepages pairs |
| 369 | + for size, count in new_hugepages: |
| 370 | + new_tokens.append(f'hugepagesz={size}') |
| 371 | + if count is not None: |
| 372 | + new_tokens.append(f'hugepages={count}') |
| 373 | + |
| 374 | + cmdline_new = ' '.join(new_tokens) |
| 375 | + |
| 376 | + return kexec_required, cmdline_new |
| 377 | + |
| 378 | + |
250 | 379 | def apply(options): |
| 380 | + kexec_required, cmdline_new = generate_cmdline_for_kexec(options) |
| 381 | + if kexec_required: |
| 382 | + if not boot_configuration_complete() and os.environ.get('VYOS_CONFIGD'): |
| 383 | + cmd( |
| 384 | + f'sudo kexec -l /boot/vmlinuz --initrd=/boot/initrd.img --command-line="{cmdline_new}"' |
| 385 | + ) |
| 386 | + cmd('sudo systemctl kexec') |
| 387 | + elif boot_configuration_complete(): |
| 388 | + Warning( |
| 389 | + 'Kernel configuration options have changed. ' |
| 390 | + 'To apply these changes, you must save the configuration and reboot the system!' |
| 391 | + ) |
| 392 | + |
251 | 393 | # System bootup beep |
252 | 394 | beep_service = 'vyos-beep.service' |
253 | 395 | if 'startup_beep' in options: |
|
0 commit comments