diff --git a/README.md b/README.md index 7795dfab..56b9f341 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,9 @@ must contain only a single item. ##### `size` The `size` specifies the size of the file system. The format for this is intended to -be human-readable, e.g.: "10g", "50 GiB". +be human-readable, e.g.: "10g", "50 GiB". The size of LVM volumes can be specified as a +percentage of the pool/VG size, eg: "50%" as of v1.4.2. + When using `compression` or `deduplication`, `size` can be set higher than actual available space, e.g.: 3 times the size of the volume, based on duplicity and/or compressibility of stored data. diff --git a/library/blivet.py b/library/blivet.py index 0708e54b..ab32af26 100644 --- a/library/blivet.py +++ b/library/blivet.py @@ -434,6 +434,24 @@ def _get_format(self): return fmt + def _get_size(self): + spec = self._volume['size'] + if isinstance(spec, str) and '%' in spec and self._blivet_pool: + try: + percentage = int(spec[:-1].strip()) + except ValueError: + raise BlivetAnsibleError("invalid percentage '%s' size specified for volume '%s'" % (self._volume['size'], self._volume['name'])) + + parent = self._blivet_pool._device + size = parent.size * (percentage / 100.0) + else: + try: + size = Size(spec) + except Exception: + raise BlivetAnsibleError("invalid size specification for volume '%s': '%s'" % (self._volume['name'], self._volume['size'])) + + return size + def _create(self): """ Schedule actions as needed to ensure the volume exists. """ pass @@ -456,10 +474,7 @@ def _manage_encryption(self): def _resize(self): """ Schedule actions as needed to ensure the device has the desired size. """ - try: - size = Size(self._volume['size']) - except Exception: - raise BlivetAnsibleError("invalid size specification for volume '%s': '%s'" % (self._volume['name'], self._volume['size'])) + size = self._get_size() if size and self._device.size != size: try: @@ -618,8 +633,12 @@ def _create(self): raise BlivetAnsibleError("failed to find pool '%s' for volume '%s'" % (self._blivet_pool['name'], self._volume['name'])) size = Size("256 MiB") + maxsize = None + if isinstance(self._volume['size'], str) and '%' in self._volume['size']: + maxsize = self._get_size() + try: - device = self._blivet.new_partition(parents=[parent], size=size, grow=True, fmt=self._get_format()) + device = self._blivet.new_partition(parents=[parent], size=size, maxsize=maxsize, grow=True, fmt=self._get_format()) except Exception: raise BlivetAnsibleError("failed set up volume '%s'" % self._volume['name']) @@ -640,6 +659,12 @@ def _get_device_id(self): return None return "%s-%s" % (self._blivet_pool._device.name, self._volume['name']) + def _get_size(self): + size = super(BlivetLVMVolume, self)._get_size() + if isinstance(self._volume['size'], str) and '%' in self._volume['size']: + size = self._blivet_pool._device.align(size, roundup=True) + return size + def _create(self): if self._device: return @@ -648,10 +673,7 @@ def _create(self): if parent is None: raise BlivetAnsibleError("failed to find pool '%s' for volume '%s'" % (self._blivet_pool['name'], self._volume['name'])) - try: - size = Size(self._volume['size']) - except Exception: - raise BlivetAnsibleError("invalid size '%s' specified for volume '%s'" % (self._volume['size'], self._volume['name'])) + size = self._get_size() fmt = self._get_format() trim_percent = (1.0 - float(parent.free_space / size)) * 100 diff --git a/tests/test-verify-volume-size.yml b/tests/test-verify-volume-size.yml index e95c4ee2..94155e20 100644 --- a/tests/test-verify-volume-size.yml +++ b/tests/test-verify-volume-size.yml @@ -10,15 +10,44 @@ bsize: size: "{{ storage_test_volume.size }}" register: storage_test_requested_size - when: _storage_test_volume_present and storage_test_volume.size is defined and storage_test_volume.type not in ('partition', 'disk', 'raid') + when: _storage_test_volume_present and storage_test_volume.type == "lvm" and "%" not in storage_test_volume.size|string + +- name: Establish base value for expected size + set_fact: + storage_test_expected_size: "{{ storage_test_requested_size.bytes }}" + when: _storage_test_volume_present and storage_test_volume.type == "lvm" and "%" not in storage_test_volume.size|string + +- debug: + var: storage_test_expected_size + +- name: Convert percentage-based size to normal size as needed + block: + - debug: + var: storage_test_pool + + - debug: + var: storage_test_blkinfo + + - name: Get the size of parent/pool device + bsize: + size: "{{ ansible_lvm.vgs[storage_test_pool.name].size_g + 'G' }}" + register: storage_test_pool_size + + - debug: + var: storage_test_pool_size + + - name: Calculate the expected size based on pool size and percentage value + set_fact: + storage_test_expected_size: "{{ (storage_test_pool_size.bytes * ((storage_test_volume.size[:-1]|int)/100.0)) }}" + when: _storage_test_volume_present and storage_test_volume.type == "lvm" and "%" in storage_test_volume.size|string - debug: var: storage_test_actual_size - debug: - var: storage_test_requested_size + var: storage_test_expected_size - assert: - that: storage_test_actual_size == storage_test_requested_size - msg: "Volume {{ storage_test_volume.name }} has unexpected size" - when: _storage_test_volume_present and storage_test_volume.size is defined and storage_test_volume.type not in ('partition', 'disk', 'raid') + that: storage_test_actual_size.bytes == storage_test_expected_size|int + msg: "Volume {{ storage_test_volume.name }} has unexpected size ({{ storage_test_expected_size|int }} / {{ storage_test_actual_size.bytes }})" + when: _storage_test_volume_present and storage_test_volume.type == "lvm" diff --git a/tests/tests_lvm_percent_size.yml b/tests/tests_lvm_percent_size.yml new file mode 100644 index 00000000..6585e7e7 --- /dev/null +++ b/tests/tests_lvm_percent_size.yml @@ -0,0 +1,169 @@ +--- +- hosts: all + become: true + vars: + storage_safe_mode: false + mount_location1: '/opt/test1' + mount_location2: '/opt/test2' + volume_group_size: '10g' + size1: "60%" + size2: "40%" + size3: "25%" + size4: "50%" + + tasks: + - include_role: + name: linux-system-roles.storage + + - include_tasks: get_unused_disk.yml + vars: + min_size: "{{ volume_group_size }}" + max_return: 1 + + - name: Test for correct handling of invalid percentage-based size specification. + block: + - name: Try to create LVM with an invalid size specification. + include_role: + name: linux-system-roles.storage + vars: + storage_pools: + - name: foo + disks: "{{ unused_disks }}" + volumes: + - name: test1 + size: "2x%" + mount_point: "{{ mount_location1 }}" + + - name: unreachable task + fail: + msg: UNREACH + + rescue: + - name: Check that we failed in the role + assert: + that: + - ansible_failed_result.msg != 'UNREACH' + msg: "Role has not failed when it should have" + + - name: Check for the expected error message + assert: + that: "{{ 'invalid percentage' in blivet_output.msg }}" + msg: "Unexpected error message output from invalid percentage size input" + + # the following does not work properly + # - name: Verify the output + # assert: + # that: "{{ blivet_output.failed and + # blivet_output.msg|regex_search('size.+exceeds.+space in pool') and + # not blivet_output.changed }}" + # msg: "Unexpected behavior w/ too-large volume size" + + - name: Create two LVM logical volumes under volume group 'foo' using percentage sizes + include_role: + name: linux-system-roles.storage + vars: + storage_pools: + - name: foo + disks: "{{ unused_disks }}" + volumes: + - name: test1 + size: "{{ size1 }}" + mount_point: "{{ mount_location1 }}" + - name: test2 + size: "{{ size2 }}" + mount_point: "{{ mount_location2 }}" + fs_type: ext4 + + - include_tasks: verify-role-results.yml + + - name: Repeat the previous invocation to verify idempotence + include_role: + name: linux-system-roles.storage + vars: + storage_pools: + - name: foo + disks: "{{ unused_disks }}" + volumes: + - name: test1 + size: "{{ size1 }}" + mount_point: "{{ mount_location1 }}" + - name: test2 + size: "{{ size2 }}" + mount_point: "{{ mount_location2 }}" + - include_tasks: verify-role-results.yml + + - name: Shrink test2 volume via percentage-based size spec + include_role: + name: linux-system-roles.storage + vars: + storage_pools: + - name: foo + disks: "{{ unused_disks }}" + volumes: + - name: test1 + size: "{{ size1 }}" + mount_point: "{{ mount_location1 }}" + - name: test2 + size: "{{ size3 }}" + mount_point: "{{ mount_location2 }}" + + - include_tasks: verify-role-results.yml + + - name: Get the size of test2 volume + command: lsblk --noheadings -o SIZE /dev/mapper/foo-test2 + register: storage_test_test1_size_1 + changed_when: false + + - name: Remove the test1 volume without changing its size + include_role: + name: linux-system-roles.storage + vars: + storage_pools: + - name: foo + disks: "{{ unused_disks }}" + state: "present" + volumes: + - name: test1 + size: "{{ size1 }}" + mount_point: "{{ mount_location1 }}" + state: absent + - name: test2 + size: "{{ size3 }}" + mount_point: "{{ mount_location2 }}" + + - include_tasks: verify-role-results.yml + + - name: Get the size of test2 volume again + command: lsblk --noheadings -o SIZE /dev/mapper/foo-test2 + register: storage_test_test1_size_2 + changed_when: false + + - name: Verify that removing test1 didn't cause a change in test2 size + assert: + that: storage_test_test1_size_1.stdout == storage_test_test1_size_2.stdout + + - name: Grow test2 using a percentage-based size spec + include_role: + name: linux-system-roles.storage + vars: + storage_pools: + - name: foo + disks: "{{ unused_disks }}" + state: "present" + volumes: + - name: test2 + size: "{{ size4 }}" + mount_point: "{{ mount_location2 }}" + + - include_tasks: verify-role-results.yml + + - name: Remove both of the LVM logical volumes in 'foo' created above + include_role: + name: linux-system-roles.storage + vars: + storage_pools: + - name: foo + disks: "{{ unused_disks }}" + state: "absent" + + - include_tasks: verify-role-results.yml