Skip to content

Commit

Permalink
Merge pull request #39 from dwlehman/percent-size
Browse files Browse the repository at this point in the history
percentage-based volume size (lvm only)
  • Loading branch information
dwlehman authored Jul 26, 2021
2 parents 0688202 + 2bbcbf5 commit 62e2f24
Show file tree
Hide file tree
Showing 4 changed files with 237 additions and 15 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
40 changes: 31 additions & 9 deletions library/blivet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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'])

Expand All @@ -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
Expand All @@ -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
Expand Down
39 changes: 34 additions & 5 deletions tests/test-verify-volume-size.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
169 changes: 169 additions & 0 deletions tests/tests_lvm_percent_size.yml
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 62e2f24

Please sign in to comment.