#!/usr/bin/python
#
# Copyright (c) 2018, Jean-Philippe Evrard <jean-philippe@evrard.me>
# 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: etcd3
short_description: Set or delete key value pairs from an etcd3 cluster
requirements:
  - etcd3
description:
  - Sets or deletes values in etcd3 cluster using its v3 API.
  - Needs python etcd3 lib to work.
extends_documentation_fragment:
  - community.general._attributes
attributes:
  check_mode:
    support: full
  diff_mode:
    support: none
options:
  key:
    type: str
    description:
      - The key where the information is stored in the cluster.
    required: true
  value:
    type: str
    description:
      - The information stored.
    required: true
  host:
    type: str
    description:
      - The IP address of the cluster.
    default: 'localhost'
  port:
    type: int
    description:
      - The port number used to connect to the cluster.
    default: 2379
  state:
    type: str
    description:
      - The state of the value for the key.
      - Can be present or absent.
    required: true
    choices: [present, absent]
  user:
    type: str
    description:
      - The etcd user to authenticate with.
  password:
    type: str
    description:
      - The password to use for authentication.
      - Required if O(user) is defined.
  ca_cert:
    type: path
    description:
      - The Certificate Authority to use to verify the etcd host.
      - Required if O(client_cert) and O(client_key) are defined.
  client_cert:
    type: path
    description:
      - PEM formatted certificate chain file to be used for SSL client authentication.
      - Required if O(client_key) is defined.
  client_key:
    type: path
    description:
      - PEM formatted file that contains your private key to be used for SSL client authentication.
      - Required if O(client_cert) is defined.
  timeout:
    type: int
    description:
      - The socket level timeout in seconds.
author:
  - Jean-Philippe Evrard (@evrardjp)
  - Victor Fauth (@vfauth)
"""

EXAMPLES = r"""
- name: Store a value "bar" under the key "foo" for a cluster located "http://localhost:2379"
  community.general.etcd3:
    key: "foo"
    value: "baz3"
    host: "localhost"
    port: 2379
    state: "present"

- name: Authenticate using user/password combination with a timeout of 10 seconds
  community.general.etcd3:
    key: "foo"
    value: "baz3"
    state: "present"
    user: "someone"
    password: "password123"
    timeout: 10

- name: Authenticate using TLS certificates
  community.general.etcd3:
    key: "foo"
    value: "baz3"
    state: "present"
    ca_cert: "/etc/ssl/certs/CA_CERT.pem"
    client_cert: "/etc/ssl/certs/cert.crt"
    client_key: "/etc/ssl/private/key.pem"
"""

RETURN = r"""
key:
  description: The key that was queried.
  returned: always
  type: str
old_value:
  description: The previous value in the cluster.
  returned: always
  type: str
"""

import traceback

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

try:
    import etcd3

    HAS_ETCD = True
    ETCD_IMP_ERR = None
except ImportError:
    ETCD_IMP_ERR = traceback.format_exc()
    HAS_ETCD = False


def run_module():
    # define the available arguments/parameters that a user can pass to
    # the module
    module_args = dict(
        key=dict(type="str", required=True, no_log=False),
        value=dict(type="str", required=True),
        host=dict(type="str", default="localhost"),
        port=dict(type="int", default=2379),
        state=dict(type="str", required=True, choices=["present", "absent"]),
        user=dict(type="str"),
        password=dict(type="str", no_log=True),
        ca_cert=dict(type="path"),
        client_cert=dict(type="path"),
        client_key=dict(type="path"),
        timeout=dict(type="int"),
    )

    # seed the result dict in the object
    # we primarily care about changed and state
    # change is if this module effectively modified the target
    # state will include any data that you want your module to pass back
    # for consumption, for example, in a subsequent task
    result = dict(
        changed=False,
    )

    # the AnsibleModule object will be our abstraction working with Ansible
    # this includes instantiation, a couple of common attr would be the
    # args/params passed to the execution, as well as if the module
    # supports check mode
    module = AnsibleModule(
        argument_spec=module_args,
        supports_check_mode=True,
        required_together=[["client_cert", "client_key"], ["user", "password"]],
    )

    # It is possible to set `ca_cert` to verify the server identity without
    # setting `client_cert` or `client_key` to authenticate the client
    # so required_together is enough
    # Due to `required_together=[['client_cert', 'client_key']]`, checking the presence
    # of either `client_cert` or `client_key` is enough
    if module.params["ca_cert"] is None and module.params["client_cert"] is not None:
        module.fail_json(msg="The 'ca_cert' parameter must be defined when 'client_cert' and 'client_key' are present.")

    result["key"] = module.params.get("key")
    module.params["cert_cert"] = module.params.pop("client_cert")
    module.params["cert_key"] = module.params.pop("client_key")

    if not HAS_ETCD:
        module.fail_json(msg=missing_required_lib("etcd3"), exception=ETCD_IMP_ERR)

    allowed_keys = ["host", "port", "ca_cert", "cert_cert", "cert_key", "timeout", "user", "password"]

    client_params = {key: value for key, value in module.params.items() if key in allowed_keys}
    try:
        etcd = etcd3.client(**client_params)
    except Exception as exp:
        module.fail_json(msg=f"Cannot connect to etcd cluster: {exp}", exception=traceback.format_exc())
    try:
        cluster_value = etcd.get(module.params["key"])
    except Exception as exp:
        module.fail_json(msg=f"Cannot reach data: {exp}", exception=traceback.format_exc())

    # Make the cluster_value[0] a string for string comparisons
    result["old_value"] = to_native(cluster_value[0])

    if module.params["state"] == "absent":
        if cluster_value[0] is not None:
            if module.check_mode:
                result["changed"] = True
            else:
                try:
                    etcd.delete(module.params["key"])
                except Exception as exp:
                    module.fail_json(
                        msg=f"Cannot delete {module.params['key']}: {exp}", exception=traceback.format_exc()
                    )
                else:
                    result["changed"] = True
    elif module.params["state"] == "present":
        if result["old_value"] != module.params["value"]:
            if module.check_mode:
                result["changed"] = True
            else:
                try:
                    etcd.put(module.params["key"], module.params["value"])
                except Exception as exp:
                    module.fail_json(
                        msg=f"Cannot add or edit key {module.params['key']}: {exp}", exception=traceback.format_exc()
                    )
                else:
                    result["changed"] = True
    else:
        module.fail_json(msg="State not recognized")

    # manipulate or modify the state as needed (this is going to be the
    # part where your module will do what it needs to do)

    # during the execution of the module, if there is an exception or a
    # conditional state that effectively causes a failure, run
    # AnsibleModule.fail_json() to pass in the message and the result

    # in the event of a successful module execution, you will want to
    # simple AnsibleModule.exit_json(), passing the key/value results
    module.exit_json(**result)


def main():
    run_module()


if __name__ == "__main__":
    main()
