#!/usr/bin/python

# Copyright (c) 2014, Anders Ingemann <aim@secoya.dk>
# 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 annotations


DOCUMENTATION = r"""
module: sensu_check
short_description: Manage Sensu checks
description:
  - Manage the checks that should be run on a machine by I(Sensu).
  - Most options do not have a default and are not added to the check definition unless specified.
  - All defaults except O(path), O(state), O(backup) and O(metric) are not managed by this module, they are simply specified
    for your convenience.
deprecated:
  removed_in: 13.0.0
  why: Sensu Core and Sensu Enterprise products have been End of Life since 2019/20.
  alternative: Use Sensu Go and its accompanying collection C(sensu.sensu_go).
extends_documentation_fragment:
  - community.general.attributes
attributes:
  check_mode:
    support: full
  diff_mode:
    support: none
options:
  name:
    type: str
    description:
      - The name of the check.
      - This is the key that is used to determine whether a check exists.
    required: true
  state:
    type: str
    description:
      - Whether the check should be present or not.
    choices: ['present', 'absent']
    default: present
  path:
    type: str
    description:
      - Path to the JSON file of the check to be added/removed.
      - It is created if it does not exist (unless O(state=absent)).
      - The parent folders need to exist when O(state=present), otherwise an error is thrown.
    default: /etc/sensu/conf.d/checks.json
  backup:
    description:
      - Create a backup file (if yes), including the timestamp information so you can get the original file back if you somehow
        clobbered it incorrectly.
    type: bool
    default: false
  command:
    type: str
    description:
      - Path to the sensu check to run (not required when O(state=absent)).
  handlers:
    type: list
    elements: str
    description:
      - List of handlers to notify when the check fails.
  subscribers:
    type: list
    elements: str
    description:
      - List of subscribers/channels this check should run for.
      - See sensu_subscribers to subscribe a machine to a channel.
  interval:
    type: int
    description:
      - Check interval in seconds.
  timeout:
    type: int
    description:
      - Timeout for the check.
      - If not specified, it defaults to 10.
  ttl:
    type: int
    description:
      - Time to live in seconds until the check is considered stale.
  handle:
    description:
      - Whether the check should be handled or not.
      - Default is V(false).
    type: bool
  subdue_begin:
    type: str
    description:
      - When to disable handling of check failures.
  subdue_end:
    type: str
    description:
      - When to enable handling of check failures.
  dependencies:
    type: list
    elements: str
    description:
      - Other checks this one depends on.
      - If dependencies fail handling of this check is disabled.
  metric:
    description:
      - Whether the check is a metric.
    type: bool
    default: false
  standalone:
    description:
      - Whether the check should be scheduled by the sensu client or server.
      - This option obviates the need for specifying the O(subscribers) option.
      - Default is V(false).
    type: bool
  publish:
    description:
      - Whether the check should be scheduled at all.
      - You can still issue it using the sensu API.
      - Default is V(false).
    type: bool
  occurrences:
    type: int
    description:
      - Number of event occurrences before the handler should take action.
      - If not specified, defaults to 1.
  refresh:
    type: int
    description:
      - Number of seconds handlers should wait before taking second action.
  aggregate:
    description:
      - Classifies the check as an aggregate check, making it available using the aggregate API.
      - Default is V(false).
    type: bool
  low_flap_threshold:
    type: int
    description:
      - The low threshold for flap detection.
  high_flap_threshold:
    type: int
    description:
      - The high threshold for flap detection.
  custom:
    type: dict
    description:
      - A hash/dictionary of custom parameters for mixing to the configuration.
      - You cannot rewrite other module parameters using this.
  source:
    type: str
    description:
      - The check source, used to create a JIT Sensu client for an external resource (for example a network switch).
author: "Anders Ingemann (@andsens)"
"""

EXAMPLES = r"""
# Fetch metrics about the CPU load every 60 seconds,
# the sensu server has a handler called 'relay' which forwards stats to graphite
- name: Get cpu metrics
  community.general.sensu_check:
    name: cpu_load
    command: /etc/sensu/plugins/system/cpu-mpstat-metrics.rb
    metric: true
    handlers: relay
    subscribers: common
    interval: 60

# Check whether nginx is running
- name: Check nginx process
  community.general.sensu_check:
    name: nginx_running
    command: /etc/sensu/plugins/processes/check-procs.rb -f /var/run/nginx.pid
    handlers: default
    subscribers: nginx
    interval: 60

# Stop monitoring the disk capacity.
# Note that the check will still show up in the sensu dashboard,
# to remove it completely you need to issue a DELETE request to the sensu api.
- name: Check disk
  community.general.sensu_check:
    name: check_disk_capacity
    state: absent
"""

import json
import traceback

from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.text.converters import to_native


def sensu_check(module, path, name, state="present", backup=False):
    changed = False
    reasons = []

    stream = None
    try:
        try:
            stream = open(path, "r")
            config = json.load(stream)
        except IOError as e:
            if e.errno == 2:  # File not found, non-fatal
                if state == "absent":
                    reasons.append("file did not exist and state is `absent'")
                    return changed, reasons
                config = {}
            else:
                module.fail_json(msg=to_native(e), exception=traceback.format_exc())
        except ValueError:
            msg = f"{path} contains invalid JSON"
            module.fail_json(msg=msg)
    finally:
        if stream:
            stream.close()

    if "checks" not in config:
        if state == "absent":
            reasons.append("`checks' section did not exist and state is `absent'")
            return changed, reasons
        config["checks"] = {}
        changed = True
        reasons.append("`checks' section did not exist")

    if state == "absent":
        if name in config["checks"]:
            del config["checks"][name]
            changed = True
            reasons.append("check was present and state is `absent'")

    if state == "present":
        if name not in config["checks"]:
            check = {}
            config["checks"][name] = check
            changed = True
            reasons.append("check was absent and state is `present'")
        else:
            check = config["checks"][name]
        simple_opts = [
            "command",
            "handlers",
            "subscribers",
            "interval",
            "timeout",
            "ttl",
            "handle",
            "dependencies",
            "standalone",
            "publish",
            "occurrences",
            "refresh",
            "aggregate",
            "low_flap_threshold",
            "high_flap_threshold",
            "source",
        ]
        for opt in simple_opts:
            if module.params[opt] is not None:
                if opt not in check or check[opt] != module.params[opt]:
                    check[opt] = module.params[opt]
                    changed = True
                    reasons.append(f"`{opt}' did not exist or was different")
            else:
                if opt in check:
                    del check[opt]
                    changed = True
                    reasons.append(f"`{opt}' was removed")

        if module.params["custom"]:
            # Convert to json
            custom_params = module.params["custom"]
            overwrited_fields = set(custom_params.keys()) & set(
                simple_opts + ["type", "subdue", "subdue_begin", "subdue_end"]
            )
            if overwrited_fields:
                msg = f'You can\'t overwriting standard module parameters via "custom". You are trying overwrite: {list(overwrited_fields)}'
                module.fail_json(msg=msg)

            for k, v in custom_params.items():
                if k in config["checks"][name]:
                    if config["checks"][name][k] != v:
                        changed = True
                        reasons.append(f"`custom param {k}' was changed")
                else:
                    changed = True
                    reasons.append(f"`custom param {k}' was added")
                check[k] = v
            simple_opts += custom_params.keys()

        # Remove obsolete custom params
        for opt in set(config["checks"][name].keys()) - set(
            simple_opts + ["type", "subdue", "subdue_begin", "subdue_end"]
        ):
            changed = True
            reasons.append(f"`custom param {opt}' was deleted")
            del check[opt]

        if module.params["metric"]:
            if "type" not in check or check["type"] != "metric":
                check["type"] = "metric"
                changed = True
                reasons.append("`type' was not defined or not `metric'")
        if not module.params["metric"] and "type" in check:
            del check["type"]
            changed = True
            reasons.append("`type' was defined")

        if module.params["subdue_begin"] is not None and module.params["subdue_end"] is not None:
            subdue = {
                "begin": module.params["subdue_begin"],
                "end": module.params["subdue_end"],
            }
            if "subdue" not in check or check["subdue"] != subdue:
                check["subdue"] = subdue
                changed = True
                reasons.append("`subdue' did not exist or was different")
        else:
            if "subdue" in check:
                del check["subdue"]
                changed = True
                reasons.append("`subdue' was removed")

    if changed and not module.check_mode:
        if backup:
            module.backup_local(path)
        try:
            try:
                stream = open(path, "w")
                stream.write(json.dumps(config, indent=2) + "\n")
            except IOError as e:
                module.fail_json(msg=to_native(e), exception=traceback.format_exc())
        finally:
            if stream:
                stream.close()

    return changed, reasons


def main():
    arg_spec = {
        "name": {"type": "str", "required": True},
        "path": {"type": "str", "default": "/etc/sensu/conf.d/checks.json"},
        "state": {"type": "str", "default": "present", "choices": ["present", "absent"]},
        "backup": {"type": "bool", "default": False},
        "command": {"type": "str"},
        "handlers": {"type": "list", "elements": "str"},
        "subscribers": {"type": "list", "elements": "str"},
        "interval": {"type": "int"},
        "timeout": {"type": "int"},
        "ttl": {"type": "int"},
        "handle": {"type": "bool"},
        "subdue_begin": {"type": "str"},
        "subdue_end": {"type": "str"},
        "dependencies": {"type": "list", "elements": "str"},
        "metric": {"type": "bool", "default": False},
        "standalone": {"type": "bool"},
        "publish": {"type": "bool"},
        "occurrences": {"type": "int"},
        "refresh": {"type": "int"},
        "aggregate": {"type": "bool"},
        "low_flap_threshold": {"type": "int"},
        "high_flap_threshold": {"type": "int"},
        "custom": {"type": "dict"},
        "source": {"type": "str"},
    }

    required_together = [["subdue_begin", "subdue_end"]]

    module = AnsibleModule(argument_spec=arg_spec, required_together=required_together, supports_check_mode=True)
    if module.params["state"] != "absent" and module.params["command"] is None:
        module.fail_json(msg="missing required arguments: command")

    path = module.params["path"]
    name = module.params["name"]
    state = module.params["state"]
    backup = module.params["backup"]

    changed, reasons = sensu_check(module, path, name, state, backup)

    module.exit_json(path=path, changed=changed, msg="OK", name=name, reasons=reasons)


if __name__ == "__main__":
    main()
