From c74cae3a5264f7446b1c373cf0b0787480276322 Mon Sep 17 00:00:00 2001 From: Vojtech Trefny Date: Fri, 12 Apr 2024 14:42:44 +0200 Subject: [PATCH 1/6] feat: Add support for creating and managing Stratis devices Resolves: RHEL-31854 --- library/blivet.py | 136 +++++++++++++++++++++++- library/blockdev_info.py | 31 +++++- tests/test-verify-pool-members.yml | 3 + tests/tests_stratis.yml | 165 +++++++++++++++++++++++++++++ tests/verify-pool-stratis.yml | 38 +++++++ vars/AlmaLinux_8.yml | 2 + vars/AlmaLinux_9.yml | 2 + vars/CentOS_8.yml | 2 + vars/CentOS_9.yml | 2 + vars/Fedora.yml | 2 + vars/RedHat_8.yml | 2 + vars/RedHat_9.yml | 2 + vars/Rocky_8.yml | 2 + vars/Rocky_9.yml | 2 + 14 files changed, 384 insertions(+), 7 deletions(-) create mode 100644 tests/tests_stratis.yml create mode 100644 tests/verify-pool-stratis.yml diff --git a/library/blivet.py b/library/blivet.py index a6715d91..ad5815f1 100644 --- a/library/blivet.py +++ b/library/blivet.py @@ -1320,11 +1320,89 @@ def _destroy(self): leaves = [a for a in ancestors if a.isleaf] +class BlivetStratisVolume(BlivetVolume): + blivet_device_class = devices.StratisFilesystemDevice + + def _update_from_device(self, param_name): + if param_name == 'fs_type': + # Blivet internally uses "stratis xfs" as filesystem type for stratis to distinguish + # between "xfs" and "xfs on stratis" but we want to use "xfs" here because that's + # the type used for mounting and what the other system tools see + self._volume['fs_type'] = self._device.format.mount_type + return True + else: + return super()._update_from_device(param_name) + + def _get_device_id(self): + if not self._blivet_pool._device: + return None + return "%s/%s" % (self._blivet_pool._device.name, self._volume['name']) + + def _get_size(self): + size = super(BlivetStratisVolume, 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 _trim_size(self, size, parent_device): + trim_percent = (1.0 - float(parent_device.free_space / size)) * 100 + log.debug("size: %s ; %s", size, trim_percent) + + if size > parent_device.free_space: + if trim_percent > MAX_TRIM_PERCENT: + raise BlivetAnsibleError("specified size for volume '%s' '%s' exceeds available space in pool '%s' (%s)" % (self._volume['name'], + size, + parent_device.name, + parent_device.free_space)) + else: + log.info("adjusting %s size from %s to %s to fit in %s free space", + self._volume['name'], + size, + parent_device.free_space, + parent_device.name) + return parent_device.free_space + return size + + def _get_format(self): + if self._volume['fs_type'] and self._volume['fs_type'] not in ('xfs', 'stratis xfs'): + raise BlivetAnsibleError("format cannot be specified for Stratis volumes") + + def _reformat(self): + if self._volume['fs_type'] and self._volume['fs_type'] not in ('xfs', 'stratis xfs'): + raise BlivetAnsibleError("format cannot be changed for Stratis volumes") + + def _manage_cache(self): + if self._volume['cached']: + raise BlivetAnsibleError("caching is not supported for Stratis volumes") + + def _create(self): + if self._device: + return + + parent_device = self._blivet_pool._device + if parent_device is None: + raise BlivetAnsibleError("failed to find pool '%s' for volume '%s'" % (self._blivet_pool._device.name, + self._volume['name'])) + + size = self._trim_size(self._get_size(), parent_device) + + try: + device = self._blivet.new_stratis_filesystem(name=self._volume['name'], + parents=[parent_device], + size=size) + except Exception as e: + raise BlivetAnsibleError("failed to set up volume '%s': %s" % (self._volume['name'], str(e))) + + self._blivet.create_device(device) + self._device = device + + _BLIVET_VOLUME_TYPES = { "disk": BlivetDiskVolume, "lvm": BlivetLVMVolume, "partition": BlivetPartitionVolume, - "raid": BlivetMDRaidVolume + "raid": BlivetMDRaidVolume, + "stratis": BlivetStratisVolume, } @@ -1823,9 +1901,63 @@ def _look_up_device(self): self._thinpools = [d for d in self._device.children if d.type == 'lvmthinpool'] +class BlivetStratisPool(BlivetPool): + blivet_device_class = devices.StratisPoolDevice + + def _type_check(self): + return self._device.type == "stratis pool" + + def _update_from_device(self, param_name): + """ Return True if param_name's value was retrieved from a looked-up device. """ + log.debug("BlivetStratisPool._update_from_device: %s", self._device) + + if param_name == 'disks': + self._pool['disks'] = [d.name for d in self._device.disks] + elif param_name == 'encryption': + self._pool['encryption'] = self._device.encrypted + else: + return False + + return True + + def _member_management_is_destructive(self): + if self._device is None: + return False + + if self._pool['encryption'] and not self._device.encrypted: + return True + elif not self._pool['encryption'] and self._device.encrypted: + return True + + return False + + def _get_format(self): + fmt = get_format("stratis") + if not fmt.supported or not fmt.formattable: + raise BlivetAnsibleError("required tools for managing Stratis are missing") + + return fmt + + def _create(self): + if not self._device: + members = self._create_members() + try: + pool_device = self._blivet.new_stratis_pool(name=self._pool['name'], + parents=members, + encrypted=self._spec_dict['encryption'], + passphrase=self._spec_dict.get('encryption_password') or None, + key_file=self._spec_dict.get('encryption_key') or None) + except Exception as e: + raise BlivetAnsibleError("failed to set up pool '%s': %s" % (self._pool['name'], str(e))) + + self._blivet.create_device(pool_device) + self._device = pool_device + + _BLIVET_POOL_TYPES = { "partition": BlivetPartitionPool, - "lvm": BlivetLVMPool + "lvm": BlivetLVMPool, + "stratis": BlivetStratisPool, } diff --git a/library/blockdev_info.py b/library/blockdev_info.py index 13858fb0..f3cc3e58 100644 --- a/library/blockdev_info.py +++ b/library/blockdev_info.py @@ -44,12 +44,10 @@ LSBLK_DEVICE_TYPES = {"part": "partition"} DEV_MD_DIR = '/dev/md' +DEV_STRATIS_DIR = '/dev/stratis' -def fixup_md_path(path): - if not path.startswith("/dev/md"): - return path - +def _fixup_md_path(path): if not os.path.exists(DEV_MD_DIR): return path @@ -63,6 +61,29 @@ def fixup_md_path(path): return ret +def _fixup_stratis_path(path): + if not os.path.exists(DEV_STRATIS_DIR): + return path + + ret = path + for pool in os.listdir(DEV_STRATIS_DIR): + for fs in os.listdir(os.path.join(DEV_STRATIS_DIR, pool)): + stratis_path = os.path.join(DEV_STRATIS_DIR, pool, fs) + if os.path.realpath(stratis_path) == os.path.realpath(path): + ret = stratis_path + break + return ret + + +def fixup_path(path): + if path.startswith("/dev/md"): + return _fixup_md_path(path) + elif path.startswith("/dev/mapper/stratis-"): + return _fixup_stratis_path(path) + else: + return path + + def get_block_info(module): buf = module.run_command(["lsblk", "-o", "NAME,FSTYPE,LABEL,UUID,TYPE,SIZE", "-p", "-P", "-a"])[1] info = dict() @@ -76,7 +97,7 @@ def get_block_info(module): raise if key: if key.lower() == "name": - value = fixup_md_path(value) + value = fixup_path(value) dev[key.lower()] = LSBLK_DEVICE_TYPES.get(value, value) if dev: diff --git a/tests/test-verify-pool-members.yml b/tests/test-verify-pool-members.yml index 0cca087d..649078e5 100644 --- a/tests/test-verify-pool-members.yml +++ b/tests/test-verify-pool-members.yml @@ -85,6 +85,9 @@ - name: Check VDO include_tasks: verify-pool-members-vdo.yml +- name: Check Stratis + include_tasks: verify-pool-stratis.yml + - name: Clean up test variables set_fact: _storage_test_expected_pv_type: null diff --git a/tests/tests_stratis.yml b/tests/tests_stratis.yml new file mode 100644 index 00000000..449d536a --- /dev/null +++ b/tests/tests_stratis.yml @@ -0,0 +1,165 @@ +--- +- name: Test stratis pool management + hosts: all + become: true + vars: + mount_location: '/opt/test1' + mount_location_2: '/opt/test2' + volume_group_size: '5g' + volume_size: '4g' + tags: + - tests::stratis + + tasks: + - name: Run the role + include_role: + name: linux-system-roles.storage + + - name: Mark tasks to be skipped + set_fact: + storage_skip_checks: + - blivet_available + - "{{ (lookup('env', + 'SYSTEM_ROLES_REMOVE_CLOUD_INIT') in ['', 'false']) | + ternary('packages_installed', '') }}" + - service_facts + + - name: Get unused disks + include_tasks: get_unused_disk.yml + vars: + min_size: "{{ volume_group_size }}" + disks_needed: 2 + + # stratisd is not started automatically and doesn't support DBus activation + # this will be covered by Blivet in the next build + - name: Start stratisd service + service: + name: stratisd + state: started + + - name: Create one Stratis pool with one volume + include_role: + name: linux-system-roles.storage + vars: + storage_pools: + - name: foo + disks: "{{ unused_disks }}" + type: stratis + volumes: + - name: test1 + size: "{{ volume_size }}" + mount_point: "{{ mount_location }}" + + - name: Verify role results + 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 }}" + type: stratis + volumes: + - name: test1 + size: "{{ volume_size }}" + mount_point: "{{ mount_location }}" + + - name: Verify role results + include_tasks: verify-role-results.yml + + - name: Add second filesystem to the pool + include_role: + name: linux-system-roles.storage + vars: + storage_pools: + - name: foo + disks: "{{ unused_disks }}" + type: stratis + volumes: + - name: test1 + size: "{{ volume_size }}" + mount_point: "{{ mount_location }}" + - name: test2 + size: "{{ volume_size }}" + mount_point: "{{ mount_location_2 }}" + + - name: Verify role results + include_tasks: verify-role-results.yml + + - name: Clean up + include_role: + name: linux-system-roles.storage + vars: + storage_pools: + - name: foo + disks: "{{ unused_disks }}" + type: stratis + state: absent + volumes: + - name: test1 + size: "{{ volume_size }}" + mount_point: "{{ mount_location }}" + state: absent + - name: test2 + size: "{{ volume_size }}" + mount_point: "{{ mount_location_2 }}" + state: absent + + - name: Verify role results + include_tasks: verify-role-results.yml + + - name: Create encrypted Stratis pool + include_role: + name: linux-system-roles.storage + vars: + storage_pools: + - name: foo + disks: "{{ unused_disks }}" + type: stratis + encryption: true + encryption_password: yabbadabbadoo + volumes: + - name: test1 + size: "{{ volume_size }}" + mount_point: "{{ mount_location }}" + + - name: Verify role results + 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 }}" + type: stratis + encryption: true + encryption_password: yabbadabbadoo + volumes: + - name: test1 + size: "{{ volume_size }}" + mount_point: "{{ mount_location }}" + + - name: Verify role results + include_tasks: verify-role-results.yml + + - name: Clean up + include_role: + name: linux-system-roles.storage + vars: + storage_pools: + - name: foo + disks: "{{ unused_disks }}" + type: stratis + state: absent + volumes: + - name: test1 + size: "{{ volume_size }}" + mount_point: "{{ mount_location }}" + state: absent + + - name: Verify role results + include_tasks: verify-role-results.yml diff --git a/tests/verify-pool-stratis.yml b/tests/verify-pool-stratis.yml new file mode 100644 index 00000000..8efae163 --- /dev/null +++ b/tests/verify-pool-stratis.yml @@ -0,0 +1,38 @@ +--- +# Only when pool is stratis +- name: Check Stratis options + when: storage_test_pool.type == 'stratis' + block: + - name: Run 'stratis report' + command: stratis report + register: storage_test_stratis_report + changed_when: false + + - name: Get information about Stratis + set_fact: + _stratis_pool_info: "{{ storage_test_stratis_report.stdout | from_json }}" + + - name: Verify that the pools was created + assert: + that: _stratis_pool_info.pools | length == 1 and + _stratis_pool_info.pools[0].name == storage_test_pool.name + msg: >- + Stratis pool '{{ storage_test_pool.name }}' not found + when: storage_test_pool.state == 'present' + + # Stratis internally uses LUKS so verify-pool-member-encryption will also + # cover this we just need to make sure this is encrypted Stratis pool + # and not Stratis on top of "normal LUKS + - name: Verify that encryption is correctly set + assert: + that: storage_test_pool.name in + _stratis_pool_info[0]['blockdevs']['datadevs'][0]['key_description'] + msg: >- + Stratis pool '{{ storage_test_pool.name }}' is not encrypted + when: + - storage_test_pool.state == 'present' + - storage_test_pool.encryption + +- name: Reset variable used by test + set_fact: + storage_test_stratis_report: null diff --git a/vars/AlmaLinux_8.yml b/vars/AlmaLinux_8.yml index 7ea8da2c..45756a96 100644 --- a/vars/AlmaLinux_8.yml +++ b/vars/AlmaLinux_8.yml @@ -9,3 +9,5 @@ blivet_package_list: - vdo - kmod-kvdo - xfsprogs + - stratisd + - stratis-cli diff --git a/vars/AlmaLinux_9.yml b/vars/AlmaLinux_9.yml index 7ea8da2c..45756a96 100644 --- a/vars/AlmaLinux_9.yml +++ b/vars/AlmaLinux_9.yml @@ -9,3 +9,5 @@ blivet_package_list: - vdo - kmod-kvdo - xfsprogs + - stratisd + - stratis-cli diff --git a/vars/CentOS_8.yml b/vars/CentOS_8.yml index 7ea8da2c..45756a96 100644 --- a/vars/CentOS_8.yml +++ b/vars/CentOS_8.yml @@ -9,3 +9,5 @@ blivet_package_list: - vdo - kmod-kvdo - xfsprogs + - stratisd + - stratis-cli diff --git a/vars/CentOS_9.yml b/vars/CentOS_9.yml index 7ea8da2c..45756a96 100644 --- a/vars/CentOS_9.yml +++ b/vars/CentOS_9.yml @@ -9,3 +9,5 @@ blivet_package_list: - vdo - kmod-kvdo - xfsprogs + - stratisd + - stratis-cli diff --git a/vars/Fedora.yml b/vars/Fedora.yml index e63cd08b..fe88f8d9 100644 --- a/vars/Fedora.yml +++ b/vars/Fedora.yml @@ -6,6 +6,8 @@ blivet_package_list: - libblockdev-lvm - libblockdev-mdraid - libblockdev-swap + - stratisd + - stratis-cli _storage_copr_packages: - repository: "rhawalsh/dm-vdo" packages: diff --git a/vars/RedHat_8.yml b/vars/RedHat_8.yml index 7ea8da2c..45756a96 100644 --- a/vars/RedHat_8.yml +++ b/vars/RedHat_8.yml @@ -9,3 +9,5 @@ blivet_package_list: - vdo - kmod-kvdo - xfsprogs + - stratisd + - stratis-cli diff --git a/vars/RedHat_9.yml b/vars/RedHat_9.yml index 7ea8da2c..45756a96 100644 --- a/vars/RedHat_9.yml +++ b/vars/RedHat_9.yml @@ -9,3 +9,5 @@ blivet_package_list: - vdo - kmod-kvdo - xfsprogs + - stratisd + - stratis-cli diff --git a/vars/Rocky_8.yml b/vars/Rocky_8.yml index 7ea8da2c..45756a96 100644 --- a/vars/Rocky_8.yml +++ b/vars/Rocky_8.yml @@ -9,3 +9,5 @@ blivet_package_list: - vdo - kmod-kvdo - xfsprogs + - stratisd + - stratis-cli diff --git a/vars/Rocky_9.yml b/vars/Rocky_9.yml index 7ea8da2c..45756a96 100644 --- a/vars/Rocky_9.yml +++ b/vars/Rocky_9.yml @@ -9,3 +9,5 @@ blivet_package_list: - vdo - kmod-kvdo - xfsprogs + - stratisd + - stratis-cli From e37c7f96665c417b529e309d38207dc108740046 Mon Sep 17 00:00:00 2001 From: Vojtech Trefny Date: Fri, 12 Apr 2024 14:45:15 +0200 Subject: [PATCH 2/6] tests: Use blockdev_info to check volume mount points We can use the information from `lsblk` we already use for other checks instead of using the Ansible mountinfo facts. This makes the check simpler and also makes it easier to check for Stratis volume mount points, because of the complicated Stratis devices structure in /dev. --- library/blockdev_info.py | 2 +- tests/test-verify-volume-fs.yml | 51 ++++++++++++++++-------------- tests/test-verify-volume-mount.yml | 48 +++++----------------------- 3 files changed, 37 insertions(+), 64 deletions(-) diff --git a/library/blockdev_info.py b/library/blockdev_info.py index f3cc3e58..61e89f66 100644 --- a/library/blockdev_info.py +++ b/library/blockdev_info.py @@ -85,7 +85,7 @@ def fixup_path(path): def get_block_info(module): - buf = module.run_command(["lsblk", "-o", "NAME,FSTYPE,LABEL,UUID,TYPE,SIZE", "-p", "-P", "-a"])[1] + buf = module.run_command(["lsblk", "-o", "NAME,FSTYPE,LABEL,UUID,TYPE,SIZE,MOUNTPOINT", "-p", "-P", "-a"])[1] info = dict() for line in buf.splitlines(): dev = dict() diff --git a/tests/test-verify-volume-fs.yml b/tests/test-verify-volume-fs.yml index 8e488c54..63b2770a 100644 --- a/tests/test-verify-volume-fs.yml +++ b/tests/test-verify-volume-fs.yml @@ -1,26 +1,31 @@ --- # type -- name: Verify fs type - assert: - that: storage_test_blkinfo.info[storage_test_volume._device].fstype == - storage_test_volume.fs_type or - (storage_test_blkinfo.info[storage_test_volume._device].fstype | length - == 0 and storage_test_volume.fs_type == "unformatted") - when: storage_test_volume.fs_type and _storage_test_volume_present +- name: Check volume filesystem + when: storage_test_volume.type != "stratis" + block: + - name: Verify fs type + assert: + that: storage_test_blkinfo.info[storage_test_volume._device].fstype == + storage_test_volume.fs_type or + (storage_test_blkinfo.info[storage_test_volume._device].fstype | length + == 0 and storage_test_volume.fs_type == "unformatted") + when: + - storage_test_volume.fs_type + - _storage_test_volume_present -# label -- name: Verify fs label - assert: - that: storage_test_blkinfo.info[storage_test_volume._device].label == - storage_test_volume.fs_label - msg: >- - Volume '{{ storage_test_volume.name }}' labels do not match when they - should - ('{{ storage_test_blkinfo.info[storage_test_volume._device].label }}', - '{{ storage_test_volume.fs_label }}') - when: - - _storage_test_volume_present | bool - # label for GFS2 is set manually with the extra `-t` fs_create_options - # so we can't verify it here because it was not set with fs_label so - # the label from blkinfo doesn't match the expected "empty" fs_label - - storage_test_volume.fs_type != "gfs2" + # label + - name: Verify fs label + assert: + that: storage_test_blkinfo.info[storage_test_volume._device].label == + storage_test_volume.fs_label + msg: >- + Volume '{{ storage_test_volume.name }}' labels do not match when they + should + ('{{ storage_test_blkinfo.info[storage_test_volume._device].label }}', + '{{ storage_test_volume.fs_label }}') + when: + - _storage_test_volume_present | bool + # label for GFS2 is set manually with the extra `-t` fs_create_options + # so we can't verify it here because it was not set with fs_label so + # the label from blkinfo doesn't match the expected "empty" fs_label + - storage_test_volume.fs_type != "gfs2" diff --git a/tests/test-verify-volume-mount.yml b/tests/test-verify-volume-mount.yml index cf86b343..17d2a015 100644 --- a/tests/test-verify-volume-mount.yml +++ b/tests/test-verify-volume-mount.yml @@ -15,20 +15,13 @@ - name: Set some facts set_fact: - storage_test_mount_device_matches: "{{ ansible_mounts | - selectattr('device', 'match', '^' ~ storage_test_device_path ~ '$') | - list }}" - storage_test_mount_point_matches: "{{ ansible_mounts | - selectattr('mount', 'match', - '^' ~ mount_prefix ~ storage_test_volume.mount_point ~ '$') | - list if storage_test_volume.mount_point else [] }}" - storage_test_mount_expected_match_count: "{{ 1 - if _storage_test_volume_present and storage_test_volume.mount_point and - storage_test_volume.mount_point.startswith('/') - else 0 }}" storage_test_swap_expected_matches: "{{ 1 if _storage_test_volume_present and storage_test_volume.fs_type == 'swap' else 0 }}" + storage_test_mount_expected_mount_point: "{{ + '[SWAP]' if storage_test_volume.fs_type == 'swap' else + '' if storage_test_volume.mount_point == 'none' else + mount_prefix + storage_test_volume.mount_point if storage_test_volume.mount_point else '' }}" vars: # assumes /opt which is /var/opt in ostree mount_prefix: "{{ '/var' if __storage_is_ostree | d(false) @@ -50,23 +43,12 @@ # - name: Verify the current mount state by device assert: - that: storage_test_mount_device_matches | length == - storage_test_mount_expected_match_count | int + that: storage_test_blkinfo.info[storage_test_volume._device].mountpoint == + storage_test_mount_expected_mount_point msg: >- Found unexpected mount state for volume '{{ storage_test_volume.name }}' device - when: _storage_test_volume_present and storage_test_volume.mount_point - -# -# Verify mount directory (state, owner, group, permissions). -# -- name: Verify the current mount state by mount point - assert: - that: storage_test_mount_point_matches | length == - storage_test_mount_expected_match_count | int - msg: >- - Found unexpected mount state for volume - '{{ storage_test_volume.name }}' mount point + when: _storage_test_volume_present - name: Verify mount directory user assert: @@ -104,18 +86,6 @@ storage_test_volume.mount_point and storage_test_volume.mount_mode -# -# Verify mount fs type. -# -- name: Verify the mount fs type - assert: - that: storage_test_mount_point_matches[0].fstype == - storage_test_volume.fs_type - msg: >- - Found unexpected mount state for volume - '{{ storage_test_volume.name }} fs type - when: storage_test_mount_expected_match_count | int == 1 - # # Verify swap status. # @@ -145,10 +115,8 @@ - name: Unset facts set_fact: - storage_test_mount_device_matches: null - storage_test_mount_point_matches: null - storage_test_mount_expected_match_count: null storage_test_swap_expected_matches: null storage_test_sys_node: null storage_test_swaps: null storage_test_found_mount_stat: null + storage_test_mount_expected_mount_point: null From c160097bd994fa21f8bc2b679858a32d8ac4a464 Mon Sep 17 00:00:00 2001 From: Vojtech Trefny Date: Fri, 26 Apr 2024 13:01:43 +0200 Subject: [PATCH 3/6] feat: Add support for adding new devices to existing Stratis pools Related: RHEL-31854 --- library/blivet.py | 27 ++++++++++++++++++++++++ tests/tests_stratis.yml | 46 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/library/blivet.py b/library/blivet.py index ad5815f1..f5290ec1 100644 --- a/library/blivet.py +++ b/library/blivet.py @@ -1931,6 +1931,33 @@ def _member_management_is_destructive(self): return False + def _manage_members(self): + """ Schedule actions as needed to configure this pool's members. """ + if not self._device: + return + + add_disks = [d for d in self._disks if d not in self._device.ancestors] + remove_disks = [bd for bd in self._device.blockdevs if not any(d in bd.ancestors for d in self._disks)] + + if remove_disks: + raise BlivetAnsibleError("cannot remove members '%s' from pool '%s': Stratis doesn't " + "support removing members from existing pools" % + (", ".join(d.name for d in remove_disks), + self._device.name)) + + if not add_disks: + return + + for disk in add_disks: + member = self._create_one_member(disk) + try: + ac = ActionAddMember(self._device, member) + self._blivet.devicetree.actions.add(ac) + except Exception as e: + raise BlivetAnsibleError("failed to add disk '%s' to pool '%s': %s" % (disk.name, + self._pool['name'], + str(e))) + def _get_format(self): fmt = get_format("stratis") if not fmt.supported or not fmt.formattable: diff --git a/tests/tests_stratis.yml b/tests/tests_stratis.yml index 449d536a..0a85fcfa 100644 --- a/tests/tests_stratis.yml +++ b/tests/tests_stratis.yml @@ -163,3 +163,49 @@ - name: Verify role results include_tasks: verify-role-results.yml + + # XXX blivet supporting this is not yet released + - name: Run test only if blivet supports this functionality + when: false + block: + - name: Create one Stratis pool on one disk + include_role: + name: linux-system-roles.storage + vars: + storage_pools: + - name: foo + disks: "{{ unused_disks[0] }}" + type: stratis + + - name: Verify role results + include_tasks: verify-role-results.yml + + - name: Add the second disk to the pool + include_role: + name: linux-system-roles.storage + vars: + storage_pools: + - name: foo + disks: "{{ [unused_disks[0], unused_disks[1]] }}" + type: stratis + + - name: Verify role results + include_tasks: verify-role-results.yml + + - name: Clean up + include_role: + name: linux-system-roles.storage + vars: + storage_pools: + - name: foo + disks: "{{ unused_disks }}" + type: stratis + state: absent + volumes: + - name: test1 + size: "{{ volume_size }}" + mount_point: "{{ mount_location }}" + state: absent + + - name: Verify role results + include_tasks: verify-role-results.yml From 77cf513770b3cc9f93f3322315f6722ff43a90f7 Mon Sep 17 00:00:00 2001 From: Vojtech Trefny Date: Mon, 6 May 2024 13:17:57 +0200 Subject: [PATCH 4/6] feat: Allow creating encrypted Stratis Pools with Clevis/Tang Tang server configuration or TPM2 can now be used when creating an encrypted Stratis Pool. Related: RHEL-31854 --- README.md | 15 +++++++++++++ library/blivet.py | 40 ++++++++++++++++++++++++++++++----- tests/tests_stratis.yml | 37 ++++++++++++++++++++++++++++++++ tests/verify-pool-stratis.yml | 15 ++++++++++++- 4 files changed, 101 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 658dad1a..5398d30c 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,21 @@ keys: This integer specifies the LUKS version to use. +- `encryption_clevis_pin` + + For Stratis pools, the clevis method that should be used to encrypt the created pool. + Accepted values are: `tang` and `tpm2` + +- `encryption_tang_url` + + When creating a Stratis pool encrypted via NBDE using a tang server, + specifies the URL of the server. + +- `encryption_tang_thumbprint` + + When creating a Stratis pool encrypted via NBDE using a tang server, + specifies the thumbprint of the server. + ### `storage_volumes` The `storage_volumes` variable is a list of volumes to manage. Each volume has the following diff --git a/library/blivet.py b/library/blivet.py index f5290ec1..bc2a537b 100644 --- a/library/blivet.py +++ b/library/blivet.py @@ -51,6 +51,15 @@ encryption_password: description: encryption_password type: str + encryption_clevis_pin: + description: encryption_clevis_pin + type: str + encryption_tang_url: + description: encryption_tang_url + type: str + encryption_tang_thumbprint: + description: encryption_tang_thumbprint + type: str name: description: name type: str @@ -1915,6 +1924,12 @@ def _update_from_device(self, param_name): self._pool['disks'] = [d.name for d in self._device.disks] elif param_name == 'encryption': self._pool['encryption'] = self._device.encrypted + elif param_name == 'encryption_clevis_pin': + self._pool['encryption_clevis_pin'] = self._device._clevis.pin + elif param_name == 'encryption_tang_url': + self._pool['encryption_tang_url'] = self._device._clevis.tang_url + elif param_name == 'encryption_tang_thumbprint': + self._pool['encryption_tang_thumbprint'] = self._device._clevis.tang_thumbprint else: return False @@ -1969,11 +1984,23 @@ def _create(self): if not self._device: members = self._create_members() try: - pool_device = self._blivet.new_stratis_pool(name=self._pool['name'], - parents=members, - encrypted=self._spec_dict['encryption'], - passphrase=self._spec_dict.get('encryption_password') or None, - key_file=self._spec_dict.get('encryption_key') or None) + if self._spec_dict['encryption'] and self._spec_dict['encryption_clevis_pin']: + clevis_config = devices.stratis.StratisClevisConfig(pin=self._spec_dict['encryption_clevis_pin'], + tang_url=self._spec_dict['encryption_tang_url'], + tang_thumbprint=self._spec_dict['encryption_tang_thumbprint']) + pool_device = self._blivet.new_stratis_pool(name=self._pool['name'], + parents=members, + encrypted=self._spec_dict['encryption'], + passphrase=self._spec_dict.get('encryption_password') or None, + key_file=self._spec_dict.get('encryption_key') or None, + clevis=clevis_config) + else: + pool_device = self._blivet.new_stratis_pool(name=self._pool['name'], + parents=members, + encrypted=self._spec_dict['encryption'], + passphrase=self._spec_dict.get('encryption_password') or None, + key_file=self._spec_dict.get('encryption_key') or None) + except Exception as e: raise BlivetAnsibleError("failed to set up pool '%s': %s" % (self._pool['name'], str(e))) @@ -2299,6 +2326,9 @@ def run_module(): encryption_key_size=dict(type='int'), encryption_luks_version=dict(type='str'), encryption_password=dict(type='str', no_log=True), + encryption_clevis_pin=dict(type='str'), + encryption_tang_url=dict(type='str'), + encryption_tang_thumbprint=dict(type='str'), name=dict(type='str'), raid_level=dict(type='str'), raid_device_count=dict(type='int'), diff --git a/tests/tests_stratis.yml b/tests/tests_stratis.yml index 0a85fcfa..139d1b21 100644 --- a/tests/tests_stratis.yml +++ b/tests/tests_stratis.yml @@ -209,3 +209,40 @@ - name: Verify role results include_tasks: verify-role-results.yml + + - name: Setup Tang server on localhost for testing + ansible.builtin.include_role: + name: fedora.linux_system_roles.nbde_server + vars: + nbde_server_manage_firewall: true + nbde_server_manage_selinux: true + nbde_server_port: 7500 + + - name: Create encrypted Stratis pool with Clevis/Tang + include_role: + name: linux-system-roles.storage + vars: + storage_pools: + - name: foo + disks: "{{ unused_disks[0] }}" + type: stratis + encryption: true + encryption_password: yabbadabbadoo + encryption_clevis_pin: tang + encryption_tang_url: localhost:7500 + + - name: Verify role results + include_tasks: verify-role-results.yml + + - name: Clean up + include_role: + name: linux-system-roles.storage + vars: + storage_pools: + - name: foo + disks: "{{ unused_disks[0] }}" + type: stratis + state: absent + + - name: Verify role results + include_tasks: verify-role-results.yml diff --git a/tests/verify-pool-stratis.yml b/tests/verify-pool-stratis.yml index 8efae163..7f380171 100644 --- a/tests/verify-pool-stratis.yml +++ b/tests/verify-pool-stratis.yml @@ -26,13 +26,26 @@ - name: Verify that encryption is correctly set assert: that: storage_test_pool.name in - _stratis_pool_info[0]['blockdevs']['datadevs'][0]['key_description'] + _stratis_pool_info.pools[0]['blockdevs']['datadevs'][0]['key_description'] msg: >- Stratis pool '{{ storage_test_pool.name }}' is not encrypted when: - storage_test_pool.state == 'present' - storage_test_pool.encryption + - name: Verify that Clevis/Tang encryption is correctly set + assert: + that: + _stratis_pool_info.pools[0]['blockdevs']['datadevs'][0]['clevis_pin'] == 'tang' and + _stratis_pool_info.pools[0]['blockdevs']['datadevs'][0]['clevis_config']['url'] == + storage_test_pool.encryption_tang_url + msg: >- + Stratis pool '{{ storage_test_pool.name }}' Clevis is not correctly configured + when: + - storage_test_pool.state == 'present' + - storage_test_pool.encryption + - storage_test_pool.encryption_clevis_pin == 'tang' + - name: Reset variable used by test set_fact: storage_test_stratis_report: null From 2966e16f089bd98ca784899a8ff4f302d9b7f74b Mon Sep 17 00:00:00 2001 From: Vojtech Trefny Date: Tue, 28 May 2024 10:45:47 +0200 Subject: [PATCH 5/6] tests: Check version of Blivet when running Stratis tests Some Stratis features are not available with older versions of Blivet. Related: RHEL-31854 --- tests/tests_stratis.yml | 127 ++++++++++++++++++++++++---------------- 1 file changed, 76 insertions(+), 51 deletions(-) diff --git a/tests/tests_stratis.yml b/tests/tests_stratis.yml index 139d1b21..c41498ad 100644 --- a/tests/tests_stratis.yml +++ b/tests/tests_stratis.yml @@ -110,64 +110,89 @@ - name: Verify role results include_tasks: verify-role-results.yml - - name: Create encrypted Stratis pool - include_role: - name: linux-system-roles.storage - vars: - storage_pools: - - name: foo - disks: "{{ unused_disks }}" - type: stratis - encryption: true - encryption_password: yabbadabbadoo - volumes: - - name: test1 - size: "{{ volume_size }}" - mount_point: "{{ mount_location }}" + - name: Gather package facts + package_facts: - - name: Verify role results - include_tasks: verify-role-results.yml + - name: Set blivet package name + set_fact: + blivet_pkg_name: "{{ ansible_facts.packages | + select('search', 'blivet') | select('search', 'python') | list }}" - - name: Repeat the previous invocation to verify idempotence - include_role: - name: linux-system-roles.storage - vars: - storage_pools: - - name: foo - disks: "{{ unused_disks }}" - type: stratis - encryption: true - encryption_password: yabbadabbadoo - volumes: - - name: test1 - size: "{{ volume_size }}" - mount_point: "{{ mount_location }}" + - name: Set blivet package version + set_fact: + blivet_pkg_version: "{{ + ansible_facts.packages[blivet_pkg_name[0]][0]['version'] + + '-' + ansible_facts.packages[blivet_pkg_name[0]][0]['release'] }}" - - name: Verify role results - include_tasks: verify-role-results.yml + - name: Set distribution version + set_fact: + is_rhel9: "{{ (ansible_facts.distribution == 'CentOS' or + ansible_facts.distribution == 'RedHat') and + ansible_facts.distribution_major_version == '9' }}" + is_rhel10: "{{ (ansible_facts.distribution == 'CentOS' or + ansible_facts.distribution == 'RedHat') and + ansible_facts.distribution_major_version == '10' }}" + is_fedora: "{{ ansible_facts.distribution == 'Fedora' }}" - - name: Clean up - include_role: - name: linux-system-roles.storage - vars: - storage_pools: - - name: foo - disks: "{{ unused_disks }}" - type: stratis - state: absent - volumes: - - name: test1 - size: "{{ volume_size }}" - mount_point: "{{ mount_location }}" + - name: Run test only if blivet supports this functionality + when: ((is_fedora and blivet_pkg_version is version("3.10.0-1", ">=")) or + (is_rhel10 and blivet_pkg_version is version("3.10.0-1", ">=")) or + (is_rhel9 and blivet_pkg_version is version("3.6.0-15", ">="))) + block: + - name: Create encrypted Stratis pool + include_role: + name: linux-system-roles.storage + vars: + storage_pools: + - name: foo + disks: "{{ unused_disks }}" + type: stratis + encryption: true + encryption_password: yabbadabbadoo + volumes: + - name: test1 + size: "{{ volume_size }}" + mount_point: "{{ mount_location }}" + + - name: Verify role results + 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 }}" + type: stratis + encryption: true + encryption_password: yabbadabbadoo + volumes: + - name: test1 + size: "{{ volume_size }}" + mount_point: "{{ mount_location }}" + + - name: Verify role results + include_tasks: verify-role-results.yml + + - name: Clean up + include_role: + name: linux-system-roles.storage + vars: + storage_pools: + - name: foo + disks: "{{ unused_disks }}" + type: stratis state: absent + volumes: + - name: test1 + size: "{{ volume_size }}" + mount_point: "{{ mount_location }}" + state: absent - - name: Verify role results - include_tasks: verify-role-results.yml + - name: Verify role results + include_tasks: verify-role-results.yml - # XXX blivet supporting this is not yet released - - name: Run test only if blivet supports this functionality - when: false - block: - name: Create one Stratis pool on one disk include_role: name: linux-system-roles.storage From 9be4bd62d9d0e6bdb450ba639a5a394386c0537e Mon Sep 17 00:00:00 2001 From: Vojtech Trefny Date: Thu, 6 Jun 2024 09:01:39 +0200 Subject: [PATCH 6/6] tests: Skip Stratis tests on RHEL/CentOS 7 Stratis is not available on RHEL/CentOS 7. Related: RHEL-31854 --- library/blivet.py | 4 +- tests/tests_stratis.yml | 321 ++++++++++++++++++++-------------------- 2 files changed, 166 insertions(+), 159 deletions(-) diff --git a/library/blivet.py b/library/blivet.py index bc2a537b..78eb5aef 100644 --- a/library/blivet.py +++ b/library/blivet.py @@ -1330,7 +1330,7 @@ def _destroy(self): class BlivetStratisVolume(BlivetVolume): - blivet_device_class = devices.StratisFilesystemDevice + blivet_device_class = devices.StratisFilesystemDevice if hasattr(devices, "StratisFilesystemDevice") else None def _update_from_device(self, param_name): if param_name == 'fs_type': @@ -1911,7 +1911,7 @@ def _look_up_device(self): class BlivetStratisPool(BlivetPool): - blivet_device_class = devices.StratisPoolDevice + blivet_device_class = devices.StratisPoolDevice if hasattr(devices, "StratisPoolDevice") else None def _type_check(self): return self._device.type == "stratis pool" diff --git a/tests/tests_stratis.yml b/tests/tests_stratis.yml index c41498ad..b68f02d2 100644 --- a/tests/tests_stratis.yml +++ b/tests/tests_stratis.yml @@ -24,92 +24,6 @@ ternary('packages_installed', '') }}" - service_facts - - name: Get unused disks - include_tasks: get_unused_disk.yml - vars: - min_size: "{{ volume_group_size }}" - disks_needed: 2 - - # stratisd is not started automatically and doesn't support DBus activation - # this will be covered by Blivet in the next build - - name: Start stratisd service - service: - name: stratisd - state: started - - - name: Create one Stratis pool with one volume - include_role: - name: linux-system-roles.storage - vars: - storage_pools: - - name: foo - disks: "{{ unused_disks }}" - type: stratis - volumes: - - name: test1 - size: "{{ volume_size }}" - mount_point: "{{ mount_location }}" - - - name: Verify role results - 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 }}" - type: stratis - volumes: - - name: test1 - size: "{{ volume_size }}" - mount_point: "{{ mount_location }}" - - - name: Verify role results - include_tasks: verify-role-results.yml - - - name: Add second filesystem to the pool - include_role: - name: linux-system-roles.storage - vars: - storage_pools: - - name: foo - disks: "{{ unused_disks }}" - type: stratis - volumes: - - name: test1 - size: "{{ volume_size }}" - mount_point: "{{ mount_location }}" - - name: test2 - size: "{{ volume_size }}" - mount_point: "{{ mount_location_2 }}" - - - name: Verify role results - include_tasks: verify-role-results.yml - - - name: Clean up - include_role: - name: linux-system-roles.storage - vars: - storage_pools: - - name: foo - disks: "{{ unused_disks }}" - type: stratis - state: absent - volumes: - - name: test1 - size: "{{ volume_size }}" - mount_point: "{{ mount_location }}" - state: absent - - name: test2 - size: "{{ volume_size }}" - mount_point: "{{ mount_location_2 }}" - state: absent - - - name: Verify role results - include_tasks: verify-role-results.yml - - name: Gather package facts package_facts: @@ -126,6 +40,9 @@ - name: Set distribution version set_fact: + is_rhel7: "{{ (ansible_facts.distribution == 'CentOS' or + ansible_facts.distribution == 'RedHat') and + ansible_facts.distribution_major_version == '7' }}" is_rhel9: "{{ (ansible_facts.distribution == 'CentOS' or ansible_facts.distribution == 'RedHat') and ansible_facts.distribution_major_version == '9' }}" @@ -134,12 +51,24 @@ ansible_facts.distribution_major_version == '10' }}" is_fedora: "{{ ansible_facts.distribution == 'Fedora' }}" - - name: Run test only if blivet supports this functionality - when: ((is_fedora and blivet_pkg_version is version("3.10.0-1", ">=")) or - (is_rhel10 and blivet_pkg_version is version("3.10.0-1", ">=")) or - (is_rhel9 and blivet_pkg_version is version("3.6.0-15", ">="))) + + - name: Completely skip this on RHEL/CentOS 7 where Stratis isn't supported + when: not is_rhel7 block: - - name: Create encrypted Stratis pool + - name: Get unused disks + include_tasks: get_unused_disk.yml + vars: + min_size: "{{ volume_group_size }}" + disks_needed: 2 + + # stratisd is not started automatically and doesn't support DBus activation + # this will be covered by Blivet in the next build + - name: Start stratisd service + service: + name: stratisd + state: started + + - name: Create one Stratis pool with one volume include_role: name: linux-system-roles.storage vars: @@ -147,8 +76,6 @@ - name: foo disks: "{{ unused_disks }}" type: stratis - encryption: true - encryption_password: yabbadabbadoo volumes: - name: test1 size: "{{ volume_size }}" @@ -165,8 +92,6 @@ - name: foo disks: "{{ unused_disks }}" type: stratis - encryption: true - encryption_password: yabbadabbadoo volumes: - name: test1 size: "{{ volume_size }}" @@ -175,7 +100,7 @@ - name: Verify role results include_tasks: verify-role-results.yml - - name: Clean up + - name: Add second filesystem to the pool include_role: name: linux-system-roles.storage vars: @@ -183,36 +108,13 @@ - name: foo disks: "{{ unused_disks }}" type: stratis - state: absent volumes: - name: test1 size: "{{ volume_size }}" mount_point: "{{ mount_location }}" - state: absent - - - name: Verify role results - include_tasks: verify-role-results.yml - - - name: Create one Stratis pool on one disk - include_role: - name: linux-system-roles.storage - vars: - storage_pools: - - name: foo - disks: "{{ unused_disks[0] }}" - type: stratis - - - name: Verify role results - include_tasks: verify-role-results.yml - - - name: Add the second disk to the pool - include_role: - name: linux-system-roles.storage - vars: - storage_pools: - - name: foo - disks: "{{ [unused_disks[0], unused_disks[1]] }}" - type: stratis + - name: test2 + size: "{{ volume_size }}" + mount_point: "{{ mount_location_2 }}" - name: Verify role results include_tasks: verify-role-results.yml @@ -231,43 +133,148 @@ size: "{{ volume_size }}" mount_point: "{{ mount_location }}" state: absent + - name: test2 + size: "{{ volume_size }}" + mount_point: "{{ mount_location_2 }}" + state: absent - name: Verify role results include_tasks: verify-role-results.yml - - name: Setup Tang server on localhost for testing - ansible.builtin.include_role: - name: fedora.linux_system_roles.nbde_server - vars: - nbde_server_manage_firewall: true - nbde_server_manage_selinux: true - nbde_server_port: 7500 - - - name: Create encrypted Stratis pool with Clevis/Tang - include_role: - name: linux-system-roles.storage - vars: - storage_pools: - - name: foo - disks: "{{ unused_disks[0] }}" - type: stratis - encryption: true - encryption_password: yabbadabbadoo - encryption_clevis_pin: tang - encryption_tang_url: localhost:7500 - - - name: Verify role results - include_tasks: verify-role-results.yml - - - name: Clean up - include_role: - name: linux-system-roles.storage - vars: - storage_pools: - - name: foo - disks: "{{ unused_disks[0] }}" - type: stratis - state: absent + - name: Run test only if blivet supports this functionality + when: ((is_fedora and blivet_pkg_version is version("3.10.0-1", ">=")) or + (is_rhel10 and blivet_pkg_version is version("3.10.0-1", ">=")) or + (is_rhel9 and blivet_pkg_version is version("3.6.0-15", ">="))) + block: + - name: Create encrypted Stratis pool + include_role: + name: linux-system-roles.storage + vars: + storage_pools: + - name: foo + disks: "{{ unused_disks }}" + type: stratis + encryption: true + encryption_password: yabbadabbadoo + volumes: + - name: test1 + size: "{{ volume_size }}" + mount_point: "{{ mount_location }}" + + - name: Verify role results + 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 }}" + type: stratis + encryption: true + encryption_password: yabbadabbadoo + volumes: + - name: test1 + size: "{{ volume_size }}" + mount_point: "{{ mount_location }}" + + - name: Verify role results + include_tasks: verify-role-results.yml + + - name: Clean up + include_role: + name: linux-system-roles.storage + vars: + storage_pools: + - name: foo + disks: "{{ unused_disks }}" + type: stratis + state: absent + volumes: + - name: test1 + size: "{{ volume_size }}" + mount_point: "{{ mount_location }}" + state: absent + + - name: Verify role results + include_tasks: verify-role-results.yml + + - name: Create one Stratis pool on one disk + include_role: + name: linux-system-roles.storage + vars: + storage_pools: + - name: foo + disks: "{{ unused_disks[0] }}" + type: stratis + + - name: Verify role results + include_tasks: verify-role-results.yml + + - name: Add the second disk to the pool + include_role: + name: linux-system-roles.storage + vars: + storage_pools: + - name: foo + disks: "{{ [unused_disks[0], unused_disks[1]] }}" + type: stratis + + - name: Verify role results + include_tasks: verify-role-results.yml + + - name: Clean up + include_role: + name: linux-system-roles.storage + vars: + storage_pools: + - name: foo + disks: "{{ unused_disks }}" + type: stratis + state: absent + volumes: + - name: test1 + size: "{{ volume_size }}" + mount_point: "{{ mount_location }}" + state: absent + + - name: Verify role results + include_tasks: verify-role-results.yml + + - name: Setup Tang server on localhost for testing + ansible.builtin.include_role: + name: fedora.linux_system_roles.nbde_server + vars: + nbde_server_manage_firewall: true + nbde_server_manage_selinux: true + nbde_server_port: 7500 + + - name: Create encrypted Stratis pool with Clevis/Tang + include_role: + name: linux-system-roles.storage + vars: + storage_pools: + - name: foo + disks: "{{ unused_disks[0] }}" + type: stratis + encryption: true + encryption_password: yabbadabbadoo + encryption_clevis_pin: tang + encryption_tang_url: localhost:7500 + + - name: Verify role results + include_tasks: verify-role-results.yml + + - name: Clean up + include_role: + name: linux-system-roles.storage + vars: + storage_pools: + - name: foo + disks: "{{ unused_disks[0] }}" + type: stratis + state: absent - - name: Verify role results - include_tasks: verify-role-results.yml + - name: Verify role results + include_tasks: verify-role-results.yml