Skip to content

Commit

Permalink
Merge pull request #2657 from OSInside/volume_quotas
Browse files Browse the repository at this point in the history
Add quota attribute to volume section
  • Loading branch information
Conan-Kudo authored Sep 26, 2024
2 parents fb32551 + 8a087f8 commit 9215ac5
Show file tree
Hide file tree
Showing 10 changed files with 132 additions and 5 deletions.
2 changes: 1 addition & 1 deletion build-tests/x86/tumbleweed/test-image-disk/appliance.kiwi
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
<oem-multipath-scan>false</oem-multipath-scan>
</oemconfig>
<systemdisk>
<volume name="home"/>
<volume name="home" quota="5G"/>
</systemdisk>
</type>
</preferences>
Expand Down
5 changes: 4 additions & 1 deletion doc/source/working_with_images/custom_volumes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ elements of the `systemdisk` element:
<volume name="usr/lib" size="1G" label="library"/>
<volume name="@root" freespace="500M"/>
<volume name="etc_volume" mountpoint="etc" copy_on_write="false"/>
<volume name="bin_volume" size="all" mountpoint="/usr/bin"/>
<volume name="bin_volume" size="all" mountpoint="/usr/bin" quota="2G"/>
</systemdisk>
</type>
</image>
Expand Down Expand Up @@ -73,6 +73,9 @@ attributes:
- `copy_on_write`: Optional attribute to set the filesystem copy-on-write
attribute for this volume.

- `quota`: Optional attribute for the `btrfs` filesystem only. Allows
to specify a quota size for the generated volume.

- `filesystem_check`: Optional attribute to indicate that this
filesystem should perform the validation to become filesystem checked.
The actual constraints if the check is performed or not depends on
Expand Down
17 changes: 17 additions & 0 deletions kiwi/schema/kiwi.rnc
Original file line number Diff line number Diff line change
Expand Up @@ -2605,6 +2605,18 @@ div {
# common element <volume>
#
div {
sch:pattern [
abstract = "true"
id = "btrfs_quota"
sch:rule [
context = "volume"
sch:assert [
test = "not(@quota) or ../../@filesystem='btrfs'"
"quota attribute is only available for the following "
"image filesystem: btrfs"
]
]
]
k.volume.freespace.attribute =
## free space to be added to this volume. The value is
## used as MB by default but you can add "M" and/or "G" as
Expand Down Expand Up @@ -2635,6 +2647,10 @@ div {
k.volume.copy_on_write.attribute =
## Apply the filesystem copy-on-write attribute for this volume
attribute copy_on_write { xsd:boolean }
k.volume.quota.attribute =
## Apply quota value to filesystem volume if supported
attribute quota { partition-size-type }
>> sch:pattern [ id = "quota" is-a = "btrfs_quota" ]
k.volume.filesystem_check =
## Indicate that this filesystem should perform the validation
## to become filesystem checked. The actual constraints if the
Expand All @@ -2650,6 +2666,7 @@ div {
k.volume.arch.attribute = k.arch.attribute
k.volume.attlist =
k.volume.copy_on_write.attribute? &
k.volume.quota.attribute? &
k.volume.filesystem_check? &
k.volume.freespace.attribute? &
k.volume.mountpoint.attribute? &
Expand Down
15 changes: 15 additions & 0 deletions kiwi/schema/kiwi.rng
Original file line number Diff line number Diff line change
Expand Up @@ -3933,6 +3933,11 @@ Allowed values are: t.linux</a:documentation>
-->
<div>
<sch:pattern abstract="true" id="btrfs_quota">
<sch:rule context="volume">
<sch:assert test="not(@quota) or ../../@filesystem='btrfs'">quota attribute is only available for the following image filesystem: btrfs</sch:assert>
</sch:rule>
</sch:pattern>
<define name="k.volume.freespace.attribute">
<attribute name="freespace">
<a:documentation>free space to be added to this volume. The value is
Expand Down Expand Up @@ -3978,6 +3983,13 @@ add "M" and/or "G" as postfix</a:documentation>
<data type="boolean"/>
</attribute>
</define>
<define name="k.volume.quota.attribute">
<attribute name="quota">
<a:documentation>Apply quota value to filesystem volume if supported</a:documentation>
<ref name="partition-size-type"/>
</attribute>
<sch:pattern id="quota" is-a="btrfs_quota"/>
</define>
<define name="k.volume.filesystem_check">
<attribute name="filesystem_check">
<a:documentation>Indicate that this filesystem should perform the validation
Expand All @@ -4003,6 +4015,9 @@ The latter is the default.</a:documentation>
<optional>
<ref name="k.volume.copy_on_write.attribute"/>
</optional>
<optional>
<ref name="k.volume.quota.attribute"/>
</optional>
<optional>
<ref name="k.volume.filesystem_check"/>
</optional>
Expand Down
15 changes: 15 additions & 0 deletions kiwi/volume_manager/btrfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,10 @@ def create_volumes(self, filesystem_name):
os.path.normpath(toplevel + os.sep + volume.realpath)
]
)
self._apply_quota(
os.path.normpath(toplevel + os.sep + volume.realpath),
volume.attributes
)
self.apply_attributes_on_volume(
toplevel, volume
)
Expand Down Expand Up @@ -439,6 +443,17 @@ def set_property_readonly_root(self):
['btrfs', 'property', 'set', sync_target, 'ro', 'true']
)

def _apply_quota(self, volume_path: str, attributes: List[str]):
for attribute in attributes:
if attribute.startswith('quota='):
quota = attribute.split('=')[1]
Command.run(
['btrfs', 'quota', 'enable', '--simple', volume_path]
)
Command.run(
['btrfs', 'qgroup', 'limit', quota, volume_path]
)

def _has_root_volume(self) -> bool:
has_root_volume = bool(self.custom_args['root_is_subvolume'])
if self.custom_args['root_is_subvolume'] is None:
Expand Down
21 changes: 20 additions & 1 deletion kiwi/xml_parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -5100,9 +5100,10 @@ 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, parent=None, size=None, arch=None):
def __init__(self, copy_on_write=None, quota=None, filesystem_check=None, freespace=None, mountpoint=None, label=None, name=None, parent=None, size=None, arch=None):
self.original_tagname_ = None
self.copy_on_write = _cast(bool, copy_on_write)
self.quota = _cast(None, quota)
self.filesystem_check = _cast(bool, filesystem_check)
self.freespace = _cast(None, freespace)
self.mountpoint = _cast(None, mountpoint)
Expand All @@ -5124,6 +5125,8 @@ def factory(*args_, **kwargs_):
factory = staticmethod(factory)
def get_copy_on_write(self): return self.copy_on_write
def set_copy_on_write(self, copy_on_write): self.copy_on_write = copy_on_write
def get_quota(self): return self.quota
def set_quota(self, quota): self.quota = quota
def get_filesystem_check(self): return self.filesystem_check
def set_filesystem_check(self, filesystem_check): self.filesystem_check = filesystem_check
def get_freespace(self): return self.freespace
Expand All @@ -5140,6 +5143,13 @@ def get_size(self): return self.size
def set_size(self, size): self.size = size
def get_arch(self): return self.arch
def set_arch(self, arch): self.arch = arch
def validate_partition_size_type(self, value):
# Validate type partition-size-type, a restriction on xs:token.
if value is not None and Validate_simpletypes_:
if not self.gds_validate_simple_patterns(
self.validate_partition_size_type_patterns_, value):
warnings_.warn('Value "%s" does not match xsd pattern restrictions: %s' % (value.encode('utf-8'), self.validate_partition_size_type_patterns_, ))
validate_partition_size_type_patterns_ = [['^(\\d+|\\d+M|\\d+G)$']]
def validate_volume_size_type(self, value):
# Validate type volume-size-type, a restriction on xs:token.
if value is not None and Validate_simpletypes_:
Expand Down Expand Up @@ -5185,6 +5195,9 @@ def exportAttributes(self, outfile, level, already_processed, namespaceprefix_='
if self.copy_on_write is not None and 'copy_on_write' not in already_processed:
already_processed.add('copy_on_write')
outfile.write(' copy_on_write="%s"' % self.gds_format_boolean(self.copy_on_write, input_name='copy_on_write'))
if self.quota is not None and 'quota' not in already_processed:
already_processed.add('quota')
outfile.write(' quota=%s' % (quote_attrib(self.quota), ))
if self.filesystem_check is not None and 'filesystem_check' not in already_processed:
already_processed.add('filesystem_check')
outfile.write(' filesystem_check="%s"' % self.gds_format_boolean(self.filesystem_check, input_name='filesystem_check'))
Expand Down Expand Up @@ -5228,6 +5241,12 @@ def buildAttributes(self, node, attrs, already_processed):
self.copy_on_write = False
else:
raise_parse_error(node, 'Bad boolean attribute')
value = find_attr_value_('quota', node)
if value is not None and 'quota' not in already_processed:
already_processed.add('quota')
self.quota = value
self.quota = ' '.join(self.quota.split())
self.validate_partition_size_type(self.quota) # validate type partition-size-type
value = find_attr_value_('filesystem_check', node)
if value is not None and 'filesystem_check' not in already_processed:
already_processed.add('filesystem_check')
Expand Down
3 changes: 3 additions & 0 deletions kiwi/xml_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -1773,6 +1773,9 @@ def get_volumes(self) -> List[volume_type]:
attributes = []
is_root_volume = False

if volume.get_quota():
attributes.append(f'quota={volume.get_quota()}')

if volume.get_copy_on_write() is False:
# by default copy-on-write is switched on for any
# filesystem. Thus only if no copy on write is requested
Expand Down
27 changes: 27 additions & 0 deletions test/data/example_btrfs_vol_config.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>

<image schemaversion="8.2" name="custom-btrfs-volume-setup">
<description type="system">
<author>Marcus Schäfer</author>
<contact>[email protected]</contact>
<specification>Some</specification>
</description>
<preferences>
<version>1.15.5</version>
<packagemanager>zypper</packagemanager>
<type image="oem" filesystem="btrfs">
<systemdisk>
<volume name="some" quota="500M"/>
</systemdisk>
</type>
</preferences>
<repository>
<source path="obs://some/repo/oss"/>
</repository>
<packages type="image">
<package name="patterns-openSUSE-base"/>
</packages>
<packages type="bootstrap">
<package name="filesystem"/>
</packages>
</image>
6 changes: 4 additions & 2 deletions test/unit/volume_manager/btrfs_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def setup(self, mock_path):
volume_type(
name='etc', parent='', size='freespace:200', realpath='/etc',
mountpoint='/etc', fullsize=False, label=None,
attributes=[], is_root_volume=False
attributes=['quota=2G'], is_root_volume=False
),
volume_type(
name='myvol', parent='', size='size:500', realpath='/data',
Expand Down Expand Up @@ -262,7 +262,7 @@ def test_create_volumes(
'tmpdir/@', volume_type(
name='etc', parent='', size='freespace:200', realpath='/etc',
mountpoint='/etc', fullsize=False, label=None,
attributes=[],
attributes=['quota=2G'],
is_root_volume=False
)
),
Expand All @@ -286,6 +286,8 @@ def test_create_volumes(
assert mock_command.call_args_list == [
call(['btrfs', 'subvolume', 'create', 'tmpdir/@/data']),
call(['btrfs', 'subvolume', 'create', 'tmpdir/@/etc']),
call(['btrfs', 'quota', 'enable', '--simple', 'tmpdir/@/etc']),
call(['btrfs', 'qgroup', 'limit', '2G', 'tmpdir/@/etc']),
call(['btrfs', 'subvolume', 'create', 'tmpdir/@/home'])
]
assert mock_mount.call_args_list == [
Expand Down
26 changes: 26 additions & 0 deletions test/unit/xml_state_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,32 @@ def test_get_volumes_custom_root_volume_name(self):
)
]

def test_get_volumes_btrfs_quota(self):
description = XMLDescription(
'../data/example_btrfs_vol_config.xml'
)
xml_data = description.load()
state = XMLState(xml_data)
volume_type = self.volume_type
assert state.get_volumes() == [
volume_type(
name='some', parent='', size='freespace:120',
realpath='some',
mountpoint='some', fullsize=False,
label=None,
attributes=['quota=500M'],
is_root_volume=False
),
volume_type(
name='', parent='', size=None,
realpath='/',
mountpoint=None, fullsize=True,
label=None,
attributes=[],
is_root_volume=True
)
]

def test_get_volumes_for_arch(self):
description = XMLDescription('../data/example_lvm_arch_config.xml')
xml_data = description.load()
Expand Down

0 comments on commit 9215ac5

Please sign in to comment.