Merge ~dbungert/curtin:resize into curtin:master

Proposed by Dan Bungert
Status: Merged
Approved by: Dan Bungert
Approved revision: 03faa3751f0d4a4eaf1b2a98cc6918a599b411e5
Merge reported by: Server Team CI bot
Merged at revision: not available
Proposed branch: ~dbungert/curtin:resize
Merge into: curtin:master
Diff against target: 970 lines (+689/-30)
9 files modified
curtin/block/schemas.py (+1/-0)
curtin/commands/block_meta.py (+12/-1)
curtin/commands/block_meta_v2.py (+109/-5)
curtin/storage_config.py (+8/-0)
curtin/util.py (+7/-0)
doc/topics/storage.rst (+17/-0)
tests/integration/test_block_meta.py (+379/-21)
tests/unittests/test_commands_block_meta.py (+133/-2)
tests/unittests/test_storage_config.py (+23/-1)
Reviewer Review Type Date Requested Status
Michael Hudson-Doyle Approve
Server Team CI bot continuous-integration Approve
Review via email: mp+417829@code.launchpad.net

Commit message

Add support for resize of ext{2,3,4}

To post a comment you must log in.
Revision history for this message
Dan Bungert (dbungert) wrote :

I'm not convinced the keyword 'resize' is what we want. Maybe 'move' as a keyword that permits both moves and resizes.

Needs a rebase to the final commit set, though maybe that matters less if the merge robot just squishes everything anyway.

The commit adding 'fake' to run_bm() is just debug stuff and probably isn't needed.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Dan Bungert (dbungert) wrote :

Also, there is some redundant partitioning between the calls to parted and the later table.apply().
Also, I'm not yet sure why resize2fs only sometimes wants a fsck, it's random and I don't like that it's random.

~dbungert/curtin:resize updated
3e6c401... by Dan Bungert

block-meta: check fs format

Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

This looks mostly OK to me. Do you need to invoke parted at all though? I guess if you let sfdisk do it all, you have to do the resizefs calls downs before the sfdisk invocation and the resize ups after. I think I'd still prefer that though. WDYT?

Also this clearly only works for ext{2,3,4} filesystems. What's the long term plan there?

~dbungert/curtin:resize updated
47fec96... by Dan Bungert

block-meta: make format action not required

b2779a4... by Dan Bungert

block-meta: let sfdisk handle resize partitioning

Revision history for this message
Dan Bungert (dbungert) wrote :

> This looks mostly OK to me. Do you need to invoke parted at all though? I
> guess if you let sfdisk do it all, you have to do the resizefs calls downs
> before the sfdisk invocation and the resize ups after. I think I'd still
> prefer that though.

Agree. Done.

> Also this clearly only works for ext{2,3,4} filesystems. What's the long term
> plan there?

Let verify_format be aware of the resize, and complain if the partition to resize is in an unsupported format. Ship the initial pull with just ext{2,3,4}. Document the formats we can resize. Later PRs can be as needed and focused on just the new filesystem type we want to resize. Next is almost certainly NTFS.

What's your thoughts on the 'resize' keyword? I'm kind of leaning now for leaving it as is, and if we later support move, adding a 'move' keyword as well.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

Looking good. But I think not quite here yet.

And there's always more scope for more tests.

~dbungert/curtin:resize updated
26361fd... by Dan Bungert

block-meta: resize use existing device handling

3f9e591... by Dan Bungert

block-meta: only attempt resize on ext[234]

6075079... by Dan Bungert

block-meta: better resize size handling

ccc2fcb... by Dan Bungert

block-meta: verify offset if present

bb577a7... by Dan Bungert

block-meta: refactor and add resize tests

Revision history for this message
Dan Bungert (dbungert) wrote :

Closer to something I'm happy with.
See diff comments.
The commit history is awful, please ignore, will fix.
My plan at this point is a bit more integration tests, and to resolve the doc comment.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
~dbungert/curtin:resize updated
7eb9c64... by Dan Bungert

block-meta: integration test gpt doing many things

create, preserve, resize, delete

0b68d0c... by Dan Bungert

block-meta: check extended part size

aff746f... by Dan Bungert

block-meta: test resize of extended partition

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
~dbungert/curtin:resize updated
d14db6c... by Dan Bungert

block-meta: integration test of many things

create, preserve, resize, delete including handling of primary,
extended, and logical partitions.

Revision history for this message
Dan Bungert (dbungert) wrote :

More or less ready, minus a short discussion on the documentation.
Please pay careful attention to commit 6d6e90c2cf26be176f31931c9df9d2ad96f45549

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

This is really really close now. I think the only gap is that I don't think this handles resizing a partition that is to be reformatted quite correctly -- there is no need to resize the filesystem in this case, so if it's too full to resize or not a format we know how to resize, we should still go ahead.

review: Needs Fixing
Revision history for this message
Dan Bungert (dbungert) wrote (last edit ):

I need to check test_mix_of_operations_msdos for flakiness

~dbungert/curtin:resize updated
ae221d2... by Dan Bungert

storage_config: add select_configs utility

ecc959a... by Dan Bungert

block-meta: find one format in verify_format

8fa9fd2... by Dan Bungert

doc: wording, v2 caution

03faa37... by Dan Bungert

block-meta: resize and preserve correctness

Revision history for this message
Dan Bungert (dbungert) wrote :

Feedback items implemented.

I can't reproduce the flaky test_mix_of_operations_msdos, and it's in the least useful spot (the bogus size reported on the extended partition by sysfs), so I think we should live with that one for now. If it shows up again I suggest we skip that particular check as it already known invalid.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

All this looks great, thanks.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/curtin/block/schemas.py b/curtin/block/schemas.py
2index 0a6e305..1834343 100644
3--- a/curtin/block/schemas.py
4+++ b/curtin/block/schemas.py
5@@ -290,6 +290,7 @@ PARTITION = {
6 'pattern': _path_dev},
7 'name': {'$ref': '#/definitions/name'},
8 'offset': {'$ref': '#/definitions/size'}, # XXX: This is not used
9+ 'resize': {'type': 'boolean'},
10 'preserve': {'$ref': '#/definitions/preserve'},
11 'size': {'$ref': '#/definitions/size'},
12 'uuid': {'$ref': '#/definitions/uuid'}, # XXX: This is not used
13diff --git a/curtin/commands/block_meta.py b/curtin/commands/block_meta.py
14index cdf30c5..5614883 100644
15--- a/curtin/commands/block_meta.py
16+++ b/curtin/commands/block_meta.py
17@@ -779,12 +779,17 @@ def verify_exists(devpath):
18 raise RuntimeError("Device %s does not exist" % devpath)
19
20
21-def verify_size(devpath, expected_size_bytes, part_info):
22+def get_part_size_bytes(devpath, part_info):
23 (found_type, _code) = ptable_uuid_to_flag_entry(part_info.get('type'))
24 if found_type == 'extended':
25 found_size_bytes = int(part_info['size']) * 512
26 else:
27 found_size_bytes = block.read_sys_block_size_bytes(devpath)
28+ return found_size_bytes
29+
30+
31+def verify_size(devpath, expected_size_bytes, part_info):
32+ found_size_bytes = get_part_size_bytes(devpath, part_info)
33 msg = (
34 'Verifying %s size, expecting %s bytes, found %s bytes' % (
35 devpath, expected_size_bytes, found_size_bytes))
36@@ -1124,6 +1129,12 @@ def _get_volume_type(device_path):
37 return lsblock[kname]['TYPE']
38
39
40+def _get_volume_fstype(device_path):
41+ lsblock = block._lsblock([device_path])
42+ kname = block.path_to_kname(device_path)
43+ return lsblock[kname]['FSTYPE']
44+
45+
46 def get_volume_spec(device_path):
47 """
48 Return the most reliable spec for a device per Ubuntu FSTAB wiki
49diff --git a/curtin/commands/block_meta_v2.py b/curtin/commands/block_meta_v2.py
50index 051649b..c1e3630 100644
51--- a/curtin/commands/block_meta_v2.py
52+++ b/curtin/commands/block_meta_v2.py
53@@ -1,20 +1,24 @@
54 # This file is part of curtin. See LICENSE file for copyright and license info.
55
56+import os
57 from typing import Optional
58
59 import attr
60
61 from curtin import (block, util)
62 from curtin.commands.block_meta import (
63+ _get_volume_fstype,
64 disk_handler as disk_handler_v1,
65 get_path_to_storage_volume,
66 make_dname,
67 partition_handler as partition_handler_v1,
68- partition_verify_sfdisk,
69+ verify_ptable_flag,
70+ verify_size,
71 )
72 from curtin.log import LOG
73 from curtin.storage_config import (
74 GPT_GUID_TO_CURTIN_MAP,
75+ select_configs,
76 )
77 from curtin.udev import udevadm_settle
78
79@@ -50,6 +54,28 @@ def align_down(size, block_size):
80 return size & ~(block_size - 1)
81
82
83+def resize_ext(path, size):
84+ util.subp(['e2fsck', '-p', '-f', path])
85+ size_k = size // 1024
86+ util.subp(['resize2fs', path, f'{size_k}k'])
87+
88+
89+def perform_resize(kname, size, direction):
90+ path = block.kname_to_path(kname)
91+ fstype = _get_volume_fstype(path)
92+ if fstype:
93+ LOG.debug('Resizing %s of type %s %s to %s',
94+ path, fstype, direction, size)
95+ resizers[fstype](path, size)
96+
97+
98+resizers = {
99+ 'ext2': resize_ext,
100+ 'ext3': resize_ext,
101+ 'ext4': resize_ext,
102+}
103+
104+
105 FLAG_TO_GUID = {
106 flag: guid for (guid, (flag, typecode)) in GPT_GUID_TO_CURTIN_MAP.items()
107 }
108@@ -214,6 +240,74 @@ def _wipe_for_action(action):
109 return 'superblock'
110
111
112+def _prepare_resize(entry, part_info, table):
113+ return {
114+ 'start': table.sectors2bytes(part_info['size']),
115+ 'end': table.sectors2bytes(entry.size),
116+ }
117+
118+
119+def needs_resize(storage_config, part_action, sfdisk_part_info):
120+ if not part_action.get('preserve'):
121+ return False
122+ if not part_action.get('resize'):
123+ return False
124+
125+ volume = part_action['id']
126+ format_actions = select_configs(storage_config, type='format',
127+ volume=volume, preserve=True)
128+ if len(format_actions) < 1:
129+ return False
130+ if len(format_actions) > 1:
131+ raise Exception(f'too many format actions for volume {volume}')
132+
133+ if not format_actions[0].get('preserve'):
134+ return False
135+
136+ devpath = os.path.realpath(sfdisk_part_info['node'])
137+ fstype = _get_volume_fstype(devpath)
138+ target_fstype = format_actions[0]['fstype']
139+ msg = (
140+ 'Verifying %s format, expecting %s, found %s' % (
141+ devpath, fstype, target_fstype))
142+ LOG.debug(msg)
143+ if fstype != target_fstype:
144+ raise RuntimeError(msg)
145+
146+ if part_action.get('resize'):
147+ msg = 'Resize requested for format %s' % (fstype, )
148+ LOG.debug(msg)
149+ if fstype not in resizers:
150+ raise RuntimeError(msg + ' is unsupported')
151+
152+ return True
153+
154+
155+def verify_offset(devpath, part_action, current_info, table):
156+ if 'offset' not in part_action:
157+ return
158+ current_offset = table.sectors2bytes(current_info['start'])
159+ action_offset = int(util.human2bytes(part_action['offset']))
160+ msg = (
161+ 'Verifying %s offset, expecting %s, found %s' % (
162+ devpath, current_offset, action_offset))
163+ LOG.debug(msg)
164+ if current_offset != action_offset:
165+ raise RuntimeError(msg)
166+
167+
168+def partition_verify_sfdisk_v2(part_action, label, sfdisk_part_info,
169+ storage_config, table):
170+ devpath = os.path.realpath(sfdisk_part_info['node'])
171+ if not part_action.get('resize'):
172+ verify_size(devpath, int(util.human2bytes(part_action['size'])),
173+ sfdisk_part_info)
174+ verify_offset(devpath, part_action, sfdisk_part_info, table)
175+ expected_flag = part_action.get('flag')
176+ if expected_flag:
177+ verify_ptable_flag(devpath, expected_flag, label, sfdisk_part_info)
178+
179+
180 def disk_handler_v2(info, storage_config, handlers):
181 disk_handler_v1(info, storage_config, handlers)
182
183@@ -239,6 +333,7 @@ def disk_handler_v2(info, storage_config, handlers):
184 table = table_cls(sector_size)
185 preserved_offsets = set()
186 wipes = {}
187+ resizes = {}
188
189 sfdisk_info = None
190 for action in part_actions:
191@@ -251,7 +346,10 @@ def disk_handler_v2(info, storage_config, handlers):
192 # vmtest infrastructure unhappy.
193 sfdisk_info = block.sfdisk_info(disk)
194 part_info = _find_part_info(sfdisk_info, entry.start)
195- partition_verify_sfdisk(action, sfdisk_info['label'], part_info)
196+ partition_verify_sfdisk_v2(action, sfdisk_info['label'], part_info,
197+ storage_config, table)
198+ if needs_resize(storage_config, action, part_info):
199+ resizes[entry.start] = _prepare_resize(entry, part_info, table)
200 preserved_offsets.add(entry.start)
201 wipe = wipes[entry.start] = _wipe_for_action(action)
202 if wipe is not None:
203@@ -266,20 +364,26 @@ def disk_handler_v2(info, storage_config, handlers):
204 LOG.debug('Wiping 1M on %s at offset %s', disk, wipe_offset)
205 block.zero_file_at_offsets(disk, [wipe_offset], exclusive=False)
206
207- # Do a superblock wipe of any partitions that are being deleted.
208- for kname, nr, offset, sz in block.sysfs_partition_data(disk):
209+ for kname, nr, offset, size in block.sysfs_partition_data(disk):
210 offset_sectors = table.bytes2sectors(offset)
211 if offset_sectors not in preserved_offsets:
212+ # Do a superblock wipe of any partitions that are being deleted.
213 block.wipe_volume(block.kname_to_path(kname), 'superblock')
214+ resize = resizes.get(offset_sectors)
215+ if resize and size > resize['end']:
216+ perform_resize(kname, resize['end'], 'down')
217
218 table.apply(disk)
219
220- # Wipe the new partitions as needed.
221 for kname, number, offset, size in block.sysfs_partition_data(disk):
222 offset_sectors = table.bytes2sectors(offset)
223 mode = wipes[offset_sectors]
224 if mode is not None:
225+ # Wipe the new partitions as needed.
226 block.wipe_volume(block.kname_to_path(kname), mode)
227+ resize = resizes.get(offset_sectors)
228+ if resize and resize['start'] < size:
229+ perform_resize(kname, resize['end'], 'up')
230
231 # Make the names if needed
232 if 'name' in info:
233diff --git a/curtin/storage_config.py b/curtin/storage_config.py
234index e9a8fdd..2ede996 100644
235--- a/curtin/storage_config.py
236+++ b/curtin/storage_config.py
237@@ -1357,4 +1357,12 @@ def extract_storage_config(probe_data, strict=False):
238 return {'storage': merged_config}
239
240
241+def select_configs(storage_config, **kwargs):
242+ """ Given a set of key=value arguments, return a list of the configs that
243+ match all specified key-value pairs.
244+ """
245+ return [cfg for cfg in storage_config.values()
246+ if all(cfg.get(k) == v for k, v in kwargs.items())]
247+
248+
249 # vi: ts=4 expandtab syntax=python
250diff --git a/curtin/util.py b/curtin/util.py
251index 5b66b55..d3c3b66 100644
252--- a/curtin/util.py
253+++ b/curtin/util.py
254@@ -501,6 +501,13 @@ def chdir(dirname):
255 os.chdir(curdir)
256
257
258+@contextmanager
259+def mount(src, target):
260+ do_mount(src, target)
261+ yield
262+ do_umount(target)
263+
264+
265 def do_mount(src, target, opts=None):
266 # mount src at target with opts and return True
267 # if already mounted, return False
268diff --git a/doc/topics/storage.rst b/doc/topics/storage.rst
269index 5fae90f..e6dd13a 100644
270--- a/doc/topics/storage.rst
271+++ b/doc/topics/storage.rst
272@@ -34,6 +34,12 @@ only differ in the interpretation of ``partition`` actions at this
273 time. ``lvm_partition`` actions will be interpreted differently at
274 some point in the future.
275
276+.. note::
277+
278+ Config version ``2`` is under active development and subject to change.
279+ Users are advised to use version ``1`` unless features enabled by version
280+ ``2`` are required.
281+
282 Configuration Types
283 -------------------
284 Each entry in the config list is a dictionary with several keys which vary
285@@ -429,6 +435,17 @@ filesystem or be mounted anywhere on the system.
286
287 If the preserve flag is set to true, curtin will verify that the partition
288 exists and that the ``size`` and ``flag`` match the configuration provided.
289+See also the ``resize`` flag, which adjusts this behavior.
290+
291+**resize**: *true, false*
292+
293+Only applicable to v2 storage configuration.
294+If the ``preserve`` flag is set to false, this value is not applicable.
295+If the ``preserve`` flag is set to true, curtin will adjust the size of the
296+partition to the new size. When adjusting smaller, the size of the contents
297+must permit that. When adjusting larger, there must already be a gap beyond
298+the partition in question.
299+Resize is supported on filesystems of types ext2, ext3, ext4.
300
301 **name**: *<name>*
302
303diff --git a/tests/integration/test_block_meta.py b/tests/integration/test_block_meta.py
304index 0c74cd6..be69bc0 100644
305--- a/tests/integration/test_block_meta.py
306+++ b/tests/integration/test_block_meta.py
307@@ -2,6 +2,7 @@
308
309 from collections import namedtuple
310 import contextlib
311+import json
312 import sys
313 import yaml
314 import os
315@@ -34,6 +35,32 @@ def loop_dev(image, sector_size=512):
316 PartData = namedtuple("PartData", ('number', 'offset', 'size'))
317
318
319+def _get_filesystem_size(dev, part_action, fstype='ext4'):
320+ if fstype not in ('ext2', 'ext3', 'ext4'):
321+ raise Exception(f'_get_filesystem_size: no support for {fstype}')
322+ num = part_action['number']
323+ cmd = ['dumpe2fs', '-h', f'{dev}p{num}']
324+ out = util.subp(cmd, capture=True)[0]
325+ for line in out.splitlines():
326+ if line.startswith('Block count'):
327+ block_count = line.split(':')[1].strip()
328+ if line.startswith('Block size'):
329+ block_size = line.split(':')[1].strip()
330+ return int(block_count) * int(block_size)
331+
332+
333+def _get_extended_partition_size(dev, num):
334+ # sysfs reports extended partitions as having 1K size
335+ # sfdisk seems to have a better idea
336+ ptable_json = util.subp(['sfdisk', '-J', dev], capture=True)[0]
337+ ptable = json.loads(ptable_json)
338+
339+ nodename = f'{dev}p{num}'
340+ partitions = ptable['partitiontable']['partitions']
341+ partition = [part for part in partitions if part['node'] == nodename][0]
342+ return partition['size'] * 512
343+
344+
345 def summarize_partitions(dev):
346 # We don't care about the kname
347 return sorted(
348@@ -55,33 +82,32 @@ class StorageConfigBuilder:
349 },
350 }
351
352- def add_image(self, *, path, size, create=False, **kw):
353- action = {
354- 'type': 'image',
355- 'id': 'id' + str(len(self.config)),
356- 'path': path,
357- 'size': size,
358- }
359- action.update(**kw)
360- self.cur_image = action['id']
361+ def _add(self, *, type, **kw):
362+ if type != 'image' and self.cur_image is None:
363+ raise Exception("no current image")
364+ action = {'id': 'id' + str(len(self.config))}
365+ action.update(type=type, **kw)
366 self.config.append(action)
367+ return action
368+
369+ def add_image(self, *, path, size, create=False, **kw):
370 if create:
371 with open(path, "wb") as f:
372 f.write(b"\0" * int(util.human2bytes(size)))
373+ action = self._add(type='image', path=path, size=size, **kw)
374+ self.cur_image = action['id']
375 return action
376
377 def add_part(self, *, size, **kw):
378- if self.cur_image is None:
379- raise Exception("no current image")
380- action = {
381- 'type': 'partition',
382- 'id': 'id' + str(len(self.config)),
383- 'device': self.cur_image,
384- 'size': size,
385- }
386- action.update(**kw)
387- self.config.append(action)
388- return action
389+ fstype = kw.pop('fstype', None)
390+ part = self._add(type='partition', device=self.cur_image, size=size,
391+ **kw)
392+ if fstype:
393+ self.add_format(part=part, fstype=fstype)
394+ return part
395+
396+ def add_format(self, *, part, fstype='ext4', **kw):
397+ return self._add(type='format', volume=part['id'], fstype=fstype, **kw)
398
399 def set_preserve(self):
400 for action in self.config:
401@@ -89,6 +115,29 @@ class StorageConfigBuilder:
402
403
404 class TestBlockMeta(IntegrationTestCase):
405+ def setUp(self):
406+ self.data = self.random_string()
407+
408+ @contextlib.contextmanager
409+ def mount(self, dev, partition_cfg):
410+ mnt_point = self.tmp_dir()
411+ num = partition_cfg['number']
412+ with util.mount(f'{dev}p{num}', mnt_point):
413+ yield mnt_point
414+
415+ @contextlib.contextmanager
416+ def open_file_on_part(self, dev, part_action, mode):
417+ with self.mount(dev, part_action) as mnt_point:
418+ with open(f'{mnt_point}/data.txt', mode) as fp:
419+ yield fp
420+
421+ def create_data(self, dev, part_action):
422+ with self.open_file_on_part(dev, part_action, 'w') as fp:
423+ fp.write(self.data)
424+
425+ def check_data(self, dev, part_action):
426+ with self.open_file_on_part(dev, part_action, 'r') as fp:
427+ self.assertEqual(self.data, fp.read())
428
429 def run_bm(self, config, *args, **kwargs):
430 config_path = self.tmp_path('config.yaml')
431@@ -223,7 +272,10 @@ class TestBlockMeta(IntegrationTestCase):
432 img = self.tmp_path('image.img')
433 config = StorageConfigBuilder(version=version)
434 config.add_image(path=img, size='100M', ptable='msdos')
435- config.add_part(size='50M', number=1, flag='extended')
436+ # curtin adds 1MiB to the size of the extend partition per contained
437+ # logical partition, but only in v1 mode
438+ size = '97M' if version == 1 else '99M'
439+ config.add_part(size=size, number=1, flag='extended')
440 config.add_part(size='10M', number=5, flag='logical')
441 config.add_part(size='10M', number=6, flag='logical')
442 self.run_bm(config.render())
443@@ -238,6 +290,7 @@ class TestBlockMeta(IntegrationTestCase):
444 # gap.
445 PartData(number=6, offset=13 << 20, size=10 << 20),
446 ])
447+ self.assertEqual(99 << 20, _get_extended_partition_size(dev, 1))
448
449 p1kname = block.partition_kname(block.path_to_kname(dev), 1)
450 self.assertTrue(block.is_extended_partition('/dev/' + p1kname))
451@@ -303,6 +356,7 @@ class TestBlockMeta(IntegrationTestCase):
452 PartData(number=5, offset=(2 << 20), size=psize),
453 PartData(number=6, offset=(3 << 20) + psize, size=psize),
454 ])
455+ self.assertEqual(90 << 20, _get_extended_partition_size(dev, 1))
456
457 config = StorageConfigBuilder(version=2)
458 config.add_image(path=img, size='100M', ptable='msdos', preserve=True)
459@@ -318,6 +372,7 @@ class TestBlockMeta(IntegrationTestCase):
460 PartData(number=1, offset=1 << 20, size=1 << 10),
461 PartData(number=5, offset=(3 << 20) + psize, size=psize),
462 ])
463+ self.assertEqual(90 << 20, _get_extended_partition_size(dev, 1))
464
465 def _test_wiping(self, ptable):
466 # Test wiping behaviour.
467@@ -415,3 +470,306 @@ class TestBlockMeta(IntegrationTestCase):
468 )
469 finally:
470 server.stop()
471+
472+ def _do_test_resize(self, start, end, fstype):
473+ start <<= 20
474+ end <<= 20
475+ img = self.tmp_path('image.img')
476+ config = StorageConfigBuilder(version=2)
477+ config.add_image(path=img, size='200M', ptable='gpt')
478+ p1 = config.add_part(size=start, offset=1 << 20, number=1,
479+ fstype=fstype)
480+ self.run_bm(config.render())
481+ with loop_dev(img) as dev:
482+ self.create_data(dev, p1)
483+ self.assertEqual(
484+ summarize_partitions(dev), [
485+ PartData(number=1, offset=1 << 20, size=start),
486+ ])
487+ fs_size = _get_filesystem_size(dev, p1, fstype)
488+ self.assertEqual(start, fs_size)
489+
490+ config.set_preserve()
491+ p1['resize'] = True
492+ p1['size'] = end
493+ self.run_bm(config.render())
494+ with loop_dev(img) as dev:
495+ self.check_data(dev, p1)
496+ self.assertEqual(
497+ summarize_partitions(dev), [
498+ PartData(number=1, offset=1 << 20, size=end),
499+ ])
500+ fs_size = _get_filesystem_size(dev, p1, fstype)
501+ self.assertEqual(end, fs_size)
502+
503+ def test_resize_up_ext2(self):
504+ self._do_test_resize(40, 80, 'ext2')
505+
506+ def test_resize_down_ext2(self):
507+ self._do_test_resize(80, 40, 'ext2')
508+
509+ def test_resize_up_ext3(self):
510+ self._do_test_resize(40, 80, 'ext3')
511+
512+ def test_resize_down_ext3(self):
513+ self._do_test_resize(80, 40, 'ext3')
514+
515+ def test_resize_up_ext4(self):
516+ self._do_test_resize(40, 80, 'ext4')
517+
518+ def test_resize_down_ext4(self):
519+ self._do_test_resize(80, 40, 'ext4')
520+
521+ def test_resize_logical(self):
522+ img = self.tmp_path('image.img')
523+ config = StorageConfigBuilder(version=2)
524+ config.add_image(path=img, size='100M', ptable='msdos')
525+ config.add_part(size='50M', number=1, flag='extended', offset=1 << 20)
526+ config.add_part(size='10M', number=5, flag='logical', offset=2 << 20)
527+ p6 = config.add_part(size='10M', number=6, flag='logical',
528+ offset=13 << 20, fstype='ext4')
529+ self.run_bm(config.render())
530+
531+ with loop_dev(img) as dev:
532+ self.create_data(dev, p6)
533+ self.assertEqual(
534+ summarize_partitions(dev), [
535+ # extended partitions get a strange size in sysfs
536+ PartData(number=1, offset=1 << 20, size=1 << 10),
537+ PartData(number=5, offset=2 << 20, size=10 << 20),
538+ # part 5 takes us to 12 MiB offset, curtin leaves a 1 MiB
539+ # gap.
540+ PartData(number=6, offset=13 << 20, size=10 << 20),
541+ ])
542+ self.assertEqual(50 << 20, _get_extended_partition_size(dev, 1))
543+
544+ config.set_preserve()
545+ p6['resize'] = True
546+ p6['size'] = '20M'
547+ self.run_bm(config.render())
548+
549+ with loop_dev(img) as dev:
550+ self.check_data(dev, p6)
551+ self.assertEqual(
552+ summarize_partitions(dev), [
553+ PartData(number=1, offset=1 << 20, size=1 << 10),
554+ PartData(number=5, offset=2 << 20, size=10 << 20),
555+ PartData(number=6, offset=13 << 20, size=20 << 20),
556+ ])
557+ self.assertEqual(50 << 20, _get_extended_partition_size(dev, 1))
558+
559+ def test_resize_extended(self):
560+ img = self.tmp_path('image.img')
561+ config = StorageConfigBuilder(version=2)
562+ config.add_image(path=img, size='100M', ptable='msdos')
563+ p1 = config.add_part(size='50M', number=1, flag='extended',
564+ offset=1 << 20)
565+ p5 = config.add_part(size='49M', number=5, flag='logical',
566+ offset=2 << 20)
567+ self.run_bm(config.render())
568+
569+ with loop_dev(img) as dev:
570+ self.assertEqual(
571+ summarize_partitions(dev), [
572+ # extended partitions get a strange size in sysfs
573+ PartData(number=1, offset=1 << 20, size=1 << 10),
574+ PartData(number=5, offset=2 << 20, size=49 << 20),
575+ ])
576+ self.assertEqual(50 << 20, _get_extended_partition_size(dev, 1))
577+
578+ config.set_preserve()
579+ p1['resize'] = True
580+ p1['size'] = '99M'
581+ p5['resize'] = True
582+ p5['size'] = '98M'
583+ self.run_bm(config.render())
584+
585+ with loop_dev(img) as dev:
586+ self.assertEqual(
587+ summarize_partitions(dev), [
588+ PartData(number=1, offset=1 << 20, size=1 << 10),
589+ PartData(number=5, offset=2 << 20, size=98 << 20),
590+ ])
591+ self.assertEqual(99 << 20, _get_extended_partition_size(dev, 1))
592+
593+ def test_split(self):
594+ img = self.tmp_path('image.img')
595+ config = StorageConfigBuilder(version=2)
596+ config.add_image(path=img, size='200M', ptable='gpt')
597+ config.add_part(size=9 << 20, offset=1 << 20, number=1)
598+ p2 = config.add_part(size='180M', offset=10 << 20, number=2,
599+ fstype='ext4')
600+ self.run_bm(config.render())
601+ with loop_dev(img) as dev:
602+ self.create_data(dev, p2)
603+ self.assertEqual(
604+ summarize_partitions(dev), [
605+ PartData(number=1, offset=1 << 20, size=9 << 20),
606+ PartData(number=2, offset=10 << 20, size=180 << 20),
607+ ])
608+ self.assertEqual(180 << 20, _get_filesystem_size(dev, p2))
609+
610+ config.set_preserve()
611+ p2['resize'] = True
612+ p2['size'] = '80M'
613+ p3 = config.add_part(size='100M', offset=90 << 20, number=3,
614+ fstype='ext4')
615+ self.run_bm(config.render())
616+ with loop_dev(img) as dev:
617+ self.check_data(dev, p2)
618+ self.assertEqual(
619+ summarize_partitions(dev), [
620+ PartData(number=1, offset=1 << 20, size=9 << 20),
621+ PartData(number=2, offset=10 << 20, size=80 << 20),
622+ PartData(number=3, offset=90 << 20, size=100 << 20),
623+ ])
624+ self.assertEqual(80 << 20, _get_filesystem_size(dev, p2))
625+ self.assertEqual(100 << 20, _get_filesystem_size(dev, p3))
626+
627+ def test_partition_unify(self):
628+ img = self.tmp_path('image.img')
629+ config = StorageConfigBuilder(version=2)
630+ config.add_image(path=img, size='200M', ptable='gpt')
631+ config.add_part(size=9 << 20, offset=1 << 20, number=1)
632+ p2 = config.add_part(size='40M', offset=10 << 20, number=2,
633+ fstype='ext4')
634+ p3 = config.add_part(size='60M', offset=50 << 20, number=3,
635+ fstype='ext4')
636+ self.run_bm(config.render())
637+ with loop_dev(img) as dev:
638+ self.create_data(dev, p2)
639+ self.assertEqual(
640+ summarize_partitions(dev), [
641+ PartData(number=1, offset=1 << 20, size=9 << 20),
642+ PartData(number=2, offset=10 << 20, size=40 << 20),
643+ PartData(number=3, offset=50 << 20, size=60 << 20),
644+ ])
645+ self.assertEqual(40 << 20, _get_filesystem_size(dev, p2))
646+ self.assertEqual(60 << 20, _get_filesystem_size(dev, p3))
647+
648+ config = StorageConfigBuilder(version=2)
649+ config.add_image(path=img, size='200M', ptable='gpt')
650+ config.add_part(size=9 << 20, offset=1 << 20, number=1)
651+ p2 = config.add_part(size='100M', offset=10 << 20, number=2,
652+ fstype='ext4', resize=True)
653+ config.set_preserve()
654+ self.run_bm(config.render())
655+ with loop_dev(img) as dev:
656+ self.check_data(dev, p2)
657+ self.assertEqual(
658+ summarize_partitions(dev), [
659+ PartData(number=1, offset=1 << 20, size=9 << 20),
660+ PartData(number=2, offset=10 << 20, size=100 << 20),
661+ ])
662+ self.assertEqual(100 << 20, _get_filesystem_size(dev, p2))
663+
664+ def test_mix_of_operations_gpt(self):
665+ # a test that keeps, creates, resizes, and deletes a partition
666+ # 200 MiB disk, using full disk
667+ # init size preserve final size
668+ # p1 - 9 MiB yes 9MiB
669+ # p2 - 90 MiB yes, resize 139MiB
670+ # p3 - 99 MiB no 50MiB
671+ img = self.tmp_path('image.img')
672+ config = StorageConfigBuilder(version=2)
673+ config.add_image(path=img, size='200M', ptable='gpt')
674+ config.add_part(size=9 << 20, offset=1 << 20, number=1)
675+ p2 = config.add_part(size='90M', offset=10 << 20, number=2,
676+ fstype='ext4')
677+ p3 = config.add_part(size='99M', offset=100 << 20, number=3,
678+ fstype='ext4')
679+ self.run_bm(config.render())
680+ with loop_dev(img) as dev:
681+ self.create_data(dev, p2)
682+ self.assertEqual(
683+ summarize_partitions(dev), [
684+ PartData(number=1, offset=1 << 20, size=9 << 20),
685+ PartData(number=2, offset=10 << 20, size=90 << 20),
686+ PartData(number=3, offset=100 << 20, size=99 << 20),
687+ ])
688+ self.assertEqual(90 << 20, _get_filesystem_size(dev, p2))
689+ self.assertEqual(99 << 20, _get_filesystem_size(dev, p3))
690+
691+ config = StorageConfigBuilder(version=2)
692+ config.add_image(path=img, size='200M', ptable='gpt')
693+ config.add_part(size=9 << 20, offset=1 << 20, number=1)
694+ p2 = config.add_part(size='139M', offset=10 << 20, number=2,
695+ fstype='ext4', resize=True)
696+ config.set_preserve()
697+ p3 = config.add_part(size='50M', offset=149 << 20, number=3,
698+ fstype='ext4')
699+ self.run_bm(config.render())
700+ with loop_dev(img) as dev:
701+ self.check_data(dev, p2)
702+ self.assertEqual(
703+ summarize_partitions(dev), [
704+ PartData(number=1, offset=1 << 20, size=9 << 20),
705+ PartData(number=2, offset=10 << 20, size=139 << 20),
706+ PartData(number=3, offset=149 << 20, size=50 << 20),
707+ ])
708+ self.assertEqual(139 << 20, _get_filesystem_size(dev, p2))
709+ self.assertEqual(50 << 20, _get_filesystem_size(dev, p3))
710+
711+ def test_mix_of_operations_msdos(self):
712+ # a test that keeps, creates, resizes, and deletes a partition
713+ # including handling of extended/logical
714+ # 200 MiB disk, initially only using front 100MiB
715+ # flag init size preserve final size
716+ # p1 - primary 9MiB yes 9MiB
717+ # p2 - extended 89MiB yes, resize 189MiB
718+ # p3 - logical 37MiB yes, resize 137MiB
719+ # p4 - logical 50MiB no 50MiB
720+ img = self.tmp_path('image.img')
721+ config = StorageConfigBuilder(version=2)
722+ config.add_image(path=img, size='200M', ptable='msdos')
723+ p1 = config.add_part(size='9M', offset=1 << 20, number=1,
724+ fstype='ext4')
725+ config.add_part(size='89M', offset=10 << 20, number=2, flag='extended')
726+ p5 = config.add_part(size='36M', offset=11 << 20, number=5,
727+ flag='logical', fstype='ext4')
728+ p6 = config.add_part(size='50M', offset=49 << 20, number=6,
729+ flag='logical', fstype='ext4')
730+ self.run_bm(config.render())
731+
732+ with loop_dev(img) as dev:
733+ self.create_data(dev, p1)
734+ self.create_data(dev, p5)
735+ self.assertEqual(
736+ summarize_partitions(dev), [
737+ PartData(number=1, offset=1 << 20, size=9 << 20),
738+ PartData(number=2, offset=10 << 20, size=1 << 10),
739+ PartData(number=5, offset=11 << 20, size=36 << 20),
740+ PartData(number=6, offset=49 << 20, size=50 << 20),
741+ ])
742+ self.assertEqual(89 << 20, _get_extended_partition_size(dev, 2))
743+ self.assertEqual(9 << 20, _get_filesystem_size(dev, p1))
744+ self.assertEqual(36 << 20, _get_filesystem_size(dev, p5))
745+ self.assertEqual(50 << 20, _get_filesystem_size(dev, p6))
746+
747+ config = StorageConfigBuilder(version=2)
748+ config.add_image(path=img, size='200M', ptable='msdos')
749+ p1 = config.add_part(size='9M', offset=1 << 20, number=1,
750+ fstype='ext4')
751+ config.add_part(size='189M', offset=10 << 20, number=2,
752+ flag='extended', resize=True)
753+ p5 = config.add_part(size='136M', offset=11 << 20, number=5,
754+ flag='logical', fstype='ext4', resize=True)
755+ config.set_preserve()
756+ p6 = config.add_part(size='50M', offset=149 << 20, number=6,
757+ flag='logical', fstype='ext4')
758+ self.run_bm(config.render())
759+
760+ with loop_dev(img) as dev:
761+ self.check_data(dev, p1)
762+ self.check_data(dev, p5)
763+ self.assertEqual(
764+ summarize_partitions(dev), [
765+ PartData(number=1, offset=1 << 20, size=9 << 20),
766+ PartData(number=2, offset=10 << 20, size=1 << 10),
767+ PartData(number=5, offset=11 << 20, size=136 << 20),
768+ PartData(number=6, offset=149 << 20, size=50 << 20),
769+ ])
770+ self.assertEqual(189 << 20, _get_extended_partition_size(dev, 2))
771+ self.assertEqual(9 << 20, _get_filesystem_size(dev, p1))
772+ self.assertEqual(136 << 20, _get_filesystem_size(dev, p5))
773+ self.assertEqual(50 << 20, _get_filesystem_size(dev, p6))
774diff --git a/tests/unittests/test_commands_block_meta.py b/tests/unittests/test_commands_block_meta.py
775index 7c8e7bf..3698d32 100644
776--- a/tests/unittests/test_commands_block_meta.py
777+++ b/tests/unittests/test_commands_block_meta.py
778@@ -3,12 +3,16 @@
779 from argparse import Namespace
780 from collections import OrderedDict
781 import copy
782-from mock import patch, call
783+from mock import (
784+ call,
785+ Mock,
786+ patch,
787+)
788 import os
789 import random
790
791 from curtin.block import dasd
792-from curtin.commands import block_meta
793+from curtin.commands import block_meta, block_meta_v2
794 from curtin import paths, util
795 from .helpers import CiTestCase
796
797@@ -2613,6 +2617,133 @@ class TestPartitionVerifySfdisk(CiTestCase):
798 self.assertEqual([], self.m_verify_ptable_flag.call_args_list)
799
800
801+class TestPartitionVerifySfdiskV2(CiTestCase):
802+
803+ def setUp(self):
804+ super(TestPartitionVerifySfdiskV2, self).setUp()
805+ base = 'curtin.commands.block_meta_v2.'
806+ self.add_patch(base + 'verify_size', 'm_verify_size')
807+ self.add_patch(base + 'verify_ptable_flag', 'm_verify_ptable_flag')
808+ self.add_patch(base + 'os.path.realpath', 'm_realpath')
809+ self.m_realpath.side_effect = lambda x: x
810+ self.info = {
811+ 'id': 'disk-sda-part-2',
812+ 'type': 'partition',
813+ 'offset': '1GB',
814+ 'device': 'sda',
815+ 'number': 2,
816+ 'size': '5GB',
817+ 'flag': 'boot',
818+ }
819+ self.part_size = int(util.human2bytes(self.info['size']))
820+ self.devpath = self.random_string()
821+ self.sfdisk_part_info = {
822+ 'node': self.devpath,
823+ 'start': (1 << 30) // 512,
824+ }
825+ self.storage_config = {self.info['id']: self.info}
826+ self.label = self.random_string()
827+ self.table = Mock()
828+ self.table.sectors2bytes = lambda x: x * 512
829+
830+ def test_partition_verify_sfdisk(self):
831+ block_meta_v2.partition_verify_sfdisk_v2(self.info, self.label,
832+ self.sfdisk_part_info,
833+ self.storage_config,
834+ self.table)
835+ self.assertEqual(
836+ [call(self.devpath, self.part_size, self.sfdisk_part_info)],
837+ self.m_verify_size.call_args_list)
838+ self.assertEqual(
839+ [call(self.devpath, self.info['flag'], self.label,
840+ self.sfdisk_part_info)],
841+ self.m_verify_ptable_flag.call_args_list)
842+
843+ def test_partition_verify_no_moves(self):
844+ self.info['preserve'] = True
845+ self.info['resize'] = True
846+ self.info['offset'] = '2GB'
847+ with self.assertRaises(RuntimeError):
848+ block_meta_v2.partition_verify_sfdisk_v2(
849+ self.info, self.label, self.sfdisk_part_info,
850+ self.storage_config, self.table)
851+
852+
853+class TestPartitionNeedsResize(CiTestCase):
854+
855+ def setUp(self):
856+ super(TestPartitionNeedsResize, self).setUp()
857+ base = 'curtin.commands.block_meta_v2.'
858+ self.add_patch(base + 'os.path.realpath', 'm_realpath')
859+ self.add_patch(base + '_get_volume_fstype', 'm_get_volume_fstype')
860+ self.m_realpath.side_effect = lambda x: x
861+ self.partition = {
862+ 'id': 'disk-sda-part-2',
863+ 'type': 'partition',
864+ 'offset': '1GB',
865+ 'device': 'sda',
866+ 'number': 2,
867+ 'size': '5GB',
868+ 'flag': 'boot',
869+ }
870+ self.devpath = self.random_string()
871+ self.sfdisk_part_info = {
872+ 'node': self.devpath,
873+ 'start': (1 << 30) // 512,
874+ }
875+ self.format = {
876+ 'id': 'id-format',
877+ 'type': 'format',
878+ 'fstype': 'ext4',
879+ 'volume': self.partition['id'],
880+ }
881+ self.storage_config = {
882+ self.partition['id']: self.partition,
883+ self.format['id']: self.format,
884+ }
885+
886+ def test_partition_resize_change_fs(self):
887+ self.partition['preserve'] = True
888+ self.partition['resize'] = True
889+ self.format['preserve'] = True
890+ self.format['fstype'] = 'ext3'
891+ self.m_get_volume_fstype.return_value = 'ext4'
892+ with self.assertRaises(RuntimeError):
893+ block_meta_v2.needs_resize(
894+ self.storage_config, self.partition, self.sfdisk_part_info)
895+
896+ def test_partition_resize_unsupported_fs(self):
897+ self.partition['preserve'] = True
898+ self.partition['resize'] = True
899+ self.format['preserve'] = True
900+ self.format['fstype'] = 'reiserfs'
901+ self.m_get_volume_fstype.return_value = 'resierfs'
902+ with self.assertRaises(RuntimeError):
903+ block_meta_v2.needs_resize(
904+ self.storage_config, self.partition, self.sfdisk_part_info)
905+
906+ def test_partition_resize_format_preserve_false(self):
907+ # though the filesystem type is not supported for resize, it's ok
908+ # because with format preserve=False, we're recreating anyhow
909+ self.partition['preserve'] = True
910+ self.partition['resize'] = True
911+ self.format['preserve'] = False
912+ self.format['fstype'] = 'reiserfs'
913+ self.m_get_volume_fstype.return_value = 'reiserfs'
914+ block_meta_v2.needs_resize(
915+ self.storage_config, self.partition, self.sfdisk_part_info)
916+
917+ def test_partition_resize_partition_preserve_false(self):
918+ # not a resize - partition is recreated
919+ self.partition['preserve'] = False
920+ self.partition['resize'] = True
921+ self.format['preserve'] = False
922+ self.format['fstype'] = 'reiserfs'
923+ self.m_get_volume_fstype.return_value = 'reiserfs'
924+ block_meta_v2.needs_resize(
925+ self.storage_config, self.partition, self.sfdisk_part_info)
926+
927+
928 class TestPartitionVerifyFdasd(CiTestCase):
929
930 def setUp(self):
931diff --git a/tests/unittests/test_storage_config.py b/tests/unittests/test_storage_config.py
932index f0f8148..d6b0a36 100644
933--- a/tests/unittests/test_storage_config.py
934+++ b/tests/unittests/test_storage_config.py
935@@ -7,7 +7,7 @@ from curtin.storage_config import ProbertParser as baseparser
936 from curtin.storage_config import (BcacheParser, BlockdevParser, DasdParser,
937 DmcryptParser, FilesystemParser, LvmParser,
938 RaidParser, MountParser, ZfsParser)
939-from curtin.storage_config import ptable_uuid_to_flag_entry
940+from curtin.storage_config import ptable_uuid_to_flag_entry, select_configs
941 from curtin import util
942
943
944@@ -1117,4 +1117,26 @@ class TestExtractStorageConfig(CiTestCase):
945 self.assertEqual(expected_dict, bitlocker[0])
946
947
948+class TestSelectConfigs(CiTestCase):
949+ def test_basic(self):
950+ id0 = {'a': 1, 'b': 2}
951+ id1 = {'a': 1, 'c': 3}
952+ sc = {'id0': id0, 'id1': id1}
953+
954+ self.assertEqual([id0, id1], select_configs(sc, a=1))
955+
956+ def test_not_found(self):
957+ id0 = {'a': 1, 'b': 2}
958+ id1 = {'a': 1, 'c': 3}
959+ sc = {'id0': id0, 'id1': id1}
960+
961+ self.assertEqual([], select_configs(sc, a=4))
962+
963+ def test_multi_criteria(self):
964+ id0 = {'a': 1, 'b': 2}
965+ id1 = {'a': 1, 'c': 3}
966+ sc = {'id0': id0, 'id1': id1}
967+
968+ self.assertEqual([id0], select_configs(sc, a=1, b=2))
969+
970 # vi: ts=4 expandtab syntax=python

Subscribers

People subscribed via source and target branches