From 7a56e5d45db7c77fec003626f0586369ac8ec653 Mon Sep 17 00:00:00 2001 From: David Moreau-Simard Date: Tue, 1 Aug 2017 23:10:32 -0400 Subject: [PATCH] Add sensu_handler Ansible module This provides a sensu_handler module in order to be able to dynamically configure a Sensu handler. --- .../modules/monitoring/sensu_handler.py | 285 ++++++++++++++++++ .../integration/targets/sensu_handler/aliases | 1 + .../targets/sensu_handler/tasks/main.yml | 120 ++++++++ .../targets/sensu_handler/tasks/pipe.yml | 20 ++ .../targets/sensu_handler/tasks/set.yml | 49 +++ .../targets/sensu_handler/tasks/tcp.yml | 52 ++++ .../targets/sensu_handler/tasks/transport.yml | 52 ++++ .../targets/sensu_handler/tasks/udp.yml | 52 ++++ 8 files changed, 631 insertions(+) create mode 100644 lib/ansible/modules/monitoring/sensu_handler.py create mode 100644 test/integration/targets/sensu_handler/aliases create mode 100644 test/integration/targets/sensu_handler/tasks/main.yml create mode 100644 test/integration/targets/sensu_handler/tasks/pipe.yml create mode 100644 test/integration/targets/sensu_handler/tasks/set.yml create mode 100644 test/integration/targets/sensu_handler/tasks/tcp.yml create mode 100644 test/integration/targets/sensu_handler/tasks/transport.yml create mode 100644 test/integration/targets/sensu_handler/tasks/udp.yml diff --git a/lib/ansible/modules/monitoring/sensu_handler.py b/lib/ansible/modules/monitoring/sensu_handler.py new file mode 100644 index 00000000000..bb56126202f --- /dev/null +++ b/lib/ansible/modules/monitoring/sensu_handler.py @@ -0,0 +1,285 @@ +#!/usr/bin/python + +# (c) 2017, Red Hat Inc. +# 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 + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'status': ['preview'], + 'supported_by': 'community' +} + +DOCUMENTATION = ''' +--- +module: sensu_handler +author: "David Moreau Simard (@dmsimard)" +short_description: Manages Sensu handler configuration +version_added: 2.4 +description: + - Manages Sensu handler configuration + - 'For more information, refer to the Sensu documentation: U(https://sensuapp.org/docs/latest/reference/handlers.html)' +options: + state: + description: + - Whether the handler should be present or not + choices: [ 'present', 'absent' ] + required: False + default: present + name: + description: + - A unique name for the handler. The name cannot contain special characters or spaces. + required: True + default: null + type: + description: + - The handler type + choices: [ 'pipe', 'tcp', 'udp', 'transport', 'set' ] + required: True + default: null + filter: + description: + - The Sensu event filter (name) to use when filtering events for the handler. + required: False + default: null + filters: + description: + - An array of Sensu event filters (names) to use when filtering events for the handler. + - Each array item must be a string. + required: False + default: null + severities: + description: + - An array of check result severities the handler will handle. + - 'NOTE: event resolution bypasses this filtering.' + choices: [ 'warning', 'critical', 'unknown' ] + required: False + default: null + mutator: + description: + - The Sensu event mutator (name) to use to mutate event data for the handler. + required: False + default: null + timeout: + description: + - The handler execution duration timeout in seconds (hard stop). + - Only used by pipe and tcp handler types. + required: False + default: 10 + handle_silenced: + description: + - If events matching one or more silence entries should be handled. + required: False + default: False + handle_flapping: + description: + - If events in the flapping state should be handled. + required: False + default: False + command: + description: + - The handler command to be executed. + - The event data is passed to the process via STDIN. + - 'NOTE: the command attribute is only required for Pipe handlers (i.e. handlers configured with "type": "pipe").' + required: False + default: null + socket: + description: + - The socket definition scope, used to configure the TCP/UDP handler socket. + - 'NOTE: the socket attribute is only required for TCP/UDP handlers (i.e. handlers configured with "type": "tcp" or "type": "udp").' + required: False + default: null + pipe: + description: + - The pipe definition scope, used to configure the Sensu transport pipe. + - 'NOTE: the pipe attribute is only required for Transport handlers (i.e. handlers configured with "type": "transport").' + required: False + default: null + handlers: + description: + - An array of Sensu event handlers (names) to use for events using the handler set. + - Each array item must be a string. + - 'NOTE: the handlers attribute is only required for handler sets (i.e. handlers configured with "type": "set").' + required: True + default: null +notes: + - Check mode is supported +requirements: [ ] +''' + +EXAMPLES = ''' +# Configure a handler that sends event data as STDIN (pipe) +- name: Configure IRC Sensu handler + sensu_handler: + name: "irc_handler" + type: "pipe" + command: "/usr/local/bin/notify-irc.sh" + severities: + - "ok" + - "critical" + - "warning" + - "unknown" + timeout: 15 + notify: + - Restart sensu-client + - Restart sensu-server + +# Delete a handler +- name: Delete IRC Sensu handler + sensu_handler: + name: "irc_handler" + state: "absent" + +# Example of a TCP handler +- name: Configure TCP Sensu handler + sensu_handler: + name: "tcp_handler" + type: "tcp" + timeout: 30 + socket: + host: "10.0.1.99" + port: 4444 + register: handler + notify: + - Restart sensu-client + - Restart sensu-server + +- name: Secure Sensu handler configuration file + file: + path: "{{ handler['file'] }}" + owner: "sensu" + group: "sensu" + mode: "0600" +''' + +RETURN = ''' +config: + description: Effective handler configuration, when state is present + returned: success + type: dict + sample: {'name': 'irc', 'type': 'pipe', 'command': '/usr/local/bin/notify-irc.sh'} +file: + description: Path to the handler configuration file + returned: success + type: string + sample: "/etc/sensu/conf.d/handlers/irc.json" +name: + description: Name of the handler + returned: success + type: string + sample: "irc" +''' + +from ansible.module_utils.basic import AnsibleModule +import json +import os + + +def main(): + module = AnsibleModule( + supports_check_mode=True, + argument_spec=dict( + state=dict(type='str', required=False, choices=['present', 'absent'], default='present'), + name=dict(type='str', required=True), + type=dict(type='str', required=False, choices=['pipe', 'tcp', 'udp', 'transport', 'set']), + filter=dict(type='str', required=False), + filters=dict(type='list', required=False), + severities=dict(type='list', required=False), + mutator=dict(type='str', required=False), + timeout=dict(type='int', required=False, default=10), + handle_silenced=dict(type='bool', required=False, default=False), + handle_flapping=dict(type='bool', required=False, default=False), + command=dict(type='str', required=False), + socket=dict(type='dict', required=False), + pipe=dict(type='dict', required=False), + handlers=dict(type='list', required=False), + ), + required_if=[ + ['state', 'present', ['type']], + ['type', 'pipe', ['command']], + ['type', 'tcp', ['socket']], + ['type', 'udp', ['socket']], + ['type', 'transport', ['pipe']], + ['type', 'set', ['handlers']] + ] + ) + + state = module.params['state'] + name = module.params['name'] + path = '/etc/sensu/conf.d/handlers/{0}.json'.format(name) + + if state == 'absent': + if os.path.exists(path): + if module.check_mode: + msg = '{path} would have been deleted'.format(path=path) + module.exit_json(msg=msg, changed=True) + else: + try: + os.remove(path) + msg = '{path} deleted successfully'.format(path=path) + module.exit_json(msg=msg, changed=True) + except OSError as e: + msg = 'Exception when trying to delete {path}: {exception}' + module.fail_json( + msg=msg.format(path=path, exception=str(e))) + else: + # Idempotency: it's okay if the file doesn't exist + msg = '{path} already does not exist'.format(path=path) + module.exit_json(msg=msg) + + # Build handler configuration from module arguments + config = {'handlers': {name: {}}} + args = ['type', 'filter', 'filters', 'severities', 'mutator', 'timeout', + 'handle_silenced', 'handle_flapping', 'command', 'socket', + 'pipe', 'handlers'] + + for arg in args: + if arg in module.params and module.params[arg] is not None: + config['handlers'][name][arg] = module.params[arg] + + # Load the current config, if there is one, so we can compare + current_config = None + try: + current_config = json.load(open(path, 'r')) + except (IOError, ValueError): + # File either doesn't exist or it's invalid JSON + pass + + if current_config is not None and current_config == config: + # Config is the same, let's not change anything + module.exit_json(msg='Handler configuration is already up to date', + config=config['handlers'][name], + file=path, + name=name) + + # Validate that directory exists before trying to write to it + if not module.check_mode and not os.path.exists(os.path.dirname(path)): + try: + os.makedirs(os.path.dirname(path)) + except OSError as e: + module.fail_json(msg='Unable to create {0}: {1}'.format(os.path.dirname(path), + str(e))) + + if module.check_mode: + module.exit_json(msg='Handler configuration would have been updated', + changed=True, + config=config['handlers'][name], + file=path, + name=name) + + try: + with open(path, 'w') as handler: + handler.write(json.dumps(config, indent=4)) + module.exit_json(msg='Handler configuration updated', + changed=True, + config=config['handlers'][name], + file=path, + name=name) + except (OSError, IOError) as e: + module.fail_json(msg='Unable to write file {0}: {1}'.format(path, + str(e))) + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/sensu_handler/aliases b/test/integration/targets/sensu_handler/aliases new file mode 100644 index 00000000000..fcb0d0ec825 --- /dev/null +++ b/test/integration/targets/sensu_handler/aliases @@ -0,0 +1 @@ +posix/ci/group1 \ No newline at end of file diff --git a/test/integration/targets/sensu_handler/tasks/main.yml b/test/integration/targets/sensu_handler/tasks/main.yml new file mode 100644 index 00000000000..8a8f896bdf7 --- /dev/null +++ b/test/integration/targets/sensu_handler/tasks/main.yml @@ -0,0 +1,120 @@ +- name: Creating a handler if the directory doesn't exist should work + sensu_handler: + name: "handler" + type: "pipe" + command: "/bin/bash" + state: "present" + +- name: Insert junk JSON in a handlers file + lineinfile: + state: "present" + create: "yes" + path: "/etc/sensu/conf.d/handlers/handler.json" + line: "{'foo' = bar}" + +- name: Configure a handler with an existing invalid file + sensu_handler: + name: "handler" + type: "pipe" + command: "/bin/bash" + state: "present" + register: handler + +- name: Configure a handler (again) + sensu_handler: + name: "handler" + type: "pipe" + command: "/bin/bash" + state: "present" + register: handler_twice + +- name: Retrieve configuration file stat + stat: + path: "{{ handler['file'] }}" + register: handler_stat + +- name: Assert that handler data was set successfully and properly + assert: + that: + - "handler | success" + - "handler | changed" + - "handler_twice | success" + - "not handler_twice | changed" + - "handler_stat.stat.exists == true" + - "handler['name'] == 'handler'" + - "handler['file'] == '/etc/sensu/conf.d/handlers/handler.json'" + - "handler['config']['type'] == 'pipe'" + - "handler['config']['command'] == '/bin/bash'" + - "handler['config']['timeout'] == 10" + - "handler['config']['handle_flapping'] == false" + - "handler['config']['handle_silenced'] == false" + +- name: Assert that the handler configuration file is actually configured properly + vars: + config: "{{ lookup('file', handler['file']) | from_json }}" + assert: + that: + - "'handler' in config['handlers']" + - "config['handlers']['handler']['type'] == 'pipe'" + - "config['handlers']['handler']['command'] == '/bin/bash'" + - "config['handlers']['handler']['timeout'] == 10" + - "config['handlers']['handler']['handle_flapping'] == false" + - "config['handlers']['handler']['handle_silenced'] == false" + +- name: Delete Sensu handler configuration + sensu_handler: + name: "handler" + state: "absent" + register: handler_delete + +- name: Delete Sensu handler configuration (again) + sensu_handler: + name: "handler" + state: "absent" + register: handler_delete_twice + +- name: Retrieve configuration file stat + stat: + path: "{{ handler['file'] }}" + register: handler_stat + +- name: Assert that handler deletion was successful + assert: + that: + - "handler_delete | success" + - "handler_delete | changed" + - "handler_delete_twice | success" + - "not handler_delete_twice | changed" + - "handler_stat.stat.exists == false" + +- name: Configuring a handler without a name should fail + sensu_handler: + type: "pipe" + command: "/bin/bash" + register: failure + ignore_errors: true + +- name: Assert that configuring a handler without a name fails + assert: + that: + - failure | failed + - "'required arguments: name' in failure['msg']" + +- name: Configuring a handler without a type should fail + sensu_handler: + name: "pipe" + command: "/bin/bash" + register: failure + ignore_errors: true + +- name: Assert that configuring a handler without a type fails + assert: + that: + - failure | failed + - "'the following are missing: type' in failure['msg']" + +- include: pipe.yml +- include: tcp.yml +- include: udp.yml +- include: set.yml +- include: transport.yml diff --git a/test/integration/targets/sensu_handler/tasks/pipe.yml b/test/integration/targets/sensu_handler/tasks/pipe.yml new file mode 100644 index 00000000000..10a80a20108 --- /dev/null +++ b/test/integration/targets/sensu_handler/tasks/pipe.yml @@ -0,0 +1,20 @@ +# Note: Pipe handlers are also tested and used as part of basic main.yml coverage +- name: Configuring a handler with missing pipe parameters should fail + sensu_handler: + name: "pipe" + type: "pipe" + register: failure + ignore_errors: true + +- name: Assert that configuring a handler with missing pipe parameters fails + assert: + that: + - failure | failed + - "'the following are missing: command' in failure['msg']" + +- name: Configure a handler with pipe parameters + sensu_handler: + name: "pipe" + type: "pipe" + command: "/bin/bash" + register: handler diff --git a/test/integration/targets/sensu_handler/tasks/set.yml b/test/integration/targets/sensu_handler/tasks/set.yml new file mode 100644 index 00000000000..71c7747024b --- /dev/null +++ b/test/integration/targets/sensu_handler/tasks/set.yml @@ -0,0 +1,49 @@ +- name: Configuring a handler with missing set parameters should fail + sensu_handler: + name: "set" + type: "set" + register: failure + ignore_errors: true + +- name: Assert that configuring a handler with missing set parameters fails + assert: + that: + - failure | failed + - "'the following are missing: handlers' in failure['msg']" + +- name: Configure a set handler + sensu_handler: + name: "set" + type: "set" + handlers: + - anotherhandler + register: handler + +- name: Retrieve configuration file stat + stat: + path: "{{ handler['file'] }}" + register: handler_stat + +- name: Validate set handler return data + assert: + that: + - "handler | success" + - "handler | changed" + - "handler_stat.stat.exists == true" + - "handler['name'] == 'set'" + - "handler['file'] == '/etc/sensu/conf.d/handlers/set.json'" + - "handler['config']['type'] == 'set'" + - "'anotherhandler' in handler['config']['handlers']" + - "handler['config']['handle_flapping'] == false" + - "handler['config']['handle_silenced'] == false" + +- name: Assert that the handler configuration file is actually configured properly + vars: + config: "{{ lookup('file', handler['file']) | from_json }}" + assert: + that: + - "'set' in config['handlers']" + - "config['handlers']['set']['type'] == 'set'" + - "'anotherhandler' in config['handlers']['set']['handlers']" + - "config['handlers']['set']['handle_flapping'] == false" + - "config['handlers']['set']['handle_silenced'] == false" diff --git a/test/integration/targets/sensu_handler/tasks/tcp.yml b/test/integration/targets/sensu_handler/tasks/tcp.yml new file mode 100644 index 00000000000..12791c2e318 --- /dev/null +++ b/test/integration/targets/sensu_handler/tasks/tcp.yml @@ -0,0 +1,52 @@ +- name: Configuring a handler with missing tcp parameters should fail + sensu_handler: + name: "tcp" + type: "tcp" + register: failure + ignore_errors: true + +- name: Assert that configuring a handler with missing tcp parameters fails + assert: + that: + - failure | failed + - "'the following are missing: socket' in failure['msg']" + +- name: Configure a tcp handler + sensu_handler: + name: "tcp" + type: "tcp" + socket: + host: 127.0.0.1 + port: 8000 + register: handler + +- name: Retrieve configuration file stat + stat: + path: "{{ handler['file'] }}" + register: handler_stat + +- name: Validate tcp handler return data + assert: + that: + - "handler | success" + - "handler | changed" + - "handler_stat.stat.exists == true" + - "handler['name'] == 'tcp'" + - "handler['file'] == '/etc/sensu/conf.d/handlers/tcp.json'" + - "handler['config']['type'] == 'tcp'" + - "handler['config']['socket']['host'] == '127.0.0.1'" + - "handler['config']['socket']['port'] == 8000" + - "handler['config']['handle_flapping'] == false" + - "handler['config']['handle_silenced'] == false" + +- name: Assert that the handler configuration file is actually configured properly + vars: + config: "{{ lookup('file', handler['file']) | from_json }}" + assert: + that: + - "'tcp' in config['handlers']" + - "config['handlers']['tcp']['type'] == 'tcp'" + - "config['handlers']['tcp']['socket']['host'] == '127.0.0.1'" + - "config['handlers']['tcp']['socket']['port'] == 8000" + - "config['handlers']['tcp']['handle_flapping'] == false" + - "config['handlers']['tcp']['handle_silenced'] == false" diff --git a/test/integration/targets/sensu_handler/tasks/transport.yml b/test/integration/targets/sensu_handler/tasks/transport.yml new file mode 100644 index 00000000000..0083d384b3d --- /dev/null +++ b/test/integration/targets/sensu_handler/tasks/transport.yml @@ -0,0 +1,52 @@ +- name: Configuring a handler with missing transport parameters should fail + sensu_handler: + name: "transport" + type: "transport" + register: failure + ignore_errors: true + +- name: Assert that configuring a handler with missing transport parameters fails + assert: + that: + - failure | failed + - "'the following are missing: pipe' in failure['msg']" + +- name: Configure a transport handler + sensu_handler: + name: "transport" + type: "transport" + pipe: + type: "topic" + name: "transport_handler" + register: handler + +- name: Retrieve configuration file stat + stat: + path: "{{ handler['file'] }}" + register: handler_stat + +- name: Validate transport handler return data + assert: + that: + - "handler | success" + - "handler | changed" + - "handler_stat.stat.exists == true" + - "handler['name'] == 'transport'" + - "handler['file'] == '/etc/sensu/conf.d/handlers/transport.json'" + - "handler['config']['type'] == 'transport'" + - "handler['config']['pipe']['type'] == 'topic'" + - "handler['config']['pipe']['name'] == 'transport_handler'" + - "handler['config']['handle_flapping'] == false" + - "handler['config']['handle_silenced'] == false" + +- name: Assert that the handler configuration file is actually configured properly + vars: + config: "{{ lookup('file', handler['file']) | from_json }}" + assert: + that: + - "'transport' in config['handlers']" + - "config['handlers']['transport']['type'] == 'transport'" + - "config['handlers']['transport']['pipe']['type'] == 'topic'" + - "config['handlers']['transport']['pipe']['name'] == 'transport_handler'" + - "config['handlers']['transport']['handle_flapping'] == false" + - "config['handlers']['transport']['handle_silenced'] == false" diff --git a/test/integration/targets/sensu_handler/tasks/udp.yml b/test/integration/targets/sensu_handler/tasks/udp.yml new file mode 100644 index 00000000000..d95cfc00ae6 --- /dev/null +++ b/test/integration/targets/sensu_handler/tasks/udp.yml @@ -0,0 +1,52 @@ +- name: Configuring a handler with missing udp parameters should fail + sensu_handler: + name: "udp" + type: "udp" + register: failure + ignore_errors: true + +- name: Assert that configuring a handler with missing udp parameters fails + assert: + that: + - failure | failed + - "'the following are missing: socket' in failure['msg']" + +- name: Configure a udp handler + sensu_handler: + name: "udp" + type: "udp" + socket: + host: 127.0.0.1 + port: 8000 + register: handler + +- name: Retrieve configuration file stat + stat: + path: "{{ handler['file'] }}" + register: handler_stat + +- name: Validate udp handler return data + assert: + that: + - "handler | success" + - "handler | changed" + - "handler_stat.stat.exists == true" + - "handler['name'] == 'udp'" + - "handler['file'] == '/etc/sensu/conf.d/handlers/udp.json'" + - "handler['config']['type'] == 'udp'" + - "handler['config']['socket']['host'] == '127.0.0.1'" + - "handler['config']['socket']['port'] == 8000" + - "handler['config']['handle_flapping'] == false" + - "handler['config']['handle_silenced'] == false" + +- name: Assert that the handler configuration file is actually configured properly + vars: + config: "{{ lookup('file', handler['file']) | from_json }}" + assert: + that: + - "'udp' in config['handlers']" + - "config['handlers']['udp']['type'] == 'udp'" + - "config['handlers']['udp']['socket']['host'] == '127.0.0.1'" + - "config['handlers']['udp']['socket']['port'] == 8000" + - "config['handlers']['udp']['handle_flapping'] == false" + - "config['handlers']['udp']['handle_silenced'] == false"