diff --git a/build-tests/x86/tumbleweed/test-image-disk/appliance.kiwi b/build-tests/x86/tumbleweed/test-image-disk/appliance.kiwi
index 9eb4f6e3bdf..d54df14db70 100644
--- a/build-tests/x86/tumbleweed/test-image-disk/appliance.kiwi
+++ b/build-tests/x86/tumbleweed/test-image-disk/appliance.kiwi
@@ -24,7 +24,7 @@
false
-
+
diff --git a/doc/source/working_with_images/custom_volumes.rst b/doc/source/working_with_images/custom_volumes.rst
index bf376a12f88..82a23abeadc 100644
--- a/doc/source/working_with_images/custom_volumes.rst
+++ b/doc/source/working_with_images/custom_volumes.rst
@@ -25,7 +25,7 @@ elements of the `systemdisk` element:
-
+
@@ -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
diff --git a/kiwi/schema/kiwi.rnc b/kiwi/schema/kiwi.rnc
index 7aa763fde03..38f629172ea 100644
--- a/kiwi/schema/kiwi.rnc
+++ b/kiwi/schema/kiwi.rnc
@@ -2605,6 +2605,18 @@ div {
# common element
#
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
@@ -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
@@ -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? &
diff --git a/kiwi/schema/kiwi.rng b/kiwi/schema/kiwi.rng
index 8857a1fc595..85907f868ee 100644
--- a/kiwi/schema/kiwi.rng
+++ b/kiwi/schema/kiwi.rng
@@ -3933,6 +3933,11 @@ Allowed values are: t.linux
-->
+
+
+ quota attribute is only available for the following image filesystem: btrfs
+
+ free space to be added to this volume. The value is
@@ -3978,6 +3983,13 @@ add "M" and/or "G" as postfix
+
+
+ Apply quota value to filesystem volume if supported
+
+
+
+ Indicate that this filesystem should perform the validation
@@ -4003,6 +4015,9 @@ The latter is the default.
+
+
+
diff --git a/kiwi/volume_manager/btrfs.py b/kiwi/volume_manager/btrfs.py
index c05e1739242..2a3b39ea0c3 100644
--- a/kiwi/volume_manager/btrfs.py
+++ b/kiwi/volume_manager/btrfs.py
@@ -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
)
@@ -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:
diff --git a/kiwi/xml_parse.py b/kiwi/xml_parse.py
index 83c97e8daa6..cbf533eca4d 100644
--- a/kiwi/xml_parse.py
+++ b/kiwi/xml_parse.py
@@ -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)
@@ -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
@@ -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_:
@@ -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'))
@@ -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')
diff --git a/kiwi/xml_state.py b/kiwi/xml_state.py
index 7625be179d8..0371d0e94cc 100644
--- a/kiwi/xml_state.py
+++ b/kiwi/xml_state.py
@@ -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
diff --git a/test/data/example_btrfs_vol_config.xml b/test/data/example_btrfs_vol_config.xml
new file mode 100644
index 00000000000..bd0ea6d244a
--- /dev/null
+++ b/test/data/example_btrfs_vol_config.xml
@@ -0,0 +1,27 @@
+
+
+
+
+ Marcus Schäfer
+ marcus.schaefer@suse.com
+ Some
+
+
+ 1.15.5
+ zypper
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/unit/volume_manager/btrfs_test.py b/test/unit/volume_manager/btrfs_test.py
index 71b74b02b73..94c83602025 100644
--- a/test/unit/volume_manager/btrfs_test.py
+++ b/test/unit/volume_manager/btrfs_test.py
@@ -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',
@@ -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
)
),
@@ -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 == [
diff --git a/test/unit/xml_state_test.py b/test/unit/xml_state_test.py
index 912800ed9ad..73356620812 100644
--- a/test/unit/xml_state_test.py
+++ b/test/unit/xml_state_test.py
@@ -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()