commit
0533a88706
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue