storage/v2: permit resize

This commit is contained in:
Dan Bungert 2022-04-11 16:34:16 -06:00
parent 942ea0f6e2
commit ce3126efb6
7 changed files with 96 additions and 15 deletions

View File

@ -291,7 +291,7 @@ class API:
class edit_partition: class edit_partition:
"""required field number """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 do wipe=null and change the format.
It is an error to modify other Partition fields. 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), annotations=annotations(partition) + usage_labels(partition),
os=partition.os, os=partition.os,
offset=partition.offset, offset=partition.offset,
resize=partition.resize,
mount=partition.mount, mount=partition.mount,
format=partition.format) format=partition.format)

View File

@ -43,7 +43,7 @@ class FilesystemManipulator:
self.model.remove_mount(mount) self.model.remove_mount(mount)
def create_filesystem(self, volume, spec): 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) # prep partitions are always wiped (and never have a filesystem)
if getattr(volume, 'flag', None) != 'prep': if getattr(volume, 'flag', None) != 'prep':
volume.wipe = None volume.wipe = None
@ -61,9 +61,9 @@ class FilesystemManipulator:
volume.flag = "swap" volume.flag = "swap"
elif volume.flag == "swap": elif volume.flag == "swap":
volume.flag = "" volume.flag = ""
if spec['fstype'] == "swap": if spec.get('fstype') == "swap":
self.model.add_mount(fs, "") 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.model.add_mount(fs, "")
self.create_mount(fs, spec) self.create_mount(fs, spec)
return fs return fs
@ -178,6 +178,7 @@ class FilesystemManipulator:
if size_change > gap_size: if size_change > gap_size:
raise Exception("partition size too large") raise Exception("partition size too large")
partition.size = new_size partition.size = new_size
partition.resize = True
for part in trailing: for part in trailing:
part.offset += size_change part.offset += size_change
self.delete_filesystem(partition.fs()) self.delete_filesystem(partition.fs())

View File

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

View File

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

View File

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

View File

@ -11,6 +11,7 @@ import unittest
from unittest.mock import patch from unittest.mock import patch
from urllib.parse import unquote from urllib.parse import unquote
from subiquitycore.tests import SubiTestCase
from subiquitycore.utils import astart_command from subiquitycore.utils import astart_command
default_timeout = 10 default_timeout = 10
@ -26,6 +27,11 @@ def first(items, key, value):
return next(find(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 timeout(multiplier=1):
def wrapper(coro): def wrapper(coro):
@wraps(coro) @wraps(coro)
@ -128,7 +134,7 @@ class Server(Client):
pass pass
class TestAPI(unittest.IsolatedAsyncioTestCase): class TestAPI(unittest.IsolatedAsyncioTestCase, SubiTestCase):
def assertDictSubset(self, expected, actual): def assertDictSubset(self, expected, actual):
"""All keys in dictionary expected, and matching values, must match """All keys in dictionary expected, and matching values, must match
keys and values in actual. Actual may contain additional keys and 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) edit_sda2 = first(edit_sda['partitions'], 'number', 2)
for key in 'size', 'number', 'mount', 'boot': 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']) self.assertEqual('ext4', edit_sda2['format'])
del_resp = await inst.post('/storage/v2/delete_partition', data) del_resp = await inst.post('/storage/v2/delete_partition', data)
@ -830,10 +836,7 @@ class TestPartitionTableEditing(TestAPI):
[sda] = resp['disks'] [sda] = resp['disks']
[p1, p2, p3, p4] = sda['partitions'] [p1, p2, p3, p4] = sda['partitions']
e1.pop('annotations') e1.pop('annotations')
e1.update({ e1.update({'mount': '/boot/efi', 'grub_device': True})
'mount': '/boot/efi',
'grub_device': True,
})
self.assertDictSubset(e1, p1) self.assertDictSubset(e1, p1)
self.assertEqual(e2, p2) self.assertEqual(e2, p2)
self.assertEqual(e3, p3) self.assertEqual(e3, p3)
@ -847,6 +850,77 @@ class TestPartitionTableEditing(TestAPI):
} }
self.assertDictSubset(e4, p4) 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): class TestGap(TestAPI):
async def test_blank_disk_is_one_big_gap(self): async def test_blank_disk_is_one_big_gap(self):