From 65f1cbc7eb8033d6dc9aca3c23feaf1cfc01ac04 Mon Sep 17 00:00:00 2001 From: "Jose M. Guisado" Date: Wed, 16 Aug 2023 14:10:45 +0200 Subject: utils: add uefi.py Add UEFI related utilities inside a new utility module: uefi.py _check_efibootmgr_json ====================== Check if the system efibootmgr executable supports JSON output. This is a private function used only by other functions from uefi.py. is_uefi_supported ================= Check if the system supports UEFI firmware. run_efibootmgr_json =================== Runs efibootmgr with json output support. Return the JSON output as a Python dict. efibootmgr_create_bootentry =========================== Create nvram boot entry. This bootentry is usually later set to boot next just once via "BootNext" nvram variable. efibootmgr_delete_bootentry =========================== Delete a nvram boot entry. Used to avoid duplicates when booting the same disk and partition from a given client. efibootmgr_bootnext =================== Set nvram "BootNext" variable to a given boot entry so after client reboot, PXE is not executed and the given boot entry takes precedence. Add dependency with efibootmgr version >= 18, and efibootmgr JSON output which is currently out of tree from util-linux repo. --- src/utils/uefi.py | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 src/utils/uefi.py diff --git a/src/utils/uefi.py b/src/utils/uefi.py new file mode 100644 index 0000000..184edba --- /dev/null +++ b/src/utils/uefi.py @@ -0,0 +1,97 @@ +# +# Copyright (C) 2023 Soleta Networks +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the +# Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. + +import json +import logging +import os +import shlex +import subprocess + +import fdisk + +def _find_bootentry(entries, label): + for entry in entries: + if entry['description'] == label: + return entry + else: + raise NameError('Boot entry {label} not found') + + +def _strip_boot_prefix(entry): + try: + num = entry['name'][4:] + except: + raise KeyError('Unable to strip "Boot" prefix from boot entry') + return num + + +def _check_efibootmgr_json(): + """ + Checks if efibootmgr --json option is supported. + Returns True if supported, False otherwise. + """ + supported = True + try: + subprocess.run(['efibootmgr', '--json'], stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, check=True) + except subprocess.CalledProcessError: + supported = False + return supported + + +def is_uefi_supported(): + return True if os.path.exists("/sys/firmware/efi") else False + + +def run_efibootmgr_json(): + if _check_efibootmgr_json() is False: + raise RuntimeError('efibootmgr does not support --json option') + + proc = subprocess.run(['efibootmgr', '--json'], capture_output=True, text=True) + dict_json = json.loads(proc.stdout) + + return dict_json + + +def efibootmgr_bootnext(description): + logging.info(f'Setting BootNext to value from boot entry with label {description}') + bootnext_cmd = 'efibootmgr -n {bootnum}' + boot_entries = run_efibootmgr_json().get('vars', []) + + entry = _find_bootentry(boot_entries, description) + num = _strip_boot_prefix(entry) # efibootmgr output uses BootXXXX for each entry, remove the "Boot" prefix. + bootnext_cmd = bootnext_cmd.format(bootnum=num) + subprocess.run(shlex.split(bootnext_cmd), check=True, + stdout=subprocess.DEVNULL) + + +def efibootmgr_delete_bootentry(label): + dict_json = run_efibootmgr_json() + efibootmgr_cmd = 'efibootmgr -q -b {bootnum} -B' + + for entry in dict_json.get('vars', []): + if entry['description'] == label: + num = entry['name'][4:] # Remove "Boot" prefix to extract num + efibootmgr_cmd = efibootmgr_cmd.format(bootnum=num) + subprocess.run(shlex.split(efibootmgr_cmd), check=True) + break + else: + logging.info(f'Cannot delete boot entry {label} because it was not found.') + + +def efibootmgr_create_bootentry(disk, part, loader, label, add_to_bootorder=True): + entries = run_efibootmgr_json().get('vars', []) + create_opt = '-c' if add_to_bootorder else '-C' + index_opt = f'-I {len(entries)}' # Requires efibootmgr version 18 + efibootmgr_cmd = f'efibootmgr {create_opt} {index_opt} -d {disk} -p {part} -L {label} -l {loader}' + logging.info(f'efibootmgr command creating boot entry: {efibootmgr_cmd}') + try: + proc = subprocess.run(shlex.split(efibootmgr_cmd), check=True, text=True) + except subprocess.CalledProcessError as e: + logging.error(f'Unexpected error adding boot entry to nvram. UEFI firmware might be buggy?.') + raise e -- cgit v1.2.3-18-g5258