From 28010f73d85619082d78aab60d697aff94d8df39 Mon Sep 17 00:00:00 2001 From: Donat Szabo Date: Mon, 31 Aug 2026 17:23:47 +0200 Subject: [PATCH 1/2] Argument spec implementaion for Bootloader role --- meta/argument_specs.yml | 137 +++++++++ plans/test_playbooks_parallel.fmf | 4 +- tasks/assert_role_vars.yml | 51 ++++ tasks/main.yml | 3 + tests/tests_invalid_input.yml | 461 ++++++++++++++++++++++++++++++ 5 files changed, 654 insertions(+), 2 deletions(-) create mode 100644 meta/argument_specs.yml create mode 100644 tasks/assert_role_vars.yml create mode 100644 tests/tests_invalid_input.yml diff --git a/meta/argument_specs.yml b/meta/argument_specs.yml new file mode 100644 index 00000000..62278f0f --- /dev/null +++ b/meta/argument_specs.yml @@ -0,0 +1,137 @@ +# SPDX-License-Identifier: MIT +--- +argument_specs: + main: + short_description: The bootloader role. + description: > + The bootloader role allows you to manage GRUB2 boot loader + settings, kernel command line parameters, boot loader + timeout, and boot loader password. + options: + bootloader_settings: + type: list + elements: dict + default: [] + description: > + List of kernel entries and their command line parameters + to configure. Each entry specifies a kernel and the + boot loader settings to apply. + options: + kernel: + type: raw + required: true + description: > + The kernel to update settings for. Accepts the + string `DEFAULT` or `ALL` to target the default or + all kernels, or a dictionary with keys `path`, + `index`, `title`, and `initrd` to identify a + specific kernel. + state: + type: str + choices: + - present + - absent + default: present + description: > + Whether the kernel entry should be present + (`present`) or removed (`absent`). + options: + type: list + elements: dict + description: > + List of boot loader arguments to apply to the + specified kernel. + options: + name: + type: str + description: > + The name of the boot loader setting. Not + required when using `previous: replaced`. + value: + type: raw + description: > + The value for the setting. Not required when + the setting has no value, for example `quiet`. + The value must not be a YAML boolean or null + type. + state: + type: str + choices: + - present + - absent + default: present + description: > + Whether the setting should be present + (`present`) or removed (`absent`). The value + `absent` removes the setting with the given + `name`. + previous: + type: str + choices: + - replaced + description: > + Whether to replace all previous settings with + the given settings. The only supported value + is `replaced`. + copy_default: + type: bool + default: false + description: > + Whether to copy the default arguments to the + created kernel. + default: + type: bool + default: false + description: > + Whether to make this kernel the default boot + entry. + + bootloader_timeout: + type: raw + default: null + description: > + The GRUB boot loader timeout in seconds. When set to + `null` or left unset, the role does not change the + timeout setting. + + bootloader_password: + type: raw + default: null + description: > + The password to protect boot parameters. When set to + `null` or left unset, the current password + configuration is not modified. The boot loader + username is always `root`. This value should come + from an Ansible vault. + + bootloader_remove_password: + type: bool + default: false + description: > + Whether to remove the boot loader password + configuration. + + bootloader_reboot_ok: + type: bool + default: false + description: > + Whether the role is allowed to reboot the managed host + when changes require a reboot to take effect. If + `false`, the role sets `bootloader_reboot_required` to + `true` instead. + + bootloader_gather_facts: + type: bool + default: false + description: > + Whether to gather bootloader facts containing boot + information for all kernels. The facts are returned + in the `bootloader_facts` variable. + + bootloader_secure_logging: + type: bool + default: true + description: > + Whether to suppress potentially sensitive output from + tasks that handle credentials by setting `no_log` to + `true` on those tasks. diff --git a/plans/test_playbooks_parallel.fmf b/plans/test_playbooks_parallel.fmf index 0d4c8c30..c5bc3a12 100644 --- a/plans/test_playbooks_parallel.fmf +++ b/plans/test_playbooks_parallel.fmf @@ -7,8 +7,8 @@ provision: role: control_node - name: managed-node1 role: managed_node - - name: managed-node2 - role: managed_node +# - name: managed-node2 +# role: managed_node environment: # ensure versions are strings! SR_ANSIBLE_VER: "2.17" diff --git a/tasks/assert_role_vars.yml b/tasks/assert_role_vars.yml new file mode 100644 index 00000000..68d8acac --- /dev/null +++ b/tasks/assert_role_vars.yml @@ -0,0 +1,51 @@ +# SPDX-License-Identifier: MIT +--- +- name: Assert bootloader_timeout is null or a non-negative integer + ansible.builtin.assert: + that: + - >- + (bootloader_timeout is none) + or (bootloader_timeout | string | trim is match('^[0-9]+$')) + fail_msg: >- + bootloader_timeout must be null or a non-negative integer, + got {{ bootloader_timeout | type_debug }} + +- name: Assert bootloader_password is null or a string + ansible.builtin.assert: + that: + - >- + (bootloader_password is none) + or (bootloader_password is string) + fail_msg: >- + bootloader_password must be null or a string, + got {{ bootloader_password | type_debug }} + +- name: Assert kernel in bootloader_settings is a string or dictionary + ansible.builtin.assert: + that: + - >- + item.kernel is string + or item.kernel is mapping + fail_msg: >- + bootloader_settings[{{ idx }}].kernel must be a string + or dictionary, got {{ item.kernel | type_debug }} + loop: "{{ bootloader_settings }}" + loop_control: + index_var: idx + label: "{{ idx }}" + when: item.kernel is defined + +- name: Assert value in bootloader_settings options is a string or integer + ansible.builtin.assert: + that: + - >- + item.1.value is string + or (item.1.value | type_debug) == 'int' + fail_msg: >- + bootloader_settings options value must be a string + or integer, got {{ item.1.value | type_debug }} + loop: >- + {{ bootloader_settings | subelements('options', skip_missing=True) }} + loop_control: + label: "{{ item.0.kernel | default('unknown') }} - {{ item.1.name | default('unnamed') }}" + when: item.1.value is defined and item.1.value is not none diff --git a/tasks/main.yml b/tasks/main.yml index 83c799fa..d157621e 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -28,6 +28,9 @@ __values_with_null_and_no_state: "{{ __values | rejectattr('state', 'defined') | selectattr('value', 'sameas', none) | list }}" +- name: Validate role parameters + ansible.builtin.include_tasks: assert_role_vars.yml + - name: Ensure required packages are installed package: name: "{{ __bootloader_packages }}" diff --git a/tests/tests_invalid_input.yml b/tests/tests_invalid_input.yml new file mode 100644 index 00000000..29c50c1b --- /dev/null +++ b/tests/tests_invalid_input.yml @@ -0,0 +1,461 @@ +# SPDX-License-Identifier: MIT +--- +- name: Verify invalid parameters are rejected + hosts: all + tasks: + - name: Skip on s390x + meta: end_host + when: ansible_facts["architecture"] == "s390x" + + - name: Run invalid input tests + block: + # ==================================================== + # Section 1: Verify role works with valid defaults + # ==================================================== + - name: Run role with valid defaults + ansible.builtin.include_role: + name: linux-system-roles.bootloader + + # ==================================================== + # Section 2: argument_specs validation (Ansible 2.10+) + # ==================================================== + - name: Run argument specs validation tests + when: ansible_version.full is version("2.11", ">=") + block: + # --- Test: non-list bootloader_settings --- + - name: Argument specs reject non-list bootloader_settings + block: + - name: Run role with non-list bootloader_settings + ansible.builtin.include_role: + name: linux-system-roles.bootloader + vars: + bootloader_settings: "not_a_list" + rescue: + - name: Mark non-list bootloader_settings rejected + ansible.builtin.set_fact: + __invalid_input_settings_type_failed: true + + - name: Assert non-list bootloader_settings was rejected + ansible.builtin.assert: + that: + - __invalid_input_settings_type_failed | default(false) + fail_msg: >- + argument_specs should reject bootloader_settings + with type string + + # --- Test: missing required kernel field --- + - name: Argument specs reject missing kernel field + block: + - name: Run role with missing kernel field + ansible.builtin.include_role: + name: linux-system-roles.bootloader + vars: + bootloader_settings: + - options: + - name: quiet + rescue: + - name: Mark missing kernel rejected + ansible.builtin.set_fact: + __invalid_input_missing_kernel_failed: true + when: >- + 'missing required arguments' in + (ansible_failed_result | default({}) | to_json) + + - name: Assert missing kernel was rejected + ansible.builtin.assert: + that: + - __invalid_input_missing_kernel_failed | default(false) + fail_msg: >- + argument_specs should reject bootloader_settings + entries missing the required kernel field + + # --- Test: invalid state choice --- + - name: Argument specs reject invalid state choice + block: + - name: Run role with invalid state choice + ansible.builtin.include_role: + name: linux-system-roles.bootloader + vars: + bootloader_settings: + - kernel: DEFAULT + state: invalid_state + rescue: + - name: Mark invalid state choice rejected + ansible.builtin.set_fact: + __invalid_input_state_choice_failed: true + + - name: Assert invalid state choice was rejected + ansible.builtin.assert: + that: + - __invalid_input_state_choice_failed | default(false) + fail_msg: >- + argument_specs should reject bootloader_settings + state value 'invalid_state' + + # --- Test: invalid option state choice --- + - name: Argument specs reject invalid option state choice + block: + - name: Run role with invalid option state choice + ansible.builtin.include_role: + name: linux-system-roles.bootloader + vars: + bootloader_settings: + - kernel: DEFAULT + options: + - name: quiet + state: invalid_option_state + rescue: + - name: Mark invalid option state choice rejected + ansible.builtin.set_fact: + __invalid_input_option_state_choice_failed: true + + - name: Assert invalid option state choice was rejected + ansible.builtin.assert: + that: + - >- + __invalid_input_option_state_choice_failed + | default(false) + fail_msg: >- + argument_specs should reject options state value + 'invalid_option_state' + + # --- Test: invalid previous choice --- + - name: Argument specs reject invalid previous choice + block: + - name: Run role with invalid previous choice + ansible.builtin.include_role: + name: linux-system-roles.bootloader + vars: + bootloader_settings: + - kernel: DEFAULT + options: + - previous: invalid_previous + rescue: + - name: Mark invalid previous choice rejected + ansible.builtin.set_fact: + __invalid_input_previous_choice_failed: true + + - name: Assert invalid previous choice was rejected + ansible.builtin.assert: + that: + - >- + __invalid_input_previous_choice_failed + | default(false) + fail_msg: >- + argument_specs should reject options previous + value 'invalid_previous' + + # --- Test: non-bool default suboption --- + - name: Argument specs reject non-bool default suboption + block: + - name: Run role with non-bool default suboption + ansible.builtin.include_role: + name: linux-system-roles.bootloader + vars: + bootloader_settings: + - kernel: DEFAULT + default: "not_a_bool" + rescue: + - name: Mark non-bool default suboption rejected + ansible.builtin.set_fact: + __invalid_input_default_subopt_type_failed: true + + - name: Assert non-bool default suboption was rejected + ansible.builtin.assert: + that: + - >- + __invalid_input_default_subopt_type_failed + | default(false) + fail_msg: >- + argument_specs should reject bootloader_settings + default suboption with type string + + # --- Test: non-bool copy_default suboption --- + - name: Argument specs reject non-bool copy_default suboption + block: + - name: Run role with non-bool copy_default suboption + ansible.builtin.include_role: + name: linux-system-roles.bootloader + vars: + bootloader_settings: + - kernel: DEFAULT + options: + - name: quiet + copy_default: "not_a_bool" + rescue: + - name: Mark non-bool copy_default suboption rejected + ansible.builtin.set_fact: + __invalid_input_copy_default_type_failed: true + + - name: Assert non-bool copy_default suboption was rejected + ansible.builtin.assert: + that: + - >- + __invalid_input_copy_default_type_failed + | default(false) + fail_msg: >- + argument_specs should reject options + copy_default suboption with type string + + # --- Test: non-bool bootloader_remove_password --- + - name: Argument specs reject non-bool bootloader_remove_password + block: + - name: Run role with non-bool bootloader_remove_password + ansible.builtin.include_role: + name: linux-system-roles.bootloader + vars: + bootloader_remove_password: "not_a_bool" + rescue: + - name: Mark non-bool bootloader_remove_password rejected + ansible.builtin.set_fact: + __invalid_input_remove_password_type_failed: true + + - name: Assert non-bool bootloader_remove_password was rejected + ansible.builtin.assert: + that: + - >- + __invalid_input_remove_password_type_failed + | default(false) + fail_msg: >- + argument_specs should reject + bootloader_remove_password with type string + + # --- Test: non-bool bootloader_reboot_ok --- + - name: Argument specs reject non-bool bootloader_reboot_ok + block: + - name: Run role with non-bool bootloader_reboot_ok + ansible.builtin.include_role: + name: linux-system-roles.bootloader + vars: + bootloader_reboot_ok: "not_a_bool" + rescue: + - name: Mark non-bool bootloader_reboot_ok rejected + ansible.builtin.set_fact: + __invalid_input_reboot_ok_type_failed: true + + - name: Assert non-bool bootloader_reboot_ok was rejected + ansible.builtin.assert: + that: + - >- + __invalid_input_reboot_ok_type_failed + | default(false) + fail_msg: >- + argument_specs should reject + bootloader_reboot_ok with type string + + # --- Test: non-bool bootloader_gather_facts --- + - name: Argument specs reject non-bool bootloader_gather_facts + block: + - name: Run role with non-bool bootloader_gather_facts + ansible.builtin.include_role: + name: linux-system-roles.bootloader + vars: + bootloader_gather_facts: "not_a_bool" + rescue: + - name: Mark non-bool bootloader_gather_facts rejected + ansible.builtin.set_fact: + __invalid_input_gather_facts_type_failed: true + + - name: Assert non-bool bootloader_gather_facts was rejected + ansible.builtin.assert: + that: + - >- + __invalid_input_gather_facts_type_failed + | default(false) + fail_msg: >- + argument_specs should reject + bootloader_gather_facts with type string + + # --- Test: non-bool bootloader_secure_logging --- + - name: Argument specs reject non-bool bootloader_secure_logging + block: + - name: Run role with non-bool bootloader_secure_logging + ansible.builtin.include_role: + name: linux-system-roles.bootloader + vars: + bootloader_secure_logging: "not_a_bool" + rescue: + - name: Mark non-bool bootloader_secure_logging rejected + ansible.builtin.set_fact: + __invalid_input_secure_logging_type_failed: true + + - name: Assert non-bool bootloader_secure_logging was rejected + ansible.builtin.assert: + that: + - >- + __invalid_input_secure_logging_type_failed + | default(false) + fail_msg: >- + argument_specs should reject + bootloader_secure_logging with type string + + # ==================================================== + # Section 3: assert_role_vars validation (all versions) + # ==================================================== + + # --- Test: bootloader_timeout as string --- + - name: Assert rejects bootloader_timeout as string + block: + - name: Run role with bootloader_timeout as string + ansible.builtin.include_role: + name: linux-system-roles.bootloader + vars: + bootloader_timeout: "not_an_int" + rescue: + - name: Mark bootloader_timeout string rejected + ansible.builtin.set_fact: + __invalid_input_timeout_string_failed: true + + - name: Assert bootloader_timeout as string was rejected + ansible.builtin.assert: + that: + - >- + __invalid_input_timeout_string_failed + | default(false) + fail_msg: >- + assert_role_vars should reject bootloader_timeout + when given a non-numeric string value + + # --- Test: bootloader_timeout as boolean --- + - name: Assert rejects bootloader_timeout as boolean + block: + - name: Run role with bootloader_timeout as boolean + ansible.builtin.include_role: + name: linux-system-roles.bootloader + vars: + bootloader_timeout: true + rescue: + - name: Mark bootloader_timeout boolean rejected + ansible.builtin.set_fact: + __invalid_input_timeout_bool_failed: true + + - name: Assert bootloader_timeout as boolean was rejected + ansible.builtin.assert: + that: + - >- + __invalid_input_timeout_bool_failed + | default(false) + fail_msg: >- + assert_role_vars should reject bootloader_timeout + when given a boolean value + + # --- Test: bootloader_password as integer --- + - name: Assert rejects bootloader_password as integer + block: + - name: Run role with bootloader_password as integer + ansible.builtin.include_role: + name: linux-system-roles.bootloader + vars: + bootloader_password: 12345 + rescue: + - name: Mark bootloader_password integer rejected + ansible.builtin.set_fact: + __invalid_input_password_int_failed: true + + - name: Assert bootloader_password as integer was rejected + ansible.builtin.assert: + that: + - >- + __invalid_input_password_int_failed + | default(false) + fail_msg: >- + assert_role_vars should reject bootloader_password + when given an integer value + + # --- Test: bootloader_password as boolean --- + - name: Assert rejects bootloader_password as boolean + block: + - name: Run role with bootloader_password as boolean + ansible.builtin.include_role: + name: linux-system-roles.bootloader + vars: + bootloader_password: true + rescue: + - name: Mark bootloader_password boolean rejected + ansible.builtin.set_fact: + __invalid_input_password_bool_failed: true + + - name: Assert bootloader_password as boolean was rejected + ansible.builtin.assert: + that: + - >- + __invalid_input_password_bool_failed + | default(false) + fail_msg: >- + assert_role_vars should reject bootloader_password + when given a boolean value + + # --- Test: kernel as integer --- + - name: Assert rejects kernel as integer + block: + - name: Run role with kernel as integer + ansible.builtin.include_role: + name: linux-system-roles.bootloader + vars: + bootloader_settings: + - kernel: 123 + rescue: + - name: Mark kernel integer rejected + ansible.builtin.set_fact: + __invalid_input_kernel_int_failed: true + + - name: Assert kernel as integer was rejected + ansible.builtin.assert: + that: + - >- + __invalid_input_kernel_int_failed + | default(false) + fail_msg: >- + assert_role_vars should reject kernel when given + an integer value + + # --- Test: value as list --- + - name: Assert rejects value as list + block: + - name: Run role with value as list + ansible.builtin.include_role: + name: linux-system-roles.bootloader + vars: + bootloader_settings: + - kernel: DEFAULT + options: + - name: quiet + value: + - item1 + - item2 + rescue: + - name: Mark value list rejected + ansible.builtin.set_fact: + __invalid_input_value_list_failed: true + + - name: Assert value as list was rejected + ansible.builtin.assert: + that: + - >- + __invalid_input_value_list_failed + | default(false) + fail_msg: >- + assert_role_vars should reject options value when + given a list value + + always: + - name: Clear test facts + ansible.builtin.set_fact: + __invalid_input_settings_type_failed: + __invalid_input_missing_kernel_failed: + __invalid_input_default_subopt_type_failed: + __invalid_input_copy_default_type_failed: + __invalid_input_state_choice_failed: + __invalid_input_option_state_choice_failed: + __invalid_input_previous_choice_failed: + __invalid_input_remove_password_type_failed: + __invalid_input_reboot_ok_type_failed: + __invalid_input_gather_facts_type_failed: + __invalid_input_secure_logging_type_failed: + __invalid_input_timeout_string_failed: + __invalid_input_timeout_bool_failed: + __invalid_input_password_int_failed: + __invalid_input_password_bool_failed: + __invalid_input_kernel_int_failed: + __invalid_input_value_list_failed: + tags: tests::cleanup From 40448611d4ef9f988ad584d595b08cc738eb4f55 Mon Sep 17 00:00:00 2001 From: Donat Szabo Date: Mon, 31 Aug 2026 17:27:11 +0200 Subject: [PATCH 2/2] Reversed accidental change to testing plan --- plans/test_playbooks_parallel.fmf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plans/test_playbooks_parallel.fmf b/plans/test_playbooks_parallel.fmf index c5bc3a12..0d4c8c30 100644 --- a/plans/test_playbooks_parallel.fmf +++ b/plans/test_playbooks_parallel.fmf @@ -7,8 +7,8 @@ provision: role: control_node - name: managed-node1 role: managed_node -# - name: managed-node2 -# role: managed_node + - name: managed-node2 + role: managed_node environment: # ensure versions are strings! SR_ANSIBLE_VER: "2.17"