# -*- coding: utf-8 -*-

# Authors:
#   Thomas Woerner <twoerner@redhat.com>
#
# Based on ipaplatform/redhat/tasks.py code from Christian Heimes
#
# Copyright (C) 2022 Red Hat
# see file 'COPYING' for use and warranty information
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

from __future__ import (absolute_import, division, print_function)

__metaclass__ = type

ANSIBLE_METADATA = {
    'metadata_version': '1.0',
    'supported_by': 'community',
    'status': ['preview'],
}

DOCUMENTATION = """
---
module: ipaclient_configure_dns_resolver
short_description: Configure DNS resolver for IPA client
description:
  Configure DNS resolver for IPA client, register files for installer
options:
  nameservers:
    description: The nameservers, required with state:present.
    type: list
    elements: str
    required: false
  searchdomains:
    description: The searchdomains, required with state:present.
    type: list
    elements: str
    required: false
  state:
    description: The state to ensure.
    type: str
    choices: ["present", "absent"]
    default: present
    required: false
author:
  - Thomas Woerner (@t-woerner)
"""

EXAMPLES = """
# Ensure DNS nameservers and domain are configured
- ipaclient_configure_dns_resolver:
    nameservers: groups.ipaservers
    searchdomains: "{{ ipaserver_domain | default(ipaclient_domain) }}"
# Ensure DNS nameservers and domain are not configured
- ipaclient_configure_dns_resolver:
    state: absent
"""

RETURN = """
"""

import os
import os.path

from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.ansible_ipa_client import (
    check_imports, services, tasks, paths, sysrestore, CheckedIPAddress
)
try:
    from ipalib.installdnsforwarders import detect_resolve1_resolv_conf
except ImportError:
    def detect_resolve1_resolv_conf():
        """
        Detect if /etc/resolv.conf is managed by systemd-resolved.

        See man(5) NetworkManager.conf
        """
        systemd_resolv_conf_files = {
            "/run/systemd/resolve/stub-resolv.conf",
            "/run/systemd/resolve/resolv.conf",
            "/lib/systemd/resolv.conf",
            "/usr/lib/systemd/resolv.conf",
        }

        try:
            dest = os.readlink(paths.RESOLV_CONF)
        except OSError:
            # not a link
            return False
        # convert path relative to /etc/resolv.conf to abs path
        dest = os.path.normpath(
            os.path.join(os.path.dirname(paths.RESOLV_CONF), dest)
        )
        return dest in systemd_resolv_conf_files


if hasattr(paths, "SYSTEMD_RESOLVED_IPA_CONF"):
    SYSTEMD_RESOLVED_IPA_CONF = paths.SYSTEMD_RESOLVED_IPA_CONF
else:
    SYSTEMD_RESOLVED_IPA_CONF = "/etc/systemd/resolved.conf.d/zzz-ipa.conf"


if hasattr(paths, "NETWORK_MANAGER_IPA_CONF"):
    NETWORK_MANAGER_IPA_CONF = paths.NETWORK_MANAGER_IPA_CONF
else:
    NETWORK_MANAGER_IPA_CONF = "/etc/NetworkManager/conf.d/zzz-ipa.conf"


NM_IPA_CONF = """
# auto-generated by IPA client installer
[main]
dns={dnsprocessing}
[global-dns]
searches={searches}
[global-dns-domain-*]
servers={servers}
"""


RESOLVE1_IPA_CONF = """
# auto-generated by IPA client installer
[Resolve]
# use DNS servers
DNS={servers}
# make default DNS server, add search suffixes
Domains=~. {searchdomains}
"""


def configure_dns_resolver(nameservers, searchdomains, fstore=None):
    """
    Configure global DNS resolver (e.g. /etc/resolv.conf).

    :param nameservers: list of IP addresses
    :param searchdomains: list of search domaons
    :param fstore: optional file store for resolv.conf backup
    """
    if not nameservers or not isinstance(nameservers, list):
        raise AssertionError("nameservers must be of type list")
    if not searchdomains or not isinstance(searchdomains, list):
        raise AssertionError("searchdomains must be of type list")

    changed = False
    if fstore is not None and not fstore.has_file(paths.RESOLV_CONF):
        fstore.backup_file(paths.RESOLV_CONF)
        changed = True

    resolve1_enabled = detect_resolve1_resolv_conf()
    if "NetworkManager" not in services.knownservices:
        # NetworkManager is not in wellknownservices for old IPA releases
        # Therefore create own service for it.
        nm_service = services.service("NetworkManager.service")
    else:
        nm_service = services.knownservices['NetworkManager']

    # At first configure systemd-resolved
    if resolve1_enabled:
        if not os.path.exists(SYSTEMD_RESOLVED_IPA_CONF):
            confd = os.path.dirname(SYSTEMD_RESOLVED_IPA_CONF)
            if not os.path.isdir(confd):
                os.mkdir(confd)
                # owned by root, readable by systemd-resolve user
                os.chmod(confd, 0o755)
                tasks.restore_context(confd, force=True)

            # Additionally to IPA server code also set servers
            cfg = RESOLVE1_IPA_CONF.format(
                servers=' '.join(nameservers),
                searchdomains=" ".join(searchdomains)
            )
            with open(SYSTEMD_RESOLVED_IPA_CONF, "w") as outf:
                os.fchmod(outf.fileno(), 0o644)
                outf.write(cfg)

            tasks.restore_context(
                SYSTEMD_RESOLVED_IPA_CONF, force=True
            )

            if "systemd-resolved" in services.knownservices:
                sdrd_service = services.knownservices["systemd-resolved"]
            else:
                sdrd_service = services.service("systemd-resolved.service")
            if sdrd_service.is_enabled():
                sdrd_service.reload_or_restart()
            changed = True

    # Then configure NetworkManager or resolve.conf
    if nm_service.is_enabled():
        if not os.path.exists(NETWORK_MANAGER_IPA_CONF):
            # write DNS override and reload network manager to have it create
            # a new resolv.conf. The file is prefixed with ``zzz`` to
            # make it the last file. Global dns options do not stack and last
            # man standing wins.
            if resolve1_enabled:
                # push DNS configuration to systemd-resolved
                dnsprocessing = "systemd-resolved"
            else:
                # update /etc/resolv.conf
                dnsprocessing = "default"

            cfg = NM_IPA_CONF.format(
                dnsprocessing=dnsprocessing,
                servers=','.join(nameservers),
                searches=','.join(searchdomains)
            )
            with open(NETWORK_MANAGER_IPA_CONF, 'w') as outf:
                os.fchmod(outf.fileno(), 0o644)
                outf.write(cfg)
            # reload NetworkManager
            nm_service.reload_or_restart()
            changed = True

    # Configure resolv.conf if NetworkManager and systemd-resoled are not
    # enabled
    elif not resolve1_enabled:
        # no NM running, no systemd-resolved detected
        # fall back to /etc/resolv.conf
        cfg = [
            "# auto-generated by IPA installer",
            "search %s" % ' '.join(searchdomains),
        ]
        for nameserver in nameservers:
            cfg.append("nameserver %s" % nameserver)
        with open(paths.RESOLV_CONF, 'w') as outf:
            outf.write('\n'.join(cfg))
        changed = True

    return changed


def unconfigure_dns_resolver(fstore=None):
    """
    Unconfigure global DNS resolver (e.g. /etc/resolv.conf).

    :param fstore: optional file store for resolv.conf restore
    """
    changed = False

    if fstore is not None and fstore.has_file(paths.RESOLV_CONF):
        fstore.restore_file(paths.RESOLV_CONF)
        changed = True

    if os.path.isfile(NETWORK_MANAGER_IPA_CONF):
        os.unlink(NETWORK_MANAGER_IPA_CONF)
        if "NetworkManager" not in services.knownservices:
            # NetworkManager is not in wellknownservices for old IPA releases
            # Therefore create own service for it.
            nm_service = services.service("NetworkManager.service")
        else:
            nm_service = services.knownservices['NetworkManager']
        if nm_service.is_enabled():
            nm_service.reload_or_restart()
        changed = True

    if os.path.isfile(SYSTEMD_RESOLVED_IPA_CONF):
        os.unlink(SYSTEMD_RESOLVED_IPA_CONF)
        if "systemd-resolved" in services.knownservices:
            sdrd_service = services.knownservices["systemd-resolved"]
        else:
            sdrd_service = services.service("systemd-resolved.service")
        if sdrd_service.is_enabled():
            sdrd_service.reload_or_restart()
        changed = True

    return changed


def main():
    module = AnsibleModule(
        argument_spec=dict(
            nameservers=dict(type="list", elements="str", required=False),
            searchdomains=dict(type="list", elements="str", required=False),
            state=dict(type="str", default="present",
                       choices=["present", "absent"]),
        ),
        supports_check_mode=False,
    )

    check_imports(module)

    nameservers = module.params.get('nameservers')
    searchdomains = module.params.get('searchdomains')

    state = module.params.get("state")

    if state == "present":
        required = ["nameservers", "searchdomains"]
        for param in required:
            value = module.params.get(param)
            if value is None or len(value) < 1:
                module.fail_json(
                    msg="Argument '%s' is required for state:present" % param)
    else:
        invalid = ["nameservers", "searchdomains"]
        for param in invalid:
            if module.params.get(param) is not None:
                module.fail_json(
                    msg="Argument '%s' can not be used with state:present" %
                    param)

    # Check nameservers to contain valid IP addresses
    if nameservers is not None:
        for value in nameservers:
            try:
                CheckedIPAddress(value)
            except Exception as e:
                module.fail_json(
                    msg="Invalid IP address %s: %s" % (value, str(e)))

    fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE)

    if state == "present":
        changed = configure_dns_resolver(nameservers,
                                         searchdomains, fstore)
    else:
        changed = unconfigure_dns_resolver(fstore)

    module.exit_json(changed=changed)


if __name__ == '__main__':
    main()
