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()