Combine "can this be" and "make this a" boot disk logic for BIOS

No behaviour change yet, this all assumes version 1 partitioning, i.e.
no mix of new and old partitions on a device. But it means we only have
to edit one batch of logic when that stops being true.
This commit is contained in:
Michael Hudson-Doyle 2022-03-17 12:08:55 +13:00
parent c226d4e2b5
commit dfc4a32b0a
3 changed files with 163 additions and 21 deletions

View File

@ -15,6 +15,9 @@
import functools
import attr
from subiquity.common.filesystem import gaps, sizes
from subiquity.models.filesystem import (
Disk,
Raid,
@ -50,6 +53,101 @@ def _is_boot_device_raid(raid):
return any(p.grub_device for p in raid._partitions)
@attr.s(auto_attribs=True)
class CreatePartPlan:
device: object
offset: int = 0
spec: dict = attr.ib(factory=dict)
args: dict = attr.ib(factory=dict)
def apply(self, manipulator):
manipulator.create_partition(
self.device, gaps.Gap(self.device, self.offset, 0), self.spec,
**self.args)
@attr.s(auto_attribs=True)
class ResizePlan:
part: object
size_delta: int = 0
def apply(self, manipulator):
self.part.size += self.size_delta
@attr.s(auto_attribs=True)
class SlidePlan:
parts: list
offset_delta: int = 0
def apply(self, manipulator):
for part in self.parts:
part.offset += self.offset_delta
@attr.s(auto_attribs=True)
class SetAttrPlan:
device: object
attr: str
val: str
def apply(self, manipulator):
setattr(self.device, self.attr, self.val)
@attr.s(auto_attribs=True)
class MultiStepPlan:
plans: list
def apply(self, manipulator):
for plan in self.plans:
plan.apply(manipulator)
def get_boot_device_plan_bios(device):
attr_plan = SetAttrPlan(device, 'grub_device', True)
if device.ptable == 'msdos':
return attr_plan
if device._has_preexisting_partition():
if device._partitions[0].flag == "bios_grub":
return attr_plan
else:
return None
create_part_plan = CreatePartPlan(
device=device,
offset=sizes.BIOS_GRUB_SIZE_BYTES,
spec=dict(size=sizes.BIOS_GRUB_SIZE_BYTES, fstype=None, mount=None),
args=dict(flag='bios_grub'))
partitions = device.partitions()
if gaps.largest_gap_size(device) >= sizes.BIOS_GRUB_SIZE_BYTES:
return MultiStepPlan(plans=[
SlidePlan(
parts=partitions,
offset_delta=sizes.BIOS_GRUB_SIZE_BYTES),
create_part_plan,
attr_plan,
])
else:
largest_i, largest_part = max(
enumerate(partitions),
key=lambda i_p: i_p[1].size)
return MultiStepPlan(plans=[
SlidePlan(
parts=partitions[:largest_i+1],
offset_delta=sizes.BIOS_GRUB_SIZE_BYTES),
ResizePlan(
part=largest_part,
size_delta=-sizes.BIOS_GRUB_SIZE_BYTES),
create_part_plan,
attr_plan,
])
@functools.singledispatch
def can_be_boot_device(device, *, with_reformatting=False):
"""Can `device` be made into a boot device?
@ -63,13 +161,12 @@ def can_be_boot_device(device, *, with_reformatting=False):
@can_be_boot_device.register(Disk)
def _can_be_boot_device_disk(disk, *, with_reformatting=False):
bl = disk._m.bootloader
if disk._has_preexisting_partition() and not with_reformatting:
if bl == Bootloader.BIOS:
if disk.ptable == "msdos":
return True
else:
return disk._partitions[0].flag == "bios_grub"
elif bl == Bootloader.UEFI:
if with_reformatting:
return True
if bl == Bootloader.BIOS:
return get_boot_device_plan_bios(disk) is not None
if disk._has_preexisting_partition():
if bl == Bootloader.UEFI:
return any(is_esp(p) for p in disk._partitions)
elif bl == Bootloader.PREP:
return any(p.flag == "prep" for p in disk._partitions)

View File

@ -103,11 +103,11 @@ class FilesystemManipulator:
spec = dict(size=part_size, fstype='fat32')
if self.model._mount_for_path("/boot/efi") is None:
spec['mount'] = '/boot/efi'
part = self._create_boot_with_resize(
self._create_boot_with_resize(
disk, spec, flag="boot", grub_device=True)
elif bootloader == Bootloader.PREP:
log.debug('_create_boot_partition - adding PReP partition')
part = self._create_boot_with_resize(
self._create_boot_with_resize(
disk,
dict(size=sizes.PREP_GRUB_SIZE_BYTES, fstype=None, mount=None),
# must be wiped or grub-install will fail
@ -115,12 +115,7 @@ class FilesystemManipulator:
flag='prep', grub_device=True)
elif bootloader == Bootloader.BIOS:
log.debug('_create_boot_partition - adding bios_grub partition')
part = self._create_boot_with_resize(
disk,
dict(size=sizes.BIOS_GRUB_SIZE_BYTES, fstype=None, mount=None),
flag='bios_grub')
disk.grub_device = True
return part
boot.get_boot_device_plan_bios(disk).apply(self)
def create_raid(self, spec):
for d in spec['devices'] | spec['spare_devices']:
@ -221,14 +216,14 @@ class FilesystemManipulator:
log.debug('model needs a bootloader partition? {}'.format(needs_boot))
can_be_boot = boot.can_be_boot_device(disk)
if needs_boot and len(disk.partitions()) == 0 and can_be_boot:
part = self._create_boot_partition(disk)
self._create_boot_partition(disk)
# adjust downward the partition size (if necessary) to accommodate
# bios/grub partition
if spec['size'] > gaps.largest_gap_size(disk):
log.debug(
"Adjusting request down: %s - %s = %s",
spec['size'], part.size, gaps.largest_gap_size(disk))
"Adjusting request down from %s to %s",
spec['size'], gaps.largest_gap_size(disk))
spec['size'] = gaps.largest_gap_size(disk)
self.create_partition(disk, gap, spec)
@ -345,10 +340,11 @@ class FilesystemManipulator:
if not self.supports_resilient_boot:
for disk in boot.all_boot_devices(self.model):
self.remove_boot_disk(disk)
if bootloader == Bootloader.BIOS:
boot.get_boot_device_plan_bios(new_boot_disk).apply(self)
return
if new_boot_disk._has_preexisting_partition():
if bootloader == Bootloader.BIOS:
new_boot_disk.grub_device = True
elif bootloader == Bootloader.UEFI:
if bootloader == Bootloader.UEFI:
should_mount = self.model._mount_for_path('/boot/efi') is None
for p in new_boot_disk.partitions():
if boot.is_esp(p):

View File

@ -23,9 +23,11 @@ from subiquity.common.filesystem.manipulator import FilesystemManipulator
from subiquity.models.tests.test_filesystem import (
make_disk,
make_model,
make_partition,
)
from subiquity.models.filesystem import (
Bootloader,
MiB,
)
@ -236,3 +238,50 @@ class TestFilesystemManipulator(unittest.TestCase):
manipulator.add_boot_disk(disk1)
part = gaps.parts_and_gaps(disk1)[0]
self.assertEqual(1024 * 1024, part.offset)
def test_add_boot_BIOS_empty(self):
manipulator = make_manipulator(Bootloader.BIOS)
disk = make_disk(manipulator.model, preserve=True)
manipulator.add_boot_disk(disk)
[part] = disk.partitions()
self.assertEqual(part.offset, MiB)
def test_add_boot_BIOS_full(self):
manipulator = make_manipulator(Bootloader.BIOS)
disk = make_disk(manipulator.model, preserve=True)
part = make_partition(
manipulator.model, disk, size=gaps.largest_gap_size(disk))
size_before = part.size
manipulator.add_boot_disk(disk)
[p1, p2] = disk.partitions()
self.assertIs(p2, part)
size_after = p2.size
self.assertEqual(p1.offset, MiB)
self.assertEqual(p2.offset, 2*MiB)
self.assertEqual(size_after, size_before - MiB)
def test_add_boot_BIOS_half_full(self):
manipulator = make_manipulator(Bootloader.BIOS)
disk = make_disk(manipulator.model, preserve=True)
part = make_partition(
manipulator.model, disk, size=gaps.largest_gap_size(disk)//2)
size_before = part.size
manipulator.add_boot_disk(disk)
[p1, p2] = disk.partitions()
size_after = p2.size
self.assertIs(p2, part)
self.assertEqual(p1.offset, MiB)
self.assertEqual(p2.offset, 2*MiB)
self.assertEqual(size_after, size_before)
def DONT_test_add_boot_BIOS_preserved(self): # needs v2 partitioning
manipulator = make_manipulator(Bootloader.BIOS)
disk = make_disk(manipulator.model, preserve=True)
half_size = gaps.largest_gap_size(disk)//2
part = make_partition(
manipulator.model, disk, size=half_size, offset=half_size)
manipulator.add_boot_disk(disk)
[p1, p2] = disk.partitions()
self.assertIs(p2, part)
self.assertEqual(p1.offset, MiB)
self.assertEqual(p2.offset, half_size)