Merge pull request #1337 from dbungert/min-size-calc

sizes: calculations for install_min and resize
This commit is contained in:
Dan Bungert 2022-06-30 11:29:54 -06:00 committed by GitHub
commit 0e796c9956
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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: