#! /usr/bin/python2 -E
# 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/>.
#
import sys
import os

import krbV
from ipapython.ipa_log_manager import *

from ipaserver.install import (replication, installutils, bindinstance,
    cainstance, certs)
from ipalib import api, errors, util
from ipalib.constants import CACERT
from ipapython import ipautil, ipaldap, version, dogtag
from ipapython.dn import DN

# 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

    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
        k = commands.keys()
        for cmd in k:
            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
        conn = ipaldap.IPAdmin(host, 636, cacert=CACERT)
        conn.do_simple_bind(bindpw=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, 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.iteritems():
            print '%s: %s' % (k, p[0])
        return

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

    entries = repl.find_replication_agreements()

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

        if verbose:
            print "  last init status: %s" % entry.single_value.get(
                'nsds5replicalastinitstatus')
            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, e:
        sys.exit("Unable to connect to %s: %s" % (replica1, e))
    except Exception, 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 e in repl_list:
            if e.single_value.get('nsDS5ReplicaHost') == replica1:
                replica2_dn = e.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, e:
        print "Failed to get data from '%s': %s" % (replica2, e)
        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, e:
            print "Unable to remove agreement on %s: %s" % (replica2, e)
            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):

    force_del = False

    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, 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, 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
            force_del = True

    # 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, e:
            sys.exit("There were issues removing a connection: %s" % e)

    # 6. Pick CA renewal master
    ca = cainstance.CAInstance(api.env.realm, certs.NSS_DIR)
    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(options.host, api.env.basedn,
                                             dm_password=options.dirman_passwd):
            api.Backend.ldap2.connect(bind_dn=DN(('cn', 'Directory Manager')),
                                      bind_pw=options.dirman_passwd)
            bind = bindinstance.BindInstance()
            bind.remove_ipa_ca_dns_records(hostname, realm.lower())
    except Exception, 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, e:
        sys.exit(str(e))
    try:
        conn = ipaldap.IPAdmin(replica2, 636, cacert=CACERT)
        conn.do_simple_bind(bindpw=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_ONELEVEL)
        conn.unbind()
    except errors.NotFound:
        sys.exit('%s does not have a CA configured.' % replica2)
    except errors.NetworkError, e:
        sys.exit("Unable to connect to %s: %s" % (ipautil.format_netloc(replica2, 636), str(e)))
    except Exception, 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, e:
        sys.exit("Unable to connect to %s: %s" % (replica1, e))
    except Exception, 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, 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:
        root_logger.error("Unable to find %s -> %s replication agreement" % (options.fromhost, thishost))
        sys.exit(1)
    if len(entry) > 1:
        root_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, e:
        sys.exit(str(e))

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

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

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

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

def main():
    options, args = parse_options()

    # Just initialize the environment. This is so the installer can have
    # access to the plugin environment
    api_env = {'in_server' : True,
               'verbose'   : options.verbose,
              }

    if os.getegid() != 0:
        api_env['log'] = None # turn off logging for non-root

    api.bootstrap(**api_env)
    api.finalize()

    dirman_passwd = None
    realm = krbV.default_context().default_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

    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":
        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 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 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)

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