#!/usr/bin/python3
# Authors: Rob Crittenden <rcritten@redhat.com>
#
# Based on ipa-replica-manage by Karl MacMillan <kmacmillan@mentalrootkit.com>
#
# Copyright (C) 2011  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 print_function

import logging
import sys
import os

from ipaplatform.paths import paths
from ipaserver.install import (replication, installutils, bindinstance,
    cainstance)
from ipalib import api, errors
from ipalib.util import has_managed_topology
from ipapython import ipautil, ipaldap, version
from ipapython.admintool import ScriptError
from ipapython.dn import DN

logger = logging.getLogger(os.path.basename(__file__))

# dict of command name and tuples of min/max num of args needed
commands = {
    "list": (0, 1, "[master fqdn]", ""),
    "connect": (1, 2, "<master fqdn> [other master fqdn]",
                "must provide the name of the servers to connect"),
    "disconnect": (1, 2, "<master fqdn> [other master fqdn]",
                   "must provide the name of the server to disconnect"),
    "del": (1, 1, "<master fqdn>",
            "must provide hostname of master to delete"),
    "re-initialize": (0, 0, "", ""),
    "force-sync": (0, 0, "", ""),
    "set-renewal-master": (0, 1, "[master fqdn]", "")
}


def parse_options():
    from optparse import OptionParser  # pylint: disable=deprecated-module

    parser = OptionParser(version=version.VERSION)
    parser.add_option("-H", "--host", dest="host", help="starting host")
    parser.add_option("-p", "--password", dest="dirman_passwd", help="Directory Manager password")
    parser.add_option("-v", "--verbose", dest="verbose", action="store_true", default=False,
                      help="provide additional information")
    parser.add_option("-f", "--force", dest="force", action="store_true", default=False,
                      help="ignore some types of errors")
    parser.add_option("--from", dest="fromhost", help="Host to get data from")

    options, args = parser.parse_args()

    valid_syntax = False

    if len(args):
        n = len(args) - 1
        for cmd in commands:
            if cmd == args[0]:
                v = commands[cmd]
                err = None
                if n < v[0]:
                    err = v[3]
                elif n > v[1]:
                    err = "too many arguments"
                else:
                    valid_syntax = True
                if err:
                    parser.error("Invalid syntax: %s\nUsage: %s [options] %s" % (err, cmd, v[2]))

    if not valid_syntax:
        cmdstr = " | ".join(commands.keys())
        parser.error("must provide a command [%s]" % cmdstr)

    return options, args

def list_replicas(realm, host, replica, dirman_passwd, verbose):

    peers = {}

    try:
        # connect to main IPA LDAP server
        ldap_uri = ipaldap.get_ldap_uri(host, 636, cacert=paths.IPA_CA_CRT)
        conn = ipaldap.LDAPClient(ldap_uri, cacert=paths.IPA_CA_CRT)
        conn.simple_bind(bind_dn=ipaldap.DIRMAN_DN,
                         bind_password=dirman_passwd)

        dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), ipautil.realm_to_suffix(realm))
        entries = conn.get_entries(dn, conn.SCOPE_ONELEVEL)

        for ent in entries:
            try:
                cadn = DN(('cn', 'CA'), DN(ent.dn))
                entry = conn.get_entry(cadn)
                peers[ent.single_value['cn']] = ['master', '']
            except errors.NotFound:
                peers[ent.single_value['cn']] = ['CA not configured', '']

    except Exception as e:
        sys.exit(
            "Failed to get data from '%s' while trying to list replicas: %s" %
            (host, e))
    finally:
        conn.unbind()

    if not replica:
        for k, p in peers.items():
            print('%s: %s' % (k, p[0]))
        return

    try:
        repl = replication.get_cs_replication_manager(realm, replica, dirman_passwd)
    except Exception as e:
        sys.exit(str(e))

    entries = repl.find_replication_agreements()

    for entry in entries:
        print('%s' % entry.single_value.get('nsds5replicahost'))

        if verbose:
            initstatus = entry.single_value.get('nsds5replicalastinitstatus')
            if initstatus is not None:
                print("  last init status: %s" % initstatus)
                print("  last init ended: %s" % str(
                    ipautil.parse_generalized_time(
                        entry.single_value['nsds5replicalastinitend'])))
            print("  last update status: %s" % entry.single_value.get(
                'nsds5replicalastupdatestatus'))
            print("  last update ended: %s" % str(
                ipautil.parse_generalized_time(
                    entry.single_value['nsds5replicalastupdateend'])))


def del_link(realm, replica1, replica2, dirman_passwd, force=False):

    repl2 = None

    try:
        repl1 = replication.get_cs_replication_manager(realm, replica1, dirman_passwd)

        repl1.hostnames = [replica1, replica2]

        repl_list1 = repl1.find_replication_agreements()

        # Find the DN of the replication agreement to remove
        replica1_dn = None
        for e in repl_list1:
            if e.single_value.get('nsDS5ReplicaHost') == replica2:
                replica1_dn = e.dn
                break

        if replica1_dn is None:
            sys.exit("'%s' has no replication agreement for '%s'" % (replica1, replica2))

        repl1.hostnames = [replica1, replica2]

    except errors.NetworkError as e:
        sys.exit("Unable to connect to %s: %s" % (replica1, e))
    except Exception as e:
        sys.exit("Failed to get data from '%s': %s" % (replica1, e))

    try:
        repl2 = replication.get_cs_replication_manager(realm, replica2, dirman_passwd)

        repl2.hostnames = [replica1, replica2]

        repl_list = repl2.find_replication_agreements()

        # Now that we've confirmed that both hostnames are vaild, make sure
        # that we aren't removing the last link from either side.
        if not force and len(repl_list) <= 1:
            print("Cannot remove the last replication link of '%s'" % replica2)
            print("Please use the 'del' command to remove it from the domain")
            sys.exit(1)

        if not force and len(repl_list1) <= 1:
            print("Cannot remove the last replication link of '%s'" % replica1)
            print("Please use the 'del' command to remove it from the domain")
            sys.exit(1)

        # Find the DN of the replication agreement to remove
        replica2_dn = None
        for entry in repl_list:
            if entry.single_value.get('nsDS5ReplicaHost') == replica1:
                replica2_dn = entry.dn
                break

        # This should never happen
        if replica2_dn is None:
            sys.exit("'%s' has no replication agreement for '%s'" % (replica1, replica2))

    except errors.NotFound:
        print("'%s' has no replication agreement for '%s'" % (replica2, replica1))
        if not force:
            return
    except Exception as exc:
        print("Failed to get data from '%s': %s" % (replica2, exc))
        if not force:
            sys.exit(1)

    if repl2:
        failed = False
        try:
            repl2.delete_agreement(replica1, replica2_dn)
            repl2.delete_referral(replica1, repl1.port)
        except Exception as exc:
            print("Unable to remove agreement on %s: %s" % (replica2, exc))
            failed = True

        if failed:
            if force:
                print("Forcing removal on '%s'" % replica1)
            else:
                sys.exit(1)

    if not repl2 and force:
        print("Forcing removal on '%s'" % replica1)

    repl1.delete_agreement(replica2, replica1_dn)
    repl1.delete_referral(replica2, repl2.port)

    print("Deleted replication agreement from '%s' to '%s'" % (replica1, replica2))

def del_master(realm, hostname, options):
    delrepl = None

    # 1. Connect to the local dogtag DS server
    try:
        thisrepl = replication.get_cs_replication_manager(realm, options.host,
                                                          options.dirman_passwd)
    except Exception as e:
        sys.exit("Failed to connect to server %s: %s" % (options.host, e))

    # 2. Ensure we have an agreement with the master
    if thisrepl.get_replication_agreement(hostname) is None:
        sys.exit("'%s' has no replication agreement for '%s'" % (options.host, hostname))

    # 3. Connect to the dogtag DS to be removed.
    try:
        delrepl = replication.get_cs_replication_manager(realm, hostname,
                                                         options.dirman_passwd)
    except Exception as e:
        if not options.force:
            print("Unable to delete replica %s: %s" % (hostname, e))
            sys.exit(1)
        else:
            print("Unable to connect to replica %s, forcing removal" % hostname)

    # 4. Get list of agreements.
    if delrepl is None:
        # server not up, just remove it from this server
        replica_names = [options.host]
    else:
        replica_entries = delrepl.find_ipa_replication_agreements()
        replica_names = [rep.single_value.get('nsds5replicahost')
                         for rep in replica_entries]

    # 5. Remove each agreement
    for r in replica_names:
        try:
            del_link(realm, r, hostname, options.dirman_passwd, force=True)
        except Exception as e:
            sys.exit("There were issues removing a connection: %s" % e)

    # 6. Pick CA renewal master
    ca = cainstance.CAInstance(api.env.realm)
    if ca.is_renewal_master(hostname):
        ca.set_renewal_master(options.host)

    # 7. And clean up the removed replica DNS entries if any.
    try:
        if bindinstance.dns_container_exists(api.env.basedn):
            bind = bindinstance.BindInstance()
            bind.update_system_records()
    except Exception as e:
        print("Failed to cleanup %s DNS entries: %s" % (hostname, e))
        print("You may need to manually remove them from the tree")

def add_link(realm, replica1, replica2, dirman_passwd, options):
    try:
        repl2 = replication.get_cs_replication_manager(realm, replica2,
                                                       dirman_passwd)
    except Exception as e:
        sys.exit(str(e))
    try:
        ldap_uri = ipaldap.get_ldap_uri(replica2, 636, cacert=paths.IPA_CA_CRT)
        conn = ipaldap.LDAPClient(ldap_uri, cacert=paths.IPA_CA_CRT)
        conn.simple_bind(bind_dn=ipaldap.DIRMAN_DN,
                         bind_password=dirman_passwd)

        dn = DN(('cn', 'CA'), ('cn', replica2), ('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'),
                ipautil.realm_to_suffix(realm))
        conn.get_entries(dn, conn.SCOPE_BASE)
        conn.unbind()
    except errors.NotFound:
        sys.exit('%s does not have a CA configured.' % replica2)
    except errors.NetworkError as e:
        sys.exit("Unable to connect to %s: %s" % (ipautil.format_netloc(replica2, 636), str(e)))
    except Exception as e:
        sys.exit("Failed to get data while trying to bind to '%s': %s" % (replica1, str(e)))

    try:
        repl1 = replication.get_cs_replication_manager(realm, replica1,
                                                       dirman_passwd)
        entries = repl1.find_replication_agreements()
        for e in entries:
            if e.single_value.get('nsDS5ReplicaHost') == replica2:
                sys.exit('This replication agreement already exists.')
        repl1.hostnames = [replica1, replica2]

    except errors.NotFound:
        sys.exit("Cannot find replica '%s'" % replica1)
    except errors.NetworkError as e:
        sys.exit("Unable to connect to %s: %s" % (replica1, e))
    except Exception as e:
        sys.exit(
            "Failed to get data from '%s' while trying to get current "
            "agreements: %s" % (replica1, e))

    repl1.setup_replication(
        replica2, repl2.port, 0, DN(('cn', 'Directory Manager')),
        dirman_passwd, is_cs_replica=True, local_port=repl1.port)
    print("Connected '%s' to '%s'" % (replica1, replica2))

def re_initialize(realm, options):

    if not options.fromhost:
        sys.exit("re-initialize requires the option --from <host name>")

    thishost = installutils.get_fqdn()

    try:
        repl = replication.get_cs_replication_manager(realm, options.fromhost,
                                                      options.dirman_passwd)
        thisrepl = replication.get_cs_replication_manager(realm, thishost,
                                                          options.dirman_passwd)
    except Exception as e:
        sys.exit(str(e))

    filter = repl.get_agreement_filter(host=thishost)
    try:
        entry = repl.conn.get_entries(
            DN(('cn', 'config')), repl.conn.SCOPE_SUBTREE, filter)
    except errors.NotFound:
        logger.error("Unable to find %s -> %s replication agreement",
                     options.fromhost, thishost)
        sys.exit(1)
    if len(entry) > 1:
        logger.error("Found multiple agreements for %s. Only initializing the "
                     "first one returned: %s", thishost, entry[0].dn)

    repl.hostnames = thisrepl.hostnames = [thishost, options.fromhost]
    thisrepl.enable_agreement(options.fromhost)
    repl.enable_agreement(thishost)

    repl.initialize_replication(entry[0].dn, repl.conn)
    repl.wait_for_repl_init(repl.conn, entry[0].dn)

def force_sync(realm, thishost, fromhost, dirman_passwd):

    try:
        repl = replication.get_cs_replication_manager(realm, fromhost,
                                                      dirman_passwd)
        repl.force_sync(repl.conn, thishost)
    except Exception as e:
        sys.exit(str(e))

def set_renewal_master(realm, replica):
    if not replica:
        replica = installutils.get_fqdn()

    ca = cainstance.CAInstance(realm)
    if ca.is_renewal_master(replica):
        sys.exit("%s is already the renewal master" % replica)

    try:
        ca.set_renewal_master(replica)
    except Exception as e:
        sys.exit("Failed to set renewal master to %s: %s" % (replica, e))

    print("%s is now the renewal master" % replica)


def exit_on_managed_topology(what, hint="topologysegment"):
    if hint == "topologysegment":
        hinttext = ("Please use `ipa topologysegment-*` commands to manage "
                   "the topology.")
    elif hint == "ipa-replica-manage-del":
        hinttext = ("Please use the `ipa-replica-manage del` command.")
    else:
        assert False, "Unexpected value"
    sys.exit("{0} is deprecated with managed IPA replication topology. {1}"
             .format(what, hinttext))


def main():
    installutils.check_server_configuration()
    options, args = parse_options()

    # Just initialize the environment. This is so the installer can have
    # access to the plugin environment
    api_env = {}
    if os.getegid() != 0:
        api_env['log'] = None # turn off logging for non-root

    api.bootstrap(
        context='cli',
        in_server=True,
        verbose=options.verbose,
        confdir=paths.ETC_IPA,
        **api_env
    )
    api.finalize()

    dirman_passwd = None
    realm = api.env.realm

    if options.host:
        host = options.host
    else:
        host = installutils.get_fqdn()

    options.host = host

    if options.dirman_passwd:
        dirman_passwd = options.dirman_passwd
    else:
        dirman_passwd = installutils.read_password("Directory Manager", confirm=False,
            validate=False, retry=False)
        if dirman_passwd is None:
            sys.exit("Directory Manager password required")

    options.dirman_passwd = dirman_passwd

    api.Backend.ldap2.connect(bind_pw=options.dirman_passwd)

    if args[0] == "list":
        replica = None
        if len(args) == 2:
            replica = args[1]
        list_replicas(realm, host, replica, dirman_passwd, options.verbose)
    elif args[0] == "del":
        if has_managed_topology(api):
            exit_on_managed_topology(
                "Removal of IPA CS replication agreement and replication data",
                hint="ipa-replica-manage-del")
        del_master(realm, args[1], options)
    elif args[0] == "re-initialize":
        re_initialize(realm, options)
    elif args[0] == "force-sync":
        if not options.fromhost:
            sys.exit("force-sync requires the option --from <host name>")
        force_sync(realm, host, options.fromhost, options.dirman_passwd)
    elif args[0] == "connect":
        if has_managed_topology(api):
            exit_on_managed_topology("Creation of IPA CS replication agreement")
        if len(args) == 3:
            replica1 = args[1]
            replica2 = args[2]
        elif len(args) == 2:
            replica1 = host
            replica2 = args[1]
        add_link(realm, replica1, replica2, dirman_passwd, options)
    elif args[0] == "disconnect":
        if has_managed_topology(api):
            exit_on_managed_topology("Removal of IPA CS replication agreement")
        if len(args) == 3:
            replica1 = args[1]
            replica2 = args[2]
        elif len(args) == 2:
            replica1 = host
            replica2 = args[1]
        del_link(realm, replica1, replica2, dirman_passwd, options.force)
    elif args[0] == 'set-renewal-master':
        replica = None
        if len(args) > 1:
            replica = args[1]
        set_renewal_master(realm, replica)

    api.Backend.ldap2.disconnect()

try:
    main()
except KeyboardInterrupt:
    sys.exit(1)
except (SystemExit, ScriptError) as e:
    sys.exit(e)
except Exception as e:
    sys.exit("unexpected error: %s" % e)
