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/modules/pacemaker_stonith.py b/plugins/modules/pacemaker_stonith.py new file mode 100644 index 0000000000..71ed21e7c1 --- /dev/null +++ b/plugins/modules/pacemaker_stonith.py @@ -0,0 +1,218 @@ +#!/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: 10.6.0 +description: + - This module can manage stonith in a Pacemaker cluster using the pacemaker CLI. +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_option: + description: + - Specify the stonith option to create. + type: list + elements: str + default: [] + stonith_operation: + 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_option: + description: + - Operation option to associate with action. + type: list + elements: str + stonith_meta: + description: + - List of meta 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_option: + 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 pacemaker stonith + hosts: localhost + gather_facts: false + tasks: + - name: Create virtual-ip stonith + community.general.pacemaker_stonith: + state: present + name: virtual-stonith + stonith_type: fence_virt + stonith_option: + - "pcmk_host_list=f1" + stonith_operation: + - operation_action: monitor + operation_option: + - "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_option=dict(type='list', elements='str', default=list()), + stonith_operation=dict(type='list', elements='dict', default=list(), options=dict( + operation_action=dict(type='str'), + operation_option=dict(type='list', elements='str'), + )), + stonith_meta=dict(type='list', elements='str'), + stonith_argument=dict(type='dict', options=dict( + argument_action=dict(type='str', choices=['before', 'after', 'group']), + argument_option=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_option'])], + supports_check_mode=True + ) + + use_old_vardict = False + default_state = "present" + + def __init_module__(self): + self.runner = pacemaker_runner(self.module, cli_action='stonith') + self.vars.set('previous_value', self._get()) + self.vars.set('value', self.vars.previous_value, change=True, diff=True) + self.vars.set('resource_type', self.vars.stonith_type) + self.vars.set('resource_option', self.vars.stonith_option) + self.vars.set('resource_operation', self.vars.stonith_operation) + self.vars.set('resource_meta', self.vars.stonith_meta) + self.vars.set('resource_argument', self.vars.stonith_argument) + + 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('state name', output_process=self._process_command_output(False)) as ctx: + return ctx.run(state='status') + + def state_absent(self): + with self.runner('state name', output_process=self._process_command_output(True, "does not exist"), check_mode_skip=True) as ctx: + ctx.run() + self.vars.set('value', self._get()) + self.vars.stdout = ctx.results_out + self.vars.stderr = ctx.results_err + self.vars.cmd = ctx.cmd + + def state_present(self): + with self.runner( + 'state name resource_type resource_option resource_operation resource_meta resource_argument wait', + output_process=self._process_command_output(True, "already exists"), + check_mode_skip=True) as ctx: + ctx.run() + self.vars.set('value', self._get()) + self.vars.stdout = ctx.results_out + self.vars.stderr = ctx.results_err + self.vars.cmd = ctx.cmd + + def state_enabled(self): + with self.runner('state name', output_process=self._process_command_output(True, "Starting"), check_mode_skip=True) as ctx: + ctx.run() + self.vars.set('value', self._get()) + self.vars.stdout = ctx.results_out + self.vars.stderr = ctx.results_err + self.vars.cmd = ctx.cmd + + def state_disabled(self): + with self.runner('state name', output_process=self._process_command_output(True, "Stopped"), check_mode_skip=True) as ctx: + ctx.run() + self.vars.set('value', self._get()) + self.vars.stdout = ctx.results_out + self.vars.stderr = ctx.results_err + self.vars.cmd = ctx.cmd + + +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..6c3a85f764 --- /dev/null +++ b/tests/unit/plugins/modules/test_pacemaker_stonith.yaml @@ -0,0 +1,206 @@ +# -*- 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_option: + - "pcmk_host_list=f1" + stonith_operation: + - operation_action: monitor + operation_option: + - "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_option: + - "pcmk_host_list=f1" + stonith_operation: + - operation_action: monitor + operation_option: + - "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" + - 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" + - 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: ""