diff --git a/meta/argument_specs.yml b/meta/argument_specs.yml new file mode 100644 index 00000000..e4cfcc35 --- /dev/null +++ b/meta/argument_specs.yml @@ -0,0 +1,82 @@ +# SPDX-License-Identifier: MIT +--- +argument_specs: + main: + short_description: The kernel_settings role. + description: > + The kernel_settings role allows you to modify kernel settings such as + `sysctl` parameters, `/sys` sysfs values, systemd CPU affinity, and + transparent hugepage settings. It uses `tuned` as the default provider + on Red Hat Enterprise Linux, derivatives, and Fedora. + + Settings are additive by default. Use `previous: replaced` in a settings + list to replace all existing values in a group, or `{"state": "empty"}` + to remove all settings in a group. Use `state: absent` on individual + settings to remove them. + options: + kernel_settings_sysctl: + type: raw + default: [] + description: > + List of `sysctl` settings to apply, or the dict `{"state": + "empty"}` to remove all `sysctl` settings. Each list item is a + dict with keys `name`, `value`, `state`, and `previous`. Use + `state: absent` to remove a setting by name. Use `previous: + replaced` as one list item to replace all existing settings with + the given values. Values must be strings or numbers, not YAML + boolean types. + kernel_settings_sysfs: + type: raw + default: [] + description: > + List of `/sys` settings to apply, or the dict `{"state": + "empty"}` to remove all `sysfs` settings. Each list item is a + dict with keys `name`, `value`, `state`, and `previous`. The + `name` is the path under `/sys`. Use `state: absent` to remove a + setting by name. Use `previous: replaced` as one list item to + replace all existing settings with the given values. + kernel_settings_systemd_cpu_affinity: + type: raw + default: null + description: > + CPU affinity for systemd as a space-delimited list of CPU + numbers, for example `1,3,5,7`, as described in the + systemd-system.conf `CPUAffinity` option. Use the dict + `{"state": "absent"}` to remove the setting. + kernel_settings_transparent_hugepages: + type: raw + default: null + description: > + Transparent hugepages setting for the memory subsystem. Set to + `always`, `madvise`, or `never`. Use the dict `{"state": + "absent"}` to remove the setting. + kernel_settings_transparent_hugepages_defrag: + type: raw + default: null + description: > + Transparent hugepages defragmentation setting for the memory + subsystem. Set to `always`, `defer`, `defer+madvise`, `madvise`, + or `never`. The supported values may differ depending on the + OS. Use the dict `{"state": "absent"}` to remove the setting. + kernel_settings_purge: + type: bool + default: false + description: > + Whether to completely wipe out the current kernel settings + configuration and replace it with the given settings. + kernel_settings_reboot_ok: + type: bool + default: false + description: > + Whether the role is allowed to reboot the managed host when a + reboot is required to apply changes. When `false`, the role sets + `kernel_settings_reboot_required` to `true` instead of + rebooting. + kernel_settings_transactional_update_reboot_ok: + type: raw + default: null + description: > + Whether to permit reboots required by transactional updates. Set + to `true` to allow reboots, `false` to disallow reboots, or leave + unset so that the role will fail to ensure the reboot requirement + is not overlooked. Accepts a boolean value or `null`. diff --git a/tasks/assert_role_vars.yml b/tasks/assert_role_vars.yml new file mode 100644 index 00000000..cdfbadc7 --- /dev/null +++ b/tasks/assert_role_vars.yml @@ -0,0 +1,168 @@ +# SPDX-License-Identifier: MIT +--- +- name: Assert kernel_settings_sysctl is a list of dicts or the empty sentinel + ansible.builtin.assert: + that: + - >- + (kernel_settings_sysctl is mapping + and kernel_settings_sysctl.keys() | list | difference(['state']) + | length == 0 + and kernel_settings_sysctl.state == 'empty') + or (kernel_settings_sysctl is sequence + and kernel_settings_sysctl is not string + and kernel_settings_sysctl is not mapping + and kernel_settings_sysctl | reject('mapping') | list | length == 0) + fail_msg: >- + kernel_settings_sysctl must be a list of dictionaries or {"state": + "empty"}, got {{ kernel_settings_sysctl | type_debug }} + +- name: Assert kernel_settings_sysctl list items are valid + ansible.builtin.assert: + that: + - >- + item.keys() | list | difference(__kernel_settings_setting_valid_keys) + | length == 0 + - not ('previous' in item) or item.previous == 'replaced' + - not ('state' in item) or item.state == 'absent' + - >- + (item.keys() | list == ['previous'] and item.previous == 'replaced') + or ('name' in item) + - >- + (item.keys() | list == ['previous'] and item.previous == 'replaced') + or ('state' in item and item.state == 'absent') + or ('value' in item) + fail_msg: >- + kernel_settings_sysctl[{{ idx }}] is invalid: + {{ item }} + vars: + __kernel_settings_setting_valid_keys: + - name + - value + - state + - previous + loop: >- + {{ kernel_settings_sysctl if (kernel_settings_sysctl is sequence + and kernel_settings_sysctl is not string + and kernel_settings_sysctl is not mapping) + else [] }} + loop_control: + index_var: idx + label: "{{ idx }}" + +- name: Assert kernel_settings_sysfs is a list of dicts or the empty sentinel + ansible.builtin.assert: + that: + - >- + (kernel_settings_sysfs is mapping + and kernel_settings_sysfs.keys() | list | difference(['state']) + | length == 0 + and kernel_settings_sysfs.state == 'empty') + or (kernel_settings_sysfs is sequence + and kernel_settings_sysfs is not string + and kernel_settings_sysfs is not mapping + and kernel_settings_sysfs | reject('mapping') | list | length == 0) + fail_msg: >- + kernel_settings_sysfs must be a list of dictionaries or {"state": + "empty"}, got {{ kernel_settings_sysfs | type_debug }} + +- name: Assert kernel_settings_sysfs list items are valid + ansible.builtin.assert: + that: + - >- + item.keys() | list | difference(__kernel_settings_setting_valid_keys) + | length == 0 + - not ('previous' in item) or item.previous == 'replaced' + - not ('state' in item) or item.state == 'absent' + - >- + (item.keys() | list == ['previous'] and item.previous == 'replaced') + or ('name' in item) + - >- + (item.keys() | list == ['previous'] and item.previous == 'replaced') + or ('state' in item and item.state == 'absent') + or ('value' in item) + fail_msg: >- + kernel_settings_sysfs[{{ idx }}] is invalid: + {{ item }} + vars: + __kernel_settings_setting_valid_keys: + - name + - value + - state + - previous + loop: >- + {{ kernel_settings_sysfs if (kernel_settings_sysfs is sequence + and kernel_settings_sysfs is not string + and kernel_settings_sysfs is not mapping) + else [] }} + loop_control: + index_var: idx + label: "{{ idx }}" + +- name: Assert kernel_settings_systemd_cpu_affinity is null, a string, or absent dict + ansible.builtin.assert: + that: + - >- + (kernel_settings_systemd_cpu_affinity is none) + or (kernel_settings_systemd_cpu_affinity is string) + or ( + kernel_settings_systemd_cpu_affinity is mapping + and kernel_settings_systemd_cpu_affinity.keys() | list + | difference(['state']) | length == 0 + and kernel_settings_systemd_cpu_affinity.state == 'absent' + ) + fail_msg: >- + kernel_settings_systemd_cpu_affinity must be null, a string, or + {"state": "absent"}, got {{ kernel_settings_systemd_cpu_affinity | type_debug }} + +- name: Assert kernel_settings_transparent_hugepages is null, a valid string, or absent dict + ansible.builtin.assert: + that: + - >- + (kernel_settings_transparent_hugepages is none) + or ( + kernel_settings_transparent_hugepages is string + and kernel_settings_transparent_hugepages in + ['always', 'madvise', 'never'] + ) + or ( + kernel_settings_transparent_hugepages is mapping + and kernel_settings_transparent_hugepages.keys() | list + | difference(['state']) | length == 0 + and kernel_settings_transparent_hugepages.state == 'absent' + ) + fail_msg: >- + kernel_settings_transparent_hugepages must be null, one of always, + madvise, never, or {"state": "absent"}, + got {{ kernel_settings_transparent_hugepages | type_debug }} + +- name: Assert kernel_settings_transparent_hugepages_defrag is null, a valid string, or absent dict + ansible.builtin.assert: + that: + - >- + (kernel_settings_transparent_hugepages_defrag is none) + or ( + kernel_settings_transparent_hugepages_defrag is string + and kernel_settings_transparent_hugepages_defrag in + ['always', 'defer', 'defer+madvise', 'madvise', 'never'] + ) + or ( + kernel_settings_transparent_hugepages_defrag is mapping + and kernel_settings_transparent_hugepages_defrag.keys() | list + | difference(['state']) | length == 0 + and kernel_settings_transparent_hugepages_defrag.state == 'absent' + ) + fail_msg: >- + kernel_settings_transparent_hugepages_defrag must be null, one of + always, defer, defer+madvise, madvise, never, or {"state": + "absent"}, got {{ kernel_settings_transparent_hugepages_defrag | type_debug }} + +- name: Assert kernel_settings_transactional_update_reboot_ok is null or a boolean + ansible.builtin.assert: + that: + - >- + (kernel_settings_transactional_update_reboot_ok is none) + or (kernel_settings_transactional_update_reboot_ok is sameas true) + or (kernel_settings_transactional_update_reboot_ok is sameas false) + fail_msg: >- + kernel_settings_transactional_update_reboot_ok must be null or a + boolean, got {{ kernel_settings_transactional_update_reboot_ok | type_debug }} diff --git a/tasks/main.yml b/tasks/main.yml index e0b43a75..80dca9ca 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -1,4 +1,7 @@ --- +- name: Validate role parameters + ansible.builtin.include_tasks: assert_role_vars.yml + - name: Check sysctl settings for boolean values fail: msg: Boolean values are not allowed for sysctl settings diff --git a/tests/tests_invalid_input.yml b/tests/tests_invalid_input.yml new file mode 100644 index 00000000..ce78a5f8 --- /dev/null +++ b/tests/tests_invalid_input.yml @@ -0,0 +1,322 @@ +# SPDX-License-Identifier: MIT +--- +# Verify meta/argument_specs.yml and tasks/assert_role_vars.yml reject bad +# parameters. Each block runs the role expecting failure; rescue records +# that outcome. +- name: Verify invalid parameters are rejected + hosts: all + tasks: + - name: Run invalid input tests + block: + # ==================================================== + # Section 1: Verify role works with valid defaults + # ==================================================== + - name: Run role with valid defaults + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + + # ==================================================== + # Section 2: argument_specs validation (Ansible 2.11+) + # ==================================================== + - name: Run argument specs validation tests + when: ansible_version.full is version('2.11', '>=') + block: + - name: Argument specs reject non-boolean kernel_settings_purge + block: + - name: Run role with non-boolean kernel_settings_purge + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + kernel_settings_purge: not_a_bool + rescue: + - name: Mark invalid kernel_settings_purge type rejected + ansible.builtin.set_fact: + __invalid_input_purge_type_failed: true + + - name: Assert invalid kernel_settings_purge type was rejected + ansible.builtin.assert: + that: + - __invalid_input_purge_type_failed | default(false) + fail_msg: >- + meta/argument_specs should reject kernel_settings_purge + values that are not booleans + + - name: Argument specs reject non-boolean kernel_settings_reboot_ok + block: + - name: Run role with non-boolean kernel_settings_reboot_ok + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + kernel_settings_reboot_ok: not_a_bool + rescue: + - name: Mark invalid kernel_settings_reboot_ok type rejected + ansible.builtin.set_fact: + __invalid_input_reboot_ok_type_failed: true + + - name: Assert invalid kernel_settings_reboot_ok type was rejected + ansible.builtin.assert: + that: + - __invalid_input_reboot_ok_type_failed | default(false) + fail_msg: >- + meta/argument_specs should reject kernel_settings_reboot_ok + values that are not booleans + + # ==================================================== + # Section 3: assert_role_vars validation (all versions) + # ==================================================== + + - name: Assert rejects kernel_settings_sysctl as a string + block: + - name: Run role with kernel_settings_sysctl as a string + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + kernel_settings_sysctl: not_a_dict_or_list + rescue: + - name: Mark kernel_settings_sysctl string rejected + ansible.builtin.set_fact: + __invalid_input_sysctl_string_failed: true + + - name: Assert kernel_settings_sysctl as string failed validation + ansible.builtin.assert: + that: + - __invalid_input_sysctl_string_failed | default(false) + fail_msg: >- + tasks/assert_role_vars.yml should reject kernel_settings_sysctl + when it is a string instead of a list of dicts or a single dict + + - name: Assert rejects kernel_settings_sysctl list with invalid key + block: + - name: Run role with kernel_settings_sysctl containing unknown key + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + kernel_settings_sysctl: + - invalid_key_name: some_value + rescue: + - name: Mark kernel_settings_sysctl invalid key rejected + ansible.builtin.set_fact: + __invalid_input_sysctl_bad_key_failed: true + + - name: Assert kernel_settings_sysctl invalid key failed validation + ansible.builtin.assert: + that: + - __invalid_input_sysctl_bad_key_failed | default(false) + fail_msg: >- + tasks/assert_role_vars.yml should reject kernel_settings_sysctl + dicts containing keys not in the valid set + + - name: Assert rejects kernel_settings_sysctl as a single-setting mapping + block: + - name: Run role with kernel_settings_sysctl as a single setting dict + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + kernel_settings_sysctl: + name: fs.file-max + value: 379724 + rescue: + - name: Mark kernel_settings_sysctl single-setting mapping rejected + ansible.builtin.set_fact: + __invalid_input_sysctl_single_dict_failed: true + + - name: Assert kernel_settings_sysctl single-setting mapping failed validation + ansible.builtin.assert: + that: + - __invalid_input_sysctl_single_dict_failed | default(false) + fail_msg: >- + tasks/assert_role_vars.yml should reject kernel_settings_sysctl + when given as a single setting dict instead of a list + + - name: Assert rejects kernel_settings_sysctl empty sentinel with extra keys + block: + - name: Run role with kernel_settings_sysctl empty sentinel plus extra key + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + kernel_settings_sysctl: + state: empty + extra_key: unexpected + rescue: + - name: Mark kernel_settings_sysctl empty sentinel extra keys rejected + ansible.builtin.set_fact: + __invalid_input_sysctl_empty_extra_failed: true + + - name: Assert kernel_settings_sysctl empty sentinel extra keys failed validation + ansible.builtin.assert: + that: + - __invalid_input_sysctl_empty_extra_failed | default(false) + fail_msg: >- + tasks/assert_role_vars.yml should reject kernel_settings_sysctl + empty sentinel dicts with keys other than state + + - name: Assert rejects kernel_settings_sysctl list item missing name + block: + - name: Run role with kernel_settings_sysctl list item without name + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + kernel_settings_sysctl: + - value: 379724 + rescue: + - name: Mark kernel_settings_sysctl missing name rejected + ansible.builtin.set_fact: + __invalid_input_sysctl_missing_name_failed: true + + - name: Assert kernel_settings_sysctl missing name failed validation + ansible.builtin.assert: + that: + - __invalid_input_sysctl_missing_name_failed | default(false) + fail_msg: >- + tasks/assert_role_vars.yml should reject kernel_settings_sysctl + list items that omit the required name field + + - name: Assert rejects kernel_settings_sysctl list item with invalid previous + block: + - name: Run role with kernel_settings_sysctl list item with invalid previous + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + kernel_settings_sysctl: + - previous: not_replaced + rescue: + - name: Mark kernel_settings_sysctl invalid previous rejected + ansible.builtin.set_fact: + __invalid_input_sysctl_bad_previous_failed: true + + - name: Assert kernel_settings_sysctl invalid previous failed validation + ansible.builtin.assert: + that: + - __invalid_input_sysctl_bad_previous_failed | default(false) + fail_msg: >- + tasks/assert_role_vars.yml should reject kernel_settings_sysctl + list items with previous values other than replaced + + - name: Assert rejects kernel_settings_sysfs as an integer + block: + - name: Run role with kernel_settings_sysfs as an integer + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + kernel_settings_sysfs: 42 + rescue: + - name: Mark kernel_settings_sysfs integer rejected + ansible.builtin.set_fact: + __invalid_input_sysfs_int_failed: true + + - name: Assert kernel_settings_sysfs as integer failed validation + ansible.builtin.assert: + that: + - __invalid_input_sysfs_int_failed | default(false) + fail_msg: >- + tasks/assert_role_vars.yml should reject kernel_settings_sysfs + when it is an integer instead of a list of dicts or a single dict + + - name: Assert rejects kernel_settings_systemd_cpu_affinity as integer + block: + - name: Run role with kernel_settings_systemd_cpu_affinity as integer + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + kernel_settings_systemd_cpu_affinity: 42 + rescue: + - name: Mark kernel_settings_systemd_cpu_affinity integer rejected + ansible.builtin.set_fact: + __invalid_input_cpu_affinity_int_failed: true + + - name: Assert kernel_settings_systemd_cpu_affinity as integer failed validation + ansible.builtin.assert: + that: + - __invalid_input_cpu_affinity_int_failed | default(false) + fail_msg: >- + tasks/assert_role_vars.yml should reject + kernel_settings_systemd_cpu_affinity when it is an integer + instead of null, a string, or {"state": "absent"} + + - name: Assert rejects invalid kernel_settings_transparent_hugepages choice + block: + - name: Run role with invalid transparent_hugepages value + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + kernel_settings_transparent_hugepages: invalid_choice + rescue: + - name: Mark invalid transparent_hugepages rejected + ansible.builtin.set_fact: + __invalid_input_trans_huge_choice_failed: true + + - name: Assert invalid transparent_hugepages choice failed validation + ansible.builtin.assert: + that: + - __invalid_input_trans_huge_choice_failed | default(false) + fail_msg: >- + tasks/assert_role_vars.yml should reject + kernel_settings_transparent_hugepages values outside the + allowed set + + - name: Assert rejects invalid kernel_settings_transparent_hugepages_defrag choice + block: + - name: Run role with invalid transparent_hugepages_defrag value + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + kernel_settings_transparent_hugepages_defrag: invalid_choice + rescue: + - name: Mark invalid transparent_hugepages_defrag rejected + ansible.builtin.set_fact: + __invalid_input_trans_defrag_choice_failed: true + + - name: Assert invalid transparent_hugepages_defrag choice failed validation + ansible.builtin.assert: + that: + - __invalid_input_trans_defrag_choice_failed | default(false) + fail_msg: >- + tasks/assert_role_vars.yml should reject + kernel_settings_transparent_hugepages_defrag values outside + the allowed set + + - name: Assert rejects kernel_settings_transactional_update_reboot_ok as string + block: + - name: Run role with non-boolean transactional_update_reboot_ok + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + kernel_settings_transactional_update_reboot_ok: not_a_bool + rescue: + - name: Mark invalid transactional_update_reboot_ok rejected + ansible.builtin.set_fact: + __invalid_input_tu_reboot_type_failed: true + + - name: Assert transactional_update_reboot_ok as string failed validation + ansible.builtin.assert: + that: + - __invalid_input_tu_reboot_type_failed | default(false) + fail_msg: >- + tasks/assert_role_vars.yml should reject + kernel_settings_transactional_update_reboot_ok when it is a + string instead of null or a boolean + + - name: Assert rejects kernel_settings_transactional_update_reboot_ok as integer + block: + - name: Run role with integer transactional_update_reboot_ok + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + kernel_settings_transactional_update_reboot_ok: 42 + rescue: + - name: Mark integer transactional_update_reboot_ok rejected + ansible.builtin.set_fact: + __invalid_input_tu_reboot_int_failed: true + + - name: Assert transactional_update_reboot_ok as integer failed validation + ansible.builtin.assert: + that: + - __invalid_input_tu_reboot_int_failed | default(false) + fail_msg: >- + tasks/assert_role_vars.yml should reject + kernel_settings_transactional_update_reboot_ok when it is an + integer instead of null or a boolean + + always: + - name: Clear test facts + ansible.builtin.set_fact: + __invalid_input_purge_type_failed: + __invalid_input_reboot_ok_type_failed: + __invalid_input_sysctl_string_failed: + __invalid_input_sysctl_bad_key_failed: + __invalid_input_sysctl_single_dict_failed: + __invalid_input_sysctl_empty_extra_failed: + __invalid_input_sysctl_missing_name_failed: + __invalid_input_sysctl_bad_previous_failed: + __invalid_input_sysfs_int_failed: + __invalid_input_cpu_affinity_int_failed: + __invalid_input_trans_huge_choice_failed: + __invalid_input_trans_defrag_choice_failed: + __invalid_input_tu_reboot_type_failed: + __invalid_input_tu_reboot_int_failed: + tags: tests::cleanup