Merge branch 'issue-1820' into calamares

FIXES #1820
This commit is contained in:
Adriaan de Groot 2022-01-18 15:10:23 +01:00
commit 88afcee37a
4 changed files with 244 additions and 7 deletions

View File

@ -46,6 +46,16 @@ efiBootMgr: "efibootmgr"
# setting the option here, keep in mind that the name is sanitized # setting the option here, keep in mind that the name is sanitized
# (problematic characters, see above, are replaced). # (problematic characters, see above, are replaced).
# #
# There are some special words possible at the end of *efiBootloaderId*:
# @@SERIAL@@ can be used to obtain a uniquely-numbered suffix
# that is added to the Id (yielding, e.g., `dirname1` or `dirname72`)
# @@RANDOM@@ can be used to obtain a unique 4-digit hex suffix
# @@PHRASE@@ can be used to obtain a unique 1-to-3-word suffix
# from a dictionary of space-themed words
# Note that these must be at the **end** of the *efiBootloaderId* value.
# There must also be at most one of them. If there is none, no suffix-
# processing is done and the *efiBootloaderId* is used unchanged.
#
# efiBootloaderId: "dirname" # efiBootloaderId: "dirname"
# Optionally install a copy of the GRUB EFI bootloader as the EFI # Optionally install a copy of the GRUB EFI bootloader as the EFI

View File

@ -268,10 +268,166 @@ def create_loader(loader_path, entry):
loader_file.write(line) loader_file.write(line)
def efi_label(): class suffix_iterator(object):
"""
Wrapper for one of the "generator" classes below to behave like
a proper Python iterator. The iterator is initialized with a
maximum number of attempts to generate a new suffix.
"""
def __init__(self, attempts, generator):
self.generator = generator
self.attempts = attempts
self.counter = 0
def __iter__(self):
return self
def __next__(self):
self.counter += 1
if self.counter <= self.attempts:
return self.generator.next()
raise StopIteration
class serialEfi(object):
"""
EFI Id generator that appends a serial number to the given name.
"""
def __init__(self, name):
self.name = name
# So the first call to next() will bump it to 0
self.counter = -1
def next(self):
self.counter += 1
if self.counter > 0:
return "{!s}{!s}".format(self.name, self.counter)
else:
return self.name
def render_in_base(value, base_values, length=-1):
"""
Renders @p value in base-N, where N is the number of
items in @p base_values. When rendering, use the items
of @p base_values (e.g. use "0123456789" to get regular decimal
rendering, or "ABCDEFGHIJ" for letters-as-numbers 'encoding').
If length is positive, pads out to at least that long with
leading "zeroes", whatever base_values[0] is.
"""
if value < 0:
raise ValueError("Cannot render negative values")
if len(base_values) < 2:
raise ValueError("Insufficient items for base-N rendering")
if length < 1:
length = 1
digits = []
base = len(base_values)
while value > 0:
place = value % base
value = value // base
digits.append(base_values[place])
while len(digits) < length:
digits.append(base_values[0])
return "".join(reversed(digits))
class randomEfi(object):
"""
EFI Id generator that appends a random 4-digit hex number to the given name.
"""
def __init__(self, name):
self.name = name
# So the first call to next() will bump it to 0
self.counter = -1
def next(self):
self.counter += 1
if self.counter > 0:
import random
v = random.randint(0, 65535) # 16 bits
return "{!s}{!s}".format(self.name, render_in_base(v, "0123456789ABCDEF", 4))
else:
return self.name
class phraseEfi(object):
"""
EFI Id generator that appends a random phrase to the given name.
"""
words = ("Sun", "Moon", "Mars", "Soyuz", "Falcon", "Kuaizhou", "Gaganyaan")
def __init__(self, name):
self.name = name
# So the first call to next() will bump it to 0
self.counter = -1
def next(self):
self.counter += 1
if self.counter > 0:
import random
desired_length = 1 + self.counter // 5
v = random.randint(0, len(self.words) ** desired_length)
return "{!s}{!s}".format(self.name, render_in_base(v, self.words))
else:
return self.name
def get_efi_suffix_generator(name):
"""
Handle EFI bootloader Ids with @@<something>@@ for suffix-processing.
"""
if "@@" not in name:
raise ValueError("Misplaced call to get_efi_suffix_generator, no @@")
parts = name.split("@@")
if len(parts) != 3:
raise ValueError("EFI Id {!r} is malformed".format(name))
if parts[2]:
# Supposed to be empty because the string ends with "@@"
raise ValueError("EFI Id {!r} is malformed".format(name))
if parts[1] not in ("SERIAL", "RANDOM", "PHRASE"):
raise ValueError("EFI suffix {!r} is unknown".format(parts[1]))
generator = None
if parts[1] == "SERIAL":
generator = serialEfi(parts[0])
elif parts[1] == "RANDOM":
generator = randomEfi(parts[0])
elif parts[1] == "PHRASE":
generator = phraseEfi(parts[0])
if generator is None:
raise ValueError("EFI suffix {!r} is unsupported".format(parts[1]))
return generator
def change_efi_suffix(efi_directory, bootloader_id):
"""
Returns a label based on @p bootloader_id that is usable within
@p efi_directory. If there is a @@<something>@@ suffix marker
in the given id, tries to generate a unique label.
"""
if bootloader_id.endswith("@@"):
# Do 10 attempts with any suffix generator
g = suffix_iterator(10, get_efi_suffix_generator(bootloader_id))
else:
# Just one attempt
g = [bootloader_id]
for candidate_name in g:
if not os.path.exists(os.path.join(efi_directory, candidate_name)):
return candidate_name
return bootloader_id
def efi_label(efi_directory):
"""
Returns a sanitized label, possibly unique, that can be
used within @p efi_directory.
"""
if "efiBootloaderId" in libcalamares.job.configuration: if "efiBootloaderId" in libcalamares.job.configuration:
efi_bootloader_id = libcalamares.job.configuration[ efi_bootloader_id = change_efi_suffix( efi_directory, calamares.job.configuration["efiBootloaderId"] )
"efiBootloaderId"]
else: else:
branding = libcalamares.globalstorage.value("branding") branding = libcalamares.globalstorage.value("branding")
efi_bootloader_id = branding["bootloaderEntryName"] efi_bootloader_id = branding["bootloaderEntryName"]
@ -390,7 +546,7 @@ def run_grub_mkconfig(partitions, output_file):
check_target_env_call([libcalamares.job.configuration["grubMkconfig"], "-o", output_file]) check_target_env_call([libcalamares.job.configuration["grubMkconfig"], "-o", output_file])
def run_grub_install(fw_type, partitions, efi_directory=None): def run_grub_install(fw_type, partitions, efi_directory):
""" """
Runs grub-install in the target environment Runs grub-install in the target environment
@ -407,7 +563,7 @@ def run_grub_install(fw_type, partitions, efi_directory=None):
check_target_env_call(["sh", "-c", "echo ZPOOL_VDEV_NAME_PATH=1 >> /etc/environment"]) check_target_env_call(["sh", "-c", "echo ZPOOL_VDEV_NAME_PATH=1 >> /etc/environment"])
if fw_type == "efi": if fw_type == "efi":
efi_bootloader_id = efi_label() efi_bootloader_id = efi_label(efi_directory)
efi_target, efi_grub_file, efi_boot_file = get_grub_efi_parameters() efi_target, efi_grub_file, efi_boot_file = get_grub_efi_parameters()
if is_zfs: if is_zfs:
@ -462,7 +618,7 @@ def install_grub(efi_directory, fw_type):
if not os.path.isdir(install_efi_directory): if not os.path.isdir(install_efi_directory):
os.makedirs(install_efi_directory) os.makedirs(install_efi_directory)
efi_bootloader_id = efi_label() efi_bootloader_id = efi_label(efi_directory)
efi_target, efi_grub_file, efi_boot_file = get_grub_efi_parameters() efi_target, efi_grub_file, efi_boot_file = get_grub_efi_parameters()
@ -506,7 +662,7 @@ def install_secureboot(efi_directory):
""" """
Installs the secureboot shim in the system by calling efibootmgr. Installs the secureboot shim in the system by calling efibootmgr.
""" """
efi_bootloader_id = efi_label() efi_bootloader_id = efi_label(efi_directory)
install_path = libcalamares.globalstorage.value("rootMountPoint") install_path = libcalamares.globalstorage.value("rootMountPoint")
install_efi_directory = install_path + efi_directory install_efi_directory = install_path + efi_directory

View File

@ -0,0 +1,7 @@
# We have tests to exercise some of the module internals.
# Those tests conventionally live in Python files here in the tests/ directory. Add them.
add_test(
NAME test-bootloader-efiname
COMMAND env PYTHONPATH=.: python3 ${CMAKE_CURRENT_LIST_DIR}/test-bootloader-efiname.py
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)

View File

@ -0,0 +1,64 @@
# Calamares Boilerplate
import libcalamares
libcalamares.globalstorage = libcalamares.GlobalStorage(None)
libcalamares.globalstorage.insert("testing", True)
# Module prep-work
from src.modules.bootloader import main
# Specific Bootloader test
g = main.get_efi_suffix_generator("derp@@SERIAL@@")
assert g is not None
assert g.next() == "derp" # First time, no suffix
for n in range(9):
print(g.next())
# We called next() 10 times in total, starting from 0
assert g.next() == "derp10"
g = main.get_efi_suffix_generator("derp@@RANDOM@@")
assert g is not None
for n in range(10):
print(g.next())
# it's random, nothing to assert
g = main.get_efi_suffix_generator("derp@@PHRASE@@")
assert g is not None
for n in range(10):
print(g.next())
# it's random, nothing to assert
# Check invalid things
try:
g = main.get_efi_suffix_generator("derp")
raise TypeError("Shouldn't get generator (no indicator)")
except ValueError as e:
pass
try:
g = main.get_efi_suffix_generator("derp@@HEX@@")
raise TypeError("Shouldn't get generator (unknown indicator)")
except ValueError as e:
pass
try:
g = main.get_efi_suffix_generator("derp@@SERIAL@@x")
raise TypeError("Shouldn't get generator (trailing garbage)")
except ValueError as e:
pass
try:
g = main.get_efi_suffix_generator("derp@@SERIAL@@@@RANDOM@@")
raise TypeError("Shouldn't get generator (multiple indicators)")
except ValueError as e:
pass
# Try the generator (assuming no calamares- test files exist in /tmp)
import os
assert "calamares-single" == main.change_efi_suffix("/tmp", "calamares-single")
assert "calamares-serial" == main.change_efi_suffix("/tmp", "calamares-serial@@SERIAL@@")
try:
os.makedirs("/tmp/calamares-serial", exist_ok=True)
assert "calamares-serial1" == main.change_efi_suffix("/tmp", "calamares-serial@@SERIAL@@")
finally:
os.rmdir("/tmp/calamares-serial")