storage/v2: permit resize
This commit is contained in:
parent
942ea0f6e2
commit
ce3126efb6
|
@ -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.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in New Issue