diff --git a/lib/ansible/modules/monitoring/sensu_client.py b/lib/ansible/modules/monitoring/sensu_client.py new file mode 100644 index 00000000000..2bc7de0d5a6 --- /dev/null +++ b/lib/ansible/modules/monitoring/sensu_client.py @@ -0,0 +1,277 @@ +#!/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_client +author: "David Moreau Simard (@dmsimard)" +short_description: Manages Sensu client configuration +version_added: 2.4 +description: + - Manages Sensu client configuration. + - 'For more information, refer to the Sensu documentation: U(https://sensuapp.org/docs/latest/reference/clients.html)' +options: + state: + description: + - Whether the client should be present or not + choices: [ 'present', 'absent' ] + required: False + default: present + name: + description: + - A unique name for the client. The name cannot contain special characters or spaces. + required: False + default: System hostname as determined by Ruby Socket.gethostname (provided by Sensu) + address: + description: + - An address to help identify and reach the client. This is only informational, usually an IP address or hostname. + required: False + default: Non-loopback IPv4 address as determined by Ruby Socket.ip_address_list (provided by Sensu) + subscriptions: + description: + - An array of client subscriptions, a list of roles and/or responsibilities assigned to the system (e.g. webserver). + - These subscriptions determine which monitoring checks are executed by the client, as check requests are sent to subscriptions. + - The subscriptions array items must be strings. + required: True + default: null + safe_mode: + description: + - If safe mode is enabled for the client. Safe mode requires local check definitions in order to accept a check request and execute the check. + choices: [ 'true', 'false' ] + required: False + default: false + redact: + description: + - Client definition attributes to redact (values) when logging and sending client keepalives. + required: False + default: null + socket: + description: + - The socket definition scope, used to configure the Sensu client socket. + required: False + default: null + keepalives: + description: + - If Sensu should monitor keepalives for this client. + choices: [ 'true', 'false' ] + required: False + default: true + keepalive: + description: + - The keepalive definition scope, used to configure Sensu client keepalives behavior (e.g. keepalive thresholds, etc). + required: False + default: null + registration: + description: + - The registration definition scope, used to configure Sensu registration event handlers. + required: False + default: null + deregister: + description: + - If a deregistration event should be created upon Sensu client process stop. + choices: [ 'true', 'false' ] + required: False + default: false + deregistration: + description: + - The deregistration definition scope, used to configure automated Sensu client de-registration. + required: False + default: null + ec2: + description: + - The ec2 definition scope, used to configure the Sensu Enterprise AWS EC2 integration (Sensu Enterprise users only). + required: False + default: null + chef: + description: + - The chef definition scope, used to configure the Sensu Enterprise Chef integration (Sensu Enterprise users only). + required: False + default: null + puppet: + description: + - The puppet definition scope, used to configure the Sensu Enterprise Puppet integration (Sensu Enterprise users only). + required: False + default: null + servicenow: + description: + - The servicenow definition scope, used to configure the Sensu Enterprise ServiceNow integration (Sensu Enterprise users only). + required: False + default: null +notes: + - Check mode is supported +requirements: [ ] +''' + +EXAMPLES = ''' +# Minimum possible configuration +- name: Configure Sensu client + sensu_client: + subscriptions: + - default + +# With customization +- name: Configure Sensu client + sensu_client: + name: "{{ ansible_fqdn }}" + address: "{{ ansible_default_ipv4['address'] }}" + subscriptions: + - default + - webserver + redact: + - password + socket: + bind: 127.0.0.1 + port: 3030 + keepalive: + thresholds: + warning: 180 + critical: 300 + handlers: + - email + custom: + - broadcast: irc + occurrences: 3 + register: client + notify: + - Restart sensu-client + +- name: Secure Sensu client configuration file + file: + path: "{{ client['file'] }}" + owner: "sensu" + group: "sensu" + mode: "0600" + +- name: Delete the Sensu client configuration + sensu_client: + state: "absent" +''' + +RETURN = ''' +config: + description: Effective client configuration, when state is present + returned: success + type: dict + sample: {'name': 'client', 'subscriptions': ['default']} +file: + description: Path to the client configuration file + returned: success + type: string + sample: "/etc/sensu/conf.d/client.json" +''' + +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=False), + address=dict(type='str', required=False), + subscriptions=dict(type='list', required=False), + safe_mode=dict(type='bool', required=False, default=False), + redact=dict(type='list', required=False), + socket=dict(type='dict', required=False), + keepalives=dict(type='bool', required=False, default=True), + keepalive=dict(type='dict', required=False), + registration=dict(type='dict', required=False), + deregister=dict(type='bool', required=False), + deregistration=dict(type='dict', required=False), + ec2=dict(type='dict', required=False), + chef=dict(type='dict', required=False), + puppet=dict(type='dict', required=False), + servicenow=dict(type='dict', required=False) + ), + required_if=[ + ['state', 'present', ['subscriptions']] + ] + ) + + state = module.params['state'] + path = "/etc/sensu/conf.d/client.json" + + 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 client configuration from module arguments + config = {'client': {}} + args = ['name', 'address', 'subscriptions', 'safe_mode', 'redact', + 'socket', 'keepalives', 'keepalive', 'registration', 'deregister', + 'deregistration', 'ec2', 'chef', 'puppet', 'servicenow'] + + for arg in args: + if arg in module.params and module.params[arg] is not None: + config['client'][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='Client configuration is already up to date', + config=config['client'], + file=path) + + # 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='Client configuration would have been updated', + changed=True, + config=config['client'], + file=path) + + try: + with open(path, 'w') as client: + client.write(json.dumps(config, indent=4)) + module.exit_json(msg='Client configuration updated', + changed=True, + config=config['client'], + file=path) + 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_client/aliases b/test/integration/targets/sensu_client/aliases new file mode 100644 index 00000000000..fcb0d0ec825 --- /dev/null +++ b/test/integration/targets/sensu_client/aliases @@ -0,0 +1 @@ +posix/ci/group1 \ No newline at end of file diff --git a/test/integration/targets/sensu_client/tasks/main.yml b/test/integration/targets/sensu_client/tasks/main.yml new file mode 100644 index 00000000000..1eff406ccb1 --- /dev/null +++ b/test/integration/targets/sensu_client/tasks/main.yml @@ -0,0 +1,171 @@ +- name: Creating a client if the directory doesn't exist should work + sensu_client: + subscriptions: + - default + +- name: Set variable for client file + set_fact: + client_file: "/etc/sensu/conf.d/client.json" + +- name: Insert invalid JSON in the client file + lineinfile: + state: "present" + create: "yes" + path: "{{ client_file }}" + line: "{'foo' = bar}" + +- name: Configure Sensu client with an existing invalid file + sensu_client: + name: "client" + state: "present" + subscriptions: + - default + register: client + +- name: Retrieve configuration file stat + stat: + path: "{{ client_file }}" + register: client_stat + +- name: Assert that client data was set successfully and properly + assert: + that: + - "client | success" + - "client | changed" + - "client_stat.stat.exists == true" + - "client['config']['name'] == 'client'" + - "'default' in client['config']['subscriptions']" + - "client['file'] == client_file" + +- name: Assert that the client configuration file is actually configured properly + vars: + config: "{{ lookup('file', client_file) | from_json }}" + assert: + that: + - "config['client']['keepalives'] == true" + - "config['client']['name'] == 'client'" + - "config['client']['safe_mode'] == false" + - "'default' in config['client']['subscriptions']" + +- name: Delete Sensu client configuration + sensu_client: + state: "absent" + register: client_delete + +- name: Delete Sensu client configuration (again) + sensu_client: + state: "absent" + register: client_delete_twice + +- name: Retrieve configuration file stat + stat: + path: "{{ client_file }}" + register: client_stat + +- name: Assert that client deletion was successful + assert: + that: + - "client_delete | success" + - "client_delete | changed" + - "client_delete_twice | success" + - "not client_delete_twice | changed" + - "client_stat.stat.exists == false" + +- name: Configuring a client without subscriptions should fail + sensu_client: + name: "failure" + register: failure + ignore_errors: true + +- name: Assert failure to create client + assert: + that: + - failure | failed + - "'the following are missing: subscriptions' in failure['msg']" + +- name: Configure a new client from scratch with custom parameters + sensu_client: + name: "custom" + address: "host.fqdn" + subscriptions: + - "default" + - "webserver" + redact: + - "password" + socket: + bind: "127.0.0.1" + port: "3030" + keepalive: + thresholds: + warning: "180" + critical: "300" + handlers: + - "email" + custom: + - broadcast: "irc" + occurrences: "3" + register: client + +- name: Configure a new client from scratch with custom parameters (twice) + sensu_client: + name: "custom" + address: "host.fqdn" + subscriptions: + - "default" + - "webserver" + redact: + - "password" + socket: + bind: "127.0.0.1" + port: "3030" + keepalive: + thresholds: + warning: "180" + critical: "300" + handlers: + - "email" + custom: + - broadcast: "irc" + occurrences: "3" + register: client_twice + +- name: Retrieve configuration file stat + stat: + path: "{{ client_file }}" + register: client_stat + +- name: Assert that client data was set successfully and properly + assert: + that: + - "client | success" + - "client | changed" + - "client_twice | success" + - "not client_twice | changed" + - "client_stat.stat.exists == true" + - "client['config']['name'] == 'custom'" + - "client['config']['address'] == 'host.fqdn'" + - "'default' in client['config']['subscriptions']" + - "'webserver' in client['config']['subscriptions']" + - "'password' in client['config']['redact']" + - "client['config']['keepalive']['thresholds']['warning'] == '180'" + - "client['config']['keepalive']['thresholds']['critical'] == '300'" + - "'email' in client['config']['keepalive']['handlers']" + - "client['config']['keepalive']['occurrences'] == '3'" + - "client['file'] == client_file" + +- name: Assert that the client configuration file is actually configured properly + vars: + config: "{{ lookup('file', client_file) | from_json }}" + assert: + that: + - "config['client']['name'] == 'custom'" + - "config['client']['address'] == 'host.fqdn'" + - "config['client']['keepalives'] == true" + - "config['client']['safe_mode'] == false" + - "'default' in config['client']['subscriptions']" + - "'webserver' in config['client']['subscriptions']" + - "'password' in config['client']['redact']" + - "config['client']['keepalive']['thresholds']['warning'] == '180'" + - "config['client']['keepalive']['thresholds']['critical'] == '300'" + - "'email' in config['client']['keepalive']['handlers']" + - "config['client']['keepalive']['occurrences'] == '3'"