sizes: calculations for install_min and resize

Add calculations for a suggested minimum install size that contains
padding for swap, updates, boot.
Add calculations for helping resize, to examine the space available and
make suggestions about what the existing partition might be resized to.
This commit is contained in:
Dan Bungert 2022-06-27 10:44:48 -06:00
parent 971ba36704
commit 58c1ee834e
5 changed files with 154 additions and 1 deletions

View File

@ -13,11 +13,19 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import math
import attr
from curtin import swap
from subiquity.models.filesystem import (
align_up,
align_down,
MiB,
GiB,
)
from subiquity.common.types import GuidedResizeValues
BIOS_GRUB_SIZE_BYTES = 1 * MiB
@ -75,3 +83,66 @@ def get_efi_size(available_space):
def get_bootfs_size(available_space):
all_factors = (uefi_scale, bootfs_scale, rootfs_scale)
return scale_partitions(all_factors, available_space)[1]
# Calculation of guided resize values is primarly focues on finding a suggested
# midpoint - what will we resize down to while leaving some room for the
# existing partition and the new install?
# 1) Obtain the suggested size for the install
# (see calculate_suggested_install_min)
# 2) Look at the output from the resize tool to see the theoretical minimum
# size of the partition we might resize, and pad it a bit (2 GiB or 25%)
# 3) Subtract the two minimum suggested sizes to obtain the space that we can
# decide to keep with the existing partition, or allocate to the new install
# 4) Assume that the installs will grow proportionally to their minimum sizes,
# and split according to the ratio of the minimum sizes
def calculate_guided_resize(part_min: int, part_size: int, install_min: int,
part_align: int = MiB) -> GuidedResizeValues:
if part_min < 0:
return None
other_room_to_grow = max(2 * GiB, math.ceil(.25 * part_min))
padded_other_min = part_min + other_room_to_grow
other_min = min(align_up(padded_other_min, part_align), part_size)
plausible_free_space = part_size - other_min
if plausible_free_space < install_min:
return None
other_max = align_down(part_size - install_min, part_align)
resize_window = other_max - other_min
ratio = other_min / (other_min + install_min)
raw_recommended = math.ceil(resize_window * ratio) + other_min
recommended = align_up(raw_recommended, part_align)
return GuidedResizeValues(
minimum=other_min, recommended=recommended, maximum=other_max)
# Factors for suggested minimum install size:
# 1) Source minimum - The minimum reported as part of source selection. This
# is absolute bare minimum information to get bits on the disk and doesnt
# factor in filesystem overhead. Obtained from the size value of the
# chosen source as found at /casper/install-sources.yaml.
# 2) Room for boot - we employ a scaling system to help select the recommended
# size of a dedicated /boot and/or efi system partition (see above). If
# /boot is not actually a separate partition, this space needs to be
# accounted for as part of the planned rootfs size. The files that would
# otherwise be stored in a dedicated UEFI partition are presumed to be
# negligible on non-UEFI systems. As /boot itself uses a scaling sizing
# system, use the bootfs_scale.maximum value for the purposes of finding
# minimum suggested size. The maximum value is to be used instead of the
# minimum as the boot scaling system will also make adjustments, and we
# dont want to short change the other calculations.
# 3) Room to grow - while meaningful work can sometimes be possible on a full
# disk, its not the sort of thing to suggest in a guided install.
# Suggest for room to grow max(2GiB, 80% of source minimum).
# 4) Swap - Ubuntu policy recommends swap, either in the form of a partition or
# a swapfile. Curtin has an existing swap size recommendation at
# curtin.swap.suggested_swapsize().
def calculate_suggested_install_min(source_min: int,
part_align: int = MiB) -> int:
room_for_boot = bootfs_scale.maximum
room_to_grow = max(2 * GiB, math.ceil(.8 * source_min))
room_for_swap = swap.suggested_swapsize()
total = source_min + room_for_boot + room_to_grow + room_for_swap
return align_up(total, part_align)

View File

@ -14,15 +14,19 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import unittest
from unittest import mock
from subiquity.common.filesystem.sizes import (
bootfs_scale,
calculate_guided_resize,
calculate_suggested_install_min,
get_efi_size,
get_bootfs_size,
PartitionScaleFactors,
scale_partitions,
uefi_scale,
)
from subiquity.common.types import GuidedResizeValues
class TestPartitionSizeScaling(unittest.TestCase):
@ -74,3 +78,68 @@ class TestPartitionSizeScaling(unittest.TestCase):
bootfs_size = get_bootfs_size(disk_size)
self.assertTrue(bootfs_scale.maximum > bootfs_size)
self.assertTrue(bootfs_size > bootfs_scale.minimum)
class TestCalculateGuidedResize(unittest.TestCase):
def test_ignore_nonresizable(self):
actual = calculate_guided_resize(
part_min=-1, part_size=100 << 30, install_min=10 << 30)
self.assertIsNone(actual)
def test_too_small(self):
actual = calculate_guided_resize(
part_min=95 << 30, part_size=100 << 30, install_min=10 << 30)
self.assertIsNone(actual)
def test_even_split(self):
# 8 GiB * 1.25 == 10 GiB
size = 10 << 30
actual = calculate_guided_resize(
part_min=8 << 30, part_size=100 << 30, install_min=size)
expected = GuidedResizeValues(
minimum=size, recommended=50 << 30, maximum=(100 << 30) - size)
self.assertEqual(expected, actual)
def test_weighted_split(self):
actual = calculate_guided_resize(
part_min=40 << 30, part_size=240 << 30, install_min=10 << 30)
expected = GuidedResizeValues(
minimum=50 << 30, recommended=200 << 30, maximum=230 << 30)
self.assertEqual(expected, actual)
class TestCalculateInstallMin(unittest.TestCase):
@mock.patch('subiquity.common.filesystem.sizes.swap.suggested_swapsize')
@mock.patch('subiquity.common.filesystem.sizes.bootfs_scale')
def test_small_setups(self, bootfs_scale, swapsize):
swapsize.return_value = 1 << 30
bootfs_scale.maximum = 1 << 30
source_min = 1 << 30
# with a small source, we hit the default 2GiB padding
self.assertEqual(5 << 30, calculate_suggested_install_min(source_min))
@mock.patch('subiquity.common.filesystem.sizes.swap.suggested_swapsize')
@mock.patch('subiquity.common.filesystem.sizes.bootfs_scale')
def test_small_setups_big_swap(self, bootfs_scale, swapsize):
swapsize.return_value = 10 << 30
bootfs_scale.maximum = 1 << 30
source_min = 1 << 30
self.assertEqual(14 << 30, calculate_suggested_install_min(source_min))
@mock.patch('subiquity.common.filesystem.sizes.swap.suggested_swapsize')
@mock.patch('subiquity.common.filesystem.sizes.bootfs_scale')
def test_small_setups_big_boot(self, bootfs_scale, swapsize):
swapsize.return_value = 1 << 30
bootfs_scale.maximum = 10 << 30
source_min = 1 << 30
self.assertEqual(14 << 30, calculate_suggested_install_min(source_min))
@mock.patch('subiquity.common.filesystem.sizes.swap.suggested_swapsize')
@mock.patch('subiquity.common.filesystem.sizes.bootfs_scale')
def test_big_source(self, bootfs_scale, swapsize):
swapsize.return_value = 1 << 30
bootfs_scale.maximum = 2 << 30
source_min = 10 << 30
# a bigger source should hit 80% padding
expected = (10 + 8 + 1 + 2) << 30
self.assertEqual(expected, calculate_suggested_install_min(source_min))

View File

@ -327,9 +327,17 @@ class StorageResponseV2:
disks: List[Disk]
need_root: bool # if True, there is not yet a partition mounted at "/"
need_boot: bool # if True, there is not yet a boot partition
install_minimum_size: int
error_report: Optional[ErrorReportRef] = None
@attr.s(auto_attribs=True)
class GuidedResizeValues:
minimum: int
recommended: int
maximum: int
@attr.s(auto_attribs=True)
class AddPartitionV2:
disk_id: str

View File

@ -36,6 +36,7 @@ from subiquity.common.types import Bootloader, OsProber
log = logging.getLogger('subiquity.models.filesystem')
MiB = 1024 * 1024
GiB = 1024 * 1024 * 1024
def _set_backlinks(obj):

View File

@ -24,7 +24,6 @@ from typing import List
import pyudev
from subiquitycore.async_helpers import (
run_in_thread,
schedule_task,
@ -314,12 +313,17 @@ class FilesystemController(SubiquityController, FilesystemManipulator):
return p
raise ValueError(f'Partition {number} on {disk.id} not found')
def calculate_suggested_install_min(self):
source_min = self.app.base_model.source.current.size
return sizes.calculate_suggested_install_min(source_min)
async def v2_GET(self) -> StorageResponseV2:
disks = self.model._all(type='disk')
return StorageResponseV2(
disks=[labels.for_client(d) for d in disks],
need_root=not self.model.is_root_mounted(),
need_boot=self.model.needs_bootloader_partition(),
install_minimum_size=self.calculate_suggested_install_min(),
)
async def v2_POST(self) -> StorageResponseV2: