Skip to content

Commit c1a2851

Browse files
committed
overlay lib: draft new source OVL design... tbd
1 parent 37d401f commit c1a2851

File tree

1 file changed

+218
-2
lines changed

1 file changed

+218
-2
lines changed

repos/system_upgrade/common/libraries/overlaygen.py

Lines changed: 218 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,65 @@
1010

1111
OVERLAY_DO_NOT_MOUNT = ('tmpfs', 'devpts', 'sysfs', 'proc', 'cramfs', 'sysv', 'vfat')
1212

13+
# NOTE(pstodulk): what about using more closer values and than just multiply
14+
# the final result by magical constant?... this number is most likely going to
15+
# be lowered and affected by XFS vs EXT4 FSs that needs different spaces each
16+
# of them.
17+
_MAGICAL_CONSTANT_OVL_SIZE = 128
18+
"""
19+
Average size of created disk space images.
20+
21+
The size can be lower or higher - usually lower. The value is higher as we want
22+
to rather prevent future actions in advance instead of resolving later issues
23+
with the missing space.
24+
"""
25+
26+
_MAGICAL_CONSTANT_MIN_CONSUMED_SPACE = 2500
27+
"""
28+
Average space consumed on downloads and target userspace container installation.
29+
30+
On minimal systems it's approx 1.7GiB. Using higher value little bit to cover
31+
most expected cases. The value can be enlarged in future, but currently seems
32+
to me as a good compromise.
33+
"""
34+
1335

1436
MountPoints = namedtuple('MountPoints', ['fs_file', 'fs_vfstype'])
1537

1638

39+
def get_recommended_leapp_free_space(userspace_path=''):
40+
"""
41+
Return recommended free space on partition hosting target userspace container in MB
42+
43+
If the path to the container is set, the returned value is updated to
44+
reflect already consumed space.
45+
46+
TODO(pstodulk): this is so far the best trade off between stay safe and do
47+
do not consume too much space
48+
"""
49+
return _MAGICAL_CONSTANT_MIN_CONSUMED_SPACE
50+
51+
def _ensure_enough_diskimage_space(space_needed, directory):
52+
# TODO: not changed yet
53+
stat = os.statvfs(directory)
54+
if (stat.f_frsize * stat.f_bavail) < (space_needed * 1024 * 1024):
55+
# TODO(pstodulk): update the msg? People would be still thinking about
56+
# LEAPP_OVL_SIZE envar that is obsoleted.
57+
message = ('Not enough space available for creating required disk images in {directory}. ' +
58+
'Needed: {space_needed} MiB').format(space_needed=space_needed, directory=directory)
59+
api.current_logger().error(message)
60+
raise StopActorExecutionError(message)
61+
62+
1763
def _get_mountpoints(storage_info):
64+
# TODO: not changed yet
1865
mount_points = set()
1966
for entry in storage_info.fstab:
2067
if os.path.isdir(entry.fs_file) and entry.fs_vfstype not in OVERLAY_DO_NOT_MOUNT:
2168
mount_points.add(MountPoints(entry.fs_file, entry.fs_vfstype))
2269
elif os.path.isdir(entry.fs_file) and entry.fs_vfstype == 'vfat':
70+
# VFAT FS is not supported to be used for any system partition,
71+
# so we can safely ignore it
2372
api.current_logger().warning(
2473
'Ignoring vfat {} filesystem mount during upgrade process'.format(entry.fs_file)
2574
)
@@ -35,6 +84,29 @@ def _mount_dir(mounts_dir, mountpoint):
3584
return os.path.join(mounts_dir, _mount_name(mountpoint))
3685

3786

87+
def _prepare_required_mounts(scratch_dir, mounts_dir, storage_info, scratch_reserve):
88+
mount_points = _get_mountpoints(storage_info)
89+
# TODO(pstodulk): update the calculation for bind mounted mount_points (skip)
90+
space_needed = scratch_reserve + _MAGICAL_CONSTANT_OVL_SIZE * len(mount_points)
91+
disk_images_directory = os.path.join(scratch_dir, 'diskimages')
92+
93+
# Ensure we cleanup old disk images before we check for space constraints.
94+
# NOTE(pstodulk): Could we improve the process so we create imgs & calculate
95+
# the required disk space just once during each leapp (pre)upgrade run?
96+
run(['rm', '-rf', disk_images_directory])
97+
_create_diskimages_dir(scratch_dir, disk_images_directory)
98+
_ensure_enough_diskimage_space(space_needed, scratch_dir)
99+
100+
result = {}
101+
for mountpoint in mount_points:
102+
image = _create_mount_disk_image(disk_images_directory, mountpoint.fs_file)
103+
result[mountpoint.fs_file] = mounting.LoopMount(
104+
source=image,
105+
target=_mount_dir(mounts_dir, mountpoint.fs_file)
106+
)
107+
return result
108+
109+
38110
@contextlib.contextmanager
39111
def _build_overlay_mount(root_mount, mounts):
40112
if not root_mount:
@@ -70,6 +142,106 @@ def cleanup_scratch(scratch_dir, mounts_dir):
70142
api.current_logger().debug('Recursively removed scratch directory %s.', scratch_dir)
71143

72144

145+
def _format_disk_image_ext4(diskimage_path):
146+
"""
147+
Format the specified disk image with Ext4 filesystem.
148+
149+
The formatted file system is optimized for operations we want to do and
150+
mainly for the space it needs to take for the initialisation. So use 32MiB
151+
journal (that's enough for us as we do not plan to do too many operations
152+
inside) for any size of the disk image. Also the lazy
153+
initialisation is disabled. The formatting will be slower, but it helps
154+
us to estimate better the needed amount of the space for other actions
155+
done later.
156+
"""
157+
api.current_logger().debug('Creating ext4 filesystem in disk image at %s', diskimage_path)
158+
cmd = [
159+
'/sbin/mkfs.ext4',
160+
'-J', 'size=32',
161+
'-E', 'lazy_itable_init=0,lazy_journal_init=0',
162+
'-F', diskimage_path
163+
]
164+
try:
165+
utils.call_with_oserror_handled(cmd=cmd)
166+
except CalledProcessError as e:
167+
# FIXME(pstodulk): taken from original, but %s seems to me invalid here
168+
api.current_logger().error('Failed to create ext4 filesystem %s', exc_info=True)
169+
raise StopActorExecutionError(
170+
message=str(e)
171+
)
172+
173+
def _format_disk_image_xfs(diskimage_path):
174+
"""
175+
Format the specified disk image with XFS filesystem.
176+
177+
Set journal just to 32MiB always as we will not need to do too many operation
178+
inside, so 32MiB should enough for us.
179+
"""
180+
api.current_logger().debug('Creating XFS filesystem in disk image at %s', diskimage_path)
181+
cmd = ['/sbin/mkfs.xfs', '-l', 'size=32m', '-f', diskimage_path]
182+
try:
183+
utils.call_with_oserror_handled(cmd=cmd)
184+
except CalledProcessError as e:
185+
# FIXME(pstodulk): taken from original, but %s seems to me invalid here
186+
api.current_logger().error('Failed to create XFS filesystem %s', exc_info=True)
187+
raise StopActorExecutionError(
188+
message=str(e)
189+
)
190+
191+
192+
def _create_mount_disk_image(disk_images_directory, path, disk_size):
193+
"""
194+
Creates the mount disk image
195+
196+
The disk image is represented by a sparse file which apparent size
197+
corresponds to the free space of a particular partition/volume it
198+
represents.
199+
200+
The created disk image uses is formatted with XFS (default) or Ext4 FS
201+
and it's supposed to be used for write directories of an overlayfs built
202+
above it.
203+
"""
204+
diskimage_path = os.path.join(disk_images_directory, _mount_name(path))
205+
# TODO(pstodulk): update the disk_size
206+
207+
api.current_logger().debug('Attempting to create disk image with size %d MiB at %s', disk_size, diskimage_path)
208+
# TODO(pstodulk): not sure the hint is still valid..
209+
utils.call_with_failure_hint(
210+
cmd=['/bin/dd', 'if=/dev/zero', 'of={}'.format(diskimage_path), 'bs=1M', 'count=0', 'seek={}'.format(disk_size)],
211+
hint='Please ensure that there is enough diskspace in {}.'.format(diskimage_path)
212+
)
213+
214+
if get_env('LEAPP_OVL_IMG_FS_EXT4', '0') == '1':
215+
# This is alternative to XFS in case we find some issues, to be able
216+
# to switch simply to Ext4, so we will be able to simple investigate
217+
# possible issues between overlay <-> XFS if any happens.
218+
_format_disk_image_ext4(diskimage_path)
219+
else:
220+
_format_disk_image_xfs(diskimage_path)
221+
222+
return diskimage_path
223+
224+
225+
def _create_diskimages_dir(scratch_dir, diskimages_dir):
226+
"""
227+
Prepares directories for disk images
228+
"""
229+
api.current_logger().debug('Creating disk images directory.')
230+
try:
231+
utils.makedirs(diskimages_dir)
232+
api.current_logger().debug('Done creating disk images directory.')
233+
except OSError:
234+
api.current_logger().error('Failed to create disk images directory %s', diskimages_dir, exc_info=True)
235+
236+
# This is an attempt for giving the user a chance to resolve it on their own
237+
raise StopActorExecutionError(
238+
message='Failed to prepare environment for package download while creating directories.',
239+
details={
240+
'hint': 'Please ensure that {scratch_dir} is empty and modifiable.'.format(scratch_dir=scratch_dir)
241+
}
242+
)
243+
244+
73245
def _create_mounts_dir(scratch_dir, mounts_dir):
74246
"""
75247
Prepares directories for mounts
@@ -102,15 +274,59 @@ def _mount_dnf_cache(overlay_target):
102274

103275

104276
@contextlib.contextmanager
105-
def create_source_overlay(mounts_dir, scratch_dir, xfs_info, storage_info, mount_target=None):
277+
def create_source_overlay(mounts_dir, scratch_dir, xfs_info, storage_info, mount_target=None, scratch_reserve=0):
106278
"""
107279
Context manager that prepares the source system overlay and yields the mount.
280+
281+
The in-place upgrade itself requires to do some changes on the system to be
282+
able to perform the in-place upgrade itself - or even to be able to evaluate
283+
if the system is possible to upgrade. However, we do not want to (and must not)
284+
change the original system until we pass beyond the point of not return.
285+
286+
For that purposes we have to create a layer above the real host file system,
287+
where we can safely perform all operations without affecting the system
288+
setup, rpm database, etc. Currently overlay (OVL) technology showed it is
289+
capable to handle our requirements good enough - with some limitations.
290+
291+
This function prepares a disk image and an overlay layer for each
292+
mountpoint configured in /etc/fstab, excluding those with FS type noted
293+
in the OVERLAY_DO_NOT_MOUNT set. Such prepared OVL images are then composed
294+
together to reflect the real host filesystem. In the end everything is cleaned.
295+
296+
The new solution can be now problematic for system with too many partitions
297+
and loop devices. For such systems we keep for now the possibility of the
298+
fallback to an old solution, which has however number of issues that are
299+
fixed by the new design. To fallback to the old solution, set envar:
300+
LEAPP_OVL_FALLBACK=1
301+
302+
Disk images created for OVL are formatted with XFS by default. In case of
303+
problems, it's possible to switch to Ext4 FS using:
304+
LEAPP_OVL_IMG_FS_EXT4=1
305+
306+
:param mounts_dir: Absolute path to the directory under which all mounts should happen.
307+
:type mounts_dir: str
308+
:param scratch_dir: Absolute path to the directory in which all disk and OVL images are stored.
309+
:type scratch_dir: str
310+
:param xfs_info: The XFSPresence message.
311+
:type xfs_info: leapp.models.XFSPresence
312+
:param storage_info: The StorageInfo message.
313+
:type storage_info: leapp.models.StorageInfo
314+
:param mount_target: Directory to which whole source OVL layer should be bind mounted.
315+
If None (default), mounting.NullMount is created instead
316+
:type mount_target: Optional[str]
317+
:param scratch_reserve: Number of MB that should be extra reserved in a partition hosting the scratch_dir.
318+
:type scratch_reserve: Optional[int]
319+
:rtype: mounting.BindMount or mounting.NullMount
108320
"""
109321
api.current_logger().debug('Creating source overlay in {scratch_dir} with mounts in {mounts_dir}'.format(
110322
scratch_dir=scratch_dir, mounts_dir=mounts_dir))
111323
try:
112324
_create_mounts_dir(scratch_dir, mounts_dir)
113-
mounts = _prepare_required_mounts_old(scratch_dir, mounts_dir, _get_mountpoints(storage_info), xfs_info)
325+
if get_env('LEAPP_OVL_FALLBACK', '0') != '1':
326+
mounts = _prepare_required_mounts(scratch_dir, mounts_dir, storage_info, scratch_reserve)
327+
else:
328+
# fallback to the deprecated OVL solution
329+
mounts = _prepare_required_mounts_old(scratch_dir, mounts_dir, _get_mountpoints(storage_info), xfs_info)
114330
with mounts.pop('/') as root_mount:
115331
with mounting.OverlayMount(name='system_overlay', source='/', workdir=root_mount.target) as root_overlay:
116332
if mount_target:

0 commit comments

Comments
 (0)