diff --git a/subiquity/common/filesystem/gaps.py b/subiquity/common/filesystem/gaps.py index 1e53c3ee..d212ae65 100644 --- a/subiquity/common/filesystem/gaps.py +++ b/subiquity/common/filesystem/gaps.py @@ -23,6 +23,7 @@ from subiquity.models.filesystem import ( Disk, LVM_CHUNK_SIZE, LVM_VolGroup, + Partition, Raid, ) @@ -165,3 +166,23 @@ def largest_gap_size(device): if largest is not None: return largest.size return 0 + + +def movable_trailing_partitions_and_gap_size(partition): + pgs = parts_and_gaps(partition.device) + part_idx = pgs.index(partition) + trailing_partitions = [] + in_extended = partition.flag == "logical" + for pg in pgs[part_idx + 1:]: + if isinstance(pg, Partition): + if pg.preserve: + break + if in_extended and pg.flag != "logical": + break + trailing_partitions.append(pg) + else: + if pg.in_extended == in_extended: + return (trailing_partitions, pg.size) + else: + return (trailing_partitions, 0) + return (trailing_partitions, 0) diff --git a/subiquity/common/filesystem/manipulator.py b/subiquity/common/filesystem/manipulator.py index 9b88ea39..aabbada1 100644 --- a/subiquity/common/filesystem/manipulator.py +++ b/subiquity/common/filesystem/manipulator.py @@ -168,9 +168,15 @@ class FilesystemManipulator: if partition is not None: if 'size' in spec: - partition.size = align_up(spec['size']) - if gaps.largest_gap_size(disk) < 0: + trailing, gap_size = \ + gaps.movable_trailing_partitions_and_gap_size(partition) + new_size = align_up(spec['size']) + size_change = new_size - partition.size + if size_change > gap_size: raise Exception("partition size too large") + partition.size = new_size + for part in trailing: + part.offset += size_change self.delete_filesystem(partition.fs()) self.create_filesystem(partition, spec) return diff --git a/subiquity/common/filesystem/tests/test_gaps.py b/subiquity/common/filesystem/tests/test_gaps.py index 1c028596..2b3c0264 100644 --- a/subiquity/common/filesystem/tests/test_gaps.py +++ b/subiquity/common/filesystem/tests/test_gaps.py @@ -13,7 +13,9 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from functools import partial import unittest +from unittest import mock from subiquity.models.filesystem import ( PartitionAlignmentData, @@ -165,3 +167,153 @@ class TestDiskGaps(unittest.TestCase): gaps.Gap(d, 35, 15, True), gaps.Gap(d, 50, 50, False), ]) + + +class TestMovableTrailingPartitionsAndGapSize(unittest.TestCase): + + def use_alignment_data(self, alignment_data): + m = mock.patch('subiquity.common.filesystem.gaps.parts_and_gaps') + p = m.start() + self.addCleanup(m.stop) + p.side_effect = partial( + gaps.find_disk_gaps_v2, info=alignment_data) + + def test_no_next_gap(self): + self.use_alignment_data(PartitionAlignmentData( + part_align=10, min_gap_size=1, min_start_offset=10, + min_end_offset=10, primary_part_limit=10)) + # 0----10---20---30---40---50---60---70---80---90---100 + # #####[ p1 ]##### + m, d = make_model_and_disk(size=100) + p = make_partition(m, d, offset=10, size=80) + self.assertEqual( + ([], 0), + gaps.movable_trailing_partitions_and_gap_size(p)) + + def test_immediately_trailing_gap(self): + self.use_alignment_data(PartitionAlignmentData( + part_align=10, min_gap_size=1, min_start_offset=10, + min_end_offset=10, primary_part_limit=10)) + # 0----10---20---30---40---50---60---70---80---90---100 + # #####[ p1 ] [ p2 ] ##### + m, d = make_model_and_disk(size=100) + p1 = make_partition(m, d, offset=10, size=20) + p2 = make_partition(m, d, offset=50, size=20) + self.assertEqual( + ([], 20), + gaps.movable_trailing_partitions_and_gap_size(p1)) + self.assertEqual( + ([], 20), + gaps.movable_trailing_partitions_and_gap_size(p2)) + + def test_one_trailing_movable_partition_and_gap(self): + self.use_alignment_data(PartitionAlignmentData( + part_align=10, min_gap_size=1, min_start_offset=10, + min_end_offset=10, primary_part_limit=10)) + # 0----10---20---30---40---50---60---70---80---90---100 + # #####[ p1 ][ p2 ] ##### + m, d = make_model_and_disk(size=100) + p1 = make_partition(m, d, offset=10, size=40) + p2 = make_partition(m, d, offset=50, size=10) + self.assertEqual( + ([p2], 30), + gaps.movable_trailing_partitions_and_gap_size(p1)) + + def test_one_trailing_movable_partition_and_no_gap(self): + self.use_alignment_data(PartitionAlignmentData( + part_align=10, min_gap_size=1, min_start_offset=10, + min_end_offset=10, primary_part_limit=10)) + # 0----10---20---30---40---50---60---70---80---90---100 + # #####[ p1 ][ p2 ]##### + m, d = make_model_and_disk(size=100) + p1 = make_partition(m, d, offset=10, size=40) + p2 = make_partition(m, d, offset=50, size=40) + self.assertEqual( + ([p2], 0), + gaps.movable_trailing_partitions_and_gap_size(p1)) + + def test_full_extended_partition_then_gap(self): + self.use_alignment_data(PartitionAlignmentData( + part_align=1, min_gap_size=1, min_start_offset=10, + min_end_offset=10, primary_part_limit=10, ebr_space=2)) + # 0----10---20---30---40---50---60---70---80---90---100 + # #####[ p1 (extended) ] ##### + # ######[ p5 (logical) ] ##### + m, d = make_model_and_disk(size=100) + make_partition(m, d, offset=10, size=40, flag='extended') + p5 = make_partition(m, d, offset=12, size=38, flag='logical') + self.assertEqual( + ([], 0), + gaps.movable_trailing_partitions_and_gap_size(p5)) + + def test_full_extended_partition_then_part(self): + self.use_alignment_data(PartitionAlignmentData( + part_align=1, min_gap_size=1, min_start_offset=10, + min_end_offset=10, primary_part_limit=10, ebr_space=2)) + # 0----10---20---30---40---50---60---70---80---90---100 + # #####[ p1 (extended) ][ p2 ]##### + # ######[ p5 (logical) ] ##### + m, d = make_model_and_disk(size=100) + make_partition(m, d, offset=10, size=40, flag='extended') + make_partition(m, d, offset=50, size=40) + p5 = make_partition(m, d, offset=12, size=38, flag='logical') + self.assertEqual( + ([], 0), + gaps.movable_trailing_partitions_and_gap_size(p5)) + + def test_gap_in_extended_partition(self): + self.use_alignment_data(PartitionAlignmentData( + part_align=1, min_gap_size=1, min_start_offset=10, + min_end_offset=10, primary_part_limit=10, ebr_space=2)) + # 0----10---20---30---40---50---60---70---80---90---100 + # #####[ p1 (extended) ] ##### + # ######[ p5 (logical)] ##### + m, d = make_model_and_disk(size=100) + make_partition(m, d, offset=10, size=40, flag='extended') + p5 = make_partition(m, d, offset=12, size=30, flag='logical') + self.assertEqual( + ([], 6), + gaps.movable_trailing_partitions_and_gap_size(p5)) + + def test_trailing_logical_partition_then_gap(self): + self.use_alignment_data(PartitionAlignmentData( + part_align=1, min_gap_size=1, min_start_offset=10, + min_end_offset=10, primary_part_limit=10, ebr_space=2)) + # 0----10---20---30---40---50---60---70---80---90---100 + # #####[ p1 (extended) ]##### + # ######[ p5 (logical)] [ p6 (logical)] ##### + m, d = make_model_and_disk(size=100) + make_partition(m, d, offset=10, size=80, flag='extended') + p5 = make_partition(m, d, offset=12, size=30, flag='logical') + p6 = make_partition(m, d, offset=44, size=30, flag='logical') + self.assertEqual( + ([p6], 14), + gaps.movable_trailing_partitions_and_gap_size(p5)) + + def test_trailing_logical_partition_then_no_gap(self): + self.use_alignment_data(PartitionAlignmentData( + part_align=1, min_gap_size=1, min_start_offset=10, + min_end_offset=10, primary_part_limit=10, ebr_space=2)) + # 0----10---20---30---40---50---60---70---80---90---100 + # #####[ p1 (extended) ]##### + # ######[ p5 (logical)] [ p6 (logical) ] ##### + m, d = make_model_and_disk(size=100) + make_partition(m, d, offset=10, size=80, flag='extended') + p5 = make_partition(m, d, offset=12, size=30, flag='logical') + p6 = make_partition(m, d, offset=44, size=44, flag='logical') + self.assertEqual( + ([p6], 0), + gaps.movable_trailing_partitions_and_gap_size(p5)) + + def test_trailing_preserved_partition(self): + self.use_alignment_data(PartitionAlignmentData( + part_align=10, min_gap_size=1, min_start_offset=10, + min_end_offset=10, primary_part_limit=10)) + # 0----10---20---30---40---50---60---70---80---90---100 + # #####[ p1 ][ p2 p ] ##### + m, d = make_model_and_disk(size=100) + p1 = make_partition(m, d, offset=10, size=40) + make_partition(m, d, offset=50, size=20, preserve=True) + self.assertEqual( + ([], 0), + gaps.movable_trailing_partitions_and_gap_size(p1)) diff --git a/subiquity/models/filesystem.py b/subiquity/models/filesystem.py index 4ac29387..6de612b1 100644 --- a/subiquity/models/filesystem.py +++ b/subiquity/models/filesystem.py @@ -1404,6 +1404,11 @@ class FilesystemModel(object): def remove_partition(self, part): if part._fs or part._constructed_device: raise Exception("can only remove empty partition") + from subiquity.common.filesystem.gaps import ( + movable_trailing_partitions_and_gap_size, + ) + for p2 in movable_trailing_partitions_and_gap_size(part)[0]: + p2.offset -= part.size self._remove(part) if len(part.device._partitions) == 0: part.device.ptable = None diff --git a/subiquity/ui/views/filesystem/partition.py b/subiquity/ui/views/filesystem/partition.py index 6a05a4f5..cc43ef51 100644 --- a/subiquity/ui/views/filesystem/partition.py +++ b/subiquity/ui/views/filesystem/partition.py @@ -373,7 +373,6 @@ class PartitionStretchy(Stretchy): self.model = parent.model self.controller = parent.controller self.parent = parent - max_size = gaps.largest_gap_size(disk) if partition is None and gap is None: raise Exception('bad PartitionStretchy - needs partition or gap') @@ -395,7 +394,8 @@ class PartitionStretchy(Stretchy): else: label = _("Save") initial['size'] = humanize_size(self.partition.size) - max_size += self.partition.size + max_size = partition.size + \ + gaps.movable_trailing_partitions_and_gap_size(partition)[1] if not boot.is_esp(partition): initial.update(initial_data_for_fs(self.partition.fs()))