filesystem: add new guided API GET

This commit is contained in:
Dan Bungert 2022-06-27 17:57:33 -06:00
parent 1916f08342
commit 2f12610290
4 changed files with 189 additions and 2 deletions

View File

@ -32,7 +32,9 @@ from subiquity.common.types import (
Disk,
ErrorReportRef,
GuidedChoice,
GuidedChoiceV2,
GuidedStorageResponse,
GuidedStorageResponseV2,
KeyboardSetting,
KeyboardSetup,
IdentityData,
@ -261,6 +263,11 @@ class API:
def GET() -> StorageResponseV2: ...
def POST() -> StorageResponseV2: ...
class guided:
def GET() -> GuidedStorageResponseV2: ...
def POST(data: Payload[GuidedChoiceV2]) \
-> GuidedStorageResponseV2: ...
class reset:
def POST() -> StorageResponseV2: ...

View File

@ -338,6 +338,56 @@ class GuidedResizeValues:
maximum: int
@attr.s(auto_attribs=True)
class GuidedStorageTargetReformat:
disk_id: str
@attr.s(auto_attribs=True)
class GuidedStorageTargetResize:
disk_id: str
partition_number: int
new_size: int
minimum: Optional[int]
recommended: Optional[int]
maximum: Optional[int]
@staticmethod
def from_recommendations(part, resize_vals):
return GuidedStorageTargetResize(
disk_id=part.device.id,
partition_number=part.number,
new_size=resize_vals.recommended,
minimum=resize_vals.minimum,
recommended=resize_vals.recommended,
maximum=resize_vals.maximum,
)
@attr.s(auto_attribs=True)
class GuidedStorageTargetUseGap:
disk_id: str
gap: Gap
GuidedStorageTarget = Union[GuidedStorageTargetReformat,
GuidedStorageTargetResize,
GuidedStorageTargetUseGap]
@attr.s(auto_attribs=True)
class GuidedChoiceV2:
target: GuidedStorageTarget
use_lvm: bool = False
password: Optional[str] = attr.ib(default=None, repr=False)
@attr.s(auto_attribs=True)
class GuidedStorageResponseV2:
configured: Optional[GuidedChoiceV2] = None
possible: List[GuidedStorageTarget] = attr.Factory(list)
@attr.s(auto_attribs=True)
class AddPartitionV2:
disk_id: str

View File

@ -50,7 +50,12 @@ from subiquity.common.types import (
Bootloader,
Disk,
GuidedChoice,
GuidedChoiceV2,
GuidedStorageResponse,
GuidedStorageResponseV2,
GuidedStorageTargetReformat,
GuidedStorageTargetResize,
GuidedStorageTargetUseGap,
ModifyPartitionV2,
ProbeStatus,
ReformatDisk,
@ -322,7 +327,9 @@ class FilesystemController(SubiquityController, FilesystemManipulator):
def calculate_suggested_install_min(self):
source_min = self.app.base_model.source.current.size
return sizes.calculate_suggested_install_min(source_min)
align = max((pa.part_align
for pa in self.model._partition_alignment_data.values()))
return sizes.calculate_suggested_install_min(source_min, align)
async def v2_GET(self) -> StorageResponseV2:
disks = self.model._all(type='disk')
@ -348,6 +355,43 @@ class FilesystemController(SubiquityController, FilesystemManipulator):
self.guided(data)
return await self.v2_GET()
async def v2_guided_GET(self) -> GuidedStorageResponseV2:
"""Acquire a list of possible guided storage configuration scenarios.
Results are sorted by the size of the space potentially available to
the install."""
possible = []
install_min = self.calculate_suggested_install_min()
for disk in self.get_guided_disks(with_reformatting=True):
possible.append(GuidedStorageTargetReformat(disk_id=disk.id))
for disk in self.get_guided_disks(with_reformatting=False):
gap = gaps.largest_gap(disk)
# FIXME this gap size check can mean that a disk that is
# accepted is ignored by use_gap
if gap is not None and gap.size > install_min:
possible.append(GuidedStorageTargetUseGap(
disk_id=disk.id, gap=gap))
for disk in self.get_guided_disks(check_boot=False):
part_align = disk.alignment_data().part_align
for partition in disk.partitions():
vals = sizes.calculate_guided_resize(
partition.estimated_min_size, partition.size,
install_min, part_align=part_align)
if vals is not None:
possible.append(
GuidedStorageTargetResize.from_recommendations(
partition, vals))
# FIXME sort at the end
return GuidedStorageResponseV2(possible=possible)
async def v2_guided_POST(self, data: GuidedChoiceV2) \
-> GuidedStorageResponseV2:
log.debug(data)
return await self.v2_guided_GET()
async def v2_reformat_disk_POST(self, data: ReformatDisk) \
-> StorageResponseV2:
self.reformat(self.model._one(id=data.disk_id), data.ptable)

View File

@ -20,10 +20,17 @@ from parameterized import parameterized
from subiquity.server.controllers.filesystem import FilesystemController
from subiquitycore.tests.mocks import make_app
from subiquity.common.types import Bootloader
from subiquity.common.filesystem import gaps
from subiquity.common.types import (
Bootloader,
GuidedStorageTargetReformat,
GuidedStorageTargetResize,
GuidedStorageTargetUseGap,
)
from subiquity.models.tests.test_filesystem import (
make_disk,
make_model,
make_partition,
)
@ -127,3 +134,82 @@ class TestLayout(TestCase):
def test_bad_modes(self, mode):
with self.assertRaises(ValueError):
self.fsc.validate_layout_mode(mode)
bootloaders = [(bl, ) for bl in list(Bootloader) if bl != Bootloader.NONE]
class TestGuidedV2(IsolatedAsyncioTestCase):
def _setup(self, bootloader):
self.app = make_app()
self.app.opts.bootloader = bootloader.value
self.fsc = FilesystemController(app=self.app)
self.fsc.calculate_suggested_install_min = mock.Mock()
self.fsc.calculate_suggested_install_min.return_value = 1 << 30
self.fsc.model = self.model = make_model(bootloader)
self.model.storage_version = 2
self.fs_probe = {}
self.fsc.model._probe_data = {'filesystem': self.fs_probe}
@parameterized.expand(bootloaders)
async def test_blank_disk(self, bootloader):
self._setup(bootloader)
d = make_disk(self.model)
expected = [
GuidedStorageTargetReformat(disk_id=d.id),
GuidedStorageTargetUseGap(disk_id=d.id, gap=gaps.largest_gap(d)),
]
resp = await self.fsc.v2_guided_GET()
self.assertEqual(expected, resp.possible)
@parameterized.expand(bootloaders)
async def test_used_half_disk(self, bootloader):
self._setup(bootloader)
d = make_disk(self.model)
p1 = make_partition(self.model, d)
self.fs_probe[p1._path()] = {'ESTIMATED_MIN_SIZE': 1 << 20}
resp = await self.fsc.v2_guided_GET()
[reformat, use_gap, resize] = resp.possible
self.assertEqual(GuidedStorageTargetReformat(disk_id=d.id), reformat)
self.assertEqual(
GuidedStorageTargetUseGap(disk_id=d.id, gap=gaps.largest_gap(d)),
use_gap)
self.assertEqual(d.id, resize.disk_id)
self.assertEqual(p1.number, resize.partition_number)
self.assertTrue(isinstance(resize, GuidedStorageTargetResize))
@parameterized.expand(bootloaders)
async def test_used_full_disk(self, bootloader):
self._setup(bootloader)
d = make_disk(self.model)
p1 = make_partition(self.model, d, size=gaps.largest_gap_size(d))
self.fs_probe[p1._path()] = {'ESTIMATED_MIN_SIZE': 1 << 20}
resp = await self.fsc.v2_guided_GET()
[reformat, resize] = resp.possible
self.assertEqual(GuidedStorageTargetReformat(disk_id=d.id), reformat)
self.assertEqual(d.id, resize.disk_id)
self.assertEqual(p1.number, resize.partition_number)
self.assertTrue(isinstance(resize, GuidedStorageTargetResize))
@parameterized.expand(bootloaders)
async def test_weighted_split(self, bootloader):
self._setup(bootloader)
d = make_disk(self.model, size=250 << 30)
p1 = make_partition(self.model, d, size=240 << 30)
# add a second, filler, partition so that there is no use_gap result
make_partition(self.model, d, size=9 << 30)
self.fs_probe[p1._path()] = {'ESTIMATED_MIN_SIZE': 40 << 30}
self.fsc.calculate_suggested_install_min.return_value = 10 << 30
resp = await self.fsc.v2_guided_GET()
[reformat, resize] = resp.possible
self.assertEqual(GuidedStorageTargetReformat(disk_id=d.id), reformat)
self.assertEqual(
GuidedStorageTargetResize(
disk_id=d.id,
partition_number=p1.number,
new_size=200 << 30,
minimum=50 << 30,
recommended=200 << 30,
maximum=230 << 30,
),
resize)