diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index fac3fae8f8..1817984697 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -1060,6 +1060,8 @@ files: maintainers: matbu munchtoast $modules/pacemaker_resource.py: maintainers: munchtoast + $modules/pacemaker_stonith.py: + maintainers: munchtoast $modules/packet_: maintainers: nurfet-becirevic t0mk $modules/packet_device.py: diff --git a/plugins/module_utils/pacemaker.py b/plugins/module_utils/pacemaker.py index f0f54cce9d..443b120242 100644 --- a/plugins/module_utils/pacemaker.py +++ b/plugins/module_utils/pacemaker.py @@ -64,6 +64,7 @@ def pacemaker_runner(module, **kwargs): resource_meta=cmd_runner_fmt.stack(cmd_runner_fmt.as_opt_val)("meta"), resource_argument=cmd_runner_fmt.as_func(fmt_resource_argument), apply_all=cmd_runner_fmt.as_bool("--all"), + agent_validation=cmd_runner_fmt.as_bool("--agent-validation"), wait=cmd_runner_fmt.as_opt_eq_val("--wait"), config=cmd_runner_fmt.as_fixed("config"), force=cmd_runner_fmt.as_bool("--force"), diff --git a/plugins/modules/pacemaker_stonith.py b/plugins/modules/pacemaker_stonith.py new file mode 100644 index 0000000000..bc1bbb8fc4 --- /dev/null +++ b/plugins/modules/pacemaker_stonith.py @@ -0,0 +1,213 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2025, Dexter Le +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +module: pacemaker_stonith +short_description: Manage Pacemaker STONITH +author: + - Dexter Le (@munchtoast) +version_added: 11.2.0 +description: + - This module manages STONITH in a Pacemaker cluster using the Pacemaker CLI. +seealso: + - name: Pacemaker STONITH documentation + description: Complete documentation for Pacemaker STONITH. + link: https://clusterlabs.org/projects/pacemaker/doc/3.0/Pacemaker_Explained/html/resources.html#stonith +extends_documentation_fragment: + - community.general.attributes +attributes: + check_mode: + support: full + diff_mode: + support: none +options: + state: + description: + - Indicate desired state for cluster STONITH. + choices: [present, absent, enabled, disabled] + default: present + type: str + name: + description: + - Specify the STONITH name to create. + required: true + type: str + stonith_type: + description: + - Specify the STONITH device type. + type: str + stonith_options: + description: + - Specify the STONITH option to create. + type: list + elements: str + default: [] + stonith_operations: + description: + - List of operations to associate with STONITH. + type: list + elements: dict + default: [] + suboptions: + operation_action: + description: + - Operation action to associate with STONITH. + type: str + operation_options: + description: + - Operation options to associate with action. + type: list + elements: str + stonith_metas: + description: + - List of metadata to associate with STONITH. + type: list + elements: str + stonith_argument: + description: + - Action to associate with STONITH. + type: dict + suboptions: + argument_action: + description: + - Action to apply to STONITH. + type: str + choices: [group, before, after] + argument_options: + description: + - Options to associate with STONITH action. + type: list + elements: str + agent_validation: + description: + - Enabled agent validation for STONITH creation. + type: bool + default: false + wait: + description: + - Timeout period for polling the STONITH creation. + type: int + default: 300 +''' + +EXAMPLES = ''' +- name: Create virtual-ip STONITH + community.general.pacemaker_stonith: + state: present + name: virtual-stonith + stonith_type: fence_virt + stonith_options: + - "pcmk_host_list=f1" + stonith_operations: + - operation_action: monitor + operation_options: + - "interval=30s" +''' + +RETURN = ''' +cluster_stonith: + description: The cluster STONITH output message. + type: str + sample: "" + returned: always +''' + +from ansible_collections.community.general.plugins.module_utils.module_helper import StateModuleHelper +from ansible_collections.community.general.plugins.module_utils.pacemaker import pacemaker_runner + + +class PacemakerStonith(StateModuleHelper): + module = dict( + argument_spec=dict( + state=dict(type='str', default='present', choices=['present', 'absent', 'enabled', 'disabled']), + name=dict(type='str', required=True), + stonith_type=dict(type='str'), + stonith_options=dict(type='list', elements='str', default=[]), + stonith_operations=dict(type='list', elements='dict', default=[], options=dict( + operation_action=dict(type='str'), + operation_options=dict(type='list', elements='str'), + )), + stonith_metas=dict(type='list', elements='str'), + stonith_argument=dict(type='dict', options=dict( + argument_action=dict(type='str', choices=['before', 'after', 'group']), + argument_options=dict(type='list', elements='str'), + )), + agent_validation=dict(type='bool', default=False), + wait=dict(type='int', default=300), + ), + required_if=[('state', 'present', ['stonith_type', 'stonith_options'])], + supports_check_mode=True + ) + + def __init_module__(self): + self.runner = pacemaker_runner(self.module) + self.vars.set('previous_value', self._get()['out']) + self.vars.set('value', self.vars.previous_value, change=True, diff=True) + + def __quit_module__(self): + self.vars.set('value', self._get()['out']) + + def _process_command_output(self, fail_on_err, ignore_err_msg=""): + def process(rc, out, err): + if fail_on_err and rc != 0 and err and ignore_err_msg not in err: + self.do_raise('pcs failed with error (rc={0}): {1}'.format(rc, err)) + out = out.rstrip() + return None if out == "" else out + return process + + def _get(self): + with self.runner('cli_action state name') as ctx: + result = ctx.run(cli_action='stonith', state='status') + return dict([('rc', result[0]), + ('out', result[1] if result[1] != "" else None), + ('err', result[2])]) + + def fmt_stonith_resource(self): + return dict([("resource_name", self.vars.stonith_type)]) + + # TODO: Pluralize operation_options in separate PR and remove this helper fmt function + def fmt_stonith_operations(self): + modified_stonith_operations = [] + for stonith_operation in self.vars.stonith_operations: + modified_stonith_operations.append(dict([("operation_action", stonith_operation.get('operation_action')), + ("operation_option", stonith_operation.get('operation_options'))])) + return modified_stonith_operations + + def state_absent(self): + with self.runner('cli_action state name', output_process=self._process_command_output(True, "does not exist"), check_mode_skip=True) as ctx: + ctx.run(cli_action='stonith') + + def state_present(self): + with self.runner( + 'cli_action state name resource_type resource_option resource_operation resource_meta resource_argument agent_validation wait', + output_process=self._process_command_output(True, "already exists"), + check_mode_skip=True) as ctx: + ctx.run(cli_action='stonith', + resource_type=self.fmt_stonith_resource(), + resource_option=self.vars.stonith_options, + resource_operation=self.fmt_stonith_operations(), + resource_meta=self.vars.stonith_metas, + resource_argument=self.vars.stonith_argument) + + def state_enabled(self): + with self.runner('cli_action state name', output_process=self._process_command_output(True, "Starting"), check_mode_skip=True) as ctx: + ctx.run(cli_action='stonith') + + def state_disabled(self): + with self.runner('cli_action state name', output_process=self._process_command_output(True, "Stopped"), check_mode_skip=True) as ctx: + ctx.run(cli_action='stonith') + + +def main(): + PacemakerStonith.execute() + + +if __name__ == '__main__': + main() diff --git a/tests/unit/plugins/modules/test_pacemaker_stonith.py b/tests/unit/plugins/modules/test_pacemaker_stonith.py new file mode 100644 index 0000000000..c666918cf4 --- /dev/null +++ b/tests/unit/plugins/modules/test_pacemaker_stonith.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Author: Dexter Le (dextersydney2001@gmail.com) +# Largely adapted from test_redhat_subscription by +# Jiri Hnidek (jhnidek@redhat.com) +# +# Copyright (c) Dexter Le (dextersydney2001@gmail.com) +# Copyright (c) Jiri Hnidek (jhnidek@redhat.com) +# +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +from ansible_collections.community.general.plugins.modules import pacemaker_stonith +from .uthelper import UTHelper, RunCommandMock + +UTHelper.from_module(pacemaker_stonith, __name__, mocks=[RunCommandMock]) diff --git a/tests/unit/plugins/modules/test_pacemaker_stonith.yaml b/tests/unit/plugins/modules/test_pacemaker_stonith.yaml new file mode 100644 index 0000000000..5e1b03f85e --- /dev/null +++ b/tests/unit/plugins/modules/test_pacemaker_stonith.yaml @@ -0,0 +1,216 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Dexter Le (dextersydney2001@gmail.com) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +--- +anchors: + environ: &env-def {environ_update: {LANGUAGE: C, LC_ALL: C}, check_rc: false} +test_cases: + - id: test_missing_input + input: {} + output: + failed: true + msg: "missing required arguments: name" + - id: test_present_minimal_input_stonith_not_exists + input: + state: present + name: virtual-stonith + stonith_type: fence_virt + stonith_options: + - "pcmk_host_list=f1" + stonith_operations: + - operation_action: monitor + operation_options: + - "interval=30s" + output: + changed: true + previous_value: null + value: " * virtual-stonith\t(stonith:fence_virt):\t Started" + mocks: + run_command: + - command: ["/testbin/pcs", stonith, status, virtual-stonith] + environ: *env-def + rc: 1 + out: "" + err: "" + - command: ["/testbin/pcs", stonith, create, virtual-stonith, fence_virt, "pcmk_host_list=f1", "op", "monitor", "interval=30s", "--wait=300"] + environ: *env-def + rc: 0 + out: "" + err: "" + - command: ["/testbin/pcs", stonith, status, virtual-stonith] + environ: *env-def + rc: 0 + out: " * virtual-stonith\t(stonith:fence_virt):\t Started" + err: "" + - id: test_present_minimal_input_stonith_exists + input: + state: present + name: virtual-stonith + stonith_type: fence_virt + stonith_options: + - "pcmk_host_list=f1" + stonith_operations: + - operation_action: monitor + operation_options: + - "interval=30s" + output: + changed: false + previous_value: " * virtual-stonith\t(stonith:fence_virt):\t Started" + value: " * virtual-stonith\t(stonith:fence_virt):\t Started" + mocks: + run_command: + - command: ["/testbin/pcs", stonith, status, virtual-stonith] + environ: *env-def + rc: 0 + out: " * virtual-stonith\t(stonith:fence_virt):\t Started" + err: "" + - command: ["/testbin/pcs", stonith, create, virtual-stonith, fence_virt, "pcmk_host_list=f1", "op", "monitor", "interval=30s", "--wait=300"] + environ: *env-def + rc: 0 + out: "" + err: "Error: 'virtual-stonith' already exists.\n" + - command: ["/testbin/pcs", stonith, status, virtual-stonith] + environ: *env-def + rc: 0 + out: " * virtual-stonith\t(stonith:fence_virt):\t Started" + err: "" + - id: test_absent_minimal_input_stonith_not_exists + input: + state: absent + name: virtual-stonith + output: + changed: false + previous_value: null + value: null + mocks: + run_command: + - command: ["/testbin/pcs", stonith, status, virtual-stonith] + environ: *env-def + rc: 1 + out: "" + err: "" + - command: ["/testbin/pcs", stonith, remove, virtual-stonith] + environ: *env-def + rc: 1 + out: "" + err: "Error: Resource 'virtual-stonith' does not exist.\n" + - command: ["/testbin/pcs", stonith, status, virtual-stonith] + environ: *env-def + rc: 1 + out: "" + err: "" + - id: test_absent_minimal_input_stonith_exists + input: + state: absent + name: virtual-stonith + output: + changed: true + previous_value: " * virtual-stonith\t(stonith:fence_virt):\t Started" + value: null + mocks: + run_command: + - command: ["/testbin/pcs", stonith, status, virtual-stonith] + environ: *env-def + rc: 0 + out: " * virtual-stonith\t(stonith:fence_virt):\t Started" + err: "" + - command: ["/testbin/pcs", stonith, remove, virtual-stonith] + environ: *env-def + rc: 0 + out: "" + err: "Attempting to stop: virtual-ip... Stopped\n" + - command: ["/testbin/pcs", stonith, status, virtual-stonith] + environ: *env-def + rc: 1 + out: "" + err: "" + - id: test_enabled_minimal_input_stonith_not_exists + input: + state: enabled + name: virtual-stonith + output: + failed: true + msg: "pcs failed with error (rc=1): Error: Resource 'virtual-stonith' does not exist." + mocks: + run_command: + - command: ["/testbin/pcs", stonith, status, virtual-stonith] + environ: *env-def + rc: 1 + out: "" + err: "" + - command: ["/testbin/pcs", stonith, enable, virtual-stonith] + environ: *env-def + rc: 1 + out: "" + err: "Error: Resource 'virtual-stonith' does not exist." + - id: test_enabled_minimal_input_stonith_exists + input: + state: enabled + name: virtual-stonith + output: + changed: true + previous_value: " * virtual-stonith\t(stonith:fence_virt):\t Stopped (disabled)" + value: " * virtual-stonith\t(stonith:fence_virt):\t Starting" + mocks: + run_command: + - command: ["/testbin/pcs", stonith, status, virtual-stonith] + environ: *env-def + rc: 0 + out: " * virtual-stonith\t(stonith:fence_virt):\t Stopped (disabled)" + err: "" + - command: ["/testbin/pcs", stonith, enable, virtual-stonith] + environ: *env-def + rc: 0 + out: "" + err: "" + - command: ["/testbin/pcs", stonith, status, virtual-stonith] + environ: *env-def + rc: 0 + out: " * virtual-stonith\t(stonith:fence_virt):\t Starting" + err: "" + - id: test_disable_minimal_input_stonith_not_exists + input: + state: disabled + name: virtual-stonith + output: + failed: true + msg: "pcs failed with error (rc=1): Error: Resource 'virtual-stonith' does not exist." + mocks: + run_command: + - command: ["/testbin/pcs", stonith, status, virtual-stonith] + environ: *env-def + rc: 1 + out: "" + err: "" + - command: ["/testbin/pcs", stonith, disable, virtual-stonith] + environ: *env-def + rc: 1 + out: "" + err: "Error: Resource 'virtual-stonith' does not exist." + - id: test_disable_minimal_input_stonith_exists + input: + state: disabled + name: virtual-stonith + output: + changed: true + previous_value: " * virtual-stonith\t(stonith:fence_virt):\t Started" + value: " * virtual-stonith\t(stonith:fence_virt):\t Stopped (disabled)" + mocks: + run_command: + - command: ["/testbin/pcs", stonith, status, virtual-stonith] + environ: *env-def + rc: 0 + out: " * virtual-stonith\t(stonith:fence_virt):\t Started" + err: "" + - command: ["/testbin/pcs", stonith, disable, virtual-stonith] + environ: *env-def + rc: 0 + out: "" + err: "" + - command: ["/testbin/pcs", stonith, status, virtual-stonith] + environ: *env-def + rc: 0 + out: " * virtual-stonith\t(stonith:fence_virt):\t Stopped (disabled)" + err: ""