Skip to content

Commit

Permalink
Add parent attribute to volume setup
Browse files Browse the repository at this point in the history
For the btrfs volume management, allow to put a volume into a specific
parent volume. If not specified the volume is below the default volume
This Fixes #2316
  • Loading branch information
schaefi committed Jul 18, 2023
1 parent b44c13c commit 0ed2365
Show file tree
Hide file tree
Showing 10 changed files with 154 additions and 52 deletions.
17 changes: 15 additions & 2 deletions kiwi/mount_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
import time
import logging
from textwrap import dedent
from typing import List
from typing import (
List, Dict
)

# project
from kiwi.path import Path
Expand All @@ -41,9 +43,14 @@ class MountManager:
* :param string device: device node name
* :param string mountpoint: mountpoint directory name
* :param dict attributes: optional attributes to store
"""
def __init__(self, device: str, mountpoint: str = ''):
def __init__(
self, device: str, mountpoint: str = '',
attributes: Dict[str, str] = {}
):
self.device = device
self.attributes = attributes
if not mountpoint:
self.mountpoint_tempdir = Temporary(
prefix='kiwi_mount_manager.'
Expand All @@ -53,6 +60,12 @@ def __init__(self, device: str, mountpoint: str = ''):
Path.create(mountpoint)
self.mountpoint = mountpoint

def get_attributes(self) -> Dict[str, str]:
"""
Return attributes dict for this mount manager
"""
return self.attributes

def bind_mount(self) -> None:
"""
Bind mount the device to the mountpoint
Expand Down
8 changes: 8 additions & 0 deletions kiwi/schema/kiwi.rnc
Original file line number Diff line number Diff line change
Expand Up @@ -2564,6 +2564,13 @@ div {
## not specified the name specifies a path which has to
## exist inside the root directory.
attribute name { text }
k.volume.parent.attribute =
## The name/path of the parent volume.
## Evaluated only for the btrfs volume manager to allow
## specifying the parent subvolume to nest this volume in.
## If not specified the parent is always the volume set
## as the default volume
attribute parent { text }
k.volume.mountpoint.attribute =
## volume path. The mountpoint specifies a path which has to
## exist inside the root directory.
Expand Down Expand Up @@ -2596,6 +2603,7 @@ div {
k.volume.mountpoint.attribute? &
k.volume.label.attribute? &
k.volume.name.attribute &
k.volume.parent.attribute? &
k.volume.size.attribute?
k.volume =
## Specify which parts of the filesystem should be
Expand Down
12 changes: 12 additions & 0 deletions kiwi/schema/kiwi.rng
Original file line number Diff line number Diff line change
Expand Up @@ -3851,6 +3851,15 @@ not specified the name specifies a path which has to
exist inside the root directory.</a:documentation>
</attribute>
</define>
<define name="k.volume.parent.attribute">
<attribute name="parent">
<a:documentation>The name/path of the parent volume.
Evaluated only for the btrfs volume manager to allow
specifying the parent subvolume to nest this volume in.
If not specified the parent is always the volume set
as the default volume</a:documentation>
</attribute>
</define>
<define name="k.volume.mountpoint.attribute">
<attribute name="mountpoint">
<a:documentation>volume path. The mountpoint specifies a path which has to
Expand Down Expand Up @@ -3907,6 +3916,9 @@ The latter is the default.</a:documentation>
<ref name="k.volume.label.attribute"/>
</optional>
<ref name="k.volume.name.attribute"/>
<optional>
<ref name="k.volume.parent.attribute"/>
</optional>
<optional>
<ref name="k.volume.size.attribute"/>
</optional>
Expand Down
41 changes: 32 additions & 9 deletions kiwi/volume_manager/btrfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ def post_init(self, custom_args):

self.subvol_mount_list = []
self.toplevel_mount = None
self.toplevel_volume = None

def setup(self, name=None):
"""
Expand Down Expand Up @@ -154,6 +153,7 @@ def setup(self, name=None):
# Mount /{some-name}/.snapshots as /.snapshots inside the root
snapshots_mount = MountManager(
device=self.device,
attributes={'subvol': f'{self.root_volume_name}/.snapshots'},
mountpoint=snapshot + '/.snapshots'
)
self.subvol_mount_list.append(snapshots_mount)
Expand Down Expand Up @@ -191,6 +191,9 @@ def create_volumes(self, filesystem_name):
else:
log.info('--> sub volume %s', volume.realpath)
toplevel = self.mountpoint + f'/{self.root_volume_name}/'
if volume.parent:
toplevel = self.mountpoint + '/' + volume.parent

volume_parent_path = os.path.normpath(
toplevel + os.path.dirname(volume.realpath)
)
Expand All @@ -207,12 +210,19 @@ def create_volumes(self, filesystem_name):
)
volume_mountpoint = self.mountpoint + \
self.root_volume_name + '/'
attributes = {
'parent': volume.parent or ''
}
if self.custom_args['root_is_snapshot']:
volume_mountpoint = self.mountpoint + \
f'/{self.root_volume_name}/.snapshots/1/snapshot/'
attributes = {
'subvol': f'{self.root_volume_name}{volume.realpath}'
}

volume_mount = MountManager(
device=self.device,
attributes=attributes,
mountpoint=os.path.normpath(
volume_mountpoint + volume.realpath
)
Expand Down Expand Up @@ -245,12 +255,17 @@ def get_fstab(self, persistency_type='by-label', filesystem_name=None):
fs_check = self._is_volume_enabled_for_fs_check(
volume_mount.mountpoint
)
mount_point = volume_mount.mountpoint.replace(self.mountpoint, '')
if self.root_volume_name != '/':
mount_point = mount_point.replace(self.root_volume_name, '')

if self.custom_args['root_is_snapshot']:
mount_point = subvol_name.replace(self.root_volume_name, '')

fstab_entry = ' '.join(
[
blkid_type + '=' + device_id,
subvol_name.replace(
self.root_volume_name, ''
) if self.root_volume_name != '/' else subvol_name,
mount_point if mount_point.startswith(os.sep) else f'{os.sep}{mount_point}',
'btrfs', ','.join(mount_entry_options),
'0 {fs_passno}'.format(
fs_passno='2' if fs_check else '0'
Expand Down Expand Up @@ -295,8 +310,11 @@ def mount_volumes(self):

for volume_mount in self.subvol_mount_list:
if self.volumes_mounted_initially:
toplevel = self.root_volume_name
if self.custom_args['root_is_snapshot']:
toplevel = f'{self.root_volume_name}/.snapshots/1/snapshot'
volume_mount.mountpoint = os.path.normpath(
volume_mount.mountpoint.replace(self.toplevel_volume, '', 1)
volume_mount.mountpoint.replace(toplevel, '', 1)
)
if not os.path.exists(volume_mount.mountpoint):
Path.create(volume_mount.mountpoint)
Expand Down Expand Up @@ -432,7 +450,6 @@ def _set_default_volume(self, default_volume):
volume_id, self.mountpoint
]
)
self.toplevel_volume = default_volume
return

raise KiwiVolumeRootIDError(
Expand Down Expand Up @@ -514,10 +531,16 @@ def _get_subvol_name_from_mountpoint(self, volume_mount):
subvol_name = os.sep.join(
volume_mount.mountpoint.split(os.sep)[path_start_index:]
)
if self.toplevel_volume and self.toplevel_volume in subvol_name:
subvol_name = subvol_name.replace(self.toplevel_volume, '')
parent = self.root_volume_name
if volume_mount.get_attributes().get('parent'):
parent = volume_mount.attributes.get('parent')

if volume_mount.get_attributes().get('subvol'):
subvol_name = volume_mount.get_attributes().get('subvol')
parent = ''

return os.path.normpath(
os.sep.join([self.root_volume_name, subvol_name]).replace('//', '/')
os.sep.join([parent, subvol_name]).replace('//', '/').lstrip(os.sep)
)

def __del__(self):
Expand Down
12 changes: 11 additions & 1 deletion kiwi/xml_parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -4984,14 +4984,15 @@ class volume(GeneratedsSuper):
"""Specify which parts of the filesystem should be on an extra volume."""
subclass = None
superclass = None
def __init__(self, copy_on_write=None, filesystem_check=None, freespace=None, mountpoint=None, label=None, name=None, size=None):
def __init__(self, copy_on_write=None, filesystem_check=None, freespace=None, mountpoint=None, label=None, name=None, parent=None, size=None):
self.original_tagname_ = None
self.copy_on_write = _cast(bool, copy_on_write)
self.filesystem_check = _cast(bool, filesystem_check)
self.freespace = _cast(None, freespace)
self.mountpoint = _cast(None, mountpoint)
self.label = _cast(None, label)
self.name = _cast(None, name)
self.parent = _cast(None, parent)
self.size = _cast(None, size)
def factory(*args_, **kwargs_):
if CurrentSubclassModule_ is not None:
Expand All @@ -5016,6 +5017,8 @@ def get_label(self): return self.label
def set_label(self, label): self.label = label
def get_name(self): return self.name
def set_name(self, name): self.name = name
def get_parent(self): return self.parent
def set_parent(self, parent): self.parent = parent
def get_size(self): return self.size
def set_size(self, size): self.size = size
def validate_volume_size_type(self, value):
Expand Down Expand Up @@ -5071,6 +5074,9 @@ def exportAttributes(self, outfile, level, already_processed, namespaceprefix_='
if self.name is not None and 'name' not in already_processed:
already_processed.add('name')
outfile.write(' name=%s' % (self.gds_encode(self.gds_format_string(quote_attrib(self.name), input_name='name')), ))
if self.parent is not None and 'parent' not in already_processed:
already_processed.add('parent')
outfile.write(' parent=%s' % (self.gds_encode(self.gds_format_string(quote_attrib(self.parent), input_name='parent')), ))
if self.size is not None and 'size' not in already_processed:
already_processed.add('size')
outfile.write(' size=%s' % (quote_attrib(self.size), ))
Expand Down Expand Up @@ -5120,6 +5126,10 @@ def buildAttributes(self, node, attrs, already_processed):
if value is not None and 'name' not in already_processed:
already_processed.add('name')
self.name = value
value = find_attr_value_('parent', node)
if value is not None and 'parent' not in already_processed:
already_processed.add('parent')
self.parent = value
value = find_attr_value_('size', node)
if value is not None and 'size' not in already_processed:
already_processed.add('size')
Expand Down
6 changes: 6 additions & 0 deletions kiwi/xml_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
volume_type = NamedTuple(
'volume_type', [
('name', str),
('parent', str),
('size', str),
('realpath', str),
('mountpoint', Optional[str]),
Expand Down Expand Up @@ -1574,6 +1575,7 @@ def get_volumes(self) -> List[volume_type]:
[
volume_type(
name=volume_name,
parent=volume_parent,
size=volume_size,
realpath=path,
mountpoint=path,
Expand All @@ -1600,6 +1602,7 @@ def get_volumes(self) -> List[volume_type]:
# volume setup for a full qualified volume with name and
# mountpoint information. See below for exceptions
name = volume.get_name()
parent = volume.get_parent() or ''
mountpoint = volume.get_mountpoint()
realpath = mountpoint
size = volume.get_size()
Expand Down Expand Up @@ -1667,6 +1670,7 @@ def get_volumes(self) -> List[volume_type]:
volume_type_list.append(
volume_type(
name=name,
parent=parent,
size=size,
fullsize=fullsize,
mountpoint=mountpoint,
Expand Down Expand Up @@ -1694,6 +1698,7 @@ def get_volumes(self) -> List[volume_type]:
volume_type_list.append(
volume_type(
name=root_volume_name,
parent='',
size=size,
fullsize=fullsize,
mountpoint=None,
Expand All @@ -1708,6 +1713,7 @@ def get_volumes(self) -> List[volume_type]:
volume_type_list.append(
volume_type(
name=swap_name,
parent='',
size='size:{0}'.format(swap_mbytes),
fullsize=False,
mountpoint=None,
Expand Down
3 changes: 3 additions & 0 deletions test/unit/mount_manager_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ def setup(self, mock_path_create):
def setup_method(self, cls, mock_path_create):
self.setup()

def test_get_attributes(self):
assert self.mount_manager.get_attributes() == {}

@patch('kiwi.mount_manager.Temporary')
def test_setup_empty_mountpoint(self, mock_Temporary):
mock_Temporary.return_value.new_dir.return_value.name = 'tmpdir'
Expand Down
Loading

0 comments on commit 0ed2365

Please sign in to comment.