Merge pull request #1337 from dbungert/min-size-calc
sizes: calculations for install_min and resize
This commit is contained in:
commit
0e796c9956
|
@ -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 doesn’t
|
||||
# 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
|
||||
# don’t want to short change the other calculations.
|
||||
# 3) Room to grow - while meaningful work can sometimes be possible on a full
|
||||
# disk, it’s 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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue