diff options
Diffstat (limited to 'ansible_collections/community/rabbitmq/plugins/modules')
14 files changed, 3121 insertions, 0 deletions
diff --git a/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_binding.py b/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_binding.py new file mode 100644 index 000000000..6826c3af3 --- /dev/null +++ b/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_binding.py @@ -0,0 +1,298 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2015, Manuel Sousa <manuel.sousa@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: rabbitmq_binding +author: Manuel Sousa (@manuel-sousa) + +short_description: Manage rabbitMQ bindings +description: + - This module uses rabbitMQ REST APIs to create / delete bindings. +requirements: [ "requests >= 1.0.0" ] +options: + state: + description: + - Whether the bindings should be present or absent. + type: str + choices: [ "present", "absent" ] + default: present + name: + description: + - source exchange to create binding on. + type: str + required: true + aliases: [ "src", "source" ] + destination: + description: + - destination exchange or queue for the binding. + type: str + required: true + aliases: [ "dst", "dest" ] + destination_type: + description: + - Either queue or exchange. + type: str + required: true + choices: [ "queue", "exchange" ] + aliases: [ "type", "dest_type" ] + routing_key: + description: + - routing key for the binding. + type: str + default: "#" + arguments: + description: + - extra arguments for exchange. If defined this argument is a key/value dictionary + type: dict + required: false + default: {} +extends_documentation_fragment: + - community.rabbitmq.rabbitmq + +''' + +EXAMPLES = r''' +- name: Bind myQueue to directExchange with routing key info + community.rabbitmq.rabbitmq_binding: + name: directExchange + destination: myQueue + type: queue + routing_key: info + +- name: Bind directExchange to topicExchange with routing key *.info + community.rabbitmq.rabbitmq_binding: + name: topicExchange + destination: topicExchange + type: exchange + routing_key: '*.info' +''' + +import json +import traceback + +REQUESTS_IMP_ERR = None +try: + import requests + HAS_REQUESTS = True +except ImportError: + REQUESTS_IMP_ERR = traceback.format_exc() + HAS_REQUESTS = False + +from ansible.module_utils.six.moves.urllib import parse as urllib_parse +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible_collections.community.rabbitmq.plugins.module_utils.rabbitmq import rabbitmq_argument_spec + + +class RabbitMqBinding(object): + def __init__(self, module): + """ + :param module: + """ + self.module = module + self.name = self.module.params['name'] + self.login_user = self.module.params['login_user'] + self.login_password = self.module.params['login_password'] + self.login_host = self.module.params['login_host'] + self.login_port = self.module.params['login_port'] + self.login_protocol = self.module.params['login_protocol'] + self.vhost = self.module.params['vhost'] + self.destination = self.module.params['destination'] + self.destination_type = 'q' if self.module.params['destination_type'] == 'queue' else 'e' + self.routing_key = self.module.params['routing_key'] + self.arguments = self.module.params['arguments'] + self.verify = self.module.params['ca_cert'] + self.cert = self.module.params['client_cert'] + self.key = self.module.params['client_key'] + self.props = urllib_parse.quote(self.routing_key) if self.routing_key != '' else '~' + self.base_url = '{0}://{1}:{2}/api/bindings'.format(self.login_protocol, + self.login_host, + self.login_port) + self.url = '{0}/{1}/e/{2}/{3}/{4}/{5}'.format(self.base_url, + urllib_parse.quote(self.vhost, safe=''), + urllib_parse.quote(self.name, safe=''), + self.destination_type, + urllib_parse.quote(self.destination, safe=''), + self.props) + self.result = { + 'changed': False, + 'name': self.module.params['name'], + } + self.authentication = ( + self.login_user, + self.login_password + ) + self.request = requests + self.http_check_states = { + 200: True, + 404: False, + } + self.http_actionable_states = { + 201: True, + 204: True, + } + self.api_result = self.request.get(self.url, auth=self.authentication, verify=self.verify, cert=(self.cert, self.key)) + + def run(self): + """ + :return: + """ + self.check_presence() + self.check_mode() + self.action_mode() + + def check_presence(self): + """ + :return: + """ + if self.check_should_throw_fail(): + self.fail() + + def change_required(self): + """ + :return: + """ + if self.module.params['state'] == 'present': + if not self.is_present(): + return True + elif self.module.params['state'] == 'absent': + if self.is_present(): + return True + return False + + def is_present(self): + """ + :return: + """ + return self.http_check_states.get(self.api_result.status_code, False) + + def check_mode(self): + """ + :return: + """ + if self.module.check_mode: + result = self.result + result['changed'] = self.change_required() + result['details'] = self.api_result.json() if self.is_present() else self.api_result.text + result['arguments'] = self.module.params['arguments'] + self.module.exit_json(**result) + + def check_reply_is_correct(self): + """ + :return: + """ + if self.api_result.status_code in self.http_check_states: + return True + return False + + def check_should_throw_fail(self): + """ + :return: + """ + if not self.is_present(): + if not self.check_reply_is_correct(): + return True + return False + + def action_mode(self): + """ + :return: + """ + result = self.result + if self.change_required(): + if self.module.params['state'] == 'present': + self.create() + if self.module.params['state'] == 'absent': + self.remove() + if self.action_should_throw_fail(): + self.fail() + result['changed'] = True + result['destination'] = self.module.params['destination'] + self.module.exit_json(**result) + else: + result['changed'] = False + self.module.exit_json(**result) + + def action_reply_is_correct(self): + """ + :return: + """ + if self.api_result.status_code in self.http_actionable_states: + return True + return False + + def action_should_throw_fail(self): + """ + :return: + """ + if not self.action_reply_is_correct(): + return True + return False + + def create(self): + """ + :return: + """ + self.url = '{0}/{1}/e/{2}/{3}/{4}'.format(self.base_url, + urllib_parse.quote(self.vhost, safe=''), + urllib_parse.quote(self.name, safe=''), + self.destination_type, + urllib_parse.quote(self.destination, safe='')) + self.api_result = self.request.post(self.url, + auth=self.authentication, + verify=self.verify, + cert=(self.cert, self.key), + headers={"content-type": "application/json"}, + data=json.dumps({ + 'routing_key': self.routing_key, + 'arguments': self.arguments + })) + + def remove(self): + """ + :return: + """ + self.api_result = self.request.delete(self.url, auth=self.authentication, verify=self.verify, cert=(self.cert, self.key)) + + def fail(self): + """ + :return: + """ + self.module.fail_json( + msg="Unexpected reply from API", + status=self.api_result.status_code, + details=self.api_result.text + ) + + +def main(): + + argument_spec = rabbitmq_argument_spec() + argument_spec.update( + dict( + state=dict(default='present', choices=['present', 'absent'], type='str'), + name=dict(required=True, aliases=["src", "source"], type='str'), + destination=dict(required=True, aliases=["dst", "dest"], type='str'), + destination_type=dict(required=True, aliases=["type", "dest_type"], choices=["queue", "exchange"], + type='str'), + routing_key=dict(default='#', type='str', no_log=False), + arguments=dict(default=dict(), type='dict') + ) + ) + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + if not HAS_REQUESTS: + module.fail_json(msg=missing_required_lib("requests"), exception=REQUESTS_IMP_ERR) + + RabbitMqBinding(module).run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_exchange.py b/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_exchange.py new file mode 100644 index 000000000..41f3f742c --- /dev/null +++ b/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_exchange.py @@ -0,0 +1,234 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2015, Manuel Sousa <manuel.sousa@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: rabbitmq_exchange +author: Manuel Sousa (@manuel-sousa) + +short_description: Manage rabbitMQ exchange +description: + - This module uses rabbitMQ Rest API to create/delete exchanges +requirements: [ "requests >= 1.0.0" ] +options: + name: + description: + - Name of the exchange to create. + type: str + required: true + state: + description: + - Whether the exchange should be present or absent. + type: str + choices: [ "present", "absent" ] + required: false + default: present + durable: + description: + - Whether exchange is durable or not. + type: bool + required: false + default: true + exchange_type: + description: + - Type for the exchange. + - If using I(x-delayed-message), I(x-random), I(x-consistent-hash) or I(x-recent-history) the respective plugin on + - the RabbitMQ server must be enabled. + type: str + required: false + choices: [ "fanout", "direct", "headers", "topic", "x-delayed-message", "x-random", "x-consistent-hash", "x-recent-history" ] + aliases: [ "type" ] + default: direct + auto_delete: + description: + - If the exchange should delete itself after all queues/exchanges unbound from it. + type: bool + required: false + default: false + internal: + description: + - Exchange is available only for other exchanges. + type: bool + required: false + default: false + arguments: + description: + - Extra arguments for exchange. If defined this argument is a key/value dictionary. + type: dict + required: false + default: {} +extends_documentation_fragment: + - community.rabbitmq.rabbitmq + +''' + +EXAMPLES = r''' +- name: Create direct exchange + community.rabbitmq.rabbitmq_exchange: + name: directExchange + +- name: Create topic exchange on vhost + community.rabbitmq.rabbitmq_exchange: + name: topicExchange + type: topic + vhost: myVhost +''' + +import json +import traceback + +REQUESTS_IMP_ERR = None +try: + import requests + HAS_REQUESTS = True +except ImportError: + REQUESTS_IMP_ERR = traceback.format_exc() + HAS_REQUESTS = False + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils.six.moves.urllib import parse as urllib_parse +from ansible_collections.community.rabbitmq.plugins.module_utils.rabbitmq import rabbitmq_argument_spec + + +def main(): + + argument_spec = rabbitmq_argument_spec() + argument_spec.update( + dict( + state=dict(default='present', choices=['present', 'absent'], type='str'), + name=dict(required=True, type='str'), + durable=dict(default=True, type='bool'), + auto_delete=dict(default=False, type='bool'), + internal=dict(default=False, type='bool'), + exchange_type=dict(default='direct', aliases=['type'], + choices=['fanout', 'direct', 'headers', 'topic', 'x-delayed-message', + 'x-random', 'x-consistent-hash', 'x-recent-history'], + type='str'), + arguments=dict(default=dict(), type='dict') + ) + ) + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + url = "%s://%s:%s/api/exchanges/%s/%s" % ( + module.params['login_protocol'], + module.params['login_host'], + module.params['login_port'], + urllib_parse.quote(module.params['vhost'], ''), + urllib_parse.quote(module.params['name'], '') + ) + + if not HAS_REQUESTS: + module.fail_json(msg=missing_required_lib("requests"), exception=REQUESTS_IMP_ERR) + + # exchange plugin type to plugin name mapping + exchange_plugins = {'x-consistent-hash': 'rabbitmq_consistent_hash_exchange', + 'x-random': 'rabbitmq_random_exchange', + 'x-delayed-message': 'rabbitmq_delayed_message_exchange', + 'x-recent-history': 'rabbitmq_recent_history_exchange'} + result = dict(changed=False, name=module.params['name']) + + # Check if exchange already exists + r = requests.get(url, auth=(module.params['login_user'], module.params['login_password']), + verify=module.params['ca_cert'], cert=(module.params['client_cert'], module.params['client_key'])) + + if r.status_code == 200: + exchange_exists = True + response = r.json() + elif r.status_code == 404: + exchange_exists = False + response = r.text + else: + module.fail_json( + msg="Invalid response from RESTAPI when trying to check if exchange exists", + details=r.text + ) + + if module.params['state'] == 'present': + change_required = not exchange_exists + else: + change_required = exchange_exists + + # Check if attributes change on existing exchange + if not change_required and r.status_code == 200 and module.params['state'] == 'present': + if not ( + response['durable'] == module.params['durable'] and + response['auto_delete'] == module.params['auto_delete'] and + response['internal'] == module.params['internal'] and + response['type'] == module.params['exchange_type'] + ): + module.fail_json( + msg="RabbitMQ RESTAPI doesn't support attribute changes for existing exchanges" + ) + + # Exit if check_mode + if module.check_mode: + result['changed'] = change_required + result['details'] = response + result['arguments'] = module.params['arguments'] + module.exit_json(**result) + + # Do changes + if change_required: + if module.params['state'] == 'present': + r = requests.put( + url, + auth=(module.params['login_user'], module.params['login_password']), + headers={"content-type": "application/json"}, + data=json.dumps({ + "durable": module.params['durable'], + "auto_delete": module.params['auto_delete'], + "internal": module.params['internal'], + "type": module.params['exchange_type'], + "arguments": module.params['arguments'] + }), + verify=module.params['ca_cert'], + cert=(module.params['client_cert'], module.params['client_key']) + ) + elif module.params['state'] == 'absent': + r = requests.delete(url, auth=(module.params['login_user'], module.params['login_password']), + verify=module.params['ca_cert'], cert=(module.params['client_cert'], module.params['client_key'])) + + # RabbitMQ 3.6.7 changed this response code from 204 to 201 + if r.status_code == 204 or r.status_code == 201: + result['changed'] = True + module.exit_json(**result) + else: + rjson = r.json() + if (rjson['reason'].startswith('unknown exchange type')): + try: + module.fail_json( + msg=("Error creating exchange. You may need to enable the '%s' plugin for exchange type %s" % + (exchange_plugins[module.params['exchange_type']], module.params['exchange_type'])), + status=r.status_code, + details=r.text + ) + except KeyError: + module.fail_json( + msg=("Error creating exchange. You may need to enable a plugin for exchange type %s" % + module.params['exchange_type']), + status=r.status_code, + details=r.text + ) + else: + module.fail_json( + msg="Error creating exchange", + status=r.status_code, + details=r.text + ) + + else: + module.exit_json( + changed=False, + name=module.params['name'] + ) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_feature_flag.py b/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_feature_flag.py new file mode 100644 index 000000000..23db08b60 --- /dev/null +++ b/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_feature_flag.py @@ -0,0 +1,98 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Damian Dabrowski <damian@dabrowski.cloud> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: rabbitmq_feature_flag +short_description: Enables feature flag +description: + - Allows to enable specified feature flag. +author: "Damian Dabrowski (@damiandabrowski5)" +version_added: '1.1.0' +options: + name: + description: + - Feature flag name. + type: str + required: true + node: + description: + - Erlang node name of the target rabbit node. + type: str + default: rabbit +''' + +EXAMPLES = r''' +- name: Enable the 'maintenance_mode_status' feature flag on 'rabbit@node-1' + community.rabbitmq.rabbitmq_feature_flag: + name: maintenance_mode_status + node: rabbit@node-1 +''' + +from ansible.module_utils.basic import AnsibleModule + + +class RabbitMqFeatureFlag(object): + + def __init__(self, module, name, node): + self.module = module + self.name = name + self.node = node + self._rabbitmqctl = module.get_bin_path('rabbitmqctl', True) + self.state = self.get_flag_state() + + def _exec(self, args, force_exec_in_check_mode=False): + if not self.module.check_mode or (self.module.check_mode and force_exec_in_check_mode): + cmd = [self._rabbitmqctl, '-q', '-n', self.node] + rc, out, err = self.module.run_command(cmd + args, check_rc=True) + return out.splitlines() + return list() + + def get_flag_state(self): + global_parameters = self._exec(['list_feature_flags'], True) + + for param_item in global_parameters: + name, state = param_item.split('\t') + if name == self.name: + if state == 'enabled': + return 'enabled' + return 'disabled' + return 'unavailable' + + def enable(self): + self._exec(['enable_feature_flag', self.name]) + + +def main(): + arg_spec = dict( + name=dict(type='str', required=True), + node=dict(type='str', default='rabbit') + ) + module = AnsibleModule( + argument_spec=arg_spec, + supports_check_mode=True + ) + + name = module.params['name'] + node = module.params['node'] + + result = dict(changed=False) + rabbitmq_feature_flag = RabbitMqFeatureFlag(module, name, node) + + if rabbitmq_feature_flag.state == 'disabled': + rabbitmq_feature_flag.enable() + result['changed'] = True + if rabbitmq_feature_flag.state == 'unavailable': + module.fail_json(msg="%s feature flag is not available" % (name)) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_global_parameter.py b/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_global_parameter.py new file mode 100644 index 000000000..8cecce5fa --- /dev/null +++ b/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_global_parameter.py @@ -0,0 +1,158 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2013, Chatham Financial <oss@chathamfinancial.com> +# Copyright: (c) 2017, Juergen Kirschbaum <jk@jk-itc.de> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: rabbitmq_global_parameter +short_description: Manage RabbitMQ global parameters +description: + - Manage dynamic, cluster-wide global parameters for RabbitMQ +author: "Juergen Kirschbaum (@jgkirschbaum)" +options: + name: + description: + - Name of the global parameter being set + type: str + required: true + default: null + value: + description: + - Value of the global parameter, as a JSON term + type: str + required: false + default: null + node: + description: + - erlang node name of the rabbit we wish to configure + type: str + required: false + default: rabbit + state: + description: + - Specify if global parameter is to be added or removed + type: str + required: false + default: present + choices: [ 'present', 'absent'] +''' + +EXAMPLES = r''' +- name: Set the global parameter 'cluster_name' to a value of 'mq-cluster' (in quotes) + community.rabbitmq.rabbitmq_global_parameter: + name: cluster_name + value: "{{ 'mq-cluster' | to_json }}" + state: present +''' + +RETURN = r''' +name: + description: name of the global parameter being set + returned: success + type: str + sample: "cluster_name" +value: + description: value of the global parameter, as a JSON term + returned: changed + type: str + sample: "the-cluster-name" +''' + +import json +from ansible.module_utils.basic import AnsibleModule + + +class RabbitMqGlobalParameter(object): + def __init__(self, module, name, value, node): + self.module = module + self.name = name + self.value = value + self.node = node + + self._value = None + + self._rabbitmqctl = module.get_bin_path('rabbitmqctl', True) + + def _exec(self, args, force_exec_in_check_mode=False): + if not self.module.check_mode or (self.module.check_mode and force_exec_in_check_mode): + cmd = [self._rabbitmqctl, '-q', '-n', self.node] + rc, out, err = self.module.run_command(cmd + args, check_rc=True) + return out.splitlines() + return list() + + def get(self): + global_parameters = [param for param in self._exec(['list_global_parameters'], True) if param.strip()] + + for idx, param_item in enumerate(global_parameters): + name, value = param_item.split('\t') + # RabbitMQ 3.8.x and above return table header, ignore it + if idx == 0 and name == 'name' and value == 'value': + continue + + if name == self.name: + self._value = json.loads(value) + return True + return False + + def set(self): + self._exec(['set_global_parameter', + self.name, + json.dumps(self.value)]) + + def delete(self): + self._exec(['clear_global_parameter', self.name]) + + def has_modifications(self): + return self.value != self._value + + +def main(): + arg_spec = dict( + name=dict(type='str', required=True), + value=dict(type='str', default=None), + state=dict(default='present', choices=['present', 'absent']), + node=dict(type='str', default='rabbit') + ) + module = AnsibleModule( + argument_spec=arg_spec, + supports_check_mode=True + ) + + name = module.params['name'] + value = module.params['value'] + if isinstance(value, str): + value = json.loads(value) + state = module.params['state'] + node = module.params['node'] + + result = dict(changed=False) + rabbitmq_global_parameter = RabbitMqGlobalParameter(module, name, value, node) + + if rabbitmq_global_parameter.get(): + if state == 'absent': + rabbitmq_global_parameter.delete() + result['changed'] = True + else: + if rabbitmq_global_parameter.has_modifications(): + rabbitmq_global_parameter.set() + result['changed'] = True + elif state == 'present': + rabbitmq_global_parameter.set() + result['changed'] = True + + result['name'] = name + result['value'] = value + result['state'] = state + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_parameter.py b/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_parameter.py new file mode 100644 index 000000000..5ecbd4ec4 --- /dev/null +++ b/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_parameter.py @@ -0,0 +1,155 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2013, Chatham Financial <oss@chathamfinancial.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: rabbitmq_parameter +short_description: Manage RabbitMQ parameters +description: + - Manage dynamic, cluster-wide parameters for RabbitMQ +author: Chris Hoffman (@chrishoffman) +options: + component: + description: + - Name of the component of which the parameter is being set + type: str + required: true + name: + description: + - Name of the parameter being set + type: str + required: true + value: + description: + - Value of the parameter, as a JSON term + type: str + vhost: + description: + - vhost to apply access privileges. + type: str + default: / + node: + description: + - erlang node name of the rabbit we wish to configure + type: str + default: rabbit + state: + description: + - Specify if parameter is to be added or removed + type: str + default: present + choices: [ 'present', 'absent'] +''' + +EXAMPLES = r""" +- name: Set the federation parameter 'local_username' to a value of 'guest' (in quotes) + community.rabbitmq.rabbitmq_parameter: + component: federation + name: local-username + value: '"guest"' + state: present +""" +import json +from ansible.module_utils.basic import AnsibleModule + + +class RabbitMqParameter(object): + def __init__(self, module, component, name, value, vhost, node): + self.module = module + self.component = component + self.name = name + self.value = value + self.vhost = vhost + self.node = node + + self._value = None + + self._rabbitmqctl = module.get_bin_path('rabbitmqctl', True) + + def _exec(self, args, force_exec_in_check_mode=False): + if not self.module.check_mode or (self.module.check_mode and force_exec_in_check_mode): + cmd = [self._rabbitmqctl, '-q', '-n', self.node] + rc, out, err = self.module.run_command(cmd + args, check_rc=True) + return out.strip().splitlines() + return list() + + def get(self): + parameters = [param for param in self._exec(['list_parameters', '-p', self.vhost], True) if param.strip()] + + for param_item in parameters: + component, name, value = param_item.split('\t') + + if component == self.component and name == self.name: + self._value = json.loads(value) + return True + return False + + def set(self): + self._exec(['set_parameter', + '-p', + self.vhost, + self.component, + self.name, + json.dumps(self.value)]) + + def delete(self): + self._exec(['clear_parameter', '-p', self.vhost, self.component, self.name]) + + def has_modifications(self): + return self.value != self._value + + +def main(): + arg_spec = dict( + component=dict(required=True), + name=dict(required=True), + value=dict(default=None), + vhost=dict(default='/'), + state=dict(default='present', choices=['present', 'absent']), + node=dict(default='rabbit') + ) + module = AnsibleModule( + argument_spec=arg_spec, + supports_check_mode=True + ) + + component = module.params['component'] + name = module.params['name'] + value = module.params['value'] + if isinstance(value, str): + value = json.loads(value) + vhost = module.params['vhost'] + state = module.params['state'] + node = module.params['node'] + + result = dict(changed=False) + rabbitmq_parameter = RabbitMqParameter(module, component, name, value, vhost, node) + + if rabbitmq_parameter.get(): + if state == 'absent': + rabbitmq_parameter.delete() + result['changed'] = True + else: + if rabbitmq_parameter.has_modifications(): + rabbitmq_parameter.set() + result['changed'] = True + elif state == 'present': + rabbitmq_parameter.set() + result['changed'] = True + + result['component'] = component + result['name'] = name + result['vhost'] = vhost + result['state'] = state + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_plugin.py b/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_plugin.py new file mode 100644 index 000000000..86a5b6aeb --- /dev/null +++ b/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_plugin.py @@ -0,0 +1,187 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2013, Chatham Financial <oss@chathamfinancial.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: rabbitmq_plugin +short_description: Manage RabbitMQ plugins +description: + - This module can be used to enable or disable RabbitMQ plugins. +author: + - Chris Hoffman (@chrishoffman) +options: + names: + description: + - Comma-separated list of plugin names. Also, accepts plugin name. + type: str + required: true + aliases: [name] + new_only: + description: + - Only enable missing plugins. + - Does not disable plugins that are not in the names list. + type: bool + default: false + state: + description: + - Specify if plugins are to be enabled or disabled. + type: str + default: enabled + choices: [enabled, disabled] + broker_state: + description: + - Specify whether the broker should be online or offline for the plugin change. + type: str + default: online + choices: [online, offline] + prefix: + description: + - Specify a custom install prefix to a Rabbit. + type: str +''' + +EXAMPLES = r''' +- name: Enables the rabbitmq_management plugin + community.rabbitmq.rabbitmq_plugin: + names: rabbitmq_management + state: enabled + +- name: Enable multiple rabbitmq plugins + community.rabbitmq.rabbitmq_plugin: + names: rabbitmq_management,rabbitmq_management_visualiser + state: enabled + +- name: Disable plugin + community.rabbitmq.rabbitmq_plugin: + names: rabbitmq_management + state: disabled + +- name: Enable every plugin in list with existing plugins + community.rabbitmq.rabbitmq_plugin: + names: rabbitmq_management,rabbitmq_management_visualiser,rabbitmq_shovel,rabbitmq_shovel_management + state: enabled + new_only: true + +- name: Enables the rabbitmq_peer_discovery_aws plugin without requiring a broker connection. + community.rabbitmq.rabbitmq_plugin: + names: rabbitmq_peer_discovery_aws_plugin + state: enabled + broker_state: offline +''' + +RETURN = r''' +enabled: + description: list of plugins enabled during task run + returned: always + type: list + sample: ["rabbitmq_management"] +disabled: + description: list of plugins disabled during task run + returned: always + type: list + sample: ["rabbitmq_management"] +''' + +import os +from ansible.module_utils.basic import AnsibleModule + + +class RabbitMqPlugins(object): + + def __init__(self, module): + self.module = module + bin_path = '' + if module.params['prefix']: + if os.path.isdir(os.path.join(module.params['prefix'], 'bin')): + bin_path = os.path.join(module.params['prefix'], 'bin') + elif os.path.isdir(os.path.join(module.params['prefix'], 'sbin')): + bin_path = os.path.join(module.params['prefix'], 'sbin') + else: + # No such path exists. + module.fail_json(msg="No binary folder in prefix %s" % module.params['prefix']) + + self._rabbitmq_plugins = os.path.join(bin_path, "rabbitmq-plugins") + else: + self._rabbitmq_plugins = module.get_bin_path('rabbitmq-plugins', True) + + def _exec(self, args, force_exec_in_check_mode=False): + if not self.module.check_mode or (self.module.check_mode and force_exec_in_check_mode): + cmd = [self._rabbitmq_plugins] + rc, out, err = self.module.run_command(cmd + args, check_rc=True) + return out.splitlines() + return list() + + def get_all(self): + list_output = self._exec(['list', '-E', '-m'], True) + plugins = [] + for plugin in list_output: + if not plugin: + break + plugins.append(plugin) + + return plugins + + def enable(self, name): + self._exec(['enable', "--%s" % self.module.params['broker_state'], name]) + + def disable(self, name): + self._exec(['disable', "--%s" % self.module.params['broker_state'], name]) + + +def main(): + arg_spec = dict( + names=dict(required=True, aliases=['name']), + new_only=dict(default='no', type='bool'), + state=dict(default='enabled', choices=['enabled', 'disabled']), + broker_state=dict(default='online', choices=['online', 'offline']), + prefix=dict(required=False, default=None) + ) + module = AnsibleModule( + argument_spec=arg_spec, + supports_check_mode=True + ) + + result = dict() + names = module.params['names'].split(',') + new_only = module.params['new_only'] + state = module.params['state'] + + rabbitmq_plugins = RabbitMqPlugins(module) + enabled_plugins = rabbitmq_plugins.get_all() + + enabled = [] + disabled = [] + if state == 'enabled': + if not new_only: + for plugin in enabled_plugins: + if " " in plugin: + continue + if plugin not in names: + rabbitmq_plugins.disable(plugin) + disabled.append(plugin) + + for name in names: + if name not in enabled_plugins: + rabbitmq_plugins.enable(name) + enabled.append(name) + else: + for plugin in enabled_plugins: + if plugin in names: + rabbitmq_plugins.disable(plugin) + disabled.append(plugin) + + result['changed'] = len(enabled) > 0 or len(disabled) > 0 + result['enabled'] = enabled + result['disabled'] = disabled + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_policy.py b/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_policy.py new file mode 100644 index 000000000..441a72ce7 --- /dev/null +++ b/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_policy.py @@ -0,0 +1,253 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2013, John Dewey <john@dewey.ws> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: rabbitmq_policy +short_description: Manage the state of policies in RabbitMQ +description: + - Manage the state of a policy in RabbitMQ. +author: John Dewey (@retr0h) +options: + name: + description: + - The name of the policy to manage. + type: str + required: true + vhost: + description: + - The name of the vhost to apply to. + type: str + default: / + apply_to: + description: + - What the policy applies to. Requires RabbitMQ 3.2.0 or later. + type: str + default: all + choices: [all, exchanges, queues] + pattern: + description: + - A regex of queues to apply the policy to. Required when + C(state=present). This option is no longer required as of Ansible 2.9. + type: str + required: false + default: null + tags: + description: + - A dict or string describing the policy. Required when + C(state=present). This option is no longer required as of Ansible 2.9. + type: dict + required: false + default: null + priority: + description: + - The priority of the policy. + type: str + default: '0' + node: + description: + - Erlang node name of the rabbit we wish to configure. + type: str + default: rabbit + state: + description: + - The state of the policy. + type: str + default: present + choices: [present, absent] +''' + +EXAMPLES = r''' +- name: ensure the default vhost contains the HA policy via a dict + community.rabbitmq.rabbitmq_policy: + name: HA + pattern: .* + args: + tags: + ha-mode: all + +- name: ensure the default vhost contains the HA policy + community.rabbitmq.rabbitmq_policy: + name: HA + pattern: .* + tags: + ha-mode: all +''' + +import json +import re +from ansible_collections.community.rabbitmq.plugins.module_utils.version import LooseVersion as Version + +from ansible.module_utils.basic import AnsibleModule + + +class RabbitMqPolicy(object): + + def __init__(self, module, name): + self._module = module + self._name = name + self._vhost = module.params['vhost'] + self._pattern = module.params['pattern'] + self._apply_to = module.params['apply_to'] + self._tags = module.params['tags'] + self._priority = module.params['priority'] + self._node = module.params['node'] + self._rabbitmqctl = module.get_bin_path('rabbitmqctl', True) + + self._version = self._rabbit_version() + + def _exec(self, + args, + force_exec_in_check_mode=False, + split_lines=True, + add_vhost=True): + if (not self._module.check_mode + or (self._module.check_mode and force_exec_in_check_mode)): + cmd = [self._rabbitmqctl, '-q', '-n', self._node] + + if add_vhost: + args.insert(1, '-p') + args.insert(2, self._vhost) + + rc, out, err = self._module.run_command(cmd + args, check_rc=True) + if split_lines: + return out.splitlines() + + return out + return list() + + def _rabbit_version(self): + status = self._exec(['status'], True, False, False) + + # 3.7.x erlang style output + version_match = re.search('{rabbit,".*","(?P<version>.*)"}', status) + if version_match: + return Version(version_match.group('version')) + + # 3.8.x style ouput + version_match = re.search('RabbitMQ version: (?P<version>.*)', status) + if version_match: + return Version(version_match.group('version')) + + return None + + def _list_policies(self): + if self._version and self._version >= Version('3.7.9'): + # Remove first header line from policies list for version > 3.7.9 + return self._exec(['list_policies'], True)[1:] + + return self._exec(['list_policies'], True) + + def has_modifications(self): + if self._pattern is None or self._tags is None: + self._module.fail_json( + msg=('pattern and tags are required for ' + 'state=present')) + + if self._version and self._version >= Version('3.7.0'): + # Change fields order in rabbitmqctl output in version 3.7 + return not any( + self._policy_check(policy, apply_to_fno=3, pattern_fno=2) + for policy in self._list_policies()) + else: + return not any( + self._policy_check(policy) for policy in self._list_policies()) + + def should_be_deleted(self): + return any( + self._policy_check_by_name(policy) + for policy in self._list_policies()) + + def set(self): + args = ['set_policy'] + args.append(self._name) + args.append(self._pattern) + args.append(json.dumps(self._tags)) + args.append('--priority') + args.append(self._priority) + if self._apply_to != 'all': + args.append('--apply-to') + args.append(self._apply_to) + return self._exec(args) + + def clear(self): + return self._exec(['clear_policy', self._name]) + + def _policy_check(self, + policy, + name_fno=1, + apply_to_fno=2, + pattern_fno=3, + tags_fno=4, + priority_fno=5): + if not policy: + return False + + policy_data = policy.split('\t') + + policy_name = policy_data[name_fno] + apply_to = policy_data[apply_to_fno] + pattern = policy_data[pattern_fno].replace('\\\\', '\\') + + try: + tags = json.loads(policy_data[tags_fno]) + except json.decoder.JSONDecodeError: + tags = policy_data[tags_fno] + + priority = policy_data[priority_fno] + + return (policy_name == self._name and apply_to == self._apply_to + and tags == self._tags and priority == self._priority + and pattern == self._pattern) + + def _policy_check_by_name(self, policy): + if not policy: + return False + + policy_name = policy.split('\t')[1] + + return policy_name == self._name + + +def main(): + arg_spec = dict( + name=dict(required=True), + vhost=dict(default='/'), + pattern=dict(required=False, default=None), + apply_to=dict(default='all', choices=['all', 'exchanges', 'queues']), + tags=dict(type='dict', required=False, default=None), + priority=dict(default='0'), + node=dict(default='rabbit'), + state=dict(default='present', choices=['present', 'absent']), + ) + + module = AnsibleModule( + argument_spec=arg_spec, + supports_check_mode=True + ) + + name = module.params['name'] + state = module.params['state'] + rabbitmq_policy = RabbitMqPolicy(module, name) + + result = dict(changed=False, name=name, state=state) + + if state == 'present' and rabbitmq_policy.has_modifications(): + rabbitmq_policy.set() + result['changed'] = True + elif state == 'absent' and rabbitmq_policy.should_be_deleted(): + rabbitmq_policy.clear() + result['changed'] = True + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_publish.py b/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_publish.py new file mode 100644 index 000000000..38b5a64bb --- /dev/null +++ b/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_publish.py @@ -0,0 +1,247 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2018, John Imison <john+github@imison.net> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: rabbitmq_publish +short_description: Publish a message to a RabbitMQ queue. +description: + - Publish a message on a RabbitMQ queue using a blocking connection. +options: + url: + description: + - An URL connection string to connect to the RabbitMQ server. + - I(url) and I(host)/I(port)/I(user)/I(pass)/I(vhost) are mutually exclusive, use either or but not both. + type: str + proto: + description: + - The protocol to use. + type: str + choices: [amqps, amqp] + host: + description: + - The RabbitMQ server hostname or IP. + type: str + port: + description: + - The RabbitMQ server port. + type: int + username: + description: + - The RabbitMQ username. + type: str + password: + description: + - The RabbitMQ password. + type: str + vhost: + description: + - The virtual host to target. + - If default vhost is required, use C('%2F'). + type: str + queue: + description: + - The queue to publish a message to. If no queue is specified, RabbitMQ will return a random queue name. + - A C(queue) cannot be provided if an C(exchange) is specified. + type: str + exchange: + description: + - The exchange to publish a message to. + - An C(exchange) cannot be provided if a C(queue) is specified. + type: str + routing_key: + description: + - The routing key. + type: str + body: + description: + - The body of the message. + - A C(body) cannot be provided if a C(src) is specified. + type: str + src: + description: + - A file to upload to the queue. Automatic mime type detection is attempted if content_type is not defined (left as default). + - A C(src) cannot be provided if a C(body) is specified. + - The filename is added to the headers of the posted message to RabbitMQ. Key being the C(filename), value is the filename. + type: path + aliases: ['file'] + content_type: + description: + - The content type of the body. + type: str + default: text/plain + durable: + description: + - Set the queue to be durable. + type: bool + default: False + exclusive: + description: + - Set the queue to be exclusive. + type: bool + default: False + auto_delete: + description: + - Set the queue to auto delete. + type: bool + default: False + headers: + description: + - A dictionary of headers to post with the message. + type: dict + default: {} + cafile: + description: + - CA file used during connection to the RabbitMQ server over SSL. + - If this option is specified, also I(certfile) and I(keyfile) must be specified. + type: str + certfile: + description: + - Client certificate to establish SSL connection. + - If this option is specified, also I(cafile) and I(keyfile) must be specified. + type: str + keyfile: + description: + - Client key to establish SSL connection. + - If this option is specified, also I(cafile) and I(certfile) must be specified. + type: str + + + +requirements: [ pika ] +notes: + - This module requires the pika python library U(https://pika.readthedocs.io/). + - Pika is a pure-Python implementation of the AMQP 0-9-1 protocol that tries to stay fairly independent of the underlying network support library. + - This module is tested against RabbitMQ. Other AMQP 0.9.1 protocol based servers may work but not tested/guaranteed. + - The certificate authentication was tested with certificates created + via U(https://www.rabbitmq.com/ssl.html#automated-certificate-generation) and RabbitMQ + configuration variables C(ssl_options.verify = verify_peer) & C(ssl_options.fail_if_no_peer_cert = true). +author: "John Imison (@Im0)" +''' + +EXAMPLES = r''' +- name: Publish to an exchange + community.rabbitmq.rabbitmq_publish: + exchange: exchange1 + url: "amqp://guest:guest@192.168.0.32:5672/%2F" + body: "Hello exchange from ansible module rabbitmq_publish" + content_type: "text/plain" + +- name: Publish to an exchange with routing_key + community.rabbitmq.rabbitmq_publish: + exchange: exchange1 + routing_key: queue1 + url: "amqp://guest:guest@192.168.0.32:5672/%2F" + body: "Hello queue via exchange routing_key from ansible module rabbitmq_publish" + content_type: "text/plain" + +- name: Publish a message to a queue with headers + community.rabbitmq.rabbitmq_publish: + url: "amqp://guest:guest@192.168.0.32:5672/%2F" + queue: 'test' + body: "Hello world from ansible module rabbitmq_publish" + content_type: "text/plain" + headers: + myHeader: myHeaderValue + +- name: Publish a file to a queue + community.rabbitmq.rabbitmq_publish: + url: "amqp://guest:guest@192.168.0.32:5672/%2F" + queue: 'images' + file: 'path/to/logo.gif' + +- name: RabbitMQ auto generated queue + community.rabbitmq.rabbitmq_publish: + url: "amqp://guest:guest@192.168.0.32:5672/%2F" + body: "Hello world random queue from ansible module rabbitmq_publish" + content_type: "text/plain" + +- name: Publish with certs + community.rabbitmq.rabbitmq_publish: + url: "amqps://guest:guest@192.168.0.32:5671/%2F" + body: "Hello test queue from ansible module rabbitmq_publish via SSL certs" + queue: 'test' + content_type: "text/plain" + cafile: 'ca_certificate.pem' + certfile: 'client_certificate.pem' + keyfile: 'client_key.pem' + +''' + +RETURN = r''' +result: + description: + - If posted to an exchange, the result contains the status I(msg), content type I(content_type) the exchange name I(exchange) + - and the routing key I(routing_key). + - If posted to a queue, the result contains the status I(msg), content type I(content_type) and the queue name I(queue). + returned: success + type: dict + sample: | + 'result': { 'content_type': 'text/plain', 'msg': 'Successfully published to queue test', 'queue': 'test' } +''' + +try: + import pika + HAS_PIKA = True +except ImportError: + HAS_PIKA = False + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native, to_text +from ansible_collections.community.rabbitmq.plugins.module_utils.rabbitmq import RabbitClient + + +def main(): + argument_spec = RabbitClient.rabbitmq_argument_spec() + argument_spec.update( + exchange=dict(type='str'), + routing_key=dict(type='str', required=False, no_log=False), + body=dict(type='str', required=False), + src=dict(aliases=['file'], type='path', required=False), + content_type=dict(default="text/plain", type='str'), + durable=dict(default=False, type='bool'), + exclusive=dict(default=False, type='bool'), + auto_delete=dict(default=False, type='bool'), + headers=dict(default={}, type='dict'), + cafile=dict(type='str', required=False), + certfile=dict(type='str', required=False), + keyfile=dict(type='str', required=False, no_log=False), + ) + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=[['body', 'src'], ['queue', 'exchange']], + required_together=[['cafile', 'certfile', 'keyfile']], + supports_check_mode=False + ) + + rabbitmq = RabbitClient(module) + + if rabbitmq.basic_publish(): + rabbitmq.close_connection() + if (rabbitmq.queue is not None): + module.exit_json(changed=True, result={"msg": "Successfully published to queue %s" % rabbitmq.queue, + "queue": rabbitmq.queue, "content_type": rabbitmq.content_type} + ) + elif (rabbitmq.exchange is not None): + module.exit_json(changed=True, result={"msg": "Successfully published to exchange %s" % rabbitmq.exchange, + "routing_key": rabbitmq.routing_key, "exchange": rabbitmq.exchange, "content_type": rabbitmq.content_type} + ) + + else: + rabbitmq.close_connection() + if (rabbitmq.queue is not None): + module.fail_json(changed=False, msg="Unsuccessful publishing to queue %s" % rabbitmq.queue) + elif (rabbitmq.exchange is not None): + module.fail_json(changed=False, msg="Unsuccessful publishing to exchange %s" % rabbitmq.exchange) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_queue.py b/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_queue.py new file mode 100644 index 000000000..1290f4713 --- /dev/null +++ b/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_queue.py @@ -0,0 +1,264 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2015, Manuel Sousa <manuel.sousa@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: rabbitmq_queue +author: Manuel Sousa (@manuel-sousa) + +short_description: Manage rabbitMQ queues +description: + - This module uses rabbitMQ Rest API to create/delete queues. + - Due to limitations in the API, it cannot modify existing queues. +requirements: [ "requests >= 1.0.0" ] +options: + name: + description: + - Name of the queue. + type: str + required: true + state: + description: + - Whether the queue should be present or absent. + type: str + choices: [ "present", "absent" ] + default: present + durable: + description: + - whether queue is durable or not. + type: bool + default: true + auto_delete: + description: + - if the queue should delete itself after all queues/queues unbound from it. + type: bool + default: false + message_ttl: + description: + - How long a message can live in queue before it is discarded (milliseconds). + type: int + auto_expires: + description: + - How long a queue can be unused before it is automatically deleted (milliseconds). + type: int + max_length: + description: + - How many messages can the queue contain before it starts rejecting. + type: int + dead_letter_exchange: + description: + - Optional name of an exchange to which messages will be republished if they + - are rejected or expire. + type: str + dead_letter_routing_key: + description: + - Optional replacement routing key to use when a message is dead-lettered. + - Original routing key will be used if unset. + type: str + max_priority: + description: + - Maximum number of priority levels for the queue to support. + - If not set, the queue will not support message priorities. + - Larger numbers indicate higher priority. + type: int + arguments: + description: + - extra arguments for queue. If defined this argument is a key/value dictionary + - Arguments here take precedence over parameters. If both are defined, the + argument will be used. + type: dict + default: {} +extends_documentation_fragment: +- community.rabbitmq.rabbitmq + +''' + +EXAMPLES = r''' +- name: Create a queue + community.rabbitmq.rabbitmq_queue: + name: myQueue + +- name: Create a queue on remote host + community.rabbitmq.rabbitmq_queue: + name: myRemoteQueue + login_user: user + login_password: secret + login_host: remote.example.org + +# You may specify different types of queues using the arguments parameter. +- name: Create RabbitMQ stream + become: yes + community.rabbitmq.rabbitmq_queue: + name: test-x + arguments: + x-queue-type: stream + x-max-age: 24h +''' + +import json +import traceback + +REQUESTS_IMP_ERR = None +try: + import requests + HAS_REQUESTS = True +except ImportError: + REQUESTS_IMP_ERR = traceback.format_exc() + HAS_REQUESTS = False + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils.six.moves.urllib import parse as urllib_parse +from ansible_collections.community.rabbitmq.plugins.module_utils.rabbitmq import rabbitmq_argument_spec + + +def check_if_arg_changed(module, current_args, desired_args, arg_name): + if arg_name not in current_args: + if arg_name in desired_args: + module.fail_json( + msg=("RabbitMQ RESTAPI doesn't support attribute changes for existing queues." + "Attempting to set %s which is not currently set." % arg_name), + ) + # else don't care + else: # arg_name in current_args + if arg_name in desired_args: + if current_args[arg_name] != desired_args[arg_name]: + module.fail_json( + msg=("RabbitMQ RESTAPI doesn't support attribute changes for existing queues.\n" + "Attempting to change %s from '%s' to '%s'" % (arg_name, current_args[arg_name], desired_args[arg_name])) + ) + else: + module.fail_json( + msg=("RabbitMQ RESTAPI doesn't support attribute changes for existing queues." + "Attempting to unset %s which is currently set to '%s'." % (arg_name, current_args[arg_name])), + ) + + +def main(): + + argument_spec = rabbitmq_argument_spec() + argument_spec.update( + dict( + state=dict(default='present', choices=['present', 'absent'], type='str'), + name=dict(required=True, type='str'), + durable=dict(default=True, type='bool'), + auto_delete=dict(default=False, type='bool'), + message_ttl=dict(default=None, type='int'), + auto_expires=dict(default=None, type='int'), + max_length=dict(default=None, type='int'), + dead_letter_exchange=dict(default=None, type='str'), + dead_letter_routing_key=dict(default=None, type='str', no_log=False), + arguments=dict(default=dict(), type='dict'), + max_priority=dict(default=None, type='int') + ) + ) + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + url = "%s://%s:%s/api/queues/%s/%s" % ( + module.params['login_protocol'], + module.params['login_host'], + module.params['login_port'], + urllib_parse.quote(module.params['vhost'], ''), + urllib_parse.quote(module.params['name'], '') + ) + + if not HAS_REQUESTS: + module.fail_json(msg=missing_required_lib("requests"), exception=REQUESTS_IMP_ERR) + + result = dict(changed=False, name=module.params['name']) + + # Check if queue already exists + r = requests.get(url, auth=(module.params['login_user'], module.params['login_password']), + verify=module.params['ca_cert'], cert=(module.params['client_cert'], module.params['client_key'])) + + if r.status_code == 200: + queue_exists = True + response = r.json() + elif r.status_code == 404: + queue_exists = False + response = r.text + else: + module.fail_json( + msg="Invalid response from RESTAPI when trying to check if queue exists", + details=r.text + ) + + arg_map = { + 'message_ttl': 'x-message-ttl', + 'auto_expires': 'x-expires', + 'max_length': 'x-max-length', + 'dead_letter_exchange': 'x-dead-letter-exchange', + 'dead_letter_routing_key': 'x-dead-letter-routing-key', + 'max_priority': 'x-max-priority' + } + + # Sync arguments with parameters (the final request uses module.params['arguments']) + for k, v in arg_map.items(): + if module.params[k] is not None: + module.params['arguments'][v] = module.params[k] + + if module.params['state'] == 'present': + add_or_delete_required = not queue_exists + else: + add_or_delete_required = queue_exists + + # Check if attributes change on existing queue + if not add_or_delete_required and r.status_code == 200 and module.params['state'] == 'present': + check_if_arg_changed(module, response, module.params, 'durable') + check_if_arg_changed(module, response, module.params, 'auto_delete') + + for arg in arg_map.values(): + check_if_arg_changed(module, response['arguments'], module.params['arguments'], arg) + + # Exit if check_mode + if module.check_mode: + result['changed'] = add_or_delete_required + result['details'] = response + result['arguments'] = module.params['arguments'] + module.exit_json(**result) + + # Do changes + if add_or_delete_required: + if module.params['state'] == 'present': + r = requests.put( + url, + auth=(module.params['login_user'], module.params['login_password']), + headers={"content-type": "application/json"}, + data=json.dumps({ + "durable": module.params['durable'], + "auto_delete": module.params['auto_delete'], + "arguments": module.params['arguments'] + }), + verify=module.params['ca_cert'], + cert=(module.params['client_cert'], module.params['client_key']) + ) + elif module.params['state'] == 'absent': + r = requests.delete(url, auth=(module.params['login_user'], module.params['login_password']), + verify=module.params['ca_cert'], cert=(module.params['client_cert'], module.params['client_key'])) + + # RabbitMQ 3.6.7 changed this response code from 204 to 201 + if r.status_code == 204 or r.status_code == 201: + result['changed'] = True + module.exit_json(**result) + else: + module.fail_json( + msg="Error creating queue", + status=r.status_code, + details=r.text + ) + + else: + module.exit_json( + changed=False, + name=module.params['name'] + ) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_upgrade.py b/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_upgrade.py new file mode 100644 index 000000000..e44d7e09e --- /dev/null +++ b/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_upgrade.py @@ -0,0 +1,127 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Damian Dabrowski <damian@dabrowski.cloud> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: rabbitmq_upgrade +short_description: Execute rabbitmq-upgrade commands +description: + - Allows to execute rabbitmq-upgrade commands +author: "Damian Dabrowski (@damiandabrowski5)" +version_added: '1.1.0' +options: + action: + description: + - Specify action to be executed. + type: str + required: true + choices: ['await_online_quorum_plus_one','await_online_synchronized_mirror','post_upgrade','drain','revive'] + node: + description: + - Erlang node name of the target rabbit node. + type: str + required: false + default: rabbit +''' + +EXAMPLES = r''' +- name: Drain 'rabbit@node-1' node (in other words, put it into maintenance mode) + community.rabbitmq.rabbitmq_upgrade: + action: drain + node: rabbit@node-1 +''' + +import json +from ansible.module_utils.basic import AnsibleModule + + +class RabbitMqUpgrade(object): + + def __init__(self, module, action, node, result): + self.module = module + self.action = action + self.node = node + self.result = result + + def _exec(self, binary, args, force_exec_in_check_mode=False): + if not self.module.check_mode or (self.module.check_mode and force_exec_in_check_mode): + cmd = [self.module.get_bin_path(binary, True)] + rc, out, err = self.module.run_command(cmd + args, check_rc=True) + return out.splitlines() + return list() + + def is_node_under_maintenance(self): + cmd = self._exec('rabbitmq-diagnostics', + ['--formatter', 'json', 'status', '-n', self.node], True) + node_status = json.loads("".join(cmd)) + maint_enabled = node_status['is_under_maintenance'] + if maint_enabled: + return True + return False + + def is_maint_flag_enabled(self): + feature_flags = self._exec('rabbitmqctl', ['list_feature_flags', '-q'], True) + for param_item in feature_flags: + name, state = param_item.split('\t') + if name == 'maintenance_mode_status' and state == 'enabled': + return True + return False + + def drain(self): + if not self.is_maint_flag_enabled(): + self.module.fail_json(msg='maintenance_mode_status feature_flag is disabled.') + if not self.is_node_under_maintenance(): + self._exec('rabbitmq-upgrade', ['drain', '-n', self.node]) + self.result['changed'] = True + + def revive(self): + if not self.is_maint_flag_enabled(): + self.module.fail_json(msg='maintenance_mode_status feature_flag is disabled.') + if self.is_node_under_maintenance(): + self._exec('rabbitmq-upgrade', ['revive', '-n', self.node]) + self.result['changed'] = True + + def await_online_quorum_plus_one(self): + self._exec('rabbitmq-upgrade', ['await_online_quorum_plus_one']) + self.result['changed'] = True + + def await_online_synchronized_mirror(self): + self._exec('rabbitmq-upgrade', ['await_online_synchronized_mirror']) + self.result['changed'] = True + + def post_upgrade(self): + self._exec('rabbitmq-upgrade', ['post_upgrade']) + self.result['changed'] = True + + +def main(): + arg_spec = dict( + action=dict( + choices=['await_online_quorum_plus_one', 'await_online_synchronized_mirror', 'post_upgrade', 'drain', 'revive'], + required=True), + node=dict(type='str', default='rabbit') + ) + module = AnsibleModule( + argument_spec=arg_spec, + supports_check_mode=True + ) + + action = module.params['action'] + node = module.params['node'] + result = dict(changed=False) + + rabbitmq_upgrade = RabbitMqUpgrade(module, action, node, result) + + getattr(rabbitmq_upgrade, action)() + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_user.py b/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_user.py new file mode 100644 index 000000000..87d6864fc --- /dev/null +++ b/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_user.py @@ -0,0 +1,567 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2013, Chatham Financial <oss@chathamfinancial.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: rabbitmq_user +short_description: Manage RabbitMQ users +description: + - Add or remove users to RabbitMQ and assign permissions +author: Chris Hoffman (@chrishoffman) +options: + user: + description: + - Name of user to add + type: str + required: true + aliases: [username, name] + password: + description: + - Password of user to add. + - To change the password of an existing user, you must also specify + C(update_password=always). + type: str + tags: + description: + - User tags specified as comma delimited + type: str + permissions: + description: + - a list of dicts, each dict contains vhost, configure_priv, write_priv, and read_priv, + and represents a permission rule for that vhost. + - This option should be preferable when you care about all permissions of the user. + - You should use vhost, configure_priv, write_priv, and read_priv options instead + if you care about permissions for just some vhosts. + type: list + elements: dict + default: [] + vhost: + description: + - vhost to apply access privileges. + - This option will be ignored when permissions option is used. + type: str + default: / + node: + description: + - erlang node name of the rabbit we wish to configure + type: str + default: rabbit + configure_priv: + description: + - Regular expression to restrict configure actions on a resource + for the specified vhost. + - By default all actions are restricted. + - This option will be ignored when permissions option is used. + type: str + default: '^$' + write_priv: + description: + - Regular expression to restrict configure actions on a resource + for the specified vhost. + - By default all actions are restricted. + - This option will be ignored when permissions option is used. + type: str + default: '^$' + read_priv: + description: + - Regular expression to restrict configure actions on a resource + for the specified vhost. + - By default all actions are restricted. + - This option will be ignored when permissions option is used. + type: str + default: '^$' + topic_permissions: + description: + - A list of dicts, each dict contains vhost, exchange, read_priv and write_priv, + and represents a topic permission rule for that vhost. + - By default vhost is C(/) and exchange is C(amq.topic). + - Supported since RabbitMQ 3.7.0. If RabbitMQ is older and topic_permissions are + set, the module will fail. + type: list + elements: dict + default: [] + version_added: '1.2.0' + force: + description: + - Deletes and recreates the user. + type: bool + default: false + state: + description: + - Specify if user is to be added or removed + type: str + default: present + choices: ['present', 'absent'] + update_password: + description: + - C(on_create) will only set the password for newly created users. C(always) will update passwords if they differ. + type: str + required: false + default: on_create + choices: ['on_create', 'always'] +''' + +EXAMPLES = r''' +- name: |- + Add user to server and assign full access control on / vhost. + The user might have permission rules for other vhost but you don't care. + community.rabbitmq.rabbitmq_user: + user: joe + password: changeme + vhost: / + configure_priv: .* + read_priv: .* + write_priv: .* + state: present + +- name: |- + Add user to server and assign full access control on / vhost. + The user doesn't have permission rules for other vhosts + community.rabbitmq.rabbitmq_user: + user: joe + password: changeme + permissions: + - vhost: / + configure_priv: .* + read_priv: .* + write_priv: .* + state: present + +- name: |- + Add user to server and assign some topic permissions on / vhost. + The user doesn't have topic permission rules for other vhosts + community.rabbitmq.rabbitmq_user: + user: joe + password: changeme + topic_permissions: + - vhost: / + exchange: amq.topic + read_priv: .* + write_priv: 'prod\\.logging\\..*' + state: present +''' + +import ansible_collections.community.rabbitmq.plugins.module_utils.version as Version +import json +import re + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.collections import count + + +def normalized_permissions(vhost_permission_list): + """Older versions of RabbitMQ output permissions with slightly different names. + + In older versions of RabbitMQ, the names of the permissions had the `_priv` suffix, which was removed in versions + >= 3.7.6. For simplicity we only check the `configure` permission. If it's in the old format then all the other + ones will be wrong too. + """ + for vhost_permission in vhost_permission_list: + if 'configure_priv' in vhost_permission: + yield { + 'configure': vhost_permission['configure_priv'], + 'read': vhost_permission['read_priv'], + 'write': vhost_permission['write_priv'], + 'vhost': vhost_permission['vhost'] + } + else: + yield vhost_permission + + +def as_permission_dict(vhost_permission_list): + return dict([(vhost_permission['vhost'], vhost_permission) for vhost_permission + in normalized_permissions(vhost_permission_list)]) + + +def as_topic_permission_dict(topic_permission_list): + return dict([((perm['vhost'], perm['exchange']), perm) for perm + in topic_permission_list]) + + +def only(vhost, vhost_permissions): + return {vhost: vhost_permissions.get(vhost, {})} + + +def first(iterable): + return next(iter(iterable)) + + +class RabbitMqUser(object): + def __init__(self, module, username, password, tags, permissions, + topic_permissions, node, bulk_permissions=False): + self.module = module + self.username = username + self.password = password or '' + self.node = node + self.tags = list() if not tags else tags.replace(' ', '').split(',') + self.permissions = as_permission_dict(permissions) + self.topic_permissions = as_topic_permission_dict(topic_permissions) + self.bulk_permissions = bulk_permissions + + self.existing_tags = None + self.existing_permissions = dict() + self.existing_topic_permissions = dict() + self._rabbitmqctl = module.get_bin_path('rabbitmqctl', True) + self._version = self._check_version() + + def _check_version(self): + """Get the version of the RabbitMQ server.""" + version = self._rabbitmq_version_post_3_7(fail_on_error=False) + if not version: + version = self._rabbitmq_version_pre_3_7(fail_on_error=False) + if not version: + self.module.fail_json(msg="Could not determine the version of the RabbitMQ server.") + return version + + def _fail(self, msg, stop_execution=False): + if stop_execution: + self.module.fail_json(msg=msg) + # This is a dummy return to prevent linters from throwing errors. + return None + + def _rabbitmq_version_post_3_7(self, fail_on_error=False): + """Use the JSON formatter to get a machine readable output of the version. + + At this point we do not know which RabbitMQ server version we are dealing with and which + version of `rabbitmqctl` we are using, so we will try to use the JSON formatter and see + what happens. In some versions of + """ + def int_list_to_str(ints): + return ''.join([chr(i) for i in ints]) + + rc, output, err = self._exec(['status', '--formatter', 'json'], check_rc=False) + if rc != 0: + return self._fail(msg="Could not parse the version of the RabbitMQ server, " + "because `rabbitmqctl status` returned no output.", + stop_execution=fail_on_error) + try: + status_json = json.loads(output) + if 'rabbitmq_version' in status_json: + return Version.StrictVersion(status_json['rabbitmq_version']) + for application in status_json.get('running_applications', list()): + if application[0] == 'rabbit': + if isinstance(application[1][0], int): + return Version.StrictVersion(int_list_to_str(application[2])) + else: + return Version.StrictVersion(application[1]) + return self._fail(msg="Could not find RabbitMQ version of `rabbitmqctl status` command.", + stop_execution=fail_on_error) + except ValueError as e: + return self._fail(msg="Could not parse output of `rabbitmqctl status` as JSON: {exc}.".format(exc=repr(e)), + stop_execution=fail_on_error) + + def _rabbitmq_version_pre_3_7(self, fail_on_error=False): + """Get the version of the RabbitMQ Server. + + Before version 3.7.6 the `rabbitmqctl` utility did not support the + `--formatter` flag, so the output has to be parsed using regular expressions. + """ + version_reg_ex = r"{rabbit,\"RabbitMQ\",\"([0-9]+\.[0-9]+\.[0-9]+)\"}" + rc, output, err = self._exec(['status'], check_rc=False) + if rc != 0: + if fail_on_error: + self.module.fail_json(msg="Could not parse the version of the RabbitMQ server, because" + " `rabbitmqctl status` returned no output.") + else: + return None + reg_ex_res = re.search(version_reg_ex, output, re.IGNORECASE) + if not reg_ex_res: + return self._fail(msg="Could not parse the version of the RabbitMQ server from the output of " + "`rabbitmqctl status` command: {output}.".format(output=output), + stop_execution=fail_on_error) + try: + return Version.StrictVersion(reg_ex_res.group(1)) + except ValueError as e: + return self._fail(msg="Could not parse the version of the RabbitMQ server: {exc}.".format(exc=repr(e)), + stop_execution=fail_on_error) + + def _exec(self, args, check_rc=True): + """Execute a command using the `rabbitmqctl` utility. + + By default the _exec call will cause the module to fail, if the error code is non-zero. If the `check_rc` + flag is set to False, then the exit_code, stdout and stderr will be returned to the calling function to + perform whatever error handling it needs. + + :param args: the arguments to pass to the `rabbitmqctl` utility + :param check_rc: when set to True, fail if the utility's exit code is non-zero + :return: the output of the command or all the outputs plus the error code in case of error + """ + cmd = [self._rabbitmqctl, '-q'] + if self.node: + cmd.extend(['-n', self.node]) + rc, out, err = self.module.run_command(cmd + args) + if check_rc and rc != 0: + # check_rc is not passed to the `run_command` method directly to allow for more fine grained checking of + # error messages returned by `rabbitmqctl`. + user_error_msg_regex = r"(Only root or .* .* run rabbitmqctl)" + user_error_msg = re.search(user_error_msg_regex, out) + if user_error_msg: + self.module.fail_json(msg="Wrong user used to run the `rabbitmqctl` utility: {err}" + .format(err=user_error_msg.group(1))) + else: + self.module.fail_json(msg="rabbitmqctl exited with non-zero code: {err}".format(err=err), + rc=rc, stdout=out) + return out if check_rc else (rc, out, err) + + def get(self): + """Retrieves the list of registered users from the node. + + If the user is already present, the node will also be queried for the user's permissions and topic + permissions. + If the version of the node is >= 3.7.6 the JSON formatter will be used, otherwise the plaintext will be + parsed. + """ + if self._version >= Version.StrictVersion('3.7.6'): + users = dict([(user_entry['user'], user_entry['tags']) + for user_entry in json.loads(self._exec(['list_users', '--formatter', 'json']))]) + else: + users = self._exec(['list_users']) + + def process_tags(tags): + if not tags: + return list() + return tags.replace('[', '').replace(']', '').replace(' ', '').strip('\t').split(',') + + users_and_tags = [user_entry.split('\t') for user_entry in users.strip().split('\n')] + + users = dict() + for user_parts in users_and_tags: + users[user_parts[0]] = process_tags(user_parts[1]) if len(user_parts) > 1 else [] + + self.existing_tags = users.get(self.username, list()) + self.existing_permissions = self._get_permissions() if self.username in users else dict() + self.existing_topic_permissions = self._get_topic_permissions() if self.username in users else dict() + return self.username in users + + def _get_permissions(self): + """Get permissions of the user from RabbitMQ.""" + if self._version >= Version.StrictVersion('3.7.6'): + permissions = json.loads(self._exec(['list_user_permissions', self.username, '--formatter', 'json'])) + else: + output = self._exec(['list_user_permissions', self.username]).strip().split('\n') + perms_out = [perm.split('\t') for perm in output if perm.strip()] + # Filter out headers from the output of the command in case they are still present + perms_out = [perm for perm in perms_out if perm != ["vhost", "configure", "write", "read"]] + + permissions = list() + for vhost, configure, write, read in perms_out: + permissions.append(dict(vhost=vhost, configure=configure, write=write, read=read)) + + if self.bulk_permissions: + return as_permission_dict(permissions) + else: + return only(first(self.permissions.keys()), as_permission_dict(permissions)) + + def _get_topic_permissions(self): + """Get topic permissions of the user from RabbitMQ.""" + if self._version < Version.StrictVersion('3.7.0'): + return dict() + if self._version >= Version.StrictVersion('3.7.6'): + permissions = json.loads(self._exec(['list_user_topic_permissions', self.username, '--formatter', 'json'])) + else: + output = self._exec(['list_user_topic_permissions', self.username]).strip().split('\n') + perms_out = [perm.split('\t') for perm in output if perm.strip()] + permissions = list() + for vhost, exchange, write, read in perms_out: + permissions.append(dict(vhost=vhost, exchange=exchange, write=write, read=read)) + return as_topic_permission_dict(permissions) + + def check_password(self): + """Return `True` if the user can authenticate successfully.""" + rc, out, err = self._exec(['authenticate_user', self.username, self.password], check_rc=False) + return rc == 0 + + def add(self): + self._exec(['add_user', self.username, self.password or '']) + if not self.password: + self._exec(['clear_password', self.username]) + + def delete(self): + self._exec(['delete_user', self.username]) + + def change_password(self): + if self.password: + self._exec(['change_password', self.username, self.password]) + else: + self._exec(['clear_password', self.username]) + + def set_tags(self): + self._exec(['set_user_tags', self.username] + self.tags) + + def set_permissions(self): + permissions_to_add = list() + for vhost, permission_dict in self.permissions.items(): + if permission_dict != self.existing_permissions.get(vhost, {}): + permissions_to_add.append(permission_dict) + + permissions_to_clear = list() + for vhost in self.existing_permissions.keys(): + if vhost not in self.permissions: + permissions_to_clear.append(vhost) + + for vhost in permissions_to_clear: + cmd = 'clear_permissions -p {vhost} {username}'.format(username=self.username, vhost=vhost) + self._exec(cmd.split(' ')) + for permissions in permissions_to_add: + cmd = ('set_permissions -p {vhost} {username} {configure} {write} {read}' + .format(username=self.username, **permissions)) + self._exec(cmd.split(' ')) + self.existing_permissions = self._get_permissions() + + def set_topic_permissions(self): + permissions_to_add = list() + for vhost_exchange, permission_dict in self.topic_permissions.items(): + if permission_dict != self.existing_topic_permissions.get(vhost_exchange, {}): + permissions_to_add.append(permission_dict) + + permissions_to_clear = list() + for vhost_exchange in self.existing_topic_permissions.keys(): + if vhost_exchange not in self.topic_permissions: + permissions_to_clear.append(vhost_exchange) + + for vhost_exchange in permissions_to_clear: + vhost, exchange = vhost_exchange + cmd = ('clear_topic_permissions -p {vhost} {username} {exchange}' + .format(username=self.username, vhost=vhost, exchange=exchange)) + self._exec(cmd.split(' ')) + for permissions in permissions_to_add: + cmd = ('set_topic_permissions -p {vhost} {username} {exchange} {write} {read}' + .format(username=self.username, **permissions)) + self._exec(cmd.split(' ')) + self.existing_topic_permissions = self._get_topic_permissions() + + def has_tags_modifications(self): + return set(self.tags) != set(self.existing_tags) + + def has_permissions_modifications(self): + return self.existing_permissions != self.permissions + + def has_topic_permissions_modifications(self): + return self.existing_topic_permissions != self.topic_permissions + + +def main(): + arg_spec = dict( + user=dict(required=True, aliases=['username', 'name']), + password=dict(default=None, no_log=True), + tags=dict(default=None), + permissions=dict(default=list(), type='list', elements='dict'), + vhost=dict(default='/'), + configure_priv=dict(default='^$'), + write_priv=dict(default='^$'), + read_priv=dict(default='^$'), + topic_permissions=dict(default=list(), type='list', elements='dict'), + force=dict(default='no', type='bool'), + state=dict(default='present', choices=['present', 'absent']), + node=dict(default='rabbit'), + update_password=dict(default='on_create', choices=['on_create', 'always'], no_log=False) + ) + module = AnsibleModule( + argument_spec=arg_spec, + supports_check_mode=False + ) + + username = module.params['user'] + password = module.params['password'] + tags = module.params['tags'] + permissions = module.params['permissions'] + vhost = module.params['vhost'] + configure_priv = module.params['configure_priv'] + write_priv = module.params['write_priv'] + read_priv = module.params['read_priv'] + topic_permissions = module.params['topic_permissions'] + force = module.params['force'] + state = module.params['state'] + node = module.params['node'] + update_password = module.params['update_password'] + + if permissions: + vhosts = [permission.get('vhost', '/') for permission in permissions] + if any(vhost_count > 1 for vhost_count in count(vhosts).values()): + module.fail_json(msg="Error parsing vhost permissions: You can't " + "have two permission dicts for the same vhost") + bulk_permissions = True + else: + perm = { + 'vhost': vhost, + 'configure_priv': configure_priv, + 'write_priv': write_priv, + 'read_priv': read_priv + } + permissions.append(perm) + bulk_permissions = False + + if topic_permissions: + vhost_exchanges = [ + (permission.get('vhost', '/'), permission.get('exchange')) + for permission in topic_permissions + ] + if any(ve_count > 1 for ve_count in count(vhost_exchanges).values()): + module.fail_json(msg="Error parsing vhost topic_permissions: You can't " + "have two topic permission dicts for the same vhost " + "and the same exchange") + + for permission in permissions: + if not permission['vhost']: + module.fail_json(msg="Error parsing vhost permissions: You can't" + "have an empty vhost when setting permissions") + + for permission in topic_permissions: + permission.setdefault('vhost', '/') + permission.setdefault('exchange', 'amq.topic') + # Normalize the arguments + for perm_name in ("read", "write"): + suffixed_perm_name = "{perm_name}_priv".format(perm_name=perm_name) + if suffixed_perm_name in permission: + permission[perm_name] = permission.pop(suffixed_perm_name) + + rabbitmq_user = RabbitMqUser(module, username, password, tags, permissions, + topic_permissions, node, + bulk_permissions=bulk_permissions) + + result = dict(changed=False, user=username, state=state) + if rabbitmq_user.get(): + if state == 'absent': + rabbitmq_user.delete() + result['changed'] = True + else: + if force: + rabbitmq_user.delete() + rabbitmq_user.add() + rabbitmq_user.get() + result['changed'] = True + elif update_password == 'always': + if not rabbitmq_user.check_password(): + rabbitmq_user.change_password() + result['changed'] = True + + if rabbitmq_user.has_tags_modifications(): + rabbitmq_user.set_tags() + result['changed'] = True + + if rabbitmq_user.has_permissions_modifications(): + rabbitmq_user.set_permissions() + result['changed'] = True + + if rabbitmq_user.has_topic_permissions_modifications(): + rabbitmq_user.set_topic_permissions() + result['changed'] = True + elif state == 'present': + rabbitmq_user.add() + rabbitmq_user.set_tags() + rabbitmq_user.set_permissions() + rabbitmq_user.set_topic_permissions() + result['changed'] = True + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_user_limits.py b/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_user_limits.py new file mode 100644 index 000000000..9a92ad3fc --- /dev/null +++ b/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_user_limits.py @@ -0,0 +1,217 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2013, Chatham Financial <oss@chathamfinancial.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: rabbitmq_user_limits +short_description: Manage RabbitMQ user limits +description: + - Manage the state of user limits in RabbitMQ. Supported since RabbitMQ version 3.8.10. +author: Aitor Pazos (@aitorpazos) +version_added: '1.1.0' +options: + user: + description: + - Name of user to manage limits for. + type: str + required: true + aliases: [username, name] + max_connections: + description: + - Max number of concurrent client connections. + - Negative value means "no limit". + - Ignored when the I(state) is C(absent). + type: int + default: -1 + max_channels: + description: + - Max number of channels. + - Negative value means "no limit". + - Ignored when the I(state) is C(absent). + type: int + default: -1 + node: + description: + - Name of the RabbitMQ Erlang node to manage. + type: str + state: + description: + - Specify whether the limits are to be set or cleared. + - If set to C(absent), the limits of both I(max_connections) and I(max_channels) will be cleared. + type: str + default: present + choices: [present, absent] +notes: + - Supports C(check_mode). +''' + +EXAMPLES = r''' +- name: Limit both of the max number of connections and channels on the user 'guest'. + community.rabbitmq.rabbitmq_user_limits: + user: guest + max_connections: 64 + max_channels: 256 + state: present + +# This task implicitly clears the max number of channels limit using default value: -1. +- name: Limit the max number of connections on the user 'guest'. + community.rabbitmq.rabbitmq_user_limits: + user: guest + max_connections: 64 + state: present + +- name: Clear the limits on the user 'guest'. + community.rabbitmq.rabbitmq_user_limits: + user: guest + state: absent +''' + +RETURN = r''' # ''' + +import json +import re +from ansible_collections.community.rabbitmq.plugins.module_utils.version import LooseVersion as Version +from ansible.module_utils.basic import AnsibleModule + + +class RabbitMqUserLimits(object): + def __init__(self, module): + self._module = module + self._max_connections = module.params['max_connections'] + self._max_channels = module.params['max_channels'] + self._node = module.params['node'] + self._state = module.params['state'] + self._user = module.params['user'] + self._rabbitmqctl = module.get_bin_path('rabbitmqctl', True) + + self._version = self._rabbit_version() + + def _exec(self, + args, + force_exec_in_check_mode=False): + + if not self._module.check_mode or (self._module.check_mode and force_exec_in_check_mode): + cmd = [self._rabbitmqctl, '-q'] + if self._node is not None: + cmd.extend(['-n', self._node]) + rc, out, err = self._module.run_command(cmd + args, check_rc=True) + + return out + return "" + + def _rabbit_version(self): + status = self._exec(['status'], True) + + # 3.7.x erlang style output + version_match = re.search('{rabbit,".*","(?P<version>.*)"}', status) + if version_match: + return Version(version_match.group('version')) + + # 3.8.x style output + version_match = re.search('RabbitMQ version: (?P<version>.*)', status) + if version_match: + return Version(version_match.group('version')) + + return None + + def _assert_version(self): + if self._version and self._version < Version('3.8.10'): + self._module.fail_json(changed=False, + msg="User limits are only available for RabbitMQ >= 3.8.10. Detected version: %s" % self._version) + + def list(self): + self._assert_version() + + exec_result = self._exec(['list_user_limits', '--user', self._user], False) + max_connections = None + max_channels = None + if exec_result: + user_limits = json.loads(exec_result) + if 'max-connections' in user_limits: + max_connections = user_limits['max-connections'] + if 'max-channels' in user_limits: + max_channels = user_limits['max-channels'] + return dict( + max_connections=max_connections, + max_channels=max_channels + ) + + def set(self): + self._assert_version() + + if self._module.check_mode: + return + + if self._max_connections != -1: + json_str = '{{"max-connections": {0}}}'.format(self._max_connections) + self._exec(['set_user_limits', self._user, json_str]) + else: + self._exec(['clear_user_limits', self._user, "max-connections"]) + + if self._max_channels != -1: + json_str = '{{"max-channels": {0}}}'.format(self._max_channels) + self._exec(['set_user_limits', self._user, json_str]) + else: + self._exec(['clear_user_limits', self._user, "max-channels"]) + + def clear(self): + self._assert_version() + + if self._module.check_mode: + return + + return self._exec(['clear_user_limits', self._user, 'all']) + + +def main(): + arg_spec = dict( + user=dict(required=True, type='str', aliases=['username', 'name']), + max_connections=dict(default=-1, type='int'), + max_channels=dict(default=-1, type='int'), + state=dict(default='present', choices=['present', 'absent'], type='str'), + node=dict(default=None, type='str') + ) + + module = AnsibleModule( + argument_spec=arg_spec, + supports_check_mode=True + ) + + max_connections = module.params['max_connections'] + max_channels = module.params['max_channels'] + state = module.params['state'] + + module_result = dict(changed=False) + rabbitmq_user_limits = RabbitMqUserLimits(module) + current_status = rabbitmq_user_limits.list() + + if state == 'present': + wanted_status = dict( + max_connections=max_connections, + max_channels=max_channels + ) + else: # state == 'absent' + wanted_status = dict( + max_connections=None, + max_channels=None + ) + + if current_status != wanted_status: + module_result['changed'] = True + if state == 'present': + rabbitmq_user_limits.set() + else: # state == 'absent' + rabbitmq_user_limits.clear() + + module.exit_json(**module_result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_vhost.py b/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_vhost.py new file mode 100644 index 000000000..cfcecc6a5 --- /dev/null +++ b/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_vhost.py @@ -0,0 +1,142 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2013, Chatham Financial <oss@chathamfinancial.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: rabbitmq_vhost +short_description: Manage the state of a virtual host in RabbitMQ +description: + - Manage the state of a virtual host in RabbitMQ +author: Chris Hoffman (@chrishoffman) +options: + name: + description: + - The name of the vhost to manage + type: str + required: true + aliases: [vhost] + node: + description: + - erlang node name of the rabbit we wish to configure + type: str + default: rabbit + tracing: + description: + - Enable/disable tracing for a vhost + type: bool + default: false + aliases: [trace] + state: + description: + - The state of vhost + type: str + default: present + choices: [present, absent] +''' + +EXAMPLES = r''' +- name: Ensure that the vhost /test exists. + community.rabbitmq.rabbitmq_vhost: + name: /test + state: present +''' + +from ansible.module_utils.basic import AnsibleModule + + +class RabbitMqVhost(object): + def __init__(self, module, name, tracing, node): + self.module = module + self.name = name + self.tracing = tracing + self.node = node + + self._tracing = False + self._rabbitmqctl = module.get_bin_path('rabbitmqctl', True) + + def _exec(self, args, force_exec_in_check_mode=False): + if not self.module.check_mode or (self.module.check_mode and force_exec_in_check_mode): + cmd = [self._rabbitmqctl, '-q', '-n', self.node] + rc, out, err = self.module.run_command(cmd + args, check_rc=True) + return out.splitlines() + return list() + + def get(self): + vhosts = self._exec(['list_vhosts', 'name', 'tracing'], True) + + for vhost in vhosts: + if '\t' not in vhost: + continue + + name, tracing = vhost.split('\t') + if name == self.name: + self._tracing = self.module.boolean(tracing) + return True + return False + + def add(self): + return self._exec(['add_vhost', self.name]) + + def delete(self): + return self._exec(['delete_vhost', self.name]) + + def set_tracing(self): + if self.tracing != self._tracing: + if self.tracing: + self._enable_tracing() + else: + self._disable_tracing() + return True + return False + + def _enable_tracing(self): + return self._exec(['trace_on', '-p', self.name]) + + def _disable_tracing(self): + return self._exec(['trace_off', '-p', self.name]) + + +def main(): + arg_spec = dict( + name=dict(required=True, aliases=['vhost']), + tracing=dict(default='off', aliases=['trace'], type='bool'), + state=dict(default='present', choices=['present', 'absent']), + node=dict(default='rabbit'), + ) + + module = AnsibleModule( + argument_spec=arg_spec, + supports_check_mode=True + ) + + name = module.params['name'] + tracing = module.params['tracing'] + state = module.params['state'] + node = module.params['node'] + result = dict(changed=False, name=name, state=state) + rabbitmq_vhost = RabbitMqVhost(module, name, tracing, node) + + if rabbitmq_vhost.get(): + if state == 'absent': + rabbitmq_vhost.delete() + result['changed'] = True + else: + if rabbitmq_vhost.set_tracing(): + result['changed'] = True + elif state == 'present': + rabbitmq_vhost.add() + rabbitmq_vhost.set_tracing() + result['changed'] = True + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_vhost_limits.py b/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_vhost_limits.py new file mode 100644 index 000000000..e2f369606 --- /dev/null +++ b/ansible_collections/community/rabbitmq/plugins/modules/rabbitmq_vhost_limits.py @@ -0,0 +1,174 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Hiroyuki Matsuo <h.matsuo.engineer@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: rabbitmq_vhost_limits +author: Hiroyuki Matsuo (@h-matsuo) + +short_description: Manage the state of virtual host limits in RabbitMQ +description: + - This module sets/clears certain limits on a virtual host. + - The configurable limits are I(max_connections) and I(max-queues). + +options: + max_connections: + description: + - Max number of concurrent client connections. + - Negative value means "no limit". + - Ignored when the I(state) is C(absent). + type: int + default: -1 + max_queues: + description: + - Max number of queues. + - Negative value means "no limit". + - Ignored when the I(state) is C(absent). + type: int + default: -1 + node: + description: + - Name of the RabbitMQ Erlang node to manage. + type: str + state: + description: + - Specify whether the limits are to be set or cleared. + - If set to C(absent), the limits of both I(max_connections) and I(max-queues) will be cleared. + type: str + default: present + choices: [present, absent] + vhost: + description: + - Name of the virtual host to manage. + type: str + default: / +''' + +EXAMPLES = r''' +- name: Limit both of the max number of connections and queues on the vhost '/'. + community.rabbitmq.rabbitmq_vhost_limits: + vhost: / + max_connections: 64 + max_queues: 256 + state: present + +- name: |- + Limit the max number of connections on the vhost '/'. + This task implicitly clears the max number of queues limit using default value: -1. + community.rabbitmq.rabbitmq_vhost_limits: + vhost: / + max_connections: 64 + state: present + +- name: Clear the limits on the vhost '/'. + community.rabbitmq.rabbitmq_vhost_limits: + vhost: / + state: absent +''' + +RETURN = r''' # ''' + + +import json +from ansible.module_utils.basic import AnsibleModule + + +class RabbitMqVhostLimits(object): + def __init__(self, module): + self._module = module + self._max_connections = module.params['max_connections'] + self._max_queues = module.params['max_queues'] + self._node = module.params['node'] + self._state = module.params['state'] + self._vhost = module.params['vhost'] + self._rabbitmqctl = module.get_bin_path('rabbitmqctl', True) + + def _exec(self, args): + cmd = [self._rabbitmqctl, '-q', '-p', self._vhost] + if self._node is not None: + cmd.extend(['-n', self._node]) + rc, out, err = self._module.run_command(cmd + args, check_rc=True) + return dict(rc=rc, out=out.splitlines(), err=err.splitlines()) + + def list(self): + exec_result = self._exec(['list_vhost_limits']) + vhost_limits = exec_result['out'][0] + max_connections = None + max_queues = None + if vhost_limits: + vhost_limits = json.loads(vhost_limits) + if 'max-connections' in vhost_limits: + max_connections = vhost_limits['max-connections'] + if 'max-queues' in vhost_limits: + max_queues = vhost_limits['max-queues'] + return dict( + max_connections=max_connections, + max_queues=max_queues + ) + + def set(self): + if self._module.check_mode: + return + json_str = '{{"max-connections": {0}, "max-queues": {1}}}'.format(self._max_connections, self._max_queues) + self._exec(['set_vhost_limits', json_str]) + + def clear(self): + if self._module.check_mode: + return + self._exec(['clear_vhost_limits']) + + +def main(): + arg_spec = dict( + max_connections=dict(default=-1, type='int'), + max_queues=dict(default=-1, type='int'), + node=dict(default=None, type='str'), + state=dict(default='present', choices=['present', 'absent'], type='str'), + vhost=dict(default='/', type='str') + ) + + module = AnsibleModule( + argument_spec=arg_spec, + supports_check_mode=True + ) + + max_connections = module.params['max_connections'] + max_queues = module.params['max_queues'] + node = module.params['node'] + state = module.params['state'] + vhost = module.params['vhost'] + + module_result = dict(changed=False) + rabbitmq_vhost_limits = RabbitMqVhostLimits(module) + current_status = rabbitmq_vhost_limits.list() + + if state == 'present': + wanted_status = dict( + max_connections=max_connections, + max_queues=max_queues + ) + else: # state == 'absent' + wanted_status = dict( + max_connections=None, + max_queues=None + ) + + if current_status != wanted_status: + module_result['changed'] = True + if state == 'present': + rabbitmq_vhost_limits.set() + else: # state == 'absent' + rabbitmq_vhost_limits.clear() + + module.exit_json(**module_result) + + +if __name__ == '__main__': + main() |