New module zpool (#10146)

* Add zpool module

* Add botmeta

* Use str.format instead of f-strings

* Remove nonlocal usage

* Add check to only pass ashift to zpool add

* Extend ansible_spec and remove unnecessary validation

* Apply suggestions and fix style

* Fix indentation of yaml lists

* Add method to normalize vdevs

Fix role: none in vdevs

* Use CmdRunner instead of run_command

* Fix styling and documentation

* Use str.format instead of f-strings

* Make sure vdevs are only required when state is present

* Add support for loop devices and normalize vdev type

* Add integration tests

* Add missing test dependencies for alpine and redhat

* Skip integration tests on rhel10 until there there packages available

* Use package module for better auto detection of package manager on rhel

* Add copyright header

* Skip tests on rhel and remove redhat install requirements

* Ensure loop devices under /dev exist

* Enable usage of files as pool devices

* Remove disk setup

* Use files as disks

* Apply suggestions

* Fix argument_spec
This commit is contained in:
Tom Hesse 2025-06-07 17:52:01 +02:00 committed by GitHub
commit 928622703d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 1064 additions and 0 deletions

View file

@ -0,0 +1,14 @@
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
azp/posix/3
azp/posix/vm
destructive
needs/privileged
skip/aix
skip/freebsd
skip/osx
skip/macos
skip/rhel
skip/docker

View file

@ -0,0 +1,34 @@
---
# Copyright (c) 2025, Tom Hesse <contact@tomhesse.xyz>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
zpool_single_disk_config:
- "{{ remote_tmp_dir }}/disk0.img"
zpool_mirror_disk_config:
- "{{ remote_tmp_dir }}/disk1.img"
- "{{ remote_tmp_dir }}/disk2.img"
zpool_raidz_disk_config:
- "{{ remote_tmp_dir }}/disk3.img"
- "{{ remote_tmp_dir }}/disk4.img"
zpool_vdevs_disk_config:
vdev1:
- "{{ remote_tmp_dir }}/disk5.img"
vdev2:
- "{{ remote_tmp_dir }}/disk6.img"
vdev3:
- "{{ remote_tmp_dir }}/disk7.img"
- "{{ remote_tmp_dir }}/disk8.img"
vdev4:
- "{{ remote_tmp_dir }}/disk9.img"
- "{{ remote_tmp_dir }}/disk10.img"
zpool_disk_configs: "{{ zpool_single_disk_config + zpool_mirror_disk_config + zpool_raidz_disk_config + (zpool_vdevs_disk_config.values() | flatten) }}"
zpool_single_disk_pool_name: spool
zpool_mirror_disk_pool_name: mpool
zpool_raidz_disk_pool_name: rpool
zpool_generic_pool_name: tank

View file

@ -0,0 +1,7 @@
---
# Copyright (c) 2025, Tom Hesse <contact@tomhesse.xyz>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
dependencies:
- setup_remote_tmp_dir

View file

@ -0,0 +1,147 @@
---
# Copyright (c) 2025, Tom Hesse <contact@tomhesse.xyz>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
- name: Test adding a single disk vdev
block:
- name: Ensure a single disk pool exists
community.general.zpool:
name: "{{ zpool_generic_pool_name }}"
vdevs:
- disks: "{{ zpool_vdevs_disk_config.vdev1 }}"
state: present
- name: Add a single disk vdev
community.general.zpool:
name: "{{ zpool_generic_pool_name }}"
vdevs:
- disks: "{{ zpool_vdevs_disk_config.vdev1 }}"
- disks: "{{ zpool_vdevs_disk_config.vdev2 }}"
state: present
- name: Check if vdev was added
ansible.builtin.command:
cmd: "zpool status -P -L {{ zpool_generic_pool_name }}"
register: single_disk_pool_check
changed_when: false
- name: Assert that added disk is present
ansible.builtin.assert:
that:
- "zpool_vdevs_disk_config.vdev2[0] in single_disk_pool_check.stdout"
- name: Ensure the single disk pool is absent
community.general.zpool:
name: "{{ zpool_generic_pool_name }}"
state: absent
- name: Test adding a mirror vdev
block:
- name: Ensure a single disk pool exists
community.general.zpool:
name: "{{ zpool_generic_pool_name }}"
vdevs:
- disks: "{{ zpool_vdevs_disk_config.vdev1 }}"
state: present
- name: Add a mirror vdev
community.general.zpool:
name: "{{ zpool_generic_pool_name }}"
force: true # This is necessary because of the mismatched replication level
vdevs:
- disks: "{{ zpool_vdevs_disk_config.vdev1 }}"
- type: mirror
disks: "{{ zpool_vdevs_disk_config.vdev3 }}"
state: present
- name: Check if vdev was added
ansible.builtin.command:
cmd: "zpool status -P -L {{ zpool_generic_pool_name }}"
register: mirror_pool_check
changed_when: false
- name: Assert that added vdev is present
ansible.builtin.assert:
that:
- "zpool_vdevs_disk_config.vdev3[0] in mirror_pool_check.stdout"
- "zpool_vdevs_disk_config.vdev3[1] in mirror_pool_check.stdout"
- name: Ensure the single disk pool is absent
community.general.zpool:
name: "{{ zpool_generic_pool_name }}"
state: absent
- name: Test adding a raidz vdev
block:
- name: Ensure a single disk pool exists
community.general.zpool:
name: "{{ zpool_generic_pool_name }}"
vdevs:
- disks: "{{ zpool_vdevs_disk_config.vdev1 }}"
state: present
- name: Add a raidz vdev
community.general.zpool:
name: "{{ zpool_generic_pool_name }}"
force: true # This is necessary because of the mismatched replication level
vdevs:
- disks: "{{ zpool_vdevs_disk_config.vdev1 }}"
- type: raidz
disks: "{{ zpool_vdevs_disk_config.vdev3 }}"
state: present
- name: Check if vdev was added
ansible.builtin.command:
cmd: "zpool status -P -L {{ zpool_generic_pool_name }}"
register: raidz_pool_check
changed_when: false
- name: Assert that added vdev is present
ansible.builtin.assert:
that:
- "zpool_vdevs_disk_config.vdev3[0] in raidz_pool_check.stdout"
- "zpool_vdevs_disk_config.vdev3[1] in raidz_pool_check.stdout"
- name: Ensure the single disk pool is absent
community.general.zpool:
name: "{{ zpool_generic_pool_name }}"
state: absent
- name: Test removing an existing vdev
block:
- name: Ensure a pool with two mirrored vdevs exists
community.general.zpool:
name: "{{ zpool_generic_pool_name }}"
vdevs:
- type: mirror
disks: "{{ zpool_vdevs_disk_config.vdev3 }}"
- type: mirror
disks: "{{ zpool_vdevs_disk_config.vdev4 }}"
state: present
- name: Remove a vdev
community.general.zpool:
name: "{{ zpool_generic_pool_name }}"
vdevs:
- type: mirror
disks: "{{ zpool_vdevs_disk_config.vdev4 }}"
state: present
- name: Check if vdev was removed
ansible.builtin.command:
cmd: "zpool status -P -L {{ zpool_generic_pool_name }}"
register: remove_vdev_check
changed_when: false
- name: Assert that removed vdev is absent
ansible.builtin.assert:
that:
- "zpool_vdevs_disk_config.vdev3[0] not in remove_vdev_check.stdout"
- "zpool_vdevs_disk_config.vdev3[1] not in remove_vdev_check.stdout"
- "'Removal of vdev' in remove_vdev_check.stdout"
- name: Ensure the pool is absent
community.general.zpool:
name: "{{ zpool_generic_pool_name }}"
state: absent

View file

@ -0,0 +1,123 @@
---
# Copyright (c) 2025, Tom Hesse <contact@tomhesse.xyz>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
- name: Test single disk pool creation
block:
- name: Ensure single disk pool exists
community.general.zpool:
name: "{{ zpool_single_disk_pool_name }}"
vdevs:
- disks: "{{ zpool_single_disk_config }}"
- name: Check if single disk pool exists
ansible.builtin.command:
cmd: "zpool list -H -o name,health {{ zpool_single_disk_pool_name }}"
register: single_disk_pool_check
changed_when: false
- name: Assert that single disk pool is online
ansible.builtin.assert:
that:
- "zpool_single_disk_pool_name in single_disk_pool_check.stdout"
- "'ONLINE' in single_disk_pool_check.stdout"
- name: Test mirror disk pool creation
block:
- name: Ensure mirror disk pool exists
community.general.zpool:
name: "{{ zpool_mirror_disk_pool_name }}"
vdevs:
- type: mirror
disks: "{{ zpool_mirror_disk_config }}"
- name: Check if mirror disk pool exists
ansible.builtin.command:
cmd: "zpool list -H -o name,health {{ zpool_mirror_disk_pool_name }}"
register: mirror_disk_pool_check
changed_when: false
- name: Assert that mirror disk pool is online
ansible.builtin.assert:
that:
- "zpool_mirror_disk_pool_name in mirror_disk_pool_check.stdout"
- "'ONLINE' in mirror_disk_pool_check.stdout"
- name: Test raidz disk pool creation
block:
- name: Ensure raidz disk pool exists
community.general.zpool:
name: "{{ zpool_raidz_disk_pool_name }}"
vdevs:
- type: raidz
disks: "{{ zpool_raidz_disk_config }}"
- name: Check if raidz disk pool exists
ansible.builtin.command:
cmd: "zpool list -H -o name,health {{ zpool_raidz_disk_pool_name }}"
register: raidz_disk_pool_check
changed_when: false
- name: Assert that raidz disk pool is online
ansible.builtin.assert:
that:
- "zpool_raidz_disk_pool_name in raidz_disk_pool_check.stdout"
- "'ONLINE' in raidz_disk_pool_check.stdout"
- name: Test single disk pool deletion
block:
- name: Ensure single disk pool is absent
community.general.zpool:
name: "{{ zpool_single_disk_pool_name }}"
state: absent
- name: Check if single disk pool is absent
ansible.builtin.command:
cmd: "zpool list -H -o name,health {{ zpool_single_disk_pool_name }}"
register: single_disk_pool_check
ignore_errors: true
changed_when: false
- name: Assert that single disk pool is online
ansible.builtin.assert:
that:
- "'no such pool' in single_disk_pool_check.stderr"
- name: Test mirror disk pool deletion
block:
- name: Ensure mirror disk pool is absent
community.general.zpool:
name: "{{ zpool_mirror_disk_pool_name }}"
state: absent
- name: Check if mirror disk pool is absent
ansible.builtin.command:
cmd: "zpool list -H -o name,health {{ zpool_mirror_disk_pool_name }}"
register: mirror_disk_pool_check
ignore_errors: true
changed_when: false
- name: Assert that mirror disk pool is online
ansible.builtin.assert:
that:
- "'no such pool' in mirror_disk_pool_check.stderr"
- name: Test raidz disk pool deletion
block:
- name: Ensure raidz disk pool is absent
community.general.zpool:
name: "{{ zpool_raidz_disk_pool_name }}"
state: absent
- name: Check if raidz disk pool is absent
ansible.builtin.command:
cmd: "zpool list -H -o name,health {{ zpool_raidz_disk_pool_name }}"
register: raidz_disk_pool_check
ignore_errors: true
changed_when: false
- name: Assert that raidz disk pool is online
ansible.builtin.assert:
that:
- "'no such pool' in raidz_disk_pool_check.stderr"

View file

@ -0,0 +1,15 @@
---
# Copyright (c) 2025, Tom Hesse <contact@tomhesse.xyz>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
- name: Install required packages
community.general.apk:
name:
- zfs
- zfs-lts
- name: Load zfs module
community.general.modprobe:
name: zfs
state: present

View file

@ -0,0 +1,10 @@
---
# Copyright (c) 2025, Tom Hesse <contact@tomhesse.xyz>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
- name: Install required packages
ansible.builtin.apt:
name:
- zfsutils-linux
- util-linux

View file

@ -0,0 +1,25 @@
---
# Copyright (c) 2025, Tom Hesse <contact@tomhesse.xyz>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
- name: Execute integration tests
become: true
block:
- name: Ensure disk files exists
ansible.builtin.command:
cmd: "dd if=/dev/zero of={{ item }} bs=1M count=256 conv=fsync"
creates: "{{ item }}"
loop: "{{ zpool_disk_configs }}"
- name: Include distribution specific install_requirements.yml
ansible.builtin.include_tasks: install_requirements_{{ ansible_distribution | lower }}.yml
- name: Include create_destroy.yml
ansible.builtin.include_tasks: create_destroy.yml
- name: Include add_remove_vdevs.yml
ansible.builtin.include_tasks: add_remove_vdevs.yml
- name: Include properties.yml
ansible.builtin.include_tasks: properties.yml

View file

@ -0,0 +1,73 @@
---
# Copyright (c) 2025, Tom Hesse <contact@tomhesse.xyz>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
- name: Ensure pool with two mirrored disks exists
community.general.zpool:
name: "{{ zpool_generic_pool_name }}"
pool_properties:
ashift: 12
filesystem_properties:
compression: off
vdevs:
- type: mirror
disks: "{{ zpool_vdevs_disk_config.vdev3 }}"
- type: mirror
disks: "{{ zpool_vdevs_disk_config.vdev4 }}"
state: present
- name: Test changing of a pool property
block:
- name: Change ashift from 12 to 13
community.general.zpool:
name: "{{ zpool_generic_pool_name }}"
pool_properties:
ashift: 13
vdevs:
- type: mirror
disks: "{{ zpool_vdevs_disk_config.vdev3 }}"
- type: mirror
disks: "{{ zpool_vdevs_disk_config.vdev4 }}"
state: present
- name: Check ashift
ansible.builtin.command:
cmd: "zpool get -H -o value ashift {{ zpool_generic_pool_name }}"
changed_when: false
register: ashift_check
- name: Assert ashift has changed
ansible.builtin.assert:
that:
- "'13' in ashift_check.stdout"
- name: Test changing of a dataset property
block:
- name: Change compression from off to lz4
community.general.zpool:
name: "{{ zpool_generic_pool_name }}"
filesystem_properties:
compression: lz4
vdevs:
- type: mirror
disks: "{{ zpool_vdevs_disk_config.vdev3 }}"
- type: mirror
disks: "{{ zpool_vdevs_disk_config.vdev4 }}"
state: present
- name: Check compression
ansible.builtin.command:
cmd: "zfs get -H -o value compression {{ zpool_generic_pool_name }}"
changed_when: false
register: compression_check
- name: Assert compression has changed
ansible.builtin.assert:
that:
- "'lz4' in compression_check.stdout"
- name: Cleanup pool
community.general.zpool:
name: "{{ zpool_generic_pool_name }}"
state: absent