Merge pull request #718 from mwhudson/resilient-boot

ui for resilient boot
This commit is contained in:
Michael Hudson-Doyle 2020-04-21 12:23:37 +12:00 committed by GitHub
commit 140cdacada
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 432 additions and 308 deletions

View File

@ -13,7 +13,7 @@ Mirror:
Filesystem:
manual:
- obj: [disk index 0]
action: MAKE_BOOT
action: TOGGLE_BOOT
- &newpart
obj: [disk index 0]
action: PARTITION

View File

@ -13,7 +13,7 @@ Mirror:
Filesystem:
manual:
- obj: [disk index 0]
action: MAKE_BOOT
action: TOGGLE_BOOT
- &newpart
obj: [disk index 0]
action: PARTITION

View File

@ -13,7 +13,7 @@ Mirror:
Filesystem:
manual:
- obj: [disk index 0]
action: MAKE_BOOT
action: TOGGLE_BOOT
- &newpart
obj: [disk index 0]
action: PARTITION

View File

@ -13,7 +13,7 @@ Mirror:
Filesystem:
manual:
- obj: [disk index 0]
action: MAKE_BOOT
action: TOGGLE_BOOT
- obj: [disk index 0]
action: PARTITION
data:

View File

@ -28,7 +28,7 @@ parts:
plugin: python
source-type: git
source: git://git.launchpad.net/curtin
source-commit: e967eebc3a427cd62f12f66473ea8430889bc206
source-commit: 22b505c98abfbaf50c2ba0f7b0d02c3137566999
requirements: [requirements.txt]
organize:
'lib/python*/site-packages/usr/lib/curtin': 'usr/lib/'

View File

@ -169,17 +169,7 @@ class FilesystemController(SubiquityController):
elif 'config' in self.ai_data:
with self.context.child("applying_autoinstall"):
self.model.apply_autoinstall_config(self.ai_data['config'])
grub = self.ai_data.get('grub', {})
install_devices = grub.get('install_devices')
if install_devices:
device = install_devices[0]
for action in self.model._actions:
if action.id == device:
self.model.grub_install_device = action
break
else:
raise Exception(
"failed to find install_device {!r}".format(device))
self.model.grub = self.ai_data.get('grub', {})
self.model.swap = self.ai_data.get('swap')
def start(self):
@ -308,9 +298,12 @@ class FilesystemController(SubiquityController):
log.debug("_answers_action %r", action)
if 'obj' in action:
obj = self._action_get(action['obj'])
action_name = action['action']
if action_name == "MAKE_BOOT":
action_name = "TOGGLE_BOOT"
meth = getattr(
self.ui.body.avail_list,
"_{}_{}".format(obj.type, action['action']))
"_{}_{}".format(obj.type, action_name))
meth(obj)
yield
body = self.ui.body._w
@ -389,7 +382,7 @@ class FilesystemController(SubiquityController):
vol = fs.volume
if vol.type == "partition" and vol.device.type == "disk":
if vol.device._can_be_boot_disk():
self.make_boot_disk(vol.device)
self.add_boot_disk(vol.device)
return mount
def delete_mount(self, mount):
@ -430,8 +423,10 @@ class FilesystemController(SubiquityController):
self.model.remove_filesystem(fs)
delete_format = delete_filesystem
def create_partition(self, device, spec, flag="", wipe=None):
part = self.model.add_partition(device, spec["size"], flag, wipe)
def create_partition(self, device, spec, flag="", wipe=None,
grub_device=None):
part = self.model.add_partition(
device, spec["size"], flag, wipe, grub_device)
self.create_filesystem(part, spec)
return part
@ -446,10 +441,11 @@ class FilesystemController(SubiquityController):
if UEFI_GRUB_SIZE_BYTES*2 >= disk.size:
part_size = disk.size // 2
log.debug('_create_boot_partition - adding EFI partition')
spec = dict(size=part_size, fstype='fat32')
if self.model._mount_for_path("/boot/efi") is None:
spec['mount'] = '/boot/efi'
part = self.create_partition(
disk,
dict(size=part_size, fstype='fat32', mount='/boot/efi'),
flag="boot")
disk, spec, flag="boot", grub_device=True)
elif bootloader == Bootloader.PREP:
log.debug('_create_boot_partition - adding PReP partition')
part = self.create_partition(
@ -457,15 +453,14 @@ class FilesystemController(SubiquityController):
dict(size=PREP_GRUB_SIZE_BYTES, fstype=None, mount=None),
# must be wiped or grub-install will fail
wipe='zero',
flag='prep')
self.model.grub_install_device = part
flag='prep', grub_device=True)
elif bootloader == Bootloader.BIOS:
log.debug('_create_boot_partition - adding bios_grub partition')
part = self.create_partition(
disk,
dict(size=BIOS_GRUB_SIZE_BYTES, fstype=None, mount=None),
flag='bios_grub')
self.model.grub_install_device = disk
disk.grub_device = True
return part
def create_raid(self, spec):
@ -536,8 +531,7 @@ class FilesystemController(SubiquityController):
self.delete(subobj)
def reformat(self, disk):
if disk is self.model.grub_install_device:
self.model.grub_install_device = None
disk.grub_device = False
for p in list(disk.partitions()):
self.delete_partition(p)
self.clear(disk)
@ -562,7 +556,7 @@ class FilesystemController(SubiquityController):
needs_boot = self.model.needs_bootloader_partition()
log.debug('model needs a bootloader partition? {}'.format(needs_boot))
can_be_boot = DeviceAction.MAKE_BOOT in disk.supported_actions
can_be_boot = DeviceAction.TOGGLE_BOOT in disk.supported_actions
if needs_boot and len(disk.partitions()) == 0 and can_be_boot:
part = self._create_boot_partition(disk)
@ -635,52 +629,91 @@ class FilesystemController(SubiquityController):
else:
self.create_volgroup(spec)
def make_boot_disk(self, new_boot_disk):
boot_partition = None
def _mount_esp(self, part):
if part.fs() is None:
self.model.add_filesystem(part, 'fat32')
self.model.add_mount(part.fs(), '/boot/efi')
def remove_boot_disk(self, boot_disk):
if self.model.bootloader == Bootloader.BIOS:
install_dev = self.model.grub_install_device
if install_dev:
boot_partition = install_dev._potential_boot_partition()
boot_disk.grub_device = False
flag = 'bios_grub'
elif self.model.bootloader == Bootloader.UEFI:
mount = self.model._mount_for_path("/boot/efi")
if mount is not None:
boot_partition = mount.device.volume
flag = 'boot'
elif self.model.bootloader == Bootloader.PREP:
boot_partition = self.model.grub_install_device
if boot_partition is not None:
if boot_partition.preserve:
if self.model.bootloader == Bootloader.PREP:
boot_partition.wipe = None
elif self.model.bootloader == Bootloader.UEFI:
self.delete_mount(boot_partition.fs().mount())
else:
boot_disk = boot_partition.device
full = boot_disk.free_for_partitions == 0
self.delete_partition(boot_partition)
if full:
largest_part = max(
boot_disk.partitions(), key=lambda p: p.size)
largest_part.size += boot_partition.size
if new_boot_disk.free_for_partitions < boot_partition.size:
largest_part = max(
new_boot_disk.partitions(), key=lambda p: p.size)
largest_part.size -= (
boot_partition.size -
new_boot_disk.free_for_partitions)
if new_boot_disk._has_preexisting_partition():
flag = 'prep'
partitions = [p for p in boot_disk.partitions() if p.flag == flag]
remount = False
if boot_disk.preserve:
if self.model.bootloader == Bootloader.BIOS:
self.model.grub_install_device = new_boot_disk
elif self.model.bootloader == Bootloader.UEFI:
part = new_boot_disk._potential_boot_partition()
if part.fs() is None:
self.model.add_filesystem(part, 'fat32')
self.model.add_mount(part.fs(), '/boot/efi')
elif self.model.bootloader == Bootloader.PREP:
part = new_boot_disk._potential_boot_partition()
part.wipe = 'zero'
self.model.grub_install_device = part
return
for p in partitions:
p.grub_device = False
if self.model.bootloader == Bootloader.PREP:
p.wipe = None
elif self.model.bootloader == Bootloader.UEFI:
if p.fs():
if p.fs().mount():
self.delete_mount(p.fs().mount())
remount = True
if not p.fs().preserve and p.original_fstype():
self.delete_filesystem(p.fs())
self.model.add_filesystem(
p, p.original_fstype(), preserve=True)
else:
full = boot_disk.free_for_partitions == 0
tot_size = 0
for p in partitions:
tot_size += p.size
if p.fs() and p.fs().mount():
remount = True
self.delete_partition(p)
if full:
largest_part = max(
boot_disk.partitions(), key=lambda p: p.size)
largest_part.size += tot_size
if self.model.bootloader == Bootloader.UEFI and remount:
part = self.model._one(type='partition', grub_device=True)
if part:
self._mount_esp(part)
def add_boot_disk(self, new_boot_disk):
bootloader = self.model.bootloader
if bootloader == Bootloader.PREP:
for disk in self.model.all_disks():
if disk._is_boot_device():
self.remove_boot_disk(disk)
if new_boot_disk._has_preexisting_partition():
if bootloader == Bootloader.BIOS:
new_boot_disk.grub_device = True
elif bootloader == Bootloader.UEFI:
should_mount = self.model._mount_for_path('/boot/efi') is None
for p in new_boot_disk.partitions():
if p.flag == 'boot':
p.grub_device = True
if should_mount:
self._mount_esp(p)
should_mount = False
elif bootloader == Bootloader.PREP:
for p in new_boot_disk.partitions():
if p.flag == 'prep':
p.wipe = 'zero'
p.grub_device = True
else:
new_boot_disk.preserve = False
if bootloader == Bootloader.UEFI:
part_size = UEFI_GRUB_SIZE_BYTES
if UEFI_GRUB_SIZE_BYTES*2 >= new_boot_disk.size:
part_size = new_boot_disk.size // 2
elif bootloader == Bootloader.PREP:
part_size = PREP_GRUB_SIZE_BYTES
elif bootloader == Bootloader.BIOS:
part_size = BIOS_GRUB_SIZE_BYTES
if part_size > new_boot_disk.free_for_partitions:
largest_part = max(
new_boot_disk.partitions(), key=lambda p: p.size)
largest_part.size -= (
part_size - new_boot_disk.free_for_partitions)
self._create_boot_partition(new_boot_disk)
def guided_direct(self, disk):
@ -694,8 +727,8 @@ class FilesystemController(SubiquityController):
def guided_lvm(self, disk, lvm_options=None):
self.reformat(disk)
if DeviceAction.MAKE_BOOT in disk.supported_actions:
self.make_boot_disk(disk)
if DeviceAction.TOGGLE_BOOT in disk.supported_actions:
self.add_boot_disk(disk)
self.create_partition(
device=disk, spec=dict(
size=dehumanize_size('1G'),
@ -730,8 +763,4 @@ class FilesystemController(SubiquityController):
}
if 'swap' in rendered:
r['swap'] = rendered['swap']
if self.model.grub_install_device:
r['grub'] = {
'install_devices': [self.model.grub_install_device.id],
}
return r

View File

@ -20,7 +20,6 @@ from subiquity.controllers.filesystem import (
FilesystemController,
)
from subiquity.models.tests.test_filesystem import (
fake_up_blockdata,
make_disk,
make_model,
)
@ -77,40 +76,48 @@ class TestFilesystemController(unittest.TestCase):
a for a in controller.model._actions if a.type == 'dm_crypt']
self.assertEqual(dm_crypts, [])
def test_can_only_make_boot_once(self):
def test_can_only_add_boot_once(self):
# This is really testing model code but it's much easier to test with a
# controller around.
for bl in Bootloader:
controller, disk = make_controller_and_disk(bl)
if DeviceAction.MAKE_BOOT not in disk.supported_actions:
if DeviceAction.TOGGLE_BOOT not in disk.supported_actions:
continue
controller.make_boot_disk(disk)
controller.add_boot_disk(disk)
self.assertFalse(
disk._can_MAKE_BOOT,
"make_boot_disk(disk) did not make _can_MAKE_BOOT false with "
"bootloader {}".format(bl))
disk._can_TOGGLE_BOOT,
"add_boot_disk(disk) did not make _can_TOGGLE_BOOT false "
"with bootloader {}".format(bl))
def test_make_boot_disk_BIOS(self):
controller = make_controller(Bootloader.BIOS)
disk1 = make_disk(controller.model, preserve=False)
controller.add_boot_disk(disk1)
self.assertEqual(len(disk1.partitions()), 1)
self.assertEqual(disk1.partitions()[0].flag, "bios_grub")
self.assertTrue(disk1.grub_device)
controller.remove_boot_disk(disk1)
self.assertEqual(len(disk1.partitions()), 0)
self.assertFalse(disk1.grub_device)
disk2 = make_disk(controller.model, preserve=False)
disk2p1 = controller.model.add_partition(
disk2, size=disk2.free_for_partitions)
controller.make_boot_disk(disk1)
self.assertEqual(len(disk1.partitions()), 1)
self.assertEqual(disk1.partitions()[0].flag, "bios_grub")
self.assertEqual(controller.model.grub_install_device, disk1)
size_before = disk2p1.size
controller.make_boot_disk(disk2)
self.assertEqual(len(disk1.partitions()), 0)
controller.add_boot_disk(disk2)
self.assertEqual(len(disk2.partitions()), 2)
self.assertEqual(disk2.partitions()[1], disk2p1)
self.assertEqual(
disk2.partitions()[0].size + disk2p1.size, size_before)
self.assertEqual(disk2.partitions()[0].flag, "bios_grub")
self.assertEqual(controller.model.grub_install_device, disk2)
self.assertTrue(disk2.grub_device)
controller.remove_boot_disk(disk2)
self.assertEqual(disk2.partitions(), [disk2p1])
self.assertEqual(disk2p1.size, size_before)
self.assertFalse(disk2.grub_device)
def test_make_boot_disk_BIOS_existing(self):
controller = make_controller(Bootloader.BIOS)
@ -118,17 +125,24 @@ class TestFilesystemController(unittest.TestCase):
disk1p1 = controller.model.add_partition(
disk1, size=1 << 20, flag="bios_grub")
disk1p1.preserve = True
disk2 = make_disk(controller.model, preserve=False)
self.assertEqual(disk1.partitions(), [disk1p1])
self.assertEqual(controller.model.grub_install_device, None)
controller.make_boot_disk(disk1)
self.assertFalse(disk1.grub_device)
controller.add_boot_disk(disk1)
self.assertEqual(disk1.partitions(), [disk1p1])
self.assertEqual(controller.model.grub_install_device, disk1)
self.assertTrue(disk1.grub_device)
controller.remove_boot_disk(disk1)
self.assertEqual(disk1.partitions(), [disk1p1])
self.assertFalse(disk1.grub_device)
controller.make_boot_disk(disk2)
self.assertEqual(disk1.partitions(), [disk1p1])
self.assertEqual(controller.model.grub_install_device, disk2)
def assertIsMountedAtBootEFI(self, device):
efi_mnts = device._m._all(type="mount", path="/boot/efi")
self.assertEqual(len(efi_mnts), 1)
self.assertEqual(efi_mnts[0].device.volume, device)
def assertNotMounted(self, device):
if device.fs():
self.assertIs(device.fs().mount(), None)
def test_make_boot_disk_UEFI(self):
controller = make_controller(Bootloader.UEFI)
@ -137,25 +151,32 @@ class TestFilesystemController(unittest.TestCase):
disk2p1 = controller.model.add_partition(
disk2, size=disk2.free_for_partitions)
controller.make_boot_disk(disk1)
controller.add_boot_disk(disk1)
self.assertEqual(len(disk1.partitions()), 1)
self.assertEqual(disk1.partitions()[0].flag, "boot")
self.assertEqual(controller.model.grub_install_device, None)
efi_mnt = controller.model._mount_for_path("/boot/efi")
self.assertEqual(efi_mnt.device.volume, disk1.partitions()[0])
self.assertEqual(disk1.partitions()[0].fs().fstype, "fat32")
disk1esp = disk1.partitions()[0]
self.assertEqual(disk1esp.flag, "boot")
self.assertEqual(disk1esp.fs().fstype, "fat32")
self.assertTrue(disk1esp.grub_device)
self.assertIsMountedAtBootEFI(disk1esp)
size_before = disk2p1.size
controller.make_boot_disk(disk2)
self.assertEqual(len(disk1.partitions()), 0)
controller.add_boot_disk(disk2)
self.assertEqual(len(disk2.partitions()), 2)
self.assertEqual(disk2.partitions()[1], disk2p1)
self.assertEqual(
disk2.partitions()[0].size + disk2p1.size, size_before)
self.assertEqual(disk2.partitions()[0].flag, "boot")
self.assertEqual(controller.model.grub_install_device, None)
efi_mnt = controller.model._mount_for_path("/boot/efi")
self.assertEqual(efi_mnt.device.volume, disk2.partitions()[0])
disk2esp = disk2.partitions()[0]
self.assertEqual(disk2esp.size + disk2p1.size, size_before)
self.assertEqual(disk2esp.flag, "boot")
self.assertTrue(disk2esp.grub_device)
self.assertIsMountedAtBootEFI(disk1esp)
self.assertNotMounted(disk2esp)
controller.remove_boot_disk(disk1)
self.assertIsMountedAtBootEFI(disk2esp)
self.assertEqual(len(disk1.partitions()), 0)
controller.remove_boot_disk(disk2)
self.assertEqual(len(disk2.partitions()), 1)
self.assertEqual(disk2p1.size, size_before)
def test_make_boot_disk_UEFI_existing(self):
controller = make_controller(Bootloader.UEFI)
@ -166,21 +187,18 @@ class TestFilesystemController(unittest.TestCase):
disk2 = make_disk(controller.model, preserve=True)
self.assertEqual(disk1.partitions(), [disk1p1])
self.assertEqual(controller.model.grub_install_device, None)
efi_mnt = controller.model._mount_for_path("/boot/efi")
self.assertEqual(efi_mnt, None)
controller.make_boot_disk(disk1)
controller.add_boot_disk(disk1)
self.assertEqual(disk1.partitions(), [disk1p1])
self.assertEqual(controller.model.grub_install_device, None)
efi_mnt = controller.model._mount_for_path("/boot/efi")
self.assertEqual(efi_mnt.device.volume, disk1p1)
self.assertTrue(disk1p1.grub_device)
self.assertIsMountedAtBootEFI(disk1p1)
self.assertEqual(disk1p1.fs().fstype, "fat32")
controller.make_boot_disk(disk2)
controller.add_boot_disk(disk2)
self.assertEqual(disk1.partitions(), [disk1p1])
self.assertEqual(controller.model.grub_install_device, None)
efi_mnt = controller.model._mount_for_path("/boot/efi")
self.assertEqual(efi_mnt.device.volume, disk2.partitions()[0])
self.assertTrue(disk1p1.grub_device)
self.assertIsMountedAtBootEFI(disk1p1)
def test_make_boot_disk_PREP(self):
controller = make_controller(Bootloader.PREP)
@ -189,16 +207,14 @@ class TestFilesystemController(unittest.TestCase):
disk2p1 = controller.model.add_partition(
disk2, size=disk2.free_for_partitions)
controller.make_boot_disk(disk1)
controller.add_boot_disk(disk1)
self.assertEqual(len(disk1.partitions()), 1)
self.assertEqual(disk1.partitions()[0].flag, "prep")
self.assertEqual(disk1.partitions()[0].wipe, "zero")
self.assertEqual(
controller.model.grub_install_device,
disk1.partitions()[0])
self.assertTrue(disk1.partitions()[0].grub_device)
size_before = disk2p1.size
controller.make_boot_disk(disk2)
controller.add_boot_disk(disk2)
self.assertEqual(len(disk1.partitions()), 0)
self.assertEqual(len(disk2.partitions()), 2)
self.assertEqual(disk2.partitions()[1], disk2p1)
@ -206,9 +222,6 @@ class TestFilesystemController(unittest.TestCase):
disk2.partitions()[0].size + disk2p1.size, size_before)
self.assertEqual(disk2.partitions()[0].flag, "prep")
self.assertEqual(disk2.partitions()[0].wipe, "zero")
self.assertEqual(
controller.model.grub_install_device,
disk2.partitions()[0])
def test_make_boot_disk_PREP_existing(self):
controller = make_controller(Bootloader.PREP)
@ -216,24 +229,18 @@ class TestFilesystemController(unittest.TestCase):
disk1p1 = controller.model.add_partition(
disk1, size=8 << 20, flag="prep")
disk1p1.preserve = True
disk2 = make_disk(controller.model, preserve=False)
self.assertEqual(disk1.partitions(), [disk1p1])
self.assertEqual(controller.model.grub_install_device, None)
controller.make_boot_disk(disk1)
self.assertFalse(disk1p1.grub_device)
controller.add_boot_disk(disk1)
self.assertEqual(disk1.partitions(), [disk1p1])
self.assertEqual(controller.model.grub_install_device, disk1p1)
self.assertTrue(disk1p1.grub_device)
self.assertEqual(disk1p1.wipe, 'zero')
controller.make_boot_disk(disk2)
controller.remove_boot_disk(disk1)
self.assertEqual(disk1.partitions(), [disk1p1])
self.assertFalse(disk1p1.grub_device)
self.assertEqual(disk1p1.wipe, None)
self.assertEqual(
controller.model.grub_install_device, disk2.partitions()[0])
self.assertEqual(disk2.partitions()[0].flag, "prep")
self.assertEqual(
controller.model.grub_install_device,
disk2.partitions()[0])
def test_mounting_partition_makes_boot_disk(self):
controller = make_controller(Bootloader.UEFI)
@ -248,25 +255,3 @@ class TestFilesystemController(unittest.TestCase):
disk1, disk1p2, {'fstype': 'ext4', 'mount': '/'})
efi_mnt = controller.model._mount_for_path("/boot/efi")
self.assertEqual(efi_mnt.device.volume, disk1p1)
def test_autoinstall_grub_devices(self):
controller = make_controller(Bootloader.BIOS)
make_disk(controller.model)
fake_up_blockdata(controller.model)
controller.ai_data = {
'config': [{'type': 'disk', 'id': 'disk0'}],
'grub': {
'install_devices': ['disk0'],
},
}
controller.convert_autoinstall_config()
new_disk = controller.model._one(type="disk", id="disk0")
self.assertEqual(controller.model.grub_install_device, new_disk)
def test_make_autoinstall(self):
controller = make_controller(Bootloader.BIOS)
disk = make_disk(controller.model)
fake_up_blockdata(controller.model)
controller.guided_direct(disk)
ai_data = controller.make_autoinstall()
self.assertEqual(ai_data['grub']['install_devices'], [disk.id])

View File

@ -25,7 +25,6 @@ import os
import pathlib
import platform
from curtin.block import partition_kname
from curtin import storage_config
from curtin.util import human2bytes
@ -409,7 +408,7 @@ class DeviceAction(enum.Enum):
FORMAT = _("Format")
REMOVE = _("Remove from RAID/LVM")
DELETE = _("Delete")
MAKE_BOOT = _("Make Boot Device")
TOGGLE_BOOT = _("Make Boot Device")
def _generic_can_EDIT(obj):
@ -516,7 +515,7 @@ class _Formattable(ABC):
m = fs.mount()
if m:
r.append(_("mounted at {path}").format(path=m.path))
else:
elif getattr(self, 'flag', None) != "boot":
r.append(_("not mounted"))
elif fs.preserve:
if fs.mount() is None:
@ -777,32 +776,22 @@ class Disk(_Device):
def dasd(self):
return self._m._one(type='dasd', device_id=self.device_id)
def _potential_boot_partition(self):
if self._m.bootloader == Bootloader.NONE:
return None
if not self._partitions:
return None
if self._m.bootloader == Bootloader.BIOS:
if self._partitions[0].flag == "bios_grub":
return self._partitions[0]
else:
return None
flag = {
Bootloader.UEFI: "boot",
Bootloader.PREP: "prep",
}[self._m.bootloader]
for p in self._partitions:
# XXX should check not extended in the UEFI case too (until we fix
# that bug)
if p.flag == flag:
return p
return None
def _can_be_boot_disk(self):
if self._m.bootloader == Bootloader.BIOS and self.ptable == "msdos":
return True
bl = self._m.bootloader
if self._has_preexisting_partition():
if bl == Bootloader.BIOS:
if self.ptable == "msdos":
return True
else:
return self._partitions[0].flag == "bios_grub"
else:
flag = {Bootloader.UEFI: "boot", Bootloader.PREP: "prep"}[bl]
for p in self._partitions:
if p.flag == flag:
return True
return False
else:
return self._potential_boot_partition() is not None
return True
@property
def supported_actions(self):
@ -814,7 +803,7 @@ class Disk(_Device):
DeviceAction.REMOVE,
]
if self._m.bootloader != Bootloader.NONE:
actions.append(DeviceAction.MAKE_BOOT)
actions.append(DeviceAction.TOGGLE_BOOT)
return actions
_can_INFO = True
@ -843,24 +832,29 @@ class Disk(_Device):
self._constructed_device is None)
_can_REMOVE = property(_generic_can_REMOVE)
@property
def _can_MAKE_BOOT(self):
def _is_boot_device(self):
bl = self._m.bootloader
if bl == Bootloader.BIOS:
if self._m.grub_install_device is self:
return False
elif bl == Bootloader.UEFI:
m = self._m._mount_for_path('/boot/efi')
if m and m.device.volume.device is self:
return False
elif bl == Bootloader.PREP:
install_dev = self._m.grub_install_device
if install_dev is not None and install_dev.device is self:
return False
if self._has_preexisting_partition():
return self._can_be_boot_disk()
if bl == Bootloader.NONE:
return False
elif bl == Bootloader.BIOS:
return self.grub_device
elif bl in [Bootloader.PREP, Bootloader.UEFI]:
for p in self._partitions:
if p.grub_device:
return True
return False
@property
def _can_TOGGLE_BOOT(self):
if self._is_boot_device():
for disk in self._m.all_disks():
if disk is not self and disk._is_boot_device():
return True
return False
elif self._fs is not None or self._constructed_device is not None:
return False
else:
return self._fs is None and self._constructed_device is None
return self._can_be_boot_disk()
@property
def ok_for_raid(self):
@ -886,15 +880,31 @@ class Partition(_Formattable):
flag = attr.ib(default=None)
number = attr.ib(default=None)
preserve = attr.ib(default=False)
grub_device = attr.ib(default=False)
@property
def annotations(self):
r = super().annotations
if self.flag == "prep":
r.append("PReP")
if self.preserve:
if self.grub_device:
r.append("configured")
else:
r.append("unconfigured")
elif self.flag == "boot":
r.append("ESP")
if self.fs() and self.fs().mount():
r.append("primary ESP")
elif self.grub_device:
r.append("backup ESP")
else:
r.append("unused ESP")
elif self.flag == "bios_grub":
if self.preserve:
if self.device.grub_device:
r.append("configured")
else:
r.append("unconfigured")
r.append("bios_grub")
elif self.flag == "extended":
r.append("extended")
@ -919,7 +929,7 @@ class Partition(_Formattable):
return _("partition {}").format(self._number)
def available(self):
if self.flag in ['bios_grub', 'prep']:
if self.flag in ['bios_grub', 'prep'] or self.grub_device:
return False
if self._constructed_device is not None:
return False
@ -1278,8 +1288,8 @@ class FilesystemModel(object):
else:
self._orig_config = []
self._actions = []
self.grub_install_device = None
self.swap = None
self.grub = None
def _make_matchers(self, match):
matchers = []
@ -1545,16 +1555,8 @@ class FilesystemModel(object):
}
if self.swap is not None:
config['swap'] = self.swap
if self.grub_install_device:
dev = self.grub_install_device
if dev.type == "partition":
disk_kname = dev.device.path[5:] # chop off "/dev/"
devpath = "/dev/" + partition_kname(disk_kname, dev._number)
else:
devpath = dev.path
config['grub'] = {
'install_devices': [devpath],
}
if self.grub is not None:
config['grub'] = self.grub
return config
def load_probe_data(self, probe_data):
@ -1608,12 +1610,11 @@ class FilesystemModel(object):
return self._all(type='lvm_volgroup')
def _remove(self, obj):
if obj is self.grub_install_device:
self.grub_install_device = None
_remove_backlinks(obj)
self._actions.remove(obj)
def add_partition(self, device, size, flag="", wipe=None):
def add_partition(self, device, size, flag="", wipe=None,
grub_device=None):
if size > device.free_for_partitions:
raise Exception("%s > %s", size, device.free_for_partitions)
real_size = align_up(size)
@ -1621,7 +1622,8 @@ class FilesystemModel(object):
if device._fs is not None:
raise Exception("%s is already formatted" % (device.label,))
p = Partition(
m=self, device=device, size=real_size, flag=flag, wipe=wipe)
m=self, device=device, size=real_size, flag=flag, wipe=wipe,
grub_device=grub_device)
if flag in ("boot", "bios_grub", "prep"):
device._partitions.insert(0, device._partitions.pop())
device.ptable = device.ptable_for_new_partition()
@ -1725,10 +1727,16 @@ class FilesystemModel(object):
# s390x has no such thing
if self.bootloader == Bootloader.NONE:
return False
elif self.bootloader in [Bootloader.BIOS, Bootloader.PREP]:
return self.grub_install_device is None
elif self.bootloader == Bootloader.BIOS:
return self._one(type='disk', grub_device=True) is None
elif self.bootloader == Bootloader.UEFI:
return self._mount_for_path('/boot/efi') is None
for esp in self._all(type='partition', grub_device=True):
if esp.fs() and esp.fs().mount():
if esp.fs().mount().path == '/boot/efi':
return False
return True
elif self.bootloader == Bootloader.PREP:
return self._one(type='partition', grub_device=True) is None
else:
raise AssertionError(
"unknown bootloader type {}".format(self.bootloader))

View File

@ -216,17 +216,50 @@ class TestFilesystemModel(unittest.TestCase):
self.assertEqual(disk.annotations, [])
def test_partition_annotations(self):
model, disk = make_model_and_disk()
part = model.add_partition(disk, size=disk.free_for_partitions)
model = make_model()
part = make_partition(model)
self.assertEqual(part.annotations, ['new'])
part.preserve = True
self.assertEqual(part.annotations, ['existing'])
part.flag = "boot"
self.assertEqual(part.annotations, ['existing', 'ESP'])
part.flag = "prep"
self.assertEqual(part.annotations, ['existing', 'PReP'])
part.flag = "bios_grub"
self.assertEqual(part.annotations, ['existing', 'bios_grub'])
model = make_model()
part = make_partition(model, flag="bios_grub")
self.assertEqual(
part.annotations, ['new', 'bios_grub'])
part.preserve = True
self.assertEqual(
part.annotations, ['existing', 'unconfigured', 'bios_grub'])
part.device.grub_device = True
self.assertEqual(
part.annotations, ['existing', 'configured', 'bios_grub'])
model = make_model()
part = make_partition(model, flag="boot", grub_device=True)
self.assertEqual(part.annotations, ['new', 'backup ESP'])
fs = model.add_filesystem(part, fstype="fat32")
model.add_mount(fs, "/boot/efi")
self.assertEqual(part.annotations, ['new', 'primary ESP'])
model = make_model()
part = make_partition(model, flag="boot", preserve=True)
self.assertEqual(part.annotations, ['existing', 'unused ESP'])
part.grub_device = True
self.assertEqual(part.annotations, ['existing', 'backup ESP'])
fs = model.add_filesystem(part, fstype="fat32")
model.add_mount(fs, "/boot/efi")
self.assertEqual(part.annotations, ['existing', 'primary ESP'])
model = make_model()
part = make_partition(model, flag="prep", grub_device=True)
self.assertEqual(part.annotations, ['new', 'PReP'])
model = make_model()
part = make_partition(model, flag="prep", preserve=True)
self.assertEqual(
part.annotations, ['existing', 'PReP', 'unconfigured'])
part.grub_device = True
self.assertEqual(
part.annotations, ['existing', 'PReP', 'configured'])
def test_vg_default_annotations(self):
model, disk = make_model_and_disk()
@ -443,81 +476,81 @@ class TestFilesystemModel(unittest.TestCase):
model, disk = make_model_and_disk()
self.assertActionNotSupported(disk, DeviceAction.DELETE)
def test_disk_action_MAKE_BOOT_NONE(self):
def test_disk_action_TOGGLE_BOOT_NONE(self):
model, disk = make_model_and_disk(Bootloader.NONE)
self.assertActionNotSupported(disk, DeviceAction.MAKE_BOOT)
self.assertActionNotSupported(disk, DeviceAction.TOGGLE_BOOT)
def test_disk_action_MAKE_BOOT_BIOS(self):
def test_disk_action_TOGGLE_BOOT_BIOS(self):
model = make_model(Bootloader.BIOS)
# Disks with msdos partition tables can always be the BIOS boot disk.
dos_disk = make_disk(model, ptable='msdos', preserve=True)
self.assertActionPossible(dos_disk, DeviceAction.MAKE_BOOT)
self.assertActionPossible(dos_disk, DeviceAction.TOGGLE_BOOT)
# Even if they have existing partitions
make_partition(
model, dos_disk, size=dos_disk.free_for_partitions, preserve=True)
self.assertActionPossible(dos_disk, DeviceAction.MAKE_BOOT)
self.assertActionPossible(dos_disk, DeviceAction.TOGGLE_BOOT)
# (we never create dos partition tables so no need to test
# preserve=False case).
# GPT disks with new partition tables can always be the BIOS boot disk
gpt_disk = make_disk(model, ptable='gpt', preserve=False)
self.assertActionPossible(gpt_disk, DeviceAction.MAKE_BOOT)
self.assertActionPossible(gpt_disk, DeviceAction.TOGGLE_BOOT)
# Even if they are filled with partitions (we resize partitions to fit)
make_partition(model, gpt_disk, size=dos_disk.free_for_partitions)
self.assertActionPossible(gpt_disk, DeviceAction.MAKE_BOOT)
self.assertActionPossible(gpt_disk, DeviceAction.TOGGLE_BOOT)
# GPT disks with existing partition tables but no partitions can be the
# BIOS boot disk (in general we ignore existing empty partition tables)
gpt_disk2 = make_disk(model, ptable='gpt', preserve=True)
self.assertActionPossible(gpt_disk2, DeviceAction.MAKE_BOOT)
self.assertActionPossible(gpt_disk2, DeviceAction.TOGGLE_BOOT)
# If there is an existing *partition* though, it cannot be the boot
# disk
make_partition(model, gpt_disk2, preserve=True)
self.assertActionNotPossible(gpt_disk2, DeviceAction.MAKE_BOOT)
self.assertActionNotPossible(gpt_disk2, DeviceAction.TOGGLE_BOOT)
# Unless there is already a bios_grub partition we can reuse
gpt_disk3 = make_disk(model, ptable='gpt', preserve=True)
make_partition(
model, gpt_disk3, flag="bios_grub", preserve=True)
make_partition(
model, gpt_disk3, preserve=True)
self.assertActionPossible(gpt_disk3, DeviceAction.MAKE_BOOT)
self.assertActionPossible(gpt_disk3, DeviceAction.TOGGLE_BOOT)
# Edge case city: the bios_grub partition has to be first
gpt_disk4 = make_disk(model, ptable='gpt', preserve=True)
make_partition(
model, gpt_disk4, preserve=True)
make_partition(
model, gpt_disk4, flag="bios_grub", preserve=True)
self.assertActionNotPossible(gpt_disk4, DeviceAction.MAKE_BOOT)
self.assertActionNotPossible(gpt_disk4, DeviceAction.TOGGLE_BOOT)
def _test_MAKE_BOOT_boot_partition(self, bl, flag):
# The logic for when MAKE_BOOT is enabled for both UEFI and PREP
def _test_TOGGLE_BOOT_boot_partition(self, bl, flag):
# The logic for when TOGGLE_BOOT is enabled for both UEFI and PREP
# bootloaders turns out to be the same, modulo the special flag that
# has to be present on a partition.
model = make_model(bl)
# A disk with a new partition table can always be the UEFI/PREP boot
# disk.
new_disk = make_disk(model, preserve=False)
self.assertActionPossible(new_disk, DeviceAction.MAKE_BOOT)
self.assertActionPossible(new_disk, DeviceAction.TOGGLE_BOOT)
# Even if they are filled with partitions (we resize partitions to fit)
make_partition(model, new_disk, size=new_disk.free_for_partitions)
self.assertActionPossible(new_disk, DeviceAction.MAKE_BOOT)
self.assertActionPossible(new_disk, DeviceAction.TOGGLE_BOOT)
# A disk with an existing but empty partitions can also be the
# UEFI/PREP boot disk.
old_disk = make_disk(model, preserve=True)
self.assertActionPossible(old_disk, DeviceAction.MAKE_BOOT)
self.assertActionPossible(old_disk, DeviceAction.TOGGLE_BOOT)
# If there is an existing partition though, it cannot.
make_partition(model, old_disk, preserve=True)
self.assertActionNotPossible(old_disk, DeviceAction.MAKE_BOOT)
self.assertActionNotPossible(old_disk, DeviceAction.TOGGLE_BOOT)
# If there is an existing ESP/PReP partition though, fine!
make_partition(model, old_disk, flag=flag, preserve=True)
self.assertActionPossible(old_disk, DeviceAction.MAKE_BOOT)
self.assertActionPossible(old_disk, DeviceAction.TOGGLE_BOOT)
def test_disk_action_MAKE_BOOT_UEFI(self):
self._test_MAKE_BOOT_boot_partition(Bootloader.UEFI, "boot")
def test_disk_action_TOGGLE_BOOT_UEFI(self):
self._test_TOGGLE_BOOT_boot_partition(Bootloader.UEFI, "boot")
def test_disk_action_MAKE_BOOT_PREP(self):
self._test_MAKE_BOOT_boot_partition(Bootloader.PREP, "prep")
def test_disk_action_TOGGLE_BOOT_PREP(self):
self._test_TOGGLE_BOOT_boot_partition(Bootloader.PREP, "prep")
def test_partition_action_INFO(self):
model, part = make_model_and_partition()
@ -578,9 +611,9 @@ class TestFilesystemModel(unittest.TestCase):
disk2p1 = make_partition(model, disk2, preserve=True)
self.assertActionNotPossible(disk2p1, DeviceAction.DELETE)
def test_partition_action_MAKE_BOOT(self):
def test_partition_action_TOGGLE_BOOT(self):
model, part = make_model_and_partition()
self.assertActionNotSupported(part, DeviceAction.MAKE_BOOT)
self.assertActionNotSupported(part, DeviceAction.TOGGLE_BOOT)
def test_raid_action_INFO(self):
model, raid = make_model_and_raid()
@ -684,9 +717,9 @@ class TestFilesystemModel(unittest.TestCase):
model.add_volgroup('vg0', {raid2})
self.assertActionNotPossible(raid2, DeviceAction.DELETE)
def test_raid_action_MAKE_BOOT(self):
def test_raid_action_TOGGLE_BOOT(self):
model, raid = make_model_and_raid()
self.assertActionNotSupported(raid, DeviceAction.MAKE_BOOT)
self.assertActionNotSupported(raid, DeviceAction.TOGGLE_BOOT)
def test_vg_action_INFO(self):
model, vg = make_model_and_vg()
@ -741,9 +774,9 @@ class TestFilesystemModel(unittest.TestCase):
model.add_mount(fs, '/')
self.assertActionNotPossible(vg, DeviceAction.DELETE)
def test_vg_action_MAKE_BOOT(self):
def test_vg_action_TOGGLE_BOOT(self):
model, vg = make_model_and_vg()
self.assertActionNotSupported(vg, DeviceAction.MAKE_BOOT)
self.assertActionNotSupported(vg, DeviceAction.TOGGLE_BOOT)
def test_lv_action_INFO(self):
model, lv = make_model_and_lv()
@ -785,9 +818,9 @@ class TestFilesystemModel(unittest.TestCase):
lv2.preserve = lv2.volgroup.preserve = True
self.assertActionNotPossible(lv2, DeviceAction.DELETE)
def test_lv_action_MAKE_BOOT(self):
def test_lv_action_TOGGLE_BOOT(self):
model, lv = make_model_and_lv()
self.assertActionNotSupported(lv, DeviceAction.MAKE_BOOT)
self.assertActionNotSupported(lv, DeviceAction.TOGGLE_BOOT)
def fake_up_blockdata(model):

View File

@ -61,6 +61,7 @@ from subiquitycore.ui.utils import (
from subiquitycore.view import BaseView
from subiquity.models.filesystem import (
Bootloader,
DeviceAction,
humanize_size,
)
@ -282,8 +283,11 @@ class DeviceList(WidgetWrap):
disk._constructed_device = None
self.parent.refresh_model_inputs()
def _disk_MAKE_BOOT(self, disk):
self.parent.controller.make_boot_disk(disk)
def _disk_TOGGLE_BOOT(self, disk):
if disk._is_boot_device():
self.parent.controller.remove_boot_disk(disk)
else:
self.parent.controller.add_boot_disk(disk)
self.parent.refresh_model_inputs()
_partition_EDIT = _stretchy_shower(
@ -311,16 +315,33 @@ class DeviceList(WidgetWrap):
log.debug('_action %s %s', action, device.id)
meth(device)
def _label_REMOVE(self, action, device):
cd = device.constructed_device()
if cd:
return _("Remove from {}").format(cd.desc())
else:
return _(action.value)
def _label_PARTITION(self, action, device):
return _("Add {} Partition").format(
device.ptable_for_new_partition().upper())
def _label_TOGGLE_BOOT(self, action, device):
if device._is_boot_device():
return _("Stop Using As Boot Device")
else:
if self.parent.model.bootloader != Bootloader.PREP:
for other in self.parent.model.all_disks():
if other._is_boot_device():
return _("Add As Another Boot Device")
return _("Use As Boot Device")
def _action_menu_for_device(self, device):
device_actions = []
for action in device.supported_actions:
label = _(action.value)
if action == DeviceAction.REMOVE and device.constructed_device():
cd = device.constructed_device()
label = _("Remove from {}").format(cd.desc())
if action == DeviceAction.PARTITION:
label = _("Add {} Partition").format(
device.ptable_for_new_partition().upper())
label_meth = getattr(
self, '_label_{}'.format(action.name), lambda a, d: _(a.value))
label = label_meth(action, device)
enabled, whynot = device.action_possible(action)
if whynot:
assert not enabled

View File

@ -35,6 +35,7 @@ from subiquitycore.ui.interactive import StringEditor
from subiquitycore.ui.selector import Option, Selector
from subiquitycore.ui.container import Pile
from subiquitycore.ui.stretchy import Stretchy
from subiquitycore.ui.utils import rewrap
from subiquity.models.filesystem import (
align_up,
@ -270,34 +271,65 @@ class PartitionForm(Form):
return r
bios_grub_partition_description = _(
"Required bootloader partition\n"
"\n"
"GRUB will be installed onto the target disk's MBR.\n"
"\n"
"However, on a disk with a GPT partition table, there is not enough space "
"after the MBR for GRUB to store its second-stage core.img, so a small "
"unformatted partition is needed at the start of the disk. It will not "
"contain a filesystem and will not be mounted, and cannot be edited here.")
bios_grub_partition_description = _("""\
Bootloader partition
boot_partition_description = _(
"Required bootloader partition\n"
"\n"
'This is the ESP / "EFI system partition" required by UEFI. Grub will be '
'installed onto this partition, which must be formatted as fat32.')
{middle}
boot_partition_description_size = _(
' The only aspect of this partition that can be edited is the size.')
However, on a disk with a GPT partition table, there is not enough
space after the MBR for GRUB to store its second-stage core.img, so a
small unformatted partition is needed at the start of the disk. It
will not contain a filesystem and will not be mounted, and cannot be
edited here.
""")
boot_partition_description_reformat = _(
' You can choose whether to use the existing filesystem on this '
'partition or reformat it.')
unconfigured_bios_grub_partition_middle = _("""\
If this disk is selected as a boot device, GRUB will be installed onto
the target disk's MBR.""")
prep_partition_description = _(
"Required bootloader partition\n"
"\n"
'This is the PReP partion which is required on POWER. Grub will be '
'installed onto this partition.')
configured_bios_grub_partition_middle = _("""\
As this disk has been selected as a boot device, GRUB will be
installed onto the target disk's MBR.""")
unconfigured_boot_partition_description = _("""\
Bootloader partition
This is an ESP / "EFI system partition" as required by UEFI. If this
disk is selected as a boot device, Grub will be installed onto this
partition, which must be formatted as fat32.
""")
configured_boot_partition_description = _("""\
Bootloader partition
This is an ESP / "EFI system partition" as required by UEFI. As this
disk has been selected as a boot device, Grub will be installed onto
this partition, which must be formatted as fat32.
""")
boot_partition_description_size = _("""\
The only aspect of this partition that can be edited is the size.
""")
boot_partition_description_reformat = _("""\
You can choose whether to use the existing filesystem on this
partition or reformat it.
""")
unconfigured_prep_partition_description = _("""\
Required bootloader partition
This is the PReP partion which is required on POWER. If this disk is
selected as a boot device, Grub will be installed onto this partition.
""")
configured_prep_partition_description = _("""\
Required bootloader partition
This is the PReP partion which is required on POWER. As this disk has
been selected as a boot device, Grub will be installed onto this
partition.
""")
def initial_data_for_fs(fs):
@ -335,9 +367,11 @@ class PartitionStretchy(Stretchy):
else:
lvm_names = None
if self.partition:
if self.partition.flag in ["bios_grub", "prep"]:
if partition.flag in ["bios_grub", "prep"]:
label = None
initial['mount'] = None
elif partition.flag == "boot" and not partition.grub_device:
label = None
else:
label = _("Save")
initial['size'] = humanize_size(self.partition.size)
@ -397,6 +431,8 @@ class PartitionStretchy(Stretchy):
self.form.fstype.widget.index = 0
else:
self.form.fstype.widget.index = 2
if not self.partition.grub_device:
self.form.fstype.enabled = False
self.form.mount.enabled = False
else:
opts = [Option(("fat32", True))]
@ -419,24 +455,37 @@ class PartitionStretchy(Stretchy):
focus_index = 0
if partition is not None:
if self.partition.flag == "boot":
desc = boot_partition_description
if self.partition.preserve:
desc += boot_partition_description_reformat
if self.partition.grub_device:
desc = _(configured_boot_partition_description)
if self.partition.preserve:
desc += _(boot_partition_description_reformat)
else:
desc += _(boot_partition_description_size)
else:
desc += boot_partition_description_size
focus_index = 2
desc = _(unconfigured_boot_partition_description)
rows.extend([
Text(_(desc)),
Text(rewrap(desc)),
Text(""),
])
elif self.partition.flag == "bios_grub":
if self.partition.device.grub_device:
middle = _(configured_bios_grub_partition_middle)
else:
middle = _(unconfigured_bios_grub_partition_middle)
desc = _(bios_grub_partition_description).format(middle=middle)
rows.extend([
Text(_(bios_grub_partition_description)),
Text(rewrap(desc)),
Text(""),
])
focus_index = 2
elif self.partition.flag == "prep":
if self.partition.grub_device:
desc = _(configured_prep_partition_description)
else:
desc = _(unconfigured_prep_partition_description)
rows.extend([
Text(_(prep_partition_description)),
Text(rewrap(desc)),
Text(""),
])
focus_index = 2

View File

@ -24,7 +24,6 @@ class FilesystemViewTests(unittest.TestCase):
controller.ui = mock.Mock()
model.bootloader = Bootloader.NONE
model.all_devices.return_value = devices
model.grub_install_device = None
return FilesystemView(model, controller)
def test_simple(self):

View File

@ -196,14 +196,13 @@ class PartitionViewTests(unittest.TestCase):
disk.preserve = partition.preserve = fs.preserve = True
view, stretchy = make_partition_view(model, disk, partition)
self.assertTrue(stretchy.form.fstype.enabled)
self.assertFalse(stretchy.form.fstype.enabled)
self.assertEqual(stretchy.form.fstype.value, None)
self.assertFalse(stretchy.form.mount.enabled)
self.assertEqual(stretchy.form.mount.value, None)
view_helpers.click(stretchy.form.done_btn.base_widget)
expected_data = {
'fstype': None,
'mount': None,
'use_swap': False,
}
@ -215,6 +214,7 @@ class PartitionViewTests(unittest.TestCase):
partition = model.add_partition(disk, 512*(2**20), "boot")
fs = model.add_filesystem(partition, "fat32")
model._orig_config = model._render_actions()
partition.grub_device = True
disk.preserve = partition.preserve = fs.preserve = True
model.add_mount(fs, '/boot/efi')
view, stretchy = make_partition_view(model, disk, partition)