├── examples ├── ansible.cfg ├── Vagrantfile └── provision.yml ├── vars ├── archlinux.yml ├── redhat.yml ├── debian.yml └── main.yml ├── requirements.yml ├── .style.yapf ├── tasks ├── install_archlinux.yml ├── configure_snapshots.yml ├── manage_pools.yml ├── configure_zfs.yml ├── manage_volume.yml ├── manage_filesystem.yml ├── install_debian.yml ├── install_redhat.yml ├── main.yml ├── manage_pool.yml └── manage_services.yml ├── templates └── etc │ ├── systemd │ └── system │ │ ├── systemd-journald.service.d │ │ └── after-zfs-mount.service.j2 │ │ ├── zpool-trim@.timer.j2 │ │ ├── zpool-scrub@.timer.j2 │ │ ├── zpool-trim@.service.j2 │ │ └── zpool-scrub@.service.j2 │ ├── zrepl │ └── zrepl.yml.j2 │ ├── modprobe.d │ └── zfs.conf.j2 │ ├── yum.repos.d │ ├── zrepl.repo.j2 │ └── zfs.repo.j2 │ ├── udev │ └── rules.d │ │ └── 90-zfs-io-scheduler.rules.j2 │ ├── cron.d │ └── zfsnap.j2 │ └── default │ └── zfs.j2 ├── .bumpversion.cfg ├── .gitignore ├── .ansible-lint ├── .pre-commit-config.yaml ├── tox.ini ├── requirements.txt ├── .github └── workflows │ ├── publish.yml │ └── release.yml ├── .editorconfig ├── handlers └── main.yml ├── molecule └── default │ ├── molecule.yml │ ├── prepare.yml │ ├── verify.yml │ └── converge.yml ├── .yamllint ├── meta └── main.yml ├── .chglog ├── config.yml └── CHANGELOG.tpl.md ├── test_plugins └── list.py ├── LICENSE ├── files ├── centos │ ├── RPM-GPG-KEY-zfsonlinux │ └── RPM-GPG-KEY-zrepl └── debian │ └── zrepl-apt-key.asc ├── filter_plugins ├── split.py └── selectattr2.py ├── CHANGELOG.md ├── library └── prepare_zfs_properties.py ├── defaults └── main.yml ├── CODE_OF_CONDUCT.md └── README.md /examples/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | roles_path = ../../ 3 | -------------------------------------------------------------------------------- /vars/archlinux.yml: -------------------------------------------------------------------------------- 1 | --- 2 | __zfs_zedletdir: /usr/lib/zfs/zed.d 3 | -------------------------------------------------------------------------------- /vars/redhat.yml: -------------------------------------------------------------------------------- 1 | --- 2 | __zfs_zedletdir: /usr/libexec/zfs/zed.d 3 | -------------------------------------------------------------------------------- /vars/debian.yml: -------------------------------------------------------------------------------- 1 | --- 2 | __zfs_zedletdir: /usr/lib/zfs-linux/zed.d 3 | -------------------------------------------------------------------------------- /requirements.yml: -------------------------------------------------------------------------------- 1 | --- 2 | roles: [] 3 | collections: 4 | - community.general 5 | -------------------------------------------------------------------------------- /.style.yapf: -------------------------------------------------------------------------------- 1 | [style] 2 | based_on_style = google 3 | column_limit = 120 4 | indent_width = 4 5 | -------------------------------------------------------------------------------- /tasks/install_archlinux.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: fail Arch Linux installation 3 | ansible.builtin.fail: 4 | msg: Arch Linux is not yet supported by this role 5 | -------------------------------------------------------------------------------- /templates/etc/systemd/system/systemd-journald.service.d/after-zfs-mount.service.j2: -------------------------------------------------------------------------------- 1 | {{ ansible_managed | comment }} 2 | 3 | [Unit] 4 | After=zfs-mount.service 5 | -------------------------------------------------------------------------------- /vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | __zfs_zrepl_config_defaults: 3 | global: 4 | logging: 5 | - type: syslog 6 | level: warn 7 | format: human 8 | -------------------------------------------------------------------------------- /templates/etc/zrepl/zrepl.yml.j2: -------------------------------------------------------------------------------- 1 | #jinja2: trim_blocks: "true", lstrip_blocks: "true" 2 | {{ ansible_managed | comment }} 3 | 4 | {{ cfg | to_nice_yaml(indent=2) }} 5 | -------------------------------------------------------------------------------- /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 3.3.1 3 | commit = true 4 | message = bump: version {current_version} → {new_version} 5 | tag = true 6 | tag_name = v{new_version} 7 | sign_tags = true 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ansible 2 | *.retry 3 | .cache 4 | .molecule 5 | 6 | # Python: Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | .eggs/ 11 | 12 | # Testing 13 | .tox/ 14 | .vagrant/ 15 | pytestdebug.log 16 | -------------------------------------------------------------------------------- /templates/etc/modprobe.d/zfs.conf.j2: -------------------------------------------------------------------------------- 1 | #jinja2: trim_blocks: "true", lstrip_blocks: "true" 2 | {{ ansible_managed | comment }} 3 | 4 | {% for key, value in (zfs_kernel_module_parameters | dictsort()) %} 5 | options zfs {{ key }}={{ value }} 6 | {% endfor %} 7 | -------------------------------------------------------------------------------- /templates/etc/systemd/system/zpool-trim@.timer.j2: -------------------------------------------------------------------------------- 1 | {{ ansible_managed | comment }} 2 | 3 | [Unit] 4 | Description=Trim ZFS Pool ({{ zfs_trim_schedule }}) 5 | 6 | [Timer] 7 | OnCalendar={{ zfs_trim_schedule }} 8 | Persistent=true 9 | 10 | [Install] 11 | WantedBy=timers.target 12 | -------------------------------------------------------------------------------- /templates/etc/systemd/system/zpool-scrub@.timer.j2: -------------------------------------------------------------------------------- 1 | {{ ansible_managed | comment }} 2 | 3 | [Unit] 4 | Description=Scrub ZFS Pool ({{ zfs_scrub_schedule }}) 5 | 6 | [Timer] 7 | OnCalendar={{ zfs_scrub_schedule }} 8 | Persistent=true 9 | 10 | [Install] 11 | WantedBy=timers.target 12 | -------------------------------------------------------------------------------- /templates/etc/systemd/system/zpool-trim@.service.j2: -------------------------------------------------------------------------------- 1 | {{ ansible_managed | comment }} 2 | 3 | [Unit] 4 | Description=Trim ZFS Pool %i 5 | Requires=zfs.target 6 | After=zfs.target 7 | 8 | [Service] 9 | Type=oneshot 10 | ExecStart=/sbin/zpool trim %i 11 | Nice=19 12 | IOSchedulingClass=idle 13 | KillSignal=SIGINT 14 | -------------------------------------------------------------------------------- /templates/etc/systemd/system/zpool-scrub@.service.j2: -------------------------------------------------------------------------------- 1 | {{ ansible_managed | comment }} 2 | 3 | [Unit] 4 | Description=Scrub ZFS Pool %i 5 | Requires=zfs.target 6 | After=zfs.target 7 | 8 | [Service] 9 | Type=oneshot 10 | ExecStart=/sbin/zpool scrub %i 11 | Nice=19 12 | IOSchedulingClass=idle 13 | KillSignal=SIGINT 14 | -------------------------------------------------------------------------------- /.ansible-lint: -------------------------------------------------------------------------------- 1 | # skip the specified linter rules 2 | # see: https://ansible-lint.readthedocs.io/en/latest/default_rules.html 3 | skip_list: 4 | - name[casing] 5 | - name[missing] 6 | - name[template] 7 | 8 | # paths to be excluded from linting 9 | exclude_paths: 10 | - .cache/ 11 | - .chglog/ 12 | - .github/ 13 | - .tox/ 14 | -------------------------------------------------------------------------------- /templates/etc/yum.repos.d/zrepl.repo.j2: -------------------------------------------------------------------------------- 1 | {{ ansible_managed | comment }} 2 | 3 | [zrepl] 4 | name = zrepl 5 | baseurl = {{ zfs_zrepl_redhat_repo_url }} 6 | metadata_expire = 7d 7 | gpgcheck = 1 8 | gpgkey = file:///etc/pki/rpm-gpg/RPM-GPG-KEY-zfsonlinux 9 | {% if zfs_redhat_repo_proxy | default('') %} 10 | proxy = {{ zfs_redhat_repo_proxy }} 11 | {% endif %} 12 | -------------------------------------------------------------------------------- /tasks/configure_snapshots.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: configure zrepl 3 | ansible.builtin.template: 4 | src: etc/zrepl/zrepl.yml.j2 5 | dest: /etc/zrepl/zrepl.yml 6 | owner: root 7 | group: root 8 | mode: "0644" 9 | validate: zrepl configcheck --config %s 10 | notify: restart zrepl 11 | when: zfs_zrepl_enabled | bool 12 | vars: 13 | cfg: "{{ __zfs_zrepl_config_defaults | combine(zfs_zrepl_config, recursive=True) }}" 14 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v4.0.1 5 | hooks: 6 | - id: check-merge-conflict 7 | - id: check-added-large-files 8 | - id: end-of-file-fixer 9 | - id: mixed-line-ending 10 | - id: trailing-whitespace 11 | 12 | - repo: https://github.com/ansible-community/ansible-lint.git 13 | rev: v5.2.0 14 | hooks: 15 | - id: ansible-lint 16 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 4.0 3 | envlist = py{311}-ansible{216,217,218} 4 | skipsdist = true 5 | 6 | [testenv] 7 | passenv = * 8 | deps = 9 | ansible216: ansible-core == 2.16.* 10 | ansible217: ansible-core == 2.17.* 11 | ansible218: ansible-core == 2.18.* 12 | molecule 13 | molecule-vagrant 14 | ansible-lint 15 | commands = molecule test 16 | setenv = 17 | TOX_ENVNAME={envname} 18 | PY_COLORS=1 19 | ANSIBLE_FORCE_COLOR=1 20 | ANSIBLE_ROLES_PATH=../ 21 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # PIP Requirements File 2 | # 3 | # The listed versions of Ansible and Jinja were tested to work with the role. 4 | # Any previous versions might not be compatible and are therefore not supported. 5 | # To install the PIP requirements run the following command: 6 | # 7 | # `python -m pip --user install -r requirements.txt` 8 | # 9 | # See the PIP requirements file documentation for details: 10 | # https://pip.pypa.io/en/stable/user_guide/#requirements-files 11 | 12 | ansible>=2.10 13 | jinja2>=2.11.2 14 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish on Ansible Galaxy 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["Create GitHub Release"] 6 | types: 7 | - completed 8 | 9 | jobs: 10 | publish-on-ansible-galaxy: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | 16 | - name: Publish 17 | uses: robertdebock/galaxy-action@1.2.0 18 | with: 19 | galaxy_api_key: ${{ secrets.GALAXY_API_KEY }} 20 | git_branch: main 21 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps maintain consistent coding styles for multiple developers 2 | # working on the same project across various editors and IDEs. 3 | # Read more: https://editorconfig.org/ 4 | 5 | root = true 6 | 7 | [*] 8 | indent_style = space 9 | indent_size = 4 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | 15 | [*.md] 16 | indent_size = 2 17 | trim_trailing_whitespace = false 18 | 19 | [*.{yml,yaml,toml}*] 20 | indent_size = 2 21 | 22 | [*.py] 23 | # isort options 24 | line_length = 120 25 | -------------------------------------------------------------------------------- /templates/etc/udev/rules.d/90-zfs-io-scheduler.rules.j2: -------------------------------------------------------------------------------- 1 | #jinja2: trim_blocks: "true", lstrip_blocks: "true" 2 | {{ ansible_managed | comment }} 3 | 4 | {% for item in device_properties.results %} 5 | {% set properties = dict(item.stdout_lines | map('split', '=', 1)) %} 6 | ACTION=="add|change", KERNEL=="sd[a-z]", {{ 7 | 'ENV{ID_SERIAL}=="' ~ properties.ID_SERIAL ~ '"' if properties.ID_SERIAL is defined else 8 | 'ENV{ID_PART_TABLE_UUID}=="' ~ properties.ID_PART_TABLE_UUID ~ '"' if properties.ID_PART_TABLE_UUID is defined else 9 | 'ENV{ID_PATH}=="' ~ properties.ID_PATH ~ '"'}}, ATTR{queue/scheduler}="none" 10 | {% endfor %} 11 | -------------------------------------------------------------------------------- /handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: reload udev rules 3 | ansible.builtin.shell: "udevadm control --reload && udevadm trigger" 4 | changed_when: true 5 | 6 | - name: update initramfs (Debian) 7 | ansible.builtin.command: update-initramfs -u -k all 8 | changed_when: true 9 | when: ansible_os_family | lower == 'debian' 10 | 11 | - name: mkinitcpio (Arch Linux) 12 | ansible.builtin.command: mkinitcpio -P 13 | changed_when: true 14 | when: ansible_os_family | lower == 'archlinux' 15 | 16 | - name: restart zrepl 17 | ansible.builtin.systemd: 18 | name: zrepl 19 | state: restarted 20 | when: zfs_zrepl_enabled | bool 21 | -------------------------------------------------------------------------------- /molecule/default/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | options: 5 | role-file: requirements.yml 6 | requirements-file: requirements.yml 7 | driver: 8 | name: vagrant 9 | platforms: 10 | - name: molecule-zfs-debian11-$TOX_ENVNAME 11 | box: debian/bullseye64 12 | memory: 512 13 | cpus: 1 14 | 15 | - name: molecule-zfs-centosstream8-$TOX_ENVNAME 16 | box: bento/centos-stream-8 17 | memory: 512 18 | cpus: 1 19 | provisioner: 20 | name: ansible 21 | lint: ansible-lint --force-color 22 | lint: | 23 | set -e 24 | yamllint . 25 | ansible-lint 26 | verifier: 27 | name: ansible 28 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | # extend default yamllint configuration 3 | # https://yamllint.readthedocs.io/en/stable/configuration.html#default-configuration 4 | extends: default 5 | 6 | rules: 7 | braces: 8 | max-spaces-inside: 1 9 | level: error 10 | brackets: 11 | max-spaces-inside: 1 12 | level: error 13 | colons: 14 | max-spaces-after: -1 15 | level: error 16 | commas: 17 | max-spaces-after: -1 18 | level: error 19 | empty-lines: 20 | max: 3 21 | level: error 22 | hyphens: 23 | level: error 24 | new-lines: 25 | type: unix 26 | comments-indentation: disable 27 | document-start: disable 28 | line-length: disable 29 | truthy: disable 30 | 31 | ignore: | 32 | .tox/ 33 | .cache/ 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create GitHub Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | jobs: 9 | create-github-release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | 15 | - name: Extract release notes 16 | id: extract-release-notes 17 | uses: ffurrer2/extract-release-notes@v1 18 | 19 | - name: Create release 20 | uses: actions/create-release@v1 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | with: 24 | tag_name: ${{ github.ref }} 25 | release_name: ${{ github.ref }} 26 | draft: false 27 | prerelease: false 28 | body: ${{ steps.extract-release-notes.outputs.release_notes }} 29 | -------------------------------------------------------------------------------- /meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependencies: [] 3 | 4 | galaxy_info: 5 | author: André Lehmann 6 | namespace: aisbergg 7 | role_name: zfs 8 | description: Install ZFS, create or import zpools and manage datasets. 9 | company: none 10 | issue_tracker_url: https://github.com/aisbergg/ansible-role-zfs/issues 11 | license: MIT 12 | min_ansible_version: "2.10" 13 | 14 | platforms: 15 | - name: EL 16 | versions: 17 | - "8" 18 | - name: Debian 19 | versions: 20 | - bullseye 21 | - buster 22 | - name: Ubuntu 23 | versions: 24 | - hirsute 25 | - groovy 26 | - focal 27 | - bionic 28 | # - name: ArchLinux 29 | # versions: 30 | # - all 31 | 32 | galaxy_tags: 33 | - zfs 34 | - zpool 35 | - filesystem 36 | -------------------------------------------------------------------------------- /.chglog/config.yml: -------------------------------------------------------------------------------- 1 | style: github 2 | template: CHANGELOG.tpl.md 3 | info: 4 | title: CHANGELOG 5 | repository_url: https://github.com/aisbergg/ansible-role-zfs 6 | options: 7 | commits: 8 | filters: 9 | Type: 10 | - build 11 | - chore 12 | - ci 13 | - docs 14 | - feat 15 | - fix 16 | - perf 17 | - refactor 18 | commit_groups: 19 | title_maps: 20 | build: Build System 21 | chore: Chores 22 | ci: CI Configuration 23 | docs: Documentation 24 | feat: Features 25 | fix: Bug Fixes 26 | perf: Performance Improvements 27 | refactor: Code Refactoring 28 | header: 29 | pattern: "^(\\w*)(?:\\(([\\w\\$\\.\\-\\*\\s]*)\\))?!?\\:\\s(.*)$" 30 | pattern_maps: 31 | - Type 32 | - Scope 33 | - Subject 34 | notes: 35 | keywords: 36 | - BREAKING CHANGE 37 | - SECURITY 38 | -------------------------------------------------------------------------------- /examples/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | Vagrant.configure(2) do |config| 3 | # see http://www.vagrantbox.es/ 4 | config.vm.box = "debian/bookworm64" 5 | # config.vm.network "private_network", ip: "172.10.10.10" 6 | config.vm.synced_folder '.', '/vagrant', disabled: true 7 | config.vm.provider "virtualbox" do |v| 8 | v.gui = false 9 | v.customize ["modifyvm", :id, "--memory", "1024"] 10 | v.customize ["modifyvm", :id, "--usb", "off"] 11 | v.customize ["modifyvm", :id, "--usbehci", "off"] 12 | end 13 | 14 | config.vm.disk :disk, name: "disk1", size: "100MB" 15 | config.vm.disk :disk, name: "disk2", size: "100MB" 16 | 17 | # provisioning with shell 18 | config.vm.provision "shell", inline: "apt-get update && apt-get install -y --no-install-recommends python3" 19 | 20 | # provisioning with ansible 21 | config.vm.provision "ansible" do |ansible| 22 | ansible.verbose = "v" 23 | ansible.playbook = "provision.yml" 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /templates/etc/yum.repos.d/zfs.repo.j2: -------------------------------------------------------------------------------- 1 | {{ ansible_managed | comment }} 2 | 3 | [zfs] 4 | name = ZFS on Linux for EL{{ ansible_distribution_version }} - dkms 5 | baseurl = {{ zfs_redhat_repo_dkms_url }} 6 | enabled = {{ (zfs_redhat_style == 'dkms') | ternary(1,0) }} 7 | metadata_expire = 7d 8 | gpgcheck = 1 9 | gpgkey = file:///etc/pki/rpm-gpg/RPM-GPG-KEY-zfsonlinux 10 | {% if zfs_redhat_repo_proxy | default('') %} 11 | proxy = {{ zfs_redhat_repo_proxy }} 12 | {% endif %} 13 | 14 | [zfs-kmod] 15 | name = ZFS on Linux for EL{{ ansible_distribution_version }} - kmod 16 | baseurl = {{ zfs_redhat_repo_kmod_url }} 17 | enabled = {{ (zfs_redhat_style == 'kmod') | ternary(1,0) }} 18 | metadata_expire = 7d 19 | gpgcheck = 1 20 | gpgkey = file:///etc/pki/rpm-gpg/RPM-GPG-KEY-zfsonlinux 21 | {% if zfs_redhat_repo_proxy | default('') %} 22 | proxy = {{ zfs_redhat_repo_proxy }} 23 | {% endif %} 24 | -------------------------------------------------------------------------------- /tasks/manage_pools.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: get list of available zpools 3 | ansible.builtin.shell: "set -o pipefail; zpool list | awk 'FNR >1' | awk '{print $1}'" 4 | args: 5 | executable: /bin/bash 6 | register: available_zpools 7 | changed_when: false 8 | check_mode: false 9 | 10 | - name: get list of importable zpools 11 | ansible.builtin.shell: "set -o pipefail; zpool import | sed -nr 's/^\\s*pool: (.*)/\\1/p'" 12 | args: 13 | executable: /bin/bash 14 | register: importable_zpools 15 | changed_when: false 16 | check_mode: false 17 | 18 | - name: include single pool management tasks 19 | ansible.builtin.include_tasks: manage_pool.yml 20 | vars: 21 | zfs_pool: "{{ zfs_pools_defaults | combine(_zfs_pool) }}" 22 | zfs_dataset_properties: "{{ 23 | zfs_filesystems_properties_defaults | 24 | combine(zfs_pool.filesystem_properties | 25 | default({})) 26 | }}" 27 | loop: "{{ zfs_pools }}" 28 | loop_control: 29 | loop_var: _zfs_pool 30 | -------------------------------------------------------------------------------- /test_plugins/list.py: -------------------------------------------------------------------------------- 1 | from types import GeneratorType 2 | 3 | 4 | class TestModule(object): 5 | 6 | def tests(self): 7 | return { 8 | 'list': self.is_list, 9 | } 10 | 11 | def is_list(self, value): 12 | """Test if a value a list or generator type. 13 | 14 | Jinja2 provides the tests `iterable` and `sequence`, but those also 15 | match strings and dicts as well. To determine, if a value is essentially 16 | a list, you need to check the following: 17 | 18 | value is not string and value is not mapping and value is iterable 19 | 20 | This test is a shortcut, which allows to check for a list or generator 21 | simply with: 22 | 23 | value is list 24 | 25 | Args: 26 | value: A value, that shall be type tested 27 | 28 | Returns: 29 | bool: True, if value is of type list or generator, False otherwise. 30 | """ 31 | return isinstance(value, (list, GeneratorType)) 32 | -------------------------------------------------------------------------------- /tasks/configure_zfs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: configure ZFS kernel parameters 3 | ansible.builtin.template: 4 | src: etc/modprobe.d/zfs.conf.j2 5 | dest: /etc/modprobe.d/zfs.conf 6 | owner: root 7 | group: root 8 | mode: "0644" 9 | notify: 10 | - update initramfs (Debian) 11 | - mkinitcpio (Arch Linux) 12 | 13 | - name: configure ZFS 14 | ansible.builtin.template: 15 | src: etc/default/zfs.j2 16 | dest: /etc/default/zfs 17 | owner: root 18 | group: root 19 | mode: "0644" 20 | 21 | - block: 22 | - name: get serial of devices 23 | ansible.builtin.command: "udevadm info -q property {{ item }}" 24 | register: device_properties 25 | changed_when: false 26 | check_mode: false 27 | failed_when: false 28 | loop: "{{ zfs_config_none_ioscheduler }}" 29 | 30 | - name: set IO scheduler of HDDs to 'none' 31 | ansible.builtin.template: 32 | src: etc/udev/rules.d/90-zfs-io-scheduler.rules.j2 33 | dest: /etc/udev/rules.d/90-zfs-io-scheduler.rules 34 | mode: "0644" 35 | notify: reload udev rules 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Andre Lehmann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tasks/manage_volume.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: assert correctness of ZFS volume parameters 3 | ansible.builtin.assert: 4 | that: 5 | - zfs_volume.name is defined and zfs_volume.name is string 6 | - (zfs_volume.properties | default({})) is mapping 7 | - (zfs_volume.state | default('present')) in ['present', 'absent'] 8 | quiet: true 9 | fail_msg: ZFS volume parameters are invalid 10 | 11 | - name: gather properties of '{{ zfs_volume.name }}' 12 | community.general.zfs_facts: 13 | name: "{{ zfs_volume.name }}" 14 | failed_when: false 15 | changed_when: false 16 | register: zfs_volume_facts 17 | 18 | - name: prepare volume properties 19 | prepare_zfs_properties: 20 | new_properties: "{{ zfs_volume_properties }}" 21 | current_properties: "{{ ansible_zfs_datasets[0] | default({}) }}" 22 | type: volume 23 | register: prepared_volume_properties 24 | 25 | - name: manage volume '{{ zfs_volume.name }}' 26 | community.general.zfs: 27 | name: "{{ zfs_volume.name }}" 28 | state: "{{ zfs_volume.state | default('present') }}" 29 | extra_zfs_properties: "{{ prepared_volume_properties.properties }}" 30 | -------------------------------------------------------------------------------- /tasks/manage_filesystem.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: assert correctness of ZFS filesystem parameters 3 | ansible.builtin.assert: 4 | that: 5 | - zfs_filesystem.name is defined and zfs_filesystem.name is string 6 | - (zfs_filesystem.properties | default({})) is mapping 7 | - (zfs_filesystem.state | default('present')) in ['present', 'absent'] 8 | quiet: true 9 | fail_msg: ZFS filesystem parameters are invalid 10 | 11 | - name: gather properties of '{{ zfs_filesystem.name | mandatory }}' 12 | community.general.zfs_facts: 13 | name: "{{ zfs_filesystem.name }}" 14 | failed_when: false 15 | changed_when: false 16 | register: zfs_filesystem_facts 17 | 18 | - name: prepare filesystem properties 19 | prepare_zfs_properties: 20 | new_properties: "{{ zfs_filesystem_properties }}" 21 | current_properties: "{{ ansible_zfs_datasets[0] | default({}) }}" 22 | type: filesystem 23 | register: prepared_filesystem_properties 24 | 25 | - name: manage filesystem '{{ zfs_filesystem.name }}' 26 | community.general.zfs: 27 | name: "{{ zfs_filesystem.name }}" 28 | state: "{{ zfs_filesystem.state | default('present') }}" 29 | extra_zfs_properties: "{{ prepared_filesystem_properties.properties }}" 30 | -------------------------------------------------------------------------------- /molecule/default/prepare.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Prepare 3 | hosts: all 4 | become: true 5 | tasks: 6 | - when: ansible_os_family == 'Debian' 7 | block: 8 | - name: Use backrefs with alternative group syntax to avoid conflicts with variable values 9 | ansible.builtin.lineinfile: 10 | path: /etc/apt/sources.list 11 | regexp: "{{ item }}" 12 | line: \g<1> main contrib 13 | backrefs: true 14 | loop: 15 | - "^(deb .*backports).*" 16 | - "^(deb-src .*backports).*" 17 | 18 | - name: update apt cache 19 | ansible.builtin.apt: 20 | update_cache: true 21 | cache_valid_time: 3600 22 | 23 | - when: ansible_os_family == 'RedHat' 24 | block: 25 | - name: install epel 26 | ansible.builtin.dnf: 27 | name: epel-release 28 | state: present 29 | 30 | - name: create files to act as loop file 31 | community.general.filesize: 32 | path: "/zfs{{ item }}.img" 33 | size: 512M 34 | loop: 35 | - 0 36 | - 1 37 | 38 | - name: create loop devices 39 | ansible.builtin.command: "losetup -P /dev/loop{{ item }} /zfs{{ item }}.img" 40 | loop: 41 | - 0 42 | - 1 43 | -------------------------------------------------------------------------------- /.chglog/CHANGELOG.tpl.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | {{ range .Versions }} 5 | {{ $strippedTagName := regexReplaceAll "^v" .Tag.Name "" -}} 6 | - [{{ $strippedTagName }} ({{ datetime "2006-01-02" .Tag.Date }})](#{{ regexReplaceAll "\\." $strippedTagName "" }}-{{ datetime "2006-01-02" .Tag.Date }}) 7 | {{- end }} 8 | 9 | --- 10 | {{ range .Versions }} 11 | {{ $strippedTagName := regexReplaceAll "^v" .Tag.Name "" -}} 12 | 13 | ## [{{ $strippedTagName }}]({{ if .Tag.Previous }}{{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }}{{ end }}) ({{ datetime "2006-01-02" .Tag.Date }}) 14 | {{ if .Tag.Previous }} 15 | {{ range .CommitGroups -}} 16 | ### {{ .Title }} 17 | 18 | {{ range .Commits -}} 19 | - {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} 20 | {{ end }} 21 | {{ end -}} 22 | 23 | {{- if .RevertCommits -}} 24 | ### Reverts 25 | 26 | {{ range .RevertCommits -}} 27 | - {{ .Revert.Header }} 28 | {{ end }} 29 | {{ end -}} 30 | 31 | {{- if .NoteGroups -}} 32 | {{ range .NoteGroups -}} 33 | ### {{ if eq .Title "BREAKING CHANGE" }}⚠ BREAKING CHANGES{{ else if eq .Title "SECURITY" }}‼️ SECURITY{{ else}}{{ .Title }}{{ end }} 34 | 35 | {{ range .Notes }} 36 | {{ .Body }} 37 | {{ end }} 38 | {{ end -}} 39 | {{ end -}} 40 | {{ else }} 41 | Initial Release 42 | {{ end -}} 43 | {{ end -}} 44 | -------------------------------------------------------------------------------- /files/centos/RPM-GPG-KEY-zfsonlinux: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | Version: GnuPG v1.4.13 (GNU/Linux) 3 | 4 | mQENBFFLdrcBCADLsZ7FG2e2Cr/ZV6J52+CyzXXmmmIt2ibYJdflxhSaaxhuag6k 5 | 1OgWEM1R/igjWD3im66O99m30+XeDqDwwBC2flplTlAM5cVb2Q37M+q+LyGLaJgL 6 | JKIkHWfK3/arJO+QY5K2hxzvKUAO1ZJa5OYQMJmKIxLzKz3SX6YnRTtE7ohDq/bU 7 | F32ysIrW549XZUpFX4DYCbR9IEaF5kCvus4FwidBTHDC5aWKvb7qStaL/Yo9koV0 8 | CADl4/nCNAKTcmRoo8Pz+zFwFFNLKRdwu58IefLgkqon7RQBDhwMqKPs1Kw14RwJ 9 | HUv3HYw1JleFmkXv2hn1BMa0ct7jUrvenFBNABEBAAG0IVpGUyBvbiBMaW51eCA8 10 | emZzQHpmc29ubGludXgub3JnPokBOAQTAQIAIgUCUUt2twIbAwYLCQgHAwIGFQgC 11 | CQoLBBYCAwECHgECF4AACgkQqdWhwPFKtiAHWgf/SVXT92gs02HlJsz3h+vmHKHH 12 | 8esxHq8DzG9PaBTyeWLB6mMuN5IQ6Kbtpy44xYCyPyBo+MEoFyPwJXw4qU7th/NX 13 | fAaohXTT8KltyKYsibotdeUHGE4G/7ilbQl9kknlmbig9M16RCnRCDxBRiLrvdaQ 14 | X84YhQUlV2CUShRevuogNbcfORViF8jGb0vkRRTJFhsfZpq7XmW53Q3RXrzoe5Pf 15 | t9NzV2Tlx9ohyGGpPVGO4MgJ028vtoUSIVGA2a9Vg1dwKJkhZ/VvgeleZGds+jIP 16 | 0SpXCNJHgxTaE5AJ71GC8OiDst8b/syYmRFX4P1ioah9m7X3ImHiX4tZJc+Ee7kB 17 | DQRRS3a3AQgAtGrh/OjWeqyUAbw8aO6ew28u+wG0GOEaNdMPEm9120uM3XoEHxg7 18 | FpixPJHj6u9VfhZvHBQOEYiZ0sIY7qj/0wyifsTFYjSZrSCJJHJpbM4SnflTkD43 19 | uJlvcUmqMv0vfCnkaMIO71I1xqPlgOYxOQlenttcI+5xEVzv78cSoQOdddCdGFMs 20 | mdrfxh5NWJR6ehIEI1JQTl6iuZt8wJ/Tgqk2btyDYpDKvDdKLjkBpCbTwsVtQa0x 21 | 2/EvuSMbnu8rqqzqqVsOoBMzDi1ksxm1kC4fXmI9av2X+dGpGJNnn5AqIQAKsETE 22 | 8L4Ajzo8Tk2aaq2ase0i9sNdnsRtYGOjiwARAQABiQEfBBgBAgAJBQJRS3a3AhsM 23 | AAoJEKnVocDxSrYgJXYH/3gAPOr7LwA4p3BhV6NIwIaMGmGY+1dbMp/OxB+mFuOS 24 | NTTCKsBGUchGVFYjSlBtden07S8HNNTWB+bWLVfRTTgRkqomxp0DMMOZ8ry317l+ 25 | cDKVRXMPEZvX9567q1PAOGDiGxiE8296ZUp9/hSFkOqv1sdp+HSM6KVMb4MP4Sx0 26 | +sAwcEumIQAKgXMzDLdpoPDrFnoAAfUmQfpddd7NKch0NJAhdlPtQryFpKdnmvpQ 27 | oLINrelqJxuVMo0hd7q0Xc/vJT0s6pe0f0fXdqXy1ijD3qAewXLZHO3XGVSF8fLY 28 | Q5XFCu4KH2vNBmn0lZxVX5BWm3R2M5XfuT/CJYHO+mk= 29 | =A19+ 30 | -----END PGP PUBLIC KEY BLOCK----- 31 | -------------------------------------------------------------------------------- /files/centos/RPM-GPG-KEY-zrepl: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mQENBF9Ob+gBCADU2Lzzt6mvBL6dQTLsqjjYbevi5W1zSvdpLSb27W81wtskg+qa 4 | xJmTO+XqOpMjwe/qtTh18bNjzVjA47c5aPZrraq2/9ekZFaSFg7sY1u1Ocfb07sQ 5 | YoHShFFKA0C3TDEgYllxEulMdvXdQIYzAyRuM6fv+Qw1+kgODs8JRSj0np5ZimYz 6 | 9kp8zg6rmzvIjr/fk2fXQx9WXOMTpNsbR3lftUbjfUAz/2m3SlJUsmFea1Yi8WM0 7 | rGwzSnNfjHEbN1jYsFpSN28UtkqztgiWVU0ubqre77b0MKms70xIDZRCH3dQXszd 8 | X9OjPtVamqyPbx9zkWOsooNQ1cmsYUG9s7NBABEBAAG0G3pyZXBsLXJwbS1wa2dz 9 | QGNzY2h3YXJ6LmNvbYkBVAQTAQgAPhYhBPb26OpvLxRiKHi13lDjRBeCbizmBQJf 10 | Tm/oAhsDBQkDwmcABQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEFDjRBeCbizm 11 | ZyUIAKZTFH4vFTCiyBAv7fsPn+fmqLnMPcp8uZ78C1rixzvIkzxIsGr4q0r2ten0 12 | OTJS0sJ/kDr/Q7AYw/XRMjt4NDVTDfLiZN3ST+lRNbzTWnj/gDXxws2JFniVBHpm 13 | 5bQd8Amtxa0ykTnXIugiJGfSXl2zw4q6JOiBIPp9CJx7WEfi/mwrbqtOzYeFr8ab 14 | DkOuZf6L+5OStdh4s7mpF8zfNHEJSmLBg0MFGUeswxPytzDX25CFp8sUNXmdutmY 15 | wU0aNXmHukjmbxsAqpsUN09nEgjvP8DHo9Pb1/3qiVxjC6oVN87qAL3NApND4YV9 16 | f2TrhAXe7Y//KBNn3BXQab41IK25AQ0EX05v6AEIANgoOo6oQJMsR1zwi1C412Ri 17 | yj14x2ygtAJkH+vHmGCaZXFY1emyRC0tclSX8K+6ocKWxdePDOoXFYXd2jw9wpM8 18 | /hvmn9Zxore93mNryJpdb1HObdGjtDt7pqygGvNFpfhu+k5c0ek+PkWClLT6cu6Y 19 | x5WObsk8ZKGfkwcehWTPghbIEbyki6+YC0X9hMxorTVqgcoo76OjZiHsu77ysKb8 20 | 64GLlJwGG7G1OEqvMoynumuAilypglBmch1wojddZ+STkUs++zXHy6Qrev8GoS+q 21 | TEgJ6ft8GeObQABmCsWsS5IyXKz1dZrAnh5aTSZxj7PlUeKtRSaQrw2lWruj5rUA 22 | EQEAAYkBNgQYAQgAIBYhBPb26OpvLxRiKHi13lDjRBeCbizmBQJfTm/oAhsMAAoJ 23 | EFDjRBeCbizmIwMH/RIB9FMorEDvQW0R20OrZVKzWU1P5fjXVKcXlc8uEgD0Ymso 24 | sRzSE82sIQwRfmIHQP93uT6aBbJM5qdbCCPVa08SpwWfqENjGSv1E4hOINZZh0rl 25 | tYrjikfkUzyZSTF3ddYQ5OFUuxIbaCcOrFQD5RXsiS8iL4ARHmUXjgZKp5gkOsIw 26 | UbLPLxMfm75tBamKd3kitpEZUJjEbKD1q4Z9tXvnHSgx40a92vuKeQsG1dUVRGX1 27 | E6o+AM8nTMtCBcXS5L83TJkWbw0+vTT0Yu0CW7TXV1jMhdNHwZod4Io3CbQ09Cjh 28 | 4j9WnHtDU2DHv8EpdWgeWcsDRLV1T6kjy+enhvM= 29 | =gRBv 30 | -----END PGP PUBLIC KEY BLOCK----- 31 | -------------------------------------------------------------------------------- /files/debian/zrepl-apt-key.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mQENBF2owsoBCAC0EoCU6ljdaN0wtqfqS5Cl3szlwizLliVn2B6vadhEW0NTEKU1 4 | T8anCys+4JB6g7IE7QUVdsKP2B/RZli4Wz7S4N9+fPoBrs6mixDEB+NSe8w21ZX5 5 | 73neMztFZU4aPmCpUkoVYBPxQtqO52exFtJZph0/R2ITXpOykNH9CbmMXLolQlal 6 | V7RRaHrIjhabmSqELqTWNJIcxOWS2T/hKCLSldsoBkq82ruBXFkGOGcuEbtKDd33 7 | 8rriS/wh0smzcUnDo4qRao37xYjzZahDCIao5QwyDfRlNRF27b7Svlfn2wg0CMx1 8 | /IelKQCkJ3ErAAmoyiXMvVvifiva4U7JZ3V3ABEBAAG0MkNocmlzdGlhbiBTY2h3 9 | YXJ6IDx6cmVwbC1kZWJpYW4tcGtnc0Bjc2Nod2Fyei5jb20+iQFOBBMBCAA4AhsD 10 | BQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAFiEE4QFBj9PW+8udZaYtcIaZ/F8uvxYF 11 | AmFr7DsACgkQcIaZ/F8uvxa7kgf+Pdlky2mFB8vn3fAr4y7lvijVU9IqXKsKDoJG 12 | K1irMHkPfm63VPXP8KthPPItImOMAkW762euQP4KVWlbTJSysSot3FvWy5Zyy7UU 13 | TLSy/E+pW9vljbrnze5RCCHFzw7zRfCq0Mt40x3eG+rmb80lt9DbQ/Uu5jDL3L9s 14 | wnBYMYG6EmRupcHBaYsBYqqSlsaQYA7yvsA5bKxKwd5nU/fMR2pVOGKyGZxoZ59G 15 | S4xMzcCMGE7eIwq0XWv3o0cM/vcvLWnWrhVeDA0t0H3q/b2AbhCQjngPUJr/EG6O 16 | 2CW7mE8EPzcqiPR2AU/PEWqs1/Xef/vsjgIJg7qNpUaQPJGhm7kBDQRdqMLKAQgA 17 | riGPsfyuXVdgx7rkw4t9e+FVsBEb37oMUGyVyd14ZyiBLOFIUU2B3r6RKdIUmlkq 18 | Z64ccbpgu7GatFypnhxp6i4Gbp93boxIFk4okgMDENVflblRnk8a5YcgqIV5MQWK 19 | 3MezUyKNLoraPPREohzblLTmHX36xzzAxxR8oA4+4oQsbWWzcPTg9KH2nhJiT/NJ 20 | gQXO0DRwu0vD0qdZaJqOTPiWfJBpHeGxKRn863g1cTS0BxFOgrta8Is7MkJJNorP 21 | Dv8iPL3DI2VV/j1F6/AV2naHXDS+ZW2JZsXMp+zgk1iXO74mtv/y9CD/qLxEr9TY 22 | SyjtBUH7eaUXIgOZymz7HwARAQABiQE8BBgBCAAmAhsMFiEE4QFBj9PW+8udZaYt 23 | cIaZ/F8uvxYFAmFr6JgFCQWkWU4ACgkQcIaZ/F8uvxYhCQgAmIxLKH6x1g6eS1Ez 24 | 6CO9MYvuI74b9c1Gd8fCd7XQXJTO6I4oP3lP/oINg0Zodj3bgin58VNbLQG5zxD5 25 | +G/YYFDX6pAh1dYhHEkb9im95TbOPdGhPWHiFHgr1B6TRGUQQqhPAhcv7eeYl2UM 26 | hyTQ1gcc7y8wT3ZtIctqoxalisM7pKP+hmpguRFHskV7Y0VYiOGNH/iteie1Glhy 27 | FEHaVT6nDhHf/qhRgkA9czE0+x6vGyoxISo07/i0dtRghoebFRx3XkG297XKzIqr 28 | RVYlgvE4RA0pq4p2gb/7q1pcQk3SniQcakOzYX7s50oCzkouggA4ZcMNm4LszXVU 29 | 6sz0Zw== 30 | =Gv0n 31 | -----END PGP PUBLIC KEY BLOCK----- 32 | -------------------------------------------------------------------------------- /templates/etc/cron.d/zfsnap.j2: -------------------------------------------------------------------------------- 1 | #jinja2: trim_blocks: "true", lstrip_blocks: "true" 2 | {{ ansible_managed | comment }} 3 | {% macro opt(option, var_name) %}{{ option ~ ' ' if snapshot[var_name] | default(false) | bool else '' }}{% endmacro %} 4 | {% macro snapshot_config(targets) %} 5 | {% for target in targets %} 6 | {% if target.snapshots | default([]) | length > 0 %} 7 | 8 | # snapshots for '{{ target.name }}' 9 | {% for snapshot in target.snapshots %} 10 | {{ snapshot.cron_schedule | default('0 0 * * *') }} root sh -c '/usr/local/sbin/zfsnap snapshot {{ opt('-r', 'recursive') }}{{ opt('-s', 'skip_resilvering_pools') }}{{ opt('-S', 'skip_scrubbing_pools') }}{{ opt('-z', 'round_snapshot_creation_time') }}-a {{ snapshot.ttl | default('1w') }} -p "{{ snapshot.prefix | default('') }}" {{ target.name }} && /usr/local/sbin/zfsnap destroy {{ opt('-r', 'recursive') }}{{ opt('-s', 'skip_resilvering_pools') }}{{ opt('-S', 'skip_scrubbing_pools') }}-p "{{ snapshot.prefix | default('') }}" {{ target.name }}' 11 | {% endfor %} 12 | {% endif %} 13 | {% endfor %} 14 | {% endmacro %} 15 | 16 | # ZFSNAP schedule 17 | # for more information see https://www.zfsnap.org/zfsnap_manpage.html 18 | 19 | {% if zfs_pools | default([]) | length > 0 %} 20 | # ------------------------------------------------------------------------------ 21 | # ZPool Snapshots 22 | # ------------------------------------------------------------------------------ 23 | {{ snapshot_config(zfs_pools) }} 24 | 25 | 26 | {% endif %} 27 | {% if zfs_filesystems | default([]) | length > 0 %} 28 | # ------------------------------------------------------------------------------ 29 | # Filesystem Snapshots 30 | # ------------------------------------------------------------------------------ 31 | {{ snapshot_config(zfs_filesystems) }} 32 | 33 | 34 | {% endif %} 35 | {% if zfs_volumes | default([]) | length > 0 %} 36 | # ------------------------------------------------------------------------------ 37 | # Volume Snapshots 38 | # ------------------------------------------------------------------------------ 39 | {{ snapshot_config(zfs_volumes) }} 40 | 41 | 42 | {% endif %} 43 | -------------------------------------------------------------------------------- /filter_plugins/split.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2021 Andre Lehmann 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | 24 | class FilterModule(object): 25 | 26 | def filters(self): 27 | return {"split": self.split} 28 | 29 | def split(self, value, sep=None, maxsplit=-1): 30 | """Split a string by a specified seperator string. 31 | 32 | Splitting a string with Jinja can already accomplished by executing the 33 | "split" method on strings, but when you want to use split in combination 34 | with "map" for example, you need a filter like this one. 35 | 36 | Args: 37 | value (str): The string value to be splitted. 38 | sep (str, optional): The seperator string. 39 | maxsplit (int, optional): Number of splits to do. Defaults to "-1". 40 | 41 | Returns: 42 | list: A splitted representation of the string. 43 | 44 | Examples: 45 | {{ 'Hello World' | split(' ') }} -> ['Hello', 'World'] 46 | """ 47 | return str(value).split(sep, maxsplit) 48 | -------------------------------------------------------------------------------- /tasks/install_debian.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # ------------------------------------------------------------------------------ 3 | # ZFS Installation 4 | # ------------------------------------------------------------------------------ 5 | 6 | - name: update APT cache (Debian) 7 | ansible.builtin.apt: 8 | update_cache: true 9 | 10 | - name: gather package facts (Debian) 11 | ansible.builtin.package_facts: 12 | 13 | - name: install Linux kernel headers (Debian) 14 | ansible.builtin.apt: 15 | # there might be installed kernels for which no headers are available, therefore we simply skip those 16 | name: "{{ item }}" 17 | state: present 18 | loop: "{{ 19 | ansible_facts.packages.keys() 20 | | select('match', '^linux-image-.+') 21 | | map('replace', 'image', 'headers') 22 | | list 23 | }}" 24 | ignore_errors: true 25 | 26 | - name: install ZFS (Debian) 27 | ansible.builtin.apt: 28 | name: "{{ item }}" 29 | state: present 30 | default_release: "{{ zfs_debian_repo if zfs_debian_repo | length > 0 else omit }}" 31 | # install the packages one after another 32 | loop: 33 | - zfs-dkms 34 | - zfsutils-linux 35 | - zfs-zed 36 | - zfs-initramfs 37 | 38 | - name: ensure ZFS kernel module is loaded (Debian) 39 | community.general.modprobe: 40 | name: zfs 41 | 42 | - name: ensure ZFS kernel module is loaded in boot (Debian) 43 | ansible.builtin.copy: 44 | content: zfs 45 | dest: /etc/modules-load.d/zfs.conf 46 | owner: root 47 | group: root 48 | mode: "0644" 49 | 50 | 51 | # ------------------------------------------------------------------------------ 52 | # Zrepl Installation 53 | # ------------------------------------------------------------------------------ 54 | 55 | - when: zfs_zrepl_enabled 56 | block: 57 | - when: zfs_manage_repository | bool 58 | block: 59 | - name: import zrepl repo GPG key (Debian) 60 | ansible.builtin.apt_key: 61 | data: "{{ lookup('file', 'debian/zrepl-apt-key.asc') }}" 62 | id: E101418FD3D6FBCB9D65A62D708699FC5F2EBF16 63 | state: present 64 | 65 | - name: add zrepl repository (Debian) 66 | ansible.builtin.apt_repository: 67 | filename: zrepl 68 | repo: >- 69 | deb [arch=amd64] {{ zfs_zrepl_debian_repo_url }}/{{ ansible_distribution | lower }} 70 | {{ ansible_distribution_release | lower }} 71 | main 72 | state: present 73 | 74 | - name: install zrepl (Debian) 75 | ansible.builtin.apt: 76 | name: zrepl 77 | state: present 78 | -------------------------------------------------------------------------------- /examples/provision.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Setup host 3 | hosts: all 4 | become: true 5 | become_method: sudo 6 | 7 | tasks: 8 | # the aisbergg.zfs role requires the backports repository but leaves it to the user to add it 9 | - name: add backports-contrib repository 10 | ansible.builtin.copy: 11 | content: | 12 | deb https://deb.debian.org/debian {{ ansible_distribution_release }} main contrib 13 | deb https://security.debian.org/debian-security {{ ansible_distribution_release }}-security main 14 | deb https://deb.debian.org/debian {{ ansible_distribution_release }}-updates main 15 | deb https://deb.debian.org/debian {{ ansible_distribution_release }}-backports main contrib 16 | dest: /etc/apt/sources.list 17 | owner: root 18 | group: root 19 | mode: "0644" 20 | 21 | - name: upgrade packages and potentially install a newer kernel 22 | ansible.builtin.apt: 23 | update_cache: true 24 | upgrade: full 25 | cache_valid_time: 3600 26 | register: apt_upgrade 27 | 28 | - when: apt_upgrade.changed 29 | block: # 30 | - name: reboot if a new kernel was installed 31 | ansible.builtin.reboot: 32 | reboot_timeout: 600 33 | msg: "Rebooting because we might have a new kernel" 34 | 35 | - name: gather facts again after reboot 36 | ansible.builtin.setup: 37 | 38 | - name: gather package facts 39 | ansible.builtin.package_facts: 40 | 41 | - name: remove unused kernels 42 | ansible.builtin.apt: 43 | name: "{{ 44 | ansible_facts.packages.keys() 45 | | select('match', '^linux-(image|headers)-\\d+\\.\\d+\\.\\d+.*') 46 | | reject('match', '^linux-(image|headers)-'~kernel_version~'.*') 47 | | list 48 | }}" 49 | state: absent 50 | vars: 51 | kernel_version: "{{ ansible_kernel | regex_replace('^(\\d+\\.\\d+\\.\\d+-d+).*', '\\1') }}" 52 | ignore_errors: true 53 | 54 | - name: manage ZFS filesystems 55 | ansible.builtin.include_role: 56 | name: aisbergg.zfs 57 | vars: 58 | zfs_pools: 59 | - name: pool 60 | vdev: >- 61 | mirror 62 | sdb 63 | sdc 64 | scrub: true 65 | properties: 66 | ashift: 12 67 | filesystem_properties: 68 | mountpoint: /mnt/raid1 69 | compression: lz4 70 | # properties of zfs_filesystems_properties_defaults also apply here 71 | 72 | zfs_filesystems: 73 | - name: pool/vol1 74 | - name: pool/vol2 75 | 76 | # schedule for ZFS scrubs 77 | zfs_scrub_schedule: monthly 78 | # schedule for TRIM 79 | zfs_trim_schedule: weekly 80 | -------------------------------------------------------------------------------- /tasks/install_redhat.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # ------------------------------------------------------------------------------ 3 | # ZFS Installation 4 | # ------------------------------------------------------------------------------ 5 | 6 | - when: zfs_manage_repository | bool 7 | block: 8 | - name: add ZFS repository (RedHat) 9 | ansible.builtin.template: 10 | src: etc/yum.repos.d/zfs.repo.j2 11 | dest: /etc/yum.repos.d/zfs.repo 12 | owner: root 13 | group: root 14 | mode: "0644" 15 | 16 | - name: copy ZFS repo GPG key (RedHat) 17 | ansible.builtin.copy: 18 | src: centos/RPM-GPG-KEY-zfsonlinux 19 | dest: /etc/pki/rpm-gpg/RPM-GPG-KEY-zfsonlinux 20 | owner: root 21 | group: root 22 | mode: "0644" 23 | 24 | - name: import ZFS repo GPG key (RedHat) 25 | ansible.builtin.rpm_key: 26 | key: /etc/pki/rpm-gpg/RPM-GPG-KEY-zfsonlinux 27 | state: present 28 | fingerprint: C93A FFFD 9F3F 7B03 C310 CEB6 A9D5 A1C0 F14A B620 29 | 30 | - name: install Linux kernel headers (RedHat) 31 | ansible.builtin.dnf: 32 | name: "kernel-devel-{{ ansible_kernel }}" 33 | state: present 34 | when: zfs_redhat_style == 'dkms' 35 | 36 | - name: install ZFS (RedHat) 37 | ansible.builtin.dnf: 38 | name: zfs 39 | state: present 40 | 41 | - name: ensure ZFS kernel module is loaded (RedHat) 42 | community.general.modprobe: 43 | name: zfs 44 | 45 | - name: ensure ZFS kernel module is loaded in boot (RedHat) 46 | ansible.builtin.copy: 47 | content: zfs 48 | dest: /etc/modules-load.d/zfs.conf 49 | owner: root 50 | group: root 51 | mode: "0644" 52 | 53 | 54 | # ------------------------------------------------------------------------------ 55 | # Zrepl Installation 56 | # ------------------------------------------------------------------------------ 57 | 58 | - when: zfs_zrepl_enabled 59 | block: 60 | - when: zfs_manage_repository | bool 61 | block: 62 | - name: add zrepl repository (RedHat) 63 | ansible.builtin.template: 64 | src: etc/yum.repos.d/zrepl.repo.j2 65 | dest: /etc/yum.repos.d/zrepl.repo 66 | owner: root 67 | group: root 68 | mode: "0644" 69 | 70 | - name: copy zrepl repo GPG key (RedHat) 71 | ansible.builtin.copy: 72 | src: centos/RPM-GPG-KEY-zrepl 73 | dest: /etc/pki/rpm-gpg/RPM-GPG-KEY-zrepl 74 | owner: root 75 | group: root 76 | mode: "0644" 77 | 78 | - name: import zrepl repo GPG key (RedHat) 79 | ansible.builtin.rpm_key: 80 | key: /etc/pki/rpm-gpg/RPM-GPG-KEY-zrepl 81 | state: present 82 | fingerprint: F6F6 E8EA 6F2F 1462 2878 B5DE 50E3 4417 826E 2CE6 83 | 84 | - name: install zrepl (RedHat) 85 | ansible.builtin.dnf: 86 | name: zrepl 87 | state: present 88 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | - [3.3.1 (2023-01-03)](#331-2023-01-03) 6 | - [3.3.0 (2022-12-18)](#330-2022-12-18) 7 | - [3.2.0 (2022-03-31)](#320-2022-03-31) 8 | - [3.1.0 (2022-01-28)](#310-2022-01-28) 9 | - [3.0.0 (2021-10-12)](#300-2021-10-12) 10 | - [2.0.0 (2021-04-02)](#200-2021-04-02) 11 | - [1.0.0 (2020-08-25)](#100-2020-08-25) 12 | 13 | --- 14 | 15 | 16 | ## [3.3.1](https://github.com/aisbergg/ansible-role-zfs/compare/v3.3.0...v3.3.1) (2023-01-03) 17 | 18 | ### Bug Fixes 19 | 20 | - fix syntax of requirements file 21 | 22 | ### Chores 23 | 24 | - explicitly set Ansible as verifier 25 | - use fully-qualified collection name 26 | 27 | 28 | 29 | ## [3.3.0](https://github.com/aisbergg/ansible-role-zfs/compare/v3.2.0...v3.3.0) (2022-12-18) 30 | 31 | ### Bug Fixes 32 | 33 | - linting 34 | - replace deprecated decorator 35 | - rename vars/centos.yml to redhat.yml 36 | 37 | ### Documentation 38 | 39 | - document yum/dnf variable 40 | - **README.md:** update documentation 41 | 42 | ### Features 43 | 44 | - add option 'zfs_trim_schedule' to control the TRIM schedule for drives 45 | - add option 'zfs_manage_repository' to control the package repository management 46 | - add proxy support for yum/dnf repositories. 47 | 48 | 49 | 50 | ## [3.2.0](https://github.com/aisbergg/ansible-role-zfs/compare/v3.1.0...v3.2.0) (2022-03-31) 51 | 52 | ### Bug Fixes 53 | 54 | - make udev tasks also work in check mode 55 | - run ZED script only if it is enabled 56 | - run mount generator tasks before service mgt tasks 57 | 58 | ### CI Configuration 59 | 60 | - add branch explicitly to make Ansible import action happy 61 | - bump Ansible Galaxy action version 62 | 63 | ### Chores 64 | 65 | - don't use bump2version to include the CHANGELOG in the bump commit, it doesn't do a good job 66 | 67 | ### Documentation 68 | 69 | - update links to manpages 70 | 71 | ### Features 72 | 73 | - load kernel module on boot, even if no pools are created first 74 | 75 | 76 | 77 | ## [3.1.0](https://github.com/aisbergg/ansible-role-zfs/compare/v3.0.0...v3.1.0) (2022-01-28) 78 | 79 | ### Bug Fixes 80 | 81 | - correct 'boolean' test name 82 | - update zrepl PGP key 83 | 84 | ### CI Configuration 85 | 86 | - fix automatic release and publish process 87 | 88 | ### Chores 89 | 90 | - include changelog in bump commits 91 | 92 | 93 | 94 | ## [3.0.0](https://github.com/aisbergg/ansible-role-zfs/compare/v2.0.0...v3.0.0) (2021-10-12) 95 | 96 | ### CI Configuration 97 | 98 | - add Github action for automatic releases 99 | 100 | ### Chores 101 | 102 | - update changelog 103 | - update development configs 104 | - **.ansible-lint:** update linter config 105 | - **.pre-commit-config.yaml:** bump pre-commit hook versions 106 | - **CHANGELOG.tpl.md:** update changelog template 107 | - **requirements.yml:** add role requirements 108 | 109 | ### Code Refactoring 110 | 111 | - rename variables, add assertions and other modifications 112 | 113 | ### Documentation 114 | 115 | - **README.md:** add proper documentation 116 | 117 | 118 | 119 | ## [2.0.0](https://github.com/aisbergg/ansible-role-zfs/compare/v1.0.0...v2.0.0) (2021-04-02) 120 | 121 | ### Code Refactoring 122 | 123 | - major overhaul 124 | 125 | 126 | 127 | ## [1.0.0]() (2020-08-25) 128 | 129 | Initial Release 130 | -------------------------------------------------------------------------------- /tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: include OS-specific variables 3 | tags: [zfs, always] 4 | ansible.builtin.include_vars: "{{ item }}" 5 | with_first_found: 6 | - "{{ ansible_distribution | lower }}_{{ ansible_distribution_major_version }}.yml" 7 | - "{{ ansible_distribution | lower }}.yml" 8 | - "{{ ansible_os_family | lower }}_{{ ansible_distribution_major_version }}.yml" 9 | - "{{ ansible_os_family | lower }}.yml" 10 | 11 | - name: assert correctness of configuration 12 | tags: [zfs, always] 13 | ansible.builtin.assert: 14 | that: 15 | - zfs_redhat_style in ['kmod', 'dkms'] 16 | - zfs_redhat_repo_dkms_url is string 17 | - zfs_redhat_repo_kmod_url is string 18 | - zfs_debian_repo is string 19 | - (zfs_service_import_cache_enabled | bool) is boolean 20 | - (zfs_service_import_scan_enabled | bool) is boolean 21 | - (zfs_service_mount_enabled | bool) is boolean 22 | - (zfs_service_share_enabled | bool) is boolean 23 | - (zfs_service_volume_wait_enabled | bool) is boolean 24 | - (zfs_service_zed_enabled | bool) is boolean 25 | - (zfs_use_zfs_mount_generator | bool) is boolean 26 | - zfs_scrub_schedule is string 27 | - zfs_config_none_ioscheduler is list 28 | - zfs_pools is list 29 | - zfs_pools_defaults is mapping 30 | - zfs_volumes is list 31 | - zfs_volumes_properties_defaults is mapping 32 | - zfs_filesystems is list 33 | - zfs_filesystems_properties_defaults is mapping 34 | - zfs_zrepl_config is mapping 35 | - (zfs_zrepl_enabled | bool) is boolean 36 | - zfs_zrepl_redhat_repo_url is string 37 | - zfs_zrepl_debian_repo_url is string 38 | quiet: true 39 | fail_msg: configuration for ZFS Role is invalid, check your parameters 40 | 41 | - name: include installation tasks 42 | tags: [zfs, zfs_install, install] 43 | ansible.builtin.include_tasks: "{{ task_file }}" 44 | loop_control: 45 | loop_var: task_file 46 | with_first_found: 47 | - "install_{{ ansible_distribution | lower }}_{{ ansible_distribution_major_version }}.yml" 48 | - "install_{{ ansible_distribution | lower }}.yml" 49 | - "install_{{ ansible_os_family | lower }}_{{ ansible_distribution_major_version }}.yml" 50 | - "install_{{ ansible_os_family | lower }}.yml" 51 | 52 | - name: include configuration tasks 53 | tags: [zfs, zfs_config, config] 54 | ansible.builtin.import_tasks: configure_zfs.yml 55 | 56 | - name: include pool management tasks 57 | tags: [zfs, zfs_pool] 58 | ansible.builtin.include_tasks: manage_pools.yml 59 | 60 | - name: include volume management tasks 61 | tags: [zfs, zfs_volumes, zfs_dataset] 62 | ansible.builtin.include_tasks: manage_volume.yml 63 | vars: 64 | zfs_volume_properties: "{{ zfs_volumes_properties_defaults | combine(zfs_volume.properties | default({})) }}" 65 | loop: "{{ zfs_volumes }}" 66 | loop_control: 67 | loop_var: zfs_volume 68 | 69 | - name: include filesystem management tasks 70 | tags: [zfs, zfs_filesystem, zfs_dataset] 71 | ansible.builtin.include_tasks: manage_filesystem.yml 72 | vars: 73 | zfs_filesystem_properties: "{{ 74 | zfs_filesystems_properties_defaults | 75 | combine(zfs_filesystem.properties | 76 | default({})) 77 | }}" 78 | loop: "{{ zfs_filesystems }}" 79 | loop_control: 80 | loop_var: zfs_filesystem 81 | 82 | - name: include snapshot configuration tasks 83 | tags: [zfs, zfs_snapshots, zfs_config, config] 84 | ansible.builtin.import_tasks: configure_snapshots.yml 85 | 86 | - name: include service management tasks 87 | tags: [zfs, zfs_service, service] 88 | ansible.builtin.import_tasks: manage_services.yml 89 | -------------------------------------------------------------------------------- /filter_plugins/selectattr2.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2023 Andre Lehmann 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from collections.abc import Iterable 24 | 25 | from ansible import errors 26 | from jinja2.runtime import Undefined 27 | 28 | try: 29 | from jinja2.filters import pass_environment 30 | except ImportError: 31 | # compatibility for jinja2 < 3.0 32 | from jinja2.filters import environmentfilter 33 | pass_environment = environmentfilter 34 | 35 | 36 | class FilterModule(object): 37 | 38 | def filters(self): 39 | return {"selectattr2": self.selectattr2} 40 | 41 | @pass_environment 42 | def selectattr2(self, environment, seq, attr, func_name, *args, **kwargs): 43 | """Filter a sequence of objects by applying a test to the specified 44 | attribute of each object, and only selecting the objects with the test 45 | succeeding. 46 | 47 | The filter works much like the 'selectattr', but it does not fail in 48 | case an attribute doesn't exists. In case an attribute is missing, a 49 | default value is used. The default value can be specified as a 50 | keyed-arg. 51 | 52 | Args: 53 | seq (Iterable): The sequence to be filtered. 54 | attr (str): The attribute used for filtering. 55 | func_name (str): The name of filter function. 56 | 57 | Raises: 58 | errors.AnsibleFilterError: Raised if 'seq' is not an iterable and 59 | doesn't contain a mapping. 60 | 61 | Yields: 62 | The next item of the sequence, that passes the test. 63 | 64 | Examples: 65 | {{ users | selectattr2('state', '==', 'present', default='present') | list }} 66 | """ 67 | if not isinstance(seq, Iterable): 68 | raise errors.AnsibleFilterError("'{}' is not an iterable".format(seq)) 69 | 70 | default = kwargs.pop('default', Undefined()) 71 | attr = [int(x) if x.isdigit() else x for x in attr.split(".")] 72 | 73 | def func(item): 74 | if not isinstance(item, dict): 75 | raise errors.AnsibleFilterError("'{}' is not a mapping".format(item)) 76 | 77 | for part in attr: 78 | item = environment.getitem(item, part) 79 | if isinstance(item, Undefined): 80 | item = default 81 | break 82 | 83 | return environment.call_test(func_name, item, args, kwargs) 84 | 85 | if seq: 86 | for item in seq: 87 | if func(item): 88 | yield item 89 | -------------------------------------------------------------------------------- /molecule/default/verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Verify 3 | hosts: all 4 | become: true 5 | vars: 6 | 7 | tasks: 8 | - name: gather package facts 9 | ansible.builtin.package_facts: 10 | 11 | - name: check if ZFS is installed 12 | ansible.builtin.assert: 13 | that: 14 | - "'zfs-dkms' in ansible_facts.packages" 15 | - "ansible_os_family == 'RedHat' or 'zfs-initramfs' in ansible_facts.packages" 16 | - "ansible_os_family == 'RedHat' or 'zfs-zed' in ansible_facts.packages" 17 | - "ansible_os_family == 'RedHat' or 'zfsutils-linux' in ansible_facts.packages" 18 | - "'zrepl' in ansible_facts.packages" 19 | 20 | - name: gather service facts 21 | ansible.builtin.service_facts: 22 | 23 | - name: check if services are enabled and running 24 | ansible.builtin.assert: 25 | that: 26 | - "'zfs-import-cache.service' in ansible_facts.services" 27 | - "'zfs-import-scan.service' in ansible_facts.services" 28 | - "'zfs-import.service' in ansible_facts.services" 29 | - "'zfs-load-key.service' in ansible_facts.services" 30 | - "ansible_os_family == 'RedHat' or 'zfs-load-module.service' in ansible_facts.services" 31 | - "'zfs-mount.service' in ansible_facts.services" 32 | - "'zfs-scrub@.service' in ansible_facts.services" 33 | - "'zfs-share.service' in ansible_facts.services" 34 | - "'zfs-volume-wait.service' in ansible_facts.services" 35 | - "'zfs-zed.service' in ansible_facts.services" 36 | - "ansible_facts.services['zfs-import-cache.service'].status == 'enabled'" 37 | - "ansible_os_family == 'RedHat' or ansible_facts.services['zfs-load-module.service'].status == 'enabled'" 38 | - "ansible_facts.services['zfs-volume-wait.service'].status == 'enabled'" 39 | - "ansible_facts.services['zrepl.service'].status == 'enabled'" 40 | 41 | - name: get number of zpools 42 | ansible.builtin.shell: "set -o pipefail; zpool list | tail -n+2 | wc -l" 43 | args: 44 | executable: /bin/bash 45 | register: number_of_zpools 46 | changed_when: false 47 | 48 | - name: assert number of pools 49 | ansible.builtin.assert: 50 | that: number_of_zpools.stdout == '1' 51 | 52 | - name: get zfs filesystems 53 | ansible.builtin.shell: "set -o pipefail; zfs list | awk 'FNR >1' | awk '{print $1}'" 54 | args: 55 | executable: /bin/bash 56 | register: zfs_filesystems 57 | changed_when: false 58 | 59 | - name: assert filesystems 60 | ansible.builtin.assert: 61 | that: 62 | - "zfs_filesystems.stdout_lines | length == 11" 63 | - "'rpool' in zfs_filesystems.stdout_lines" 64 | - "'rpool/ROOT' in zfs_filesystems.stdout_lines" 65 | - "'rpool/ROOT/default' in zfs_filesystems.stdout_lines" 66 | - "'rpool/home' in zfs_filesystems.stdout_lines" 67 | - "'rpool/home/root' in zfs_filesystems.stdout_lines" 68 | - "'rpool/var' in zfs_filesystems.stdout_lines" 69 | - "'rpool/var/cache' in zfs_filesystems.stdout_lines" 70 | - "'rpool/var/lib' in zfs_filesystems.stdout_lines" 71 | - "'rpool/var/lib/docker' in zfs_filesystems.stdout_lines" 72 | - "'rpool/var/log' in zfs_filesystems.stdout_lines" 73 | - "'rpool/var/spool' in zfs_filesystems.stdout_lines" 74 | 75 | - name: restart zrepl to ensure at least one snapshot has been created 76 | ansible.builtin.systemd: 77 | name: zrepl.service 78 | state: restarted 79 | 80 | - name: get zfs filesystems 81 | ansible.builtin.shell: "set -o pipefail; zfs list -t snapshot | awk 'FNR >1' | awk '{print $1}'" 82 | args: 83 | executable: /bin/bash 84 | register: zfs_filesystems 85 | changed_when: false 86 | 87 | - name: assert filesystems 88 | ansible.builtin.assert: 89 | that: 90 | - "zfs_filesystems.stdout_lines | length > 0" 91 | - "'rpool/home@auto_' in zfs_filesystems.stdout" 92 | -------------------------------------------------------------------------------- /library/prepare_zfs_properties.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from ansible.module_utils.basic import AnsibleModule 3 | 4 | 5 | def get_changed_properties(current, new, unchangeable=None): 6 | """Get the changed properties. 7 | 8 | :param current: Current properties 9 | :type current: dict 10 | :param new: New properties to be set 11 | :type new: dict 12 | :param unchangeable: Set of unchangeable properties, defaults to None 13 | :type unchangeable: set, optional 14 | :raises ValueError: If the value of an unchangeable property is tried to be changed 15 | :return: Changed properties 16 | :rtype: dict 17 | """ 18 | unchangeable = unchangeable or set() 19 | changed_properties = {} 20 | for name, new_value in new.items(): 21 | # convert bool types 22 | if isinstance(new_value, bool): 23 | new_value = "on" if new_value else "off" 24 | else: 25 | new_value = str(new_value) 26 | 27 | # check if new value differs from current one and if the value is changeable 28 | current_value = current.get(name, None) 29 | if current_value is None: 30 | # the dataset does not yet exist -> add new value 31 | changed_properties[name] = new_value 32 | 33 | elif new_value != current_value: 34 | if name in unchangeable: 35 | dataset_type = current["type"] 36 | raise ValueError( 37 | "The value of {} property '{}' cannot be changed after creation." 38 | .format(dataset_type, name)) 39 | if name.startswith("feature@"): 40 | if (current_value in ["active", "enabled"] 41 | and new_value == "disabled"): 42 | changed_properties[name] = new_value 43 | else: 44 | changed_properties[name] = new_value 45 | 46 | return changed_properties 47 | 48 | 49 | def main(): 50 | # arguments definition 51 | module_args = dict( 52 | new_properties=dict(type="dict", required=True), 53 | current_properties=dict(type="dict", default={}), 54 | type=dict(type="str", 55 | default="filesystem", 56 | choices=["filesystem", "volume", "zpool"]), 57 | ) 58 | module = AnsibleModule(argument_spec=module_args, supports_check_mode=True) 59 | 60 | # parse arguments 61 | new_properties = module.params["new_properties"] 62 | current_properties = module.params["current_properties"] 63 | type_ = module.params["type"] 64 | 65 | result = {} 66 | 67 | try: 68 | if type_ in ["filesystem", "volume"]: 69 | dataset_type = current_properties.get("type", type_) 70 | unchangeable_properties = { 71 | "filesystem": { 72 | "casesensitivity", 73 | "encryption", 74 | "keyformat", 75 | "normalization", 76 | "pbkdf2iters", 77 | "utf8only", 78 | }, 79 | "volume": { 80 | "encryption", 81 | "keyformat", 82 | "pbkdf2iters", 83 | "volblocksize", 84 | "volsize", 85 | }, 86 | "snapshot": { 87 | "casesensitivity", 88 | "encryption", 89 | "keyformat", 90 | "normalization", 91 | "pbkdf2iters", 92 | "utf8only", 93 | } 94 | }.get(dataset_type) 95 | result["properties"] = get_changed_properties( 96 | current_properties, new_properties, unchangeable_properties) 97 | else: 98 | result["properties"] = get_changed_properties( 99 | current_properties, new_properties) 100 | except ValueError as err: 101 | module.fail_json(msg=str(err)) 102 | 103 | result["changed"] = (len(result["properties"]) > 0) 104 | module.exit_json(**result) 105 | 106 | 107 | if __name__ == '__main__': 108 | main() 109 | -------------------------------------------------------------------------------- /defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # ------------------------------------------------------------------------------ 3 | # ZFS Installation 4 | # ------------------------------------------------------------------------------ 5 | 6 | # enables/disables management of ZFS package repository 7 | zfs_manage_repository: true 8 | 9 | # installation on CentOS systems 10 | # can be either kmod or dkms; for more information see: 11 | # https://openzfs.github.io/openzfs-docs/Getting%20Started/RHEL%20and%20CentOS.html 12 | zfs_redhat_style: kmod 13 | zfs_redhat_repo_dkms_url: "http://download.zfsonlinux.org/epel/{{ ansible_distribution_version }}/$basearch/" 14 | zfs_redhat_repo_kmod_url: "http://download.zfsonlinux.org/epel/{{ ansible_distribution_version }}/kmod/$basearch/" 15 | zfs_redhat_repo_proxy: 16 | 17 | # installation on Debian systems 18 | zfs_debian_repo: "{{ ansible_distribution_release }}-backports" 19 | 20 | 21 | 22 | # ------------------------------------------------------------------------------ 23 | # ZFS Service Management 24 | # ------------------------------------------------------------------------------ 25 | 26 | # import ZFS pools by cache file 27 | zfs_service_import_cache_enabled: true 28 | # import ZFS pools by device scanning 29 | zfs_service_import_scan_enabled: false 30 | # ZFS mount mechanism 31 | zfs_service_mount_enabled: "{{ not (zfs_use_zfs_mount_generator | bool) }}" 32 | # ZFS file system shares 33 | zfs_service_share_enabled: false 34 | # wait for ZFS Volume (zvol) links in /dev 35 | zfs_service_volume_wait_enabled: true 36 | # ZFS Event Daemon (zed) 37 | zfs_service_zed_enabled: false 38 | 39 | # use systemd mount generator, to mount ZVolumes via systemd 40 | zfs_use_zfs_mount_generator: false 41 | 42 | 43 | 44 | # ------------------------------------------------------------------------------ 45 | # ZFS Configuration 46 | # ------------------------------------------------------------------------------ 47 | 48 | # https://openzfs.github.io/openzfs-docs/Performance%20and%20Tuning/Module%20Parameters.html 49 | zfs_kernel_module_parameters: {} 50 | 51 | # time schedule for zpool scrubs; for valid time formats see: 52 | # https://www.freedesktop.org/software/systemd/man/systemd.time.html#Calendar%20Events 53 | zfs_scrub_schedule: monthly 54 | 55 | # time schedule for trimming zpools; for valid time formats see: 56 | # https://www.freedesktop.org/software/systemd/man/systemd.time.html#Calendar%20Events 57 | zfs_trim_schedule: weekly 58 | 59 | # set IO scheduler for listed HDDs to 'none' 60 | zfs_config_none_ioscheduler: [] 61 | 62 | 63 | 64 | # ------------------------------------------------------------------------------ 65 | # ZFS Pools and Datasets 66 | # ------------------------------------------------------------------------------ 67 | 68 | zfs_pools: [] 69 | # - name: 70 | # vdev: >- 71 | # mirror 72 | # dev1 73 | # dev1 74 | # mirror 75 | # dev1 76 | # dev2 77 | # log dd 78 | # scrub: true 79 | # dont_enable_features: false 80 | # properties: 81 | # ashift: 9 82 | # filesystem_properties: 83 | # extra_import_options: "" 84 | # extra_create_options: "" 85 | # https://openzfs.github.io/openzfs-docs/man/7/zpoolprops.7.html 86 | zfs_pools_defaults: {} 87 | 88 | zfs_volumes: [] 89 | # https://openzfs.github.io/openzfs-docs/man/7/zfsprops.7.html 90 | zfs_volumes_properties_defaults: 91 | volblocksize: 8K 92 | volsize: 1G 93 | compression: lz4 94 | dedup: false 95 | sync: standard 96 | 97 | zfs_filesystems: [] 98 | # https://openzfs.github.io/openzfs-docs/man/7/zfsprops.7.html 99 | zfs_filesystems_properties_defaults: 100 | acltype: posix 101 | atime: false 102 | canmount: true 103 | casesensitivity: sensitive 104 | compression: lz4 105 | dedup: false 106 | normalization: formD 107 | setuid: true 108 | snapdir: hidden 109 | sync: standard 110 | utf8only: true 111 | xattr: sa 112 | 113 | 114 | 115 | # ------------------------------------------------------------------------------ 116 | # Automatic Snapshots using ZREPL 117 | # ------------------------------------------------------------------------------ 118 | 119 | # configure zrepl; see https://zrepl.github.io/configuration/overview.html 120 | zfs_zrepl_config: {} 121 | 122 | # install and enable zrepl (https://github.com/zrepl/zrepl) 123 | zfs_zrepl_enabled: false 124 | 125 | # RPM repository URL of zrepl 126 | zfs_zrepl_redhat_repo_url: "https://zrepl.cschwarz.com/rpm/repo" 127 | 128 | # DEB repository URL of zrepl 129 | zfs_zrepl_debian_repo_url: "https://zrepl.cschwarz.com/apt" 130 | -------------------------------------------------------------------------------- /molecule/default/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | become: true 5 | vars: 6 | # 7 | # Installation 8 | # 9 | 10 | zfs_redhat_style: dkms 11 | 12 | # 13 | # Service 14 | # 15 | 16 | # generate mount points using systemd 17 | zfs_use_zfs_mount_generator: true 18 | # use zfs_mount_generator but don't invoke ZED (Docker triggers it quite often) 19 | zfs_service_zed_enabled: false 20 | 21 | 22 | # 23 | # Configuration 24 | # 25 | 26 | # https://openzfs.github.io/openzfs-docs/Performance%20and%20Tuning/Module%20Parameters.html#zfs-module-parameters 27 | zfs_kernel_module_parameters: 28 | # use 1/4 of the memory for ZFS ARC 29 | zfs_arc_max: "{{ (ansible_memtotal_mb * 1024**2 * 0.25) | int }}" 30 | 31 | # schedule for ZFS scrubs 32 | zfs_scrub_schedule: monthly 33 | 34 | _zfs_performance_tuning_default: 35 | # store less metadata (still redundant in mirror setups) 36 | redundant_metadata: most 37 | # use standard behaviour for synchronous writes 38 | sync: standard 39 | 40 | _zfs_performance_tuning_async_only: 41 | # store less metadata (still redundant in mirror setups) 42 | redundant_metadata: most 43 | # turn synchronous writes into asynchronous ones 44 | sync: disabled 45 | 46 | _zfs_performance_tuning_ssd: 47 | # use standard behaviour for synchronous writes 48 | sync: standard 49 | # store less metadata (still redundant in mirror setups) 50 | redundant_metadata: most 51 | # optimize synchronous operations to write directly to disk instead of writing 52 | # to log. On HDDs this decreases the latency, but won't do much on SSDs. 53 | logbias: throughput 54 | 55 | _zfs_filesytems_properties: 56 | canmount: true 57 | snapdir: hidden 58 | 59 | # make ZFS behave like a Linux FS 60 | casesensitivity: sensitive 61 | normalization: formD 62 | utf8only: on 63 | setuid: true 64 | atime: false 65 | 66 | # enable use of ACLs 67 | acltype: posix 68 | xattr: sa 69 | 70 | # compression and deduplication 71 | compression: lz4 72 | dedup: false 73 | 74 | zfs_filesystems_properties_defaults: "{{ 75 | _zfs_filesytems_properties | combine( 76 | _zfs_performance_tuning_async_only 77 | )}}" 78 | 79 | _zfs_volumes_properties: 80 | volblocksize: 8K 81 | volsize: 1G 82 | compression: lz4 83 | dedup: false 84 | 85 | # https://openzfs.github.io/openzfs-docs/man/7/zfsprops.7.html 86 | zfs_volumes_properties_defaults: "{{ 87 | _zfs_volumes_properties | combine( 88 | _zfs_performance_tuning_async_only 89 | )}}" 90 | 91 | 92 | # 93 | # ZPools 94 | # 95 | 96 | zfs_pools: 97 | - name: rpool 98 | vdev: >- 99 | mirror 100 | /dev/loop0 101 | /dev/loop1 102 | scrub: true 103 | filesystem_properties: 104 | canmount: off 105 | mountpoint: /zfs 106 | 107 | 108 | # 109 | # Datasets 110 | # 111 | 112 | zfs_filesystems: 113 | - name: rpool/ROOT 114 | properties: 115 | canmount: off 116 | mountpoint: none 117 | - name: rpool/ROOT/default 118 | properties: 119 | canmount: noauto 120 | mountpoint: /zfs 121 | 122 | - name: rpool/home 123 | - name: rpool/home/root 124 | properties: 125 | mountpoint: /root 126 | - name: rpool/var/lib/docker 127 | - name: rpool/var/log 128 | - name: rpool/var/spool 129 | - name: rpool/var/cache 130 | 131 | 132 | # 133 | # Automatic Snapthots Using ZREPL 134 | # 135 | 136 | zfs_zrepl_enabled: true 137 | zfs_zrepl_config: 138 | jobs: 139 | - name: storage 140 | type: snap 141 | filesystems: { 142 | "rpool<": true, 143 | "rpool/var<": false, 144 | } 145 | snapshotting: 146 | type: periodic 147 | interval: 12h 148 | prefix: auto_ 149 | pruning: 150 | keep: 151 | # prune automatic snapshots 152 | - type: grid 153 | # in first 24 hours keep all snapshots 154 | # in first 7 days 1 snapshot each day 155 | # in first month keep 1 snapshot each week 156 | # discard the rest 157 | # details see: https://zrepl.github.io/configuration/prune.html#policy-grid 158 | grid: 1x24h(keep=all) | 7x1d(keep=1) | 3x7d(keep=1) 159 | regex: "^auto_.*" 160 | 161 | # keep manual snapshots 162 | - type: regex 163 | regex: "^manual_.*" 164 | 165 | roles: 166 | - role: aisbergg.zfs 167 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contributes to a positive environment for our community include: 12 | 13 | * Demonstrating empathy and kindness toward other people 14 | * Being respectful of differing opinions, viewpoints, and experiences 15 | * Giving and gracefully accepting constructive feedback 16 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 17 | * Focusing on what is best not just for us as individuals, but for the overall community 18 | 19 | Examples of unacceptable behavior include: 20 | 21 | * The use of sexualized language or imagery, and sexual attention or 22 | advances of any kind 23 | * Trolling, insulting or derogatory comments, and personal or political attacks 24 | * Public or private harassment 25 | * Publishing others' private information, such as a physical or email 26 | address, without their explicit permission 27 | * Other conduct which could reasonably be considered inappropriate in a 28 | professional setting 29 | 30 | ## Enforcement Responsibilities 31 | 32 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 33 | 34 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 35 | 36 | ## Scope 37 | 38 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 39 | 40 | ## Enforcement 41 | 42 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at aisberg@posteo.de. All complaints will be reviewed and investigated promptly and fairly. 43 | 44 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 45 | 46 | ## Enforcement Guidelines 47 | 48 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 49 | 50 | ### 1. Correction 51 | 52 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 53 | 54 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 55 | 56 | ### 2. Warning 57 | 58 | **Community Impact**: A violation through a single incident or series of actions. 59 | 60 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 61 | 62 | ### 3. Temporary Ban 63 | 64 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 65 | 66 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 67 | 68 | ### 4. Permanent Ban 69 | 70 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 71 | 72 | **Consequence**: A permanent ban from any sort of public interaction within the community. 73 | 74 | ## Attribution 75 | 76 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, 77 | available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 78 | 79 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 80 | 81 | [homepage]: https://www.contributor-covenant.org 82 | 83 | For answers to common questions about this code of conduct, see the FAQ at 84 | https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. 85 | -------------------------------------------------------------------------------- /tasks/manage_pool.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: assert correctness of zpool parameters 3 | ansible.builtin.assert: 4 | that: 5 | - zfs_pool.name is defined and zfs_pool.name is string 6 | - zfs_pool.vdev is defined and zfs_pool.vdev is string 7 | - (zfs_pool.scrub | default(true) | bool) is boolean 8 | - (zfs_pool.dont_enable_features | default(false) | bool) is boolean 9 | - (zfs_pool.properties | default({})) is mapping 10 | - (zfs_pool.filesystem_properties | default({})) is mapping 11 | - (zfs_pool.extra_import_options | default('')) is string 12 | - (zfs_pool.extra_create_options | default('')) is string 13 | quiet: true 14 | fail_msg: ZPool parameters are invalid 15 | 16 | # import a zpool 17 | - when: 18 | - zfs_pool.name not in available_zpools.stdout_lines 19 | - zfs_pool.name in importable_zpools.stdout_lines 20 | block: 21 | - name: prepare zpool properties 22 | prepare_zfs_properties: 23 | new_properties: "{{ zfs_pool.properties | default({}) }}" 24 | type: zpool 25 | register: prepared_zpool_properties 26 | 27 | - name: create 'zpool import' command 28 | ansible.builtin.set_fact: 29 | zpool_import_command: >- 30 | zpool import 31 | {% for property, value in (prepared_zpool_properties.properties | dictsort) %} 32 | -o {{ (property ~ '=' ~ value) | quote }} 33 | {% endfor %} 34 | {{ zfs_pool.extra_import_options | default('') }} 35 | {{ zfs_pool.name }} 36 | 37 | - name: print ZPool import command 38 | ansible.builtin.debug: 39 | msg: "{{ zpool_import_command }}" 40 | verbosity: 1 41 | 42 | - name: import ZPool '{{ zfs_pool.name }}' 43 | ansible.builtin.command: "{{ zpool_import_command }}" 44 | register: zpool_import_result 45 | changed_when: true 46 | failed_when: (zpool_import_result.rc != 0) or (zpool_import_result.stderr | length > 0) 47 | 48 | 49 | 50 | # create a new zpool 51 | - when: 52 | - zfs_pool.name not in available_zpools.stdout_lines 53 | - zfs_pool.name not in importable_zpools.stdout_lines 54 | block: 55 | - name: prepare zpool properties 56 | prepare_zfs_properties: 57 | new_properties: "{{ zfs_pool.properties | default({}) }}" 58 | type: zpool 59 | register: prepared_zpool_properties 60 | 61 | - name: prepare dataset properties 62 | prepare_zfs_properties: 63 | new_properties: "{{ zfs_dataset_properties }}" 64 | type: filesystem 65 | register: prepared_dataset_properties 66 | 67 | - name: create 'zpool create' command 68 | ansible.builtin.set_fact: 69 | zpool_create_command: >- 70 | zpool create 71 | {{ '-d ' if zfs_pool.dont_enable_features | default(false) | bool else '' }} 72 | {% for property, value in (prepared_zpool_properties.properties | dictsort) %} 73 | -o {{ (property ~ '=' ~ value) | quote }} 74 | {% endfor %} 75 | {% for property, value in (prepared_dataset_properties.properties | dictsort) %} 76 | -O {{ (property ~ '=' ~ value) | quote }} 77 | {% endfor %} 78 | {{ zfs_pool.name }} 79 | {{ zfs_pool.extra_create_options | default('') }} 80 | {{ zfs_pool.vdev | regex_replace('\n\s*', ' ') }} 81 | 82 | - name: print ZPool creation command 83 | ansible.builtin.debug: 84 | msg: "{{ zpool_create_command }}" 85 | verbosity: 1 86 | 87 | - name: create ZPool '{{ zfs_pool.name }}' 88 | ansible.builtin.command: "{{ zpool_create_command }}" 89 | changed_when: true 90 | 91 | 92 | 93 | # set zpool properties on an existing pool 94 | - when: zfs_pool.name in available_zpools.stdout_lines 95 | block: 96 | - name: gather properties of '{{ zfs_pool.name }}' 97 | community.general.zpool_facts: 98 | name: "{{ zfs_pool.name }}" 99 | parsable: true 100 | failed_when: false 101 | changed_when: false 102 | register: zfs_zpool_facts 103 | 104 | - name: prepare zpool properties 105 | prepare_zfs_properties: 106 | new_properties: "{{ zfs_pool.properties | default({}) }}" 107 | current_properties: "{{ ansible_zfs_pools.0 | default({}) }}" 108 | type: zpool 109 | register: prepared_zpool_properties 110 | 111 | - name: manage zpool properties of '{{ zfs_pool.name }}' 112 | ansible.builtin.command: "zpool set {{ (item.0 ~ '=' ~ item.1) | quote }} {{ zfs_pool.name }}" 113 | changed_when: true 114 | loop_control: 115 | label: "{{ item.0 }}={{ item.1 }}" 116 | loop: "{{ prepared_zpool_properties.properties | default({}) | dict2items }}" 117 | 118 | 119 | 120 | # set dataset properties on an existing pool 121 | - when: (zfs_pool.name in available_zpools.stdout_lines) 122 | or (zfs_pool.name in importable_zpools.stdout_lines) 123 | block: 124 | - name: gather dataset properties of '{{ zfs_pool.name }}' 125 | community.general.zfs_facts: 126 | name: "{{ zfs_pool.name }}" 127 | failed_when: false 128 | changed_when: false 129 | 130 | - name: prepare dataset properties 131 | prepare_zfs_properties: 132 | new_properties: "{{ zfs_dataset_properties }}" 133 | current_properties: "{{ ansible_zfs_datasets[0] | default({}) }}" 134 | type: filesystem 135 | register: prepared_dataset_properties 136 | 137 | - name: manage dataset properties of '{{ zfs_pool.name }}' 138 | community.general.zfs: 139 | name: "{{ zfs_pool.name }}" 140 | state: present 141 | extra_zfs_properties: "{{ prepared_dataset_properties.properties }}" 142 | when: "prepared_dataset_properties.properties | length > 0" 143 | -------------------------------------------------------------------------------- /templates/etc/default/zfs.j2: -------------------------------------------------------------------------------- 1 | {{ ansible_managed | comment }} 2 | 3 | # ZoL userland configuration. 4 | 5 | # NOTE: This file is intended for sysv init and initramfs. 6 | # Changing some of these settings may not make any difference on 7 | # systemd-based setup, e.g. setting ZFS_MOUNT=no will not prevent systemd 8 | # from launching zfs-mount.service during boot. 9 | # See: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=901436 10 | 11 | # To enable a boolean setting, set it to yes, on, true, or 1. 12 | # Anything else will be interpreted as unset. 13 | 14 | # Run `zfs mount -a` during system start? 15 | ZFS_MOUNT='yes' 16 | 17 | # Run `zfs unmount -a` during system stop? 18 | ZFS_UNMOUNT='yes' 19 | 20 | # Run `zfs share -a` during system start? 21 | # nb: The shareiscsi, sharenfs, and sharesmb dataset properties. 22 | ZFS_SHARE='yes' 23 | 24 | # Run `zfs unshare -a` during system stop? 25 | ZFS_UNSHARE='yes' 26 | 27 | # By default, a verbatim import of all pools is performed at boot based on the 28 | # contents of the default zpool cache file. The contents of the cache are 29 | # managed automatically by the 'zpool import' and 'zpool export' commands. 30 | # 31 | # By setting this to 'yes', the system will instead search all devices for 32 | # pools and attempt to import them all at boot, even those that have been 33 | # exported. Under this mode, the search path can be controlled by the 34 | # ZPOOL_IMPORT_PATH variable and a list of pools that should not be imported 35 | # can be listed in the ZFS_POOL_EXCEPTIONS variable. 36 | # 37 | # Note that importing all visible pools may include pools that you don't 38 | # expect, such as those on removable devices and SANs, and those pools may 39 | # proceed to mount themselves in places you do not want them to. The results 40 | # can be unpredictable and possibly dangerous. Only enable this option if you 41 | # understand this risk and have complete physical control over your system and 42 | # SAN to prevent the insertion of malicious pools. 43 | ZPOOL_IMPORT_ALL_VISIBLE='no' 44 | 45 | # Specify specific path(s) to look for device nodes and/or links for the 46 | # pool import(s). See zpool(8) for more information about this variable. 47 | # It supersedes the old USE_DISK_BY_ID which indicated that it would only 48 | # try '/dev/disk/by-id'. 49 | # The old variable will still work in the code, but is deprecated. 50 | #ZPOOL_IMPORT_PATH="/dev/disk/by-vdev:/dev/disk/by-id" 51 | 52 | # List of pools that should NOT be imported at boot 53 | # when ZPOOL_IMPORT_ALL_VISIBLE is 'yes'. 54 | # This is a space separated list. 55 | #ZFS_POOL_EXCEPTIONS="test2" 56 | 57 | # List of pools that SHOULD be imported at boot by the initramfs 58 | # instead of trying to import all available pools. If this is set 59 | # then ZFS_POOL_EXCEPTIONS is ignored. 60 | # Only applicable for Debian GNU/Linux {dkms,initramfs}. 61 | # This is a semi-colon separated list. 62 | #ZFS_POOL_IMPORT="pool1;pool2" 63 | 64 | # Should the datasets be mounted verbosely? 65 | # A mount counter will be used when mounting if set to 'yes'. 66 | VERBOSE_MOUNT='no' 67 | 68 | # Should we allow overlay mounts? 69 | # This is standard in Linux, but not ZFS which comes from Solaris where this 70 | # is not allowed). 71 | DO_OVERLAY_MOUNTS='no' 72 | 73 | # Any additional option to the 'zfs import' commandline? 74 | # Include '-o' for each option wanted. 75 | # You don't need to put '-f' in here, unless you want it ALL the time. 76 | # Using the option 'zfsforce=1' on the grub/kernel command line will 77 | # do the same, but on a case-to-case basis. 78 | ZPOOL_IMPORT_OPTS="" 79 | 80 | # Full path to the ZFS cache file? 81 | # See "cachefile" in zpool(8). 82 | # The default is "@sysconfdir@/zfs/zpool.cache". 83 | #ZPOOL_CACHE="@sysconfdir@/zfs/zpool.cache" 84 | # 85 | # Setting ZPOOL_CACHE to an empty string ('') AND setting ZPOOL_IMPORT_OPTS to 86 | # "-c @sysconfdir@/zfs/zpool.cache" will _enforce_ the use of a cache file. 87 | # This is needed in some cases (extreme amounts of VDEVs, multipath etc). 88 | # Generally, the use of a cache file is usually not recommended on Linux 89 | # because it sometimes is more trouble than it's worth (laptops with external 90 | # devices or when/if device nodes changes names). 91 | #ZPOOL_IMPORT_OPTS="-c @sysconfdir@/zfs/zpool.cache" 92 | #ZPOOL_CACHE="" 93 | 94 | # Any additional option to the 'zfs mount' command line? 95 | # Include '-o' for each option wanted. 96 | MOUNT_EXTRA_OPTIONS="" 97 | 98 | # Build kernel modules with the --enable-debug switch? 99 | # Only applicable for Debian GNU/Linux {dkms,initramfs}. 100 | ZFS_DKMS_ENABLE_DEBUG='no' 101 | 102 | # Build kernel modules with the --enable-debuginfo switch? 103 | # Only applicable for Debian GNU/Linux {dkms,initramfs}. 104 | ZFS_DKMS_ENABLE_DEBUGINFO='no' 105 | 106 | # Keep debugging symbols in kernel modules? 107 | # Only applicable for Debian GNU/Linux {dkms,initramfs}. 108 | ZFS_DKMS_DISABLE_STRIP='no' 109 | 110 | # Wait for this many seconds in the initrd pre_mountroot? 111 | # This delays startup and should be '0' on most systems. 112 | # Only applicable for Debian GNU/Linux {dkms,initramfs}. 113 | ZFS_INITRD_PRE_MOUNTROOT_SLEEP='0' 114 | 115 | # Wait for this many seconds in the initrd mountroot? 116 | # This delays startup and should be '0' on most systems. This might help on 117 | # systems which have their ZFS root on a USB disk that takes just a little 118 | # longer to be available 119 | # Only applicable for Debian GNU/Linux {dkms,initramfs}. 120 | ZFS_INITRD_POST_MODPROBE_SLEEP='0' 121 | 122 | # List of additional datasets to mount after the root dataset is mounted? 123 | # 124 | # The init script will use the mountpoint specified in the 'mountpoint' 125 | # property value in the dataset to determine where it should be mounted. 126 | # 127 | # This is a space separated list, and will be mounted in the order specified, 128 | # so if one filesystem depends on a previous mountpoint, make sure to put 129 | # them in the right order. 130 | # 131 | # It is not necessary to add filesystems below the root fs here. It is 132 | # taken care of by the initrd script automatically. These are only for 133 | # additional filesystems needed. Such as /opt, /usr/local which is not 134 | # located under the root fs. 135 | # Example: If root FS is 'rpool/ROOT/rootfs', this would make sense. 136 | #ZFS_INITRD_ADDITIONAL_DATASETS="rpool/ROOT/usr rpool/ROOT/var" 137 | 138 | # Optional arguments for the ZFS Event Daemon (ZED). 139 | # See zed(8) for more information on available options. 140 | #ZED_ARGS="-M" 141 | -------------------------------------------------------------------------------- /tasks/manage_services.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # ------------------------------------------------------------------------------ 3 | # ZFS Mount Generator 4 | # ------------------------------------------------------------------------------ 5 | 6 | - when: 7 | - zfs_use_zfs_mount_generator | bool 8 | - not (ansible_distribution == 'Ubuntu' and ansible_distribution_release == 'bionic') 9 | block: # noqa: name[missing] 10 | - name: enable the ZED script 11 | ansible.builtin.file: 12 | src: "{{ __zfs_zedletdir }}/history_event-zfs-list-cacher.sh" 13 | dest: /etc/zfs/zed.d/history_event-zfs-list-cacher.sh 14 | state: link 15 | register: history_event_zedlet 16 | 17 | - name: inform ZED about script change 18 | ansible.builtin.systemd: 19 | name: zfs-zed 20 | state: restarted 21 | when: 22 | - zfs_service_zed_enabled 23 | - history_event_zedlet.changed 24 | 25 | - name: ensure mount generator cache directory exists 26 | ansible.builtin.file: 27 | path: /etc/zfs/zfs-list.cache 28 | state: directory 29 | owner: root 30 | group: root 31 | mode: "0755" 32 | 33 | - name: create generator cache content # noqa: risky-shell-pipe 34 | ansible.builtin.shell: | 35 | # see history_event-zfs-list-cacher.sh 36 | PROPS="name,mountpoint,canmount,atime,relatime,devices,exec\ 37 | ,readonly,setuid,nbmand,encroot,keylocation\ 38 | ,org.openzfs.systemd:requires,org.openzfs.systemd:requires-mounts-for\ 39 | ,org.openzfs.systemd:before,org.openzfs.systemd:after\ 40 | ,org.openzfs.systemd:wanted-by,org.openzfs.systemd:required-by\ 41 | ,org.openzfs.systemd:nofail,org.openzfs.systemd:ignore" 42 | 43 | # include only datasets, that do not use legacy mounts 44 | zfs list -H -t filesystem -o $PROPS -r -s name {{ zfs_pool.name | quote }} | awk '{if ($2 != "legacy") {print} }' 45 | 46 | register: zfs_list_cache 47 | changed_when: false 48 | check_mode: false 49 | loop: "{{ zfs_pools }}" 50 | loop_control: 51 | loop_var: zfs_pool 52 | label: "{{ zfs_pool.name }}" 53 | 54 | - name: create generator cache files 55 | ansible.builtin.copy: 56 | content: "{{ cache_entry.stdout }}" 57 | dest: "/etc/zfs/zfs-list.cache/{{ cache_entry.zfs_pool.name }}" 58 | owner: root 59 | group: root 60 | mode: "0644" 61 | loop: "{{ zfs_list_cache.results }}" 62 | loop_control: 63 | loop_var: cache_entry 64 | label: "{{ cache_entry.zfs_pool.name }}" 65 | 66 | # fix for journald when using zfs-mount.service 67 | - when: zfs_service_mount_enabled 68 | block: # noqa: name[missing] 69 | - name: ensure '/etc/systemd/system/systemd-journald.service.d' directory exists 70 | ansible.builtin.file: 71 | path: /etc/systemd/system/systemd-journald.service.d/ 72 | state: directory 73 | owner: root 74 | group: root 75 | mode: "0755" 76 | 77 | - name: create dependency for journald 78 | ansible.builtin.template: 79 | src: etc/systemd/system/systemd-journald.service.d/after-zfs-mount.service.j2 80 | dest: /etc/systemd/system/systemd-journald.service.d/after-zfs-mount.service 81 | owner: root 82 | group: root 83 | mode: "0644" 84 | register: journald_service 85 | 86 | 87 | # ------------------------------------------------------------------------------ 88 | # ZFS Services 89 | # ------------------------------------------------------------------------------ 90 | 91 | - name: manage ZFS services 92 | ansible.builtin.systemd: 93 | name: "{{ item.name }}" 94 | enabled: "{{ item.enabled }}" 95 | state: "{{ item.enabled | ternary('started', 'stopped') }}" 96 | # exclude zfs-volume-wait on Ubuntu Bionic 97 | when: >- 98 | not ( 99 | item.name == 'zfs-volume-wait' and 100 | (ansible_distribution == 'Ubuntu' and ansible_distribution_release == 'bionic') 101 | ) 102 | loop: 103 | - name: zfs.target 104 | enabled: true 105 | - name: zfs-import.target 106 | enabled: true 107 | - name: zfs-import-cache 108 | enabled: "{{ zfs_service_import_cache_enabled | bool }}" 109 | - name: zfs-import-scan 110 | enabled: "{{ zfs_service_import_scan_enabled | bool }}" 111 | - name: zfs-mount 112 | enabled: "{{ zfs_service_mount_enabled | bool }}" 113 | - name: zfs-share 114 | enabled: "{{ zfs_service_share_enabled | bool }}" 115 | - name: zfs-volume-wait 116 | enabled: "{{ zfs_service_volume_wait_enabled | bool }}" 117 | - name: zfs-zed 118 | enabled: "{{ zfs_service_zed_enabled | bool }}" 119 | 120 | 121 | # ------------------------------------------------------------------------------ 122 | # ZFS Scrub Service 123 | # ------------------------------------------------------------------------------ 124 | 125 | - block: # noqa: name[missing] 126 | - name: remove scrub cron job 127 | ansible.builtin.file: 128 | path: /etc/cron.d/zfsutils-linux 129 | state: absent 130 | 131 | - name: create scrub service 132 | ansible.builtin.template: 133 | src: etc/systemd/system/zpool-scrub@.service.j2 134 | dest: /etc/systemd/system/zpool-scrub@.service 135 | owner: root 136 | group: root 137 | mode: "0644" 138 | register: scrub_service 139 | 140 | - name: create scrub timer 141 | ansible.builtin.template: 142 | src: etc/systemd/system/zpool-scrub@.timer.j2 143 | dest: /etc/systemd/system/zpool-scrub@.timer 144 | owner: root 145 | group: root 146 | mode: "0644" 147 | register: scrub_timer 148 | 149 | - name: reload systemd 150 | ansible.builtin.systemd: 151 | daemon_reload: true 152 | when: scrub_service.changed or scrub_timer.changed or journald_service.changed 153 | 154 | - name: enable scrub timer 155 | ansible.builtin.systemd: 156 | name: "zpool-scrub@{{ item.name }}.timer" 157 | enabled: true 158 | state: started 159 | when: item.scrub | default(true) | bool 160 | loop: "{{ zfs_pools }}" 161 | loop_control: 162 | label: "{{ item.name }}" 163 | 164 | - name: gather service facts 165 | ansible.builtin.service_facts: 166 | 167 | - name: disable undefined scrub timer 168 | ansible.builtin.systemd: 169 | name: "zpool-scrub@{{ item }}.timer" 170 | enabled: false 171 | state: stopped 172 | loop: "{{ 173 | ansible_facts.services.keys() | 174 | select('match', 'zpool-scrub@.+\\.service') | 175 | map('regex_replace', 'zpool-scrub@(.+)\\.service', '\\1') | list | 176 | difference(zfs_pools | selectattr2('scrub', '==', true, default=true) | map(attribute='name') | list) 177 | }}" 178 | 179 | 180 | # ------------------------------------------------------------------------------ 181 | # ZFS Trim Service 182 | # ------------------------------------------------------------------------------ 183 | 184 | - block: 185 | - name: remove trim cron job 186 | ansible.builtin.file: 187 | path: /etc/cron.d/zfsutils-linux 188 | state: absent 189 | 190 | - name: create trim service 191 | ansible.builtin.template: 192 | src: etc/systemd/system/zpool-trim@.service.j2 193 | dest: /etc/systemd/system/zpool-trim@.service 194 | owner: root 195 | group: root 196 | mode: "0644" 197 | register: trim_service 198 | 199 | - name: create trim timer 200 | ansible.builtin.template: 201 | src: etc/systemd/system/zpool-trim@.timer.j2 202 | dest: /etc/systemd/system/zpool-trim@.timer 203 | owner: root 204 | group: root 205 | mode: "0644" 206 | register: trim_timer 207 | 208 | - name: reload systemd 209 | ansible.builtin.systemd: 210 | daemon_reload: true 211 | when: trim_service.changed or trim_timer.changed or journald_service.changed 212 | 213 | - name: enable trim timer 214 | ansible.builtin.systemd: 215 | name: "zpool-trim@{{ item.name }}.timer" 216 | enabled: true 217 | state: started 218 | when: item.trim | default(false) | bool 219 | loop: "{{ zfs_pools }}" 220 | loop_control: 221 | label: "{{ item.name }}" 222 | 223 | - name: gather service facts 224 | ansible.builtin.service_facts: 225 | 226 | - name: disable undefined trim timer 227 | ansible.builtin.systemd: 228 | name: "zpool-trim@{{ item }}.timer" 229 | enabled: false 230 | state: stopped 231 | loop: "{{ 232 | ansible_facts.services.keys() | 233 | select('match', 'zpool-trim@.+\\.service') | 234 | map('regex_replace', 'zpool-trim@(.+)\\.service', '\\1') | list | 235 | difference(zfs_pools | selectattr2('trim', '==', true, default=true) | map(attribute='name') | list) 236 | }}" 237 | 238 | 239 | # ------------------------------------------------------------------------------ 240 | # ZREPL Service 241 | # ------------------------------------------------------------------------------ 242 | 243 | - name: manage zrepl service 244 | ansible.builtin.systemd: 245 | name: zrepl 246 | enabled: "{{ zfs_zrepl_enabled | bool }}" 247 | state: "{{ zfs_zrepl_enabled | bool | ternary('started', 'stopped') }}" 248 | when: ('zrepl.service' in ansible_facts.services) 249 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ansible Role: `aisbergg.zfs` 2 | 3 | This Ansible role installs the ZFS filesystem module, creates or imports zpools and manages ZFS datasets on Debian and RedHat systems. 4 | 5 | Destructive operations such as ZFS pool deletions are out of scope and not supported by the role. Therefore, you don't have to worry to much about losing data when using this role. 6 | 7 | **Table of Contents:** 8 | 9 | - [Requirements](#requirements) 10 | - [Role Variables](#role-variables) 11 | - [Dependencies](#dependencies) 12 | - [Example Playbooks](#example-playbooks) 13 | - [Basic Example](#basic-example) 14 | - [Advanced Example](#advanced-example) 15 | - [License](#license) 16 | - [Author Information](#author-information) 17 | 18 | --- 19 | 20 | ## Requirements 21 | 22 | - For Debian systems, the `backports` `contrib` repository needs to be enabled first. It is not included in the role. See [Debian Backports](https://backports.debian.org/Instructions/) for instructions. 23 | - The target system needs to be managed with Systemd. 24 | 25 | ## Role Variables 26 | 27 | **Bold** variables are required. 28 | 29 | | Variable | Default | Comments | 30 | |----------|---------|----------| 31 | | `zfs_manage_repository` | `true` | Manage package repositories (YUM, APT). | 32 | | `zfs_redhat_style` | `kmod` | Style of ZFS module installation. Can be either kmod or dkms. Applies only to RedHat systems. See [official documentation](https://openzfs.github.io/openzfs-docs/Getting%20Started/RHEL-based%20distro/index.html#rhel-based-distro) for information on DKMS and kmod version of openZFS. | 33 | | `zfs_redhat_repo_dkms_url` | `http://download.zfsonlinux.org/`
`epel/{{ ansible_distribution_version }}/$basearch/` | Repository URL used for DKMS installation of ZFS. Applies only to RedHat systems. | 34 | | `zfs_redhat_repo_kmod_url` | `http://download.zfsonlinux.org/`
`epel/{{ ansible_distribution_version }}/kmod/$basearch/` | Repository URL used for kmod installation of ZFS. Applies only to RedHat systems. | 35 | | `zfs_redhat_repo_proxy` | | YUM/DNF repository proxy URL | 36 | | `zfs_debian_repo` | `{{ ansible_distribution_release }}-backports` | Repository used for installation. Applies only to Debian systems. | 37 | | `zfs_service_import_cache_enabled` | `true` | Enable service to import ZFS pools by cache file. | 38 | | `zfs_service_import_scan_enabled` | `false` | Enable service to import ZFS pools by device scanning. | 39 | | `zfs_service_mount_enabled` | `false` if zfs_use_zfs_mount_generator else `true` }}"` | Enable service to mount ZFS filesystems using the ZFS built-in mounting mechanism. | 40 | | `zfs_service_share_enabled` | `false` | Enable ZFS file system shares service. | 41 | | `zfs_service_volume_wait_enabled` | `true` | Enable service to wait for ZFS Volume links in `/dev`. | 42 | | `zfs_service_zed_enabled` | `false` | Enable ZFS Event Daemon (ZED) service. | 43 | | `zfs_use_zfs_mount_generator` | `false` | Enable Systemd Mount Generator, to automatically mount volumes on boot with Systemd. | 44 | | `zfs_kernel_module_parameters` | `{}` | Dictionary (key-value pairs) of ZFS kernel module parameters. See [official documentation](https://openzfs.github.io/openzfs-docs/Performance%20and%20Tuning/Module%20Parameters.html?highlight=zfs_arc_max) for available parameters. | 45 | | `zfs_scrub_schedule` | `monthly` | Time schedule for zpool scrubs. Valid options can be looked up [here](https://www.freedesktop.org/software/systemd/man/systemd.time.html#Calendar%20Events). | 46 | | `zfs_trim_schedule` | `weekly` | Time schedule for trim operations (for SSDs or virtual drives). Valid options can be looked up [here](https://www.freedesktop.org/software/systemd/man/systemd.time.html#Calendar%20Events). | 47 | | `zfs_config_none_ioscheduler` | `[]` | Set IO scheduler for the listed HDDs to `none`. | 48 | | `zfs_pools` | `[]` | List of ZFS Pools (zpools). | 49 | | **`zfs_pools[].name`** | | Name of the ZPool. | 50 | | **`zfs_pools[].vdev`** | | VDev definition for the ZPool. | 51 | | `zfs_pools[].scrub` | `true` | Enable scrub for this ZPool. | 52 | | `zfs_pools[].dont_enable_features` | `false` | Don't enable any feature. Use this in combination with `properties` to enable a custom set of features. | 53 | | `zfs_pools[].properties` | `{}` | ZPool properties. | 54 | | `zfs_pools[].filesystem_properties` | `{}` | Filesystem properties to apply to the whole ZPool. | 55 | | `zfs_pools[].extra_import_options` | `""` | String of extra options to pass to the ZPool import command. | 56 | | `zfs_pools[].extra_create_options` | `""` | String of extra options to pass to the ZPool create command. | 57 | | `zfs_pools_defaults` | `{}` | Default properties for ZPools. The properties can be overwritten on a per ZPool basis. | 58 | | `zfs_volumes` | `[]` | List of ZFS Volumes. | 59 | | **`zfs_volume[].name`** | | ZFS volume name. | 60 | | `zfs_volume[].properties` | `{}` | Dictionary (key-value pairs) of volume properties to be set. | 61 | | `zfs_volume[].state` | `present` | Whether to create (present), or remove (absent) the volume. | 62 | | `zfs_volumes_properties_defaults` | `volblocksize: 8K`
`volsize: 1G`
`compression: lz4`
`dedup: false`
`sync: standard`
| Default properties for ZFS volumes. The properties can be overwritten on a per Volume basis. | 63 | | `zfs_filesystems` | `[]` | List of ZFS Filesystems. | 64 | | **`zfs_filesystem[].name`** | | ZFS Filesystem name. | 65 | | `zfs_filesystem[].properties` | `{}` | Dictionary (key-value pairs) of filesystem properties to be set. | 66 | | `zfs_filesystem[].state` | `present` | Whether to create (present), or remove (absent) the filesystem. | 67 | | `zfs_filesystems_properties_defaults` | `acltype: posix`
`atime: false`
`canmount: true`
`casesensitivity: sensitive`
`compression: lz4`
`dedup: false`
`normalization: formD`
`setuid: true`
`snapdir: hidden`
`sync: standard`
`utf8only: true`
`xattr: sa`
| Default properties for ZFS filesystems. The properties can be overwritten on a per FS basis. | 68 | | `zfs_zrepl_config` | `{}` | Configuration for ZREPL. See the [official documentation](https://zrepl.github.io/configuration.html) for a list of available parameters. Examples can be found [here](https://github.com/zrepl/zrepl/tree/master/config/samples). | 69 | | `zfs_zrepl_enabled` | `false` | Install and enable [ZREPL](https://github.com/zrepl/zrepl) for replication and snapshots. | 70 | | `zfs_zrepl_redhat_repo_url` | `https://zrepl.cschwarz.com/`
`rpm/repo` | Repository URL used for ZREPL installation. Applies only to RedHat systems. | 71 | | `zfs_zrepl_debian_repo_url` | `https://zrepl.cschwarz.com/`
`apt` | Repository URL used for ZREPL installation. Applies only to Debian systems. | 72 | 73 | 74 | ## Dependencies 75 | 76 | Depends on `community.general` collection. 77 | 78 | ## Example Playbooks 79 | 80 | ### Basic Example 81 | 82 | A simple example to create a ZFS pool with a mirror vdev and two disks. You can test this one out using Vagrant+VirtualBox and the `Vagrantfile` provided in the `examples/` directory. Simply run `vagrant up` in the `examples/` directory and a virtual machine will be spun up with ZFS installed and a pool created. 83 | 84 | > Note: Depending on the mechanism used to install ZFS (DKMS or kmod), it might take some time to compile the kernel module and for the role to finish. This is especially true for the first run. 85 | 86 | ```yaml 87 | - hosts: all 88 | tasks: 89 | # ensure you have enabled backports repository on Debian systems first 90 | 91 | - ansible.builtin.include_role: 92 | name: aisbergg.zfs 93 | vars: 94 | zfs_pools: 95 | - name: pool 96 | vdev: >- 97 | mirror 98 | sdb 99 | sdc 100 | scrub: true 101 | properties: 102 | ashift: 12 103 | filesystem_properties: 104 | mountpoint: /mnt/raid1 105 | compression: lz4 106 | # properties of zfs_filesystems_properties_defaults also apply here 107 | zfs_filesystems: 108 | - name: pool1/vol1 109 | - name: pool1/vol2 110 | 111 | # schedule for ZFS scrubs 112 | zfs_scrub_schedule: monthly 113 | # schedule for TRIM 114 | zfs_trim_schedule: weekly 115 | ``` 116 | 117 | ### Advanced Example 118 | 119 | This example is a more advanced setup with multiple pools, volumes and filesystems. It also includes ZREPL configuration for automatic snapshots. It reflects a system setup of mine where I installed the whole system on ZFS. It consists of two pools, `rpool` and `bpool`. `rpool` is the root pool with the system installed on it and `bpool` is the boot pool with the boot partition. 120 | 121 | > Note: Depending on the mechanism used to install ZFS (DKMS or kmod), it might take some time to compile the kernel module and for the role to finish. This is especially true for the first run. 122 | > 123 | ```yaml 124 | - hosts: all 125 | vars: 126 | # 127 | # Service 128 | # 129 | 130 | # generate mount points using systemd 131 | zfs_use_zfs_mount_generator: true 132 | # use zfs_mount_generator but don't invoke ZED (Docker triggers it quite often) 133 | zfs_service_zed_enabled: false 134 | 135 | # 136 | # Configuration 137 | # 138 | 139 | # https://openzfs.github.io/openzfs-docs/Performance%20and%20Tuning/Module%20Parameters.html#zfs-module-parameters 140 | zfs_kernel_module_parameters: 141 | # use 1/4 of the memory for ZFS ARC 142 | zfs_arc_max: "{{ (ansible_memtotal_mb * 1024**2 * 0.25) | int }}" 143 | 144 | # schedule for ZFS scrubs 145 | zfs_scrub_schedule: monthly 146 | # schedule for TRIM 147 | zfs_trim_schedule: weekly 148 | 149 | _zfs_performance_tuning_default: 150 | # store less metadata (still redundant in mirror setups) 151 | redundant_metadata: most 152 | # use standard behaviour for synchronous writes 153 | sync: standard 154 | 155 | _zfs_performance_tuning_async_only: 156 | # store less metadata (still redundant in mirror setups) 157 | redundant_metadata: most 158 | # turn synchronous writes into asynchronous ones 159 | sync: disabled 160 | 161 | _zfs_performance_tuning_ssd: 162 | # use standard behaviour for synchronous writes 163 | sync: standard 164 | # store less metadata (still redundant in mirror setups) 165 | redundant_metadata: most 166 | # optimize synchronous operations to write directly to disk instead of writing 167 | # to log. On HDDs this decreases the latency, but won't do much on SSDs. 168 | logbias: throughput 169 | 170 | _zfs_filesytems_properties: 171 | canmount: true 172 | snapdir: hidden 173 | 174 | # make ZFS behave like a Linux FS 175 | casesensitivity: sensitive 176 | normalization: formD 177 | utf8only: on 178 | setuid: true 179 | atime: false 180 | 181 | # enable use of ACLs 182 | acltype: posix 183 | xattr: sa 184 | 185 | # compression and deduplication 186 | compression: lz4 187 | dedup: false 188 | 189 | zfs_filesystems_properties_defaults: "{{ 190 | _zfs_filesytems_properties | combine( 191 | _zfs_performance_tuning_async_only 192 | )}}" 193 | 194 | _zfs_volumes_properties: 195 | volblocksize: 8K 196 | volsize: 1G 197 | compression: lz4 198 | dedup: false 199 | 200 | # https://openzfs.github.io/openzfs-docs/man/7/zfsprops.7.html 201 | zfs_volumes_properties_defaults: "{{ 202 | _zfs_volumes_properties | combine( 203 | _zfs_performance_tuning_async_only 204 | )}}" 205 | 206 | # 207 | # ZPools 208 | # 209 | 210 | zfs_pools: 211 | - name: rpool 212 | vdev: >- 213 | mirror 214 | r1 215 | r2 216 | scrub: true 217 | properties: 218 | ashift: 12 219 | filesystem_properties: 220 | # don't mount, just supply a base path for sub datasets 221 | canmount: off 222 | mountpoint: / 223 | 224 | - name: bpool 225 | vdev: >- 226 | mirror 227 | {{ _zfs_boot_partition1 }} 228 | {{ _zfs_boot_partition2 }} 229 | scrub: true 230 | properties: 231 | ashift: 12 232 | "feature@async_destroy": enabled 233 | "feature@bookmarks": enabled 234 | "feature@embedded_data": enabled 235 | "feature@empty_bpobj": enabled 236 | "feature@enabled_txg": enabled 237 | "feature@extensible_dataset": enabled 238 | "feature@filesystem_limits": enabled 239 | "feature@hole_birth": enabled 240 | "feature@large_blocks": enabled 241 | "feature@lz4_compress": enabled 242 | "feature@spacemap_histogram": enabled 243 | dont_enable_features: true 244 | filesystem_properties: 245 | canmount: off 246 | mountpoint: /boot 247 | 248 | # 249 | # Datasets 250 | # 251 | 252 | zfs_filesystems: 253 | # root 254 | - name: rpool/ROOT 255 | properties: 256 | canmount: off 257 | mountpoint: none 258 | - name: rpool/ROOT/default 259 | properties: 260 | canmount: noauto 261 | mountpoint: / 262 | 263 | - name: rpool/home 264 | - name: rpool/home/root 265 | properties: 266 | mountpoint: /root 267 | - name: rpool/var/lib/docker 268 | - name: rpool/var/log 269 | - name: rpool/var/spool 270 | - name: rpool/var/cache 271 | 272 | # boot 273 | - name: bpool/default 274 | properties: 275 | mountpoint: /boot 276 | 277 | # 278 | # Automatic Snapthots Using ZREPL 279 | # 280 | 281 | zfs_zrepl_enabled: true 282 | zfs_zrepl_config: 283 | jobs: 284 | - name: storage 285 | type: snap 286 | filesystems: { 287 | "rpool<": true, 288 | "rpool/var<": false, 289 | } 290 | snapshotting: 291 | type: periodic 292 | interval: 12h 293 | prefix: auto_ 294 | pruning: 295 | keep: 296 | # prune automatic snapshots 297 | - type: grid 298 | # in first 24 hours keep all snapshots 299 | # in first 7 days 1 snapshot each day 300 | # in first month keep 1 snapshot each week 301 | # discard the rest 302 | # details see: https://zrepl.github.io/configuration/prune.html#policy-grid 303 | grid: 1x24h(keep=all) | 7x1d(keep=1) | 3x7d(keep=1) 304 | regex: "^auto_.*" 305 | 306 | # keep manual snapshots 307 | - type: regex 308 | regex: "^manual_.*" 309 | 310 | roles: 311 | - aisbergg.zfs 312 | ``` 313 | 314 | ## License 315 | 316 | MIT 317 | 318 | ## Author Information 319 | 320 | Andre Lehmann (aisberg@posteo.de) 321 | --------------------------------------------------------------------------------