#!/usr/bin/python3

# --- BEGIN COPYRIGHT BLOCK ---
# Copyright (C) 2020 William Brown <william@blackhats.net.au>
# All rights reserved.
#
# License: GPL (version 3 or any later version).
# See LICENSE for details.
# --- END COPYRIGHT BLOCK ---
#
# PYTHON_ARGCOMPLETE_OK

import argparse
import argcomplete
import json
import signal
import sys
from lib389 import DirSrv
from lib389.cli_base import (
    connect_instance,
    disconnect_instance,
    setup_script_logger,
    format_error_to_dict,
    format_pretty_error)
from lib389.cli_base.dsrc import dsrc_to_ldap, dsrc_arg_concat
from lib389._constants import DSRC_HOME

from lib389.migrate.openldap.config import olConfig, olOverlayType_from_str
from lib389.migrate.ldif import LdifMetadata
from lib389.migrate.plan import Migration

parser = argparse.ArgumentParser(
    formatter_class=argparse.RawDescriptionHelpFormatter,
    description="""Migrate from OpenLDAP to 389 Directory Server.

This command automates the process of converting an OpenLDAP server to a 389 Directory Server
instance. This is a "best effort" as OpenLDAP and 389 Directory Server are not identical,
so some features still may require hand migration, or can not be migrated at all. This tool
intends to migrate the majority of major content such as database data, index configuration,
schema and some overlays (plugins).

Content we can migrate:

* Schema
* Database content (from ldif backup)
* Database indexes
* MemberOf Overlay (memberof)
* Referential Integrity Overlay (refint)
* Attribute Unique Overlay (unique)

Some content that can *not* be migrated include some overlays (plugins), access controls
and replication configuration. Examples of plugins that can not be migrated:

* Access/Audit logging (built into 389-ds by default)
* Chaining (Requires manual migration, may not be equivalent)
* Constraints (No equivalent plugin)
* Dynamic Directory Services (Requires manual migration to Class of Service Plugin)
* Dynamic Groups/Lists (Requires manual migration to Roles Plugin)
* Proxy Cache (No equivalent plugin, 389-ds supports read-only replicas)
* Password Policy (Built into 389-ds, requires manual migration)
* Rewrite/Remap (No equivalent plugin)
* Sync Provider (Requires manual migration to Replication OR Content Sync Plugin)
* Value Sorting (No equivalent plugin)

This must be run on the server running the 389 Directory Instance as it requires filesystem
access. You must run this tool as either root or dirsrv users.

The following is required from your openldap instance:

  * slapd.d configuration directory in ldif/dynamic format
  * (optional) ldif file backup of the database from slapcat

These can be created on the OpenLDAP host and copied to the 389 Directory Server host. No
destructive actions are applied to the OpenLDAP instance.

If you are already using the ldif/dynamic format from /etc/openldap/slapd.d, you should
take a copy of this to use in the migration.

    cp -a /etc/openldap/slapd.d /root/slapd.d

If you are using the slapd.conf configuration file, you can convert this to the dynamic
configuration without affecting your running instance with:

    slaptest -f /etc/openldap/slapd.conf -F /root/slapd.d

To optionally allow your database content to be migrated you may create an ldif backup of the
content that 389 Directory Server can import. You must run this for each backend (suffix)
of your instance with the command:

    # If using slapd.conf config format
    slapcat -f /etc/openldap/slapd.conf -b SUFFIX -l /root/suffix.ldif
    # If using slapd.d config format
    slapcat -F /etc/openldap/slapd.d -b SUFFIX -l /root/suffix.ldif

You must already have a 389 Directory Server you want to import into. You can create
this with the `dscreate` tool. Data and configuration in this instance WILL be
modified or removed (ie potentially destructive actions).

It is strongly advised you test this tool on a non-production system first to be
sure the process and changes are understood.

This only needs to be run on the first-instance in a 389 Directory Server topology. All
other replicas should be configured from this instance post migration.
""")
parser.add_argument('-v', '--verbose',
                    help="Display verbose operation tracing during command execution",
                    action='store_true', default=False, dest='verbose')

parser.add_argument('instance',
        help="The name of the 389-ds instance to have openldap data migrated into",
    )
parser.add_argument('slapd_config',
        help="The path to the openldap slapd.d configuration that will be migrated",
    )
parser.add_argument('slapd_ldif', nargs='*',
        help="The path to exported database ldifs to be imported.",
    )
# Migration options
parser.add_argument('--confirm',
        default=False, action='store_true',
        help="Confirm that you want to apply these migration actions to the 389-ds instance. By default no actions are taken."
)
parser.add_argument('--ignore-overlay', nargs='*',
        help="Ignore the following openldap overlays from having their configuration migrated to equivalent 389-ds plugins. Valid options are memberof, refint, unique.",
        default=[]
)
parser.add_argument('--ignore-schema-oid', nargs='*',
        help="Ignore the following openldap schema attribute or class OIDS from being migrated to 389-ds. This *may* create inconsistent schema which could cause the migration to fail. Use with caution.",
        default=[]
)
parser.add_argument('--ignore-attribute', nargs='*',
        help="Ignore the following attributes from entries that are loaded from the ldif. For example, you may not want to import userPassword hashes.",
        default=[]
)
# General options
parser.add_argument('-D', '--binddn',
        help="The 389 Directory Server account to bind as for executing migration operations",
        default=None
    )
parser.add_argument('-w', '--bindpw',
        help="Password for binddn",
        default=None
    )
parser.add_argument('-W', '--prompt',
        action='store_true', default=False,
        help="Prompt for password for the bind DN"
    )
parser.add_argument('-y', '--pwdfile',
        help="Specifies a file containing the password for the binddn",
        default=None
    )
parser.add_argument('-Z', '--starttls',
        help="Connect to 389 Directory Server with StartTLS",
        default=False, action='store_true'
    )
parser.add_argument('-b', '--basedn',
        help=argparse.SUPPRESS,
        default=None
    )
parser.add_argument('-j', '--json',
        help=argparse.SUPPRESS,
        default=False, action='store_true'
    )

# handle a control-c gracefully
def signal_handler(signal, frame):
    print('\n\nExiting...')
    sys.exit(0)

def do_migration(inst, log, args, skip_overlays):
    log.debug("do_migration -- begin preparation --")
    log.debug("Instance isLocal: %s" % inst.isLocal)
    # Do the thing

    # Map some args out.
    skip_schema_oids = args.ignore_schema_oid
    skip_entry_attributes = args.ignore_attribute

    # Parse the openldap config
    config = olConfig(args.slapd_config, log)

    # Do we have any ldifs to import?
    ldifmeta = LdifMetadata(args.slapd_ldif, log)

    # Create the migration plan.
    migration = Migration(
        inst,
        config.schema,
        config.databases,
        ldifmeta.get_suffixes(),
        skip_schema_oids,
        skip_overlays,
        skip_entry_attributes,
    )

    # Present it for review.
    log.debug("--> raw migration plan")
    log.debug(migration.__unicode__())
    log.debug("<-- end raw migration plan")

    log.info("The following migration steps will be performed:")
    migration.display_plan_review(log)

    # Go ahead?
    if not args.confirm:
        log.info("No actions taken. To apply migration plan, use '--confirm'")
        return False

    # Do the thing.
    log.info("Starting Migration ... This may take some time ...")
    migration.execute_plan(log)
    log.info("🎉 Migration complete!")
    log.info("----------------------")
    log.info("You should now review your instance configuration and data:")

    # Display the Post Migration Checklist items.
    migration.display_plan_post_review(log)

    # Celebrate.
    return True

if __name__ == '__main__':
    args = parser.parse_args()
    log = setup_script_logger('openldap_to_ds', args.verbose)

    log.debug("The Openldap to 389 Directory Server Migration Tool")
    # Leave this comment here: UofA let me take this code with me provided
    # I gave attribution. -- wibrown
    log.debug("Inspired by works of: ITS, The University of Adelaide")
    log.debug("Called with: %s", args)

    # Do some initial parsing. Matters for overlays.
    skip_overlays = [
        olOverlayType_from_str(x)
        for x in args.ignore_overlay
        if olOverlayType_from_str(x)
    ]

    # Now that we have our args, see how they relate with our instance.
    dsrc_inst = dsrc_to_ldap(DSRC_HOME, args.instance, log.getChild('dsrc'))

    # Now combine this with our arguments
    dsrc_inst = dsrc_arg_concat(args, dsrc_inst)

    log.debug("Instance details: %s" % dsrc_inst)

    inst = None
    result = False

    try:
        inst = connect_instance(dsrc_inst=dsrc_inst, verbose=args.verbose, args=args)
        result = do_migration(inst, log, args, skip_overlays)
    except Exception as e:
        log.debug(e, exc_info=True)
        msg = format_error_to_dict(e)
        if args.json:
            sys.stderr.write(f"{json.dumps(msg, indent=4)}\n")
        else:
            if not args.verbose:
                msg = format_pretty_error(msg)
            log.error("Error: %s" % " - ".join(str(val) for val in msg.values()))
        result = False
    disconnect_instance(inst)

    # Done!
    if result is False:
        sys.exit(1)

