#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright (c) 2017, Red Hat Inc. # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import absolute_import, division, print_function __metaclass__ = type DOCUMENTATION = ''' --- module: sensu_client author: "David Moreau Simard (@dmsimard)" short_description: Manages Sensu client configuration description: - Manages Sensu client configuration. - 'For more information, refer to the Sensu documentation: U(https://sensuapp.org/docs/latest/reference/clients.html)' extends_documentation_fragment: - community.general.attributes attributes: check_mode: support: full diff_mode: support: none options: state: type: str description: - Whether the client should be present or not choices: [ 'present', 'absent' ] default: present name: type: str description: - A unique name for the client. The name cannot contain special characters or spaces. - If not specified, it defaults to the system hostname as determined by Ruby Socket.gethostname (provided by Sensu). address: type: str description: - An address to help identify and reach the client. This is only informational, usually an IP address or hostname. - If not specified it defaults to non-loopback IPv4 address as determined by Ruby Socket.ip_address_list (provided by Sensu). subscriptions: type: list elements: str 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. 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. type: bool default: false redact: type: list elements: str description: - Client definition attributes to redact (values) when logging and sending client keepalives. socket: type: dict description: - The socket definition scope, used to configure the Sensu client socket. keepalives: description: - If Sensu should monitor keepalives for this client. type: bool default: true keepalive: type: dict description: - The keepalive definition scope, used to configure Sensu client keepalives behavior (e.g. keepalive thresholds, etc). registration: type: dict description: - The registration definition scope, used to configure Sensu registration event handlers. deregister: description: - If a deregistration event should be created upon Sensu client process stop. - Default is C(false). type: bool deregistration: type: dict description: - The deregistration definition scope, used to configure automated Sensu client de-registration. ec2: type: dict description: - The ec2 definition scope, used to configure the Sensu Enterprise AWS EC2 integration (Sensu Enterprise users only). chef: type: dict description: - The chef definition scope, used to configure the Sensu Enterprise Chef integration (Sensu Enterprise users only). puppet: type: dict description: - The puppet definition scope, used to configure the Sensu Enterprise Puppet integration (Sensu Enterprise users only). servicenow: type: dict description: - The servicenow definition scope, used to configure the Sensu Enterprise ServiceNow integration (Sensu Enterprise users only). notes: - Check mode is supported ''' EXAMPLES = ''' # Minimum possible configuration - name: Configure Sensu client community.general.sensu_client: subscriptions: - default # With customization - name: Configure Sensu client community.general.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 ansible.builtin.file: path: "{{ client['file'] }}" owner: "sensu" group: "sensu" mode: "0600" - name: Delete the Sensu client configuration community.general.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: str sample: "/etc/sensu/conf.d/client.json" ''' import json import os from ansible.module_utils.basic import AnsibleModule def main(): module = AnsibleModule( supports_check_mode=True, argument_spec=dict( state=dict(type='str', choices=['present', 'absent'], default='present'), name=dict(type='str', ), address=dict(type='str', ), subscriptions=dict(type='list', elements="str"), safe_mode=dict(type='bool', default=False), redact=dict(type='list', elements="str"), socket=dict(type='dict'), keepalives=dict(type='bool', default=True), keepalive=dict(type='dict'), registration=dict(type='dict'), deregister=dict(type='bool'), deregistration=dict(type='dict'), ec2=dict(type='dict'), chef=dict(type='dict'), puppet=dict(type='dict'), servicenow=dict(type='dict') ), 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()