Merge pull request #1267 from dbungert/use-resize

Enable resize
This commit is contained in:
Dan Bungert 2022-04-26 19:28:37 -06:00 committed by GitHub
commit 0533a88706
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 138 additions and 17 deletions

View File

@ -49,7 +49,7 @@ parts:
plugin: python
source-type: git
source: https://git.launchpad.net/curtin
source-commit: 8c13139961b9decc23a57f2f515f5248a21e1b06
source-commit: a18e5feb755f9ee04a284830114b2a09b03af175
build-packages:
- shared-mime-info
- zlib1g-dev

View File

@ -291,7 +291,7 @@ class API:
class edit_partition:
"""required field number
optional fields wipe, mount, format
optional fields wipe, mount, format, size
It is an error to do wipe=null and change the format.
It is an error to modify other Partition fields.
"""

View File

@ -310,6 +310,7 @@ def _for_client_partition(partition, *, min_size=0):
annotations=annotations(partition) + usage_labels(partition),
os=partition.os,
offset=partition.offset,
resize=partition.resize,
mount=partition.mount,
format=partition.format)

View File

@ -15,6 +15,8 @@
import logging
from curtin.block import get_resize_fstypes
from subiquity.common.filesystem import boot, gaps
from subiquity.common.types import Bootloader
from subiquity.models.filesystem import (
@ -43,7 +45,7 @@ class FilesystemManipulator:
self.model.remove_mount(mount)
def create_filesystem(self, volume, spec):
if spec['fstype'] is None:
if spec.get('fstype') is None:
# prep partitions are always wiped (and never have a filesystem)
if getattr(volume, 'flag', None) != 'prep':
volume.wipe = None
@ -61,9 +63,9 @@ class FilesystemManipulator:
volume.flag = "swap"
elif volume.flag == "swap":
volume.flag = ""
if spec['fstype'] == "swap":
if spec.get('fstype') == "swap":
self.model.add_mount(fs, "")
if spec['fstype'] is None and spec['use_swap']:
if spec.get('fstype') is None and spec.get('use_swap'):
self.model.add_mount(fs, "")
self.create_mount(fs, spec)
return fs
@ -165,19 +167,29 @@ class FilesystemManipulator:
self.delete_partition(p, True)
self.clear(disk)
def can_resize_partition(self, partition):
if not partition.preserve:
return True
if partition.format not in get_resize_fstypes():
return False
return True
def partition_disk_handler(self, disk, spec, *, partition=None, gap=None):
log.debug('partition_disk_handler: %s %s %s %s',
disk, spec, partition, gap)
if partition is not None:
if 'size' in spec:
if 'size' in spec and spec['size'] != partition.size:
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")
if not self.can_resize_partition(partition):
raise Exception("partition cannot support resize")
partition.size = new_size
partition.resize = True
for part in trailing:
part.offset += size_change
self.delete_filesystem(partition.fs())

View File

@ -29,6 +29,7 @@ from subiquity.models.tests.test_filesystem import (
make_disk,
make_model,
make_partition,
make_filesystem,
)
from subiquity.models.filesystem import (
Bootloader,
@ -663,3 +664,26 @@ class TestReformat(unittest.TestCase):
disk = make_disk(self.manipulator.model, ptable=None)
self.manipulator.reformat(disk, 'msdos')
self.assertEqual('msdos', disk.ptable)
class TestCanResize(unittest.TestCase):
def setUp(self):
self.manipulator = make_manipulator()
self.manipulator.model._probe_data = {}
def test_resize_unpreserved(self):
disk = make_disk(self.manipulator.model, ptable=None)
part = make_partition(self.manipulator.model, disk, preserve=False)
self.assertTrue(self.manipulator.can_resize_partition(part))
def test_resize_ext4(self):
disk = make_disk(self.manipulator.model, ptable=None)
part = make_partition(self.manipulator.model, disk, preserve=True)
make_filesystem(self.manipulator.model, partition=part, fstype='ext4')
self.assertTrue(self.manipulator.can_resize_partition(part))
def test_resize_invalid(self):
disk = make_disk(self.manipulator.model, ptable=None)
part = make_partition(self.manipulator.model, disk, preserve=True)
make_filesystem(self.manipulator.model, partition=part, fstype='asdf')
self.assertFalse(self.manipulator.can_resize_partition(part))

View File

@ -267,6 +267,7 @@ class Partition:
boot: Optional[bool] = None
os: Optional[OsProber] = None
offset: Optional[int] = None
resize: Optional[bool] = None
@attr.s(auto_attribs=True)

View File

@ -672,6 +672,7 @@ class Partition(_Formattable):
name = attr.ib(default=None)
multipath = attr.ib(default=None)
offset = attr.ib(default=None)
resize = attr.ib(default=None)
def available(self):
if self.flag in ['bios_grub', 'prep'] or self.grub_device:

View File

@ -21,6 +21,7 @@ from subiquity.models.filesystem import (
Bootloader,
dehumanize_size,
Disk,
Filesystem,
FilesystemModel,
get_raid_size,
humanize_size,
@ -179,6 +180,10 @@ def make_partition(model, device=None, *, preserve=False, size=None,
return partition
def make_filesystem(model, *, partition, **kw):
return Filesystem(m=model, volume=partition, **kw)
def make_model_and_partition(bootloader=None):
model, disk = make_model_and_disk(bootloader)
return model, make_partition(model, disk)

View File

@ -391,15 +391,18 @@ class FilesystemController(SubiquityController, FilesystemManipulator):
log.debug(data)
disk = self.model._one(id=data.disk_id)
partition = self.get_partition(disk, data.partition.number)
if data.partition.size not in (None, partition.size):
if data.partition.size not in (None, partition.size) \
and self.app.opts.storage_version < 2:
raise ValueError('edit_partition does not support changing size')
if data.partition.boot is not None \
and data.partition.boot != partition.boot:
raise ValueError('edit_partition does not support changing boot')
spec = {
'fstype': data.partition.format or partition.format,
'mount': data.partition.mount or partition.mount,
}
spec = {'mount': data.partition.mount or partition.mount}
if data.partition.format is not None \
and data.partition.format != partition.format:
spec['fstype'] = data.partition.format
if data.partition.size is not None:
spec['size'] = data.partition.size
self.partition_disk_handler(disk, spec, partition=partition)
return await self.v2_GET()

View File

@ -11,6 +11,7 @@ import unittest
from unittest.mock import patch
from urllib.parse import unquote
from subiquitycore.tests import SubiTestCase
from subiquitycore.utils import astart_command
default_timeout = 10
@ -26,6 +27,11 @@ def first(items, key, value):
return next(find(items, key, value))
def match(items, **kwargs):
return [item for item in items
if all(item.get(k) == v for k, v in kwargs.items())]
def timeout(multiplier=1):
def wrapper(coro):
@wraps(coro)
@ -128,7 +134,7 @@ class Server(Client):
pass
class TestAPI(unittest.IsolatedAsyncioTestCase):
class TestAPI(unittest.IsolatedAsyncioTestCase, SubiTestCase):
def assertDictSubset(self, expected, actual):
"""All keys in dictionary expected, and matching values, must match
keys and values in actual. Actual may contain additional keys and
@ -291,7 +297,7 @@ class TestFlow(TestAPI):
edit_sda2 = first(edit_sda['partitions'], 'number', 2)
for key in 'size', 'number', 'mount', 'boot':
self.assertEqual(add_sda2[key], edit_sda2[key])
self.assertEqual(add_sda2[key], edit_sda2[key], key)
self.assertEqual('ext4', edit_sda2['format'])
del_resp = await inst.post('/storage/v2/delete_partition', data)
@ -830,10 +836,7 @@ class TestPartitionTableEditing(TestAPI):
[sda] = resp['disks']
[p1, p2, p3, p4] = sda['partitions']
e1.pop('annotations')
e1.update({
'mount': '/boot/efi',
'grub_device': True,
})
e1.update({'mount': '/boot/efi', 'grub_device': True})
self.assertDictSubset(e1, p1)
self.assertEqual(e2, p2)
self.assertEqual(e3, p3)
@ -847,6 +850,77 @@ class TestPartitionTableEditing(TestAPI):
}
self.assertDictSubset(e4, p4)
@timeout()
async def test_resize(self):
# load config, edit size, use that for server
with open('examples/ubuntu-and-free-space.json', 'r') as fp:
data = json.load(fp)
# expand sda3 to use the rest of the disk
def get_size(key):
return int(data['storage']['blockdev'][key]['attrs']['size'])
sda_size = get_size('/dev/sda')
sda1_size = get_size('/dev/sda1')
sda2_size = get_size('/dev/sda2')
sda3_size = sda_size - sda1_size - sda2_size - (2 << 20)
data['storage']['blockdev']['/dev/sda3']['attrs']['size'] = \
str(sda3_size)
cfg = self.tmp_path('machine-config.json')
with open(cfg, 'w') as fp:
json.dump(data, fp)
extra = ['--storage-version', '2']
async with start_server(cfg, extra_args=extra) as inst:
# Disk has 3 existing partitions and no free space.
resp = await inst.get('/storage/v2')
[sda] = resp['disks']
[orig_p1, orig_p2, orig_p3] = sda['partitions']
p3 = orig_p3.copy()
p3['size'] = 10 << 30
data = {
'disk_id': 'disk-sda',
'partition': p3,
}
resp = await inst.post('/storage/v2/edit_partition', data)
[sda] = resp['disks']
[_, _, actual_p3, g1] = sda['partitions']
self.assertEqual(10 << 30, actual_p3['size'])
self.assertEqual(True, actual_p3['resize'])
self.assertIsNone(actual_p3['wipe'])
end_size = orig_p3['size'] - (10 << 30)
self.assertEqual(end_size, g1['size'])
expected_p1 = orig_p1.copy()
expected_p1.pop('annotations')
expected_p1.update({'mount': '/boot/efi', 'grub_device': True})
expected_p3 = actual_p3
data = {
'disk_id': 'disk-sda',
'gap': g1,
'partition': {
'format': 'ext4',
'mount': '/srv',
}
}
resp = await inst.post('/storage/v2/add_partition', data)
[sda] = resp['disks']
[actual_p1, actual_p2, actual_p3, actual_p4] = sda['partitions']
self.assertDictSubset(expected_p1, actual_p1)
self.assertEqual(orig_p2, actual_p2)
self.assertEqual(expected_p3, actual_p3)
self.assertEqual(end_size, actual_p4['size'])
self.assertEqual('Partition', actual_p4['$type'])
v1resp = await inst.get('/storage')
config = v1resp['config']
[sda3] = match(config, type='partition', number=3)
[sda3_format] = match(config, type='format', volume=sda3['id'])
self.assertTrue(sda3['preserve'])
self.assertTrue(sda3['resize'])
self.assertTrue(sda3_format['preserve'])
class TestGap(TestAPI):
async def test_blank_disk_is_one_big_gap(self):