#!/bin/sh

# Copyright (C) 2014-2019 Internet Systems Consortium, Inc. ("ISC")
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

# This is kea-admin script that conducts administrative tasks on the Kea
# installation. Currently supported operations are:
#
# - database init
# - database version check
# - database version upgrade
# - lease database dump


# Get the location of the kea-admin scripts
prefix=/usr/local
export prefix
SCRIPTS_DIR_DEFAULT=${prefix}/share/kea/scripts
scripts_dir=${SCRIPTS_DIR_DEFAULT}
VERSION=1.7.8

# These are the default parameters. They will likely not work in any
# specific deployment.
db_host="localhost"
db_user="keatest"
db_password="keatest"
db_name="keatest"

# lease dump parameters
dump_type=0
dump_file=""
dump_qry=""

# Include utilities. Use installed version if available and
# use build version if it isn't.
# shellcheck disable=SC1091
if [ -e ${prefix}/share/kea/scripts/admin-utils.sh ]; then
    . ${prefix}/share/kea/scripts/admin-utils.sh
else
    . /pobj/kea-1.7.8/kea-1.7.8/src/bin/admin/admin-utils.sh
fi

# Prints out usage version.
usage() {
    printf "kea-admin %s\n" "$VERSION"
    printf "\n"
    printf "This is a kea-admin script that conducts administrative tasks on\n"
    printf "the Kea installation.\n"
    printf "\n"
    printf "Usage: %s COMMAND BACKEND [parameters]\n" "$0"
    printf "\n"
    printf "COMMAND: Currently supported operations are:\n"
    printf "\n"
    printf " - db-init: Initializes new database. Useful for first time installation.\n"
    printf " - db-version: Checks version of the existing database scheme. Useful\n"
    printf " -             for checking databaseB version when preparing for an upgrade.\n"
    printf " - db-upgrade: Upgrades your database scheme\n"
    printf " - lease-dump: Dump current leases to a CSV file\n"
    printf "\n"
    printf "BACKEND - one of the supported backends: memfile|mysql|pgsql|cql\n"
    printf "\n"
    printf "PARAMETERS: Parameters are optional in general, but may be required\n"
    printf "            for specific operation.\n"
    printf " -h or --host hostname - specifies a hostname of a database to connect to\n"
    printf " -u or --user name - specifies username when connecting to a database\n"
    printf " -p or --password pass - specifies a password when connecting to a database\n"
    printf " -n or --name database - specifies a database name to connect to\n"
    printf " -d or --directory - path to upgrade scripts (default: %s)\n" "${SCRIPTS_DIR_DEFAULT}"
    printf " -v or --version - print kea-admin version and quit.\n"
    printf "\n"
    printf " Parameters specific to lease-dump:\n"
    printf "     -4 to dump IPv4 leases to file\n"
    printf "     -6 to dump IPv6 leases to file\n"
    printf "     -o or --output - name of file to which leases will be dumped\n"
}


### Logging functions ###

# Logs message at the error level.
# Takes one parameter that is printed as is.
log_error() {
    printf "ERROR/kea-admin: %s\n" "${1}"
}

# Logs message at the warning level.
# Takes one parameter that is printed as is.
log_warning() {
    printf "WARNING/kea-admin: %s\n" "${1}"
}

# Logs message at the info level.
# Takes one parameter that is printed as is.
log_info() {
    printf "INFO/kea-admin: %s\n" "${1}"
}

### Convenience functions ###

# Checks if the value is in the list. An example usage of this function
# is to determine whether the kea-admin command belongs to the list of
# supported commands.
is_in_list() {
    local member=${1}  # Value to be checked
    local list="${2}"  # Comma separated list of items
    _inlist=0          # Return value: 0 if not in list, 1 otherwise.
    if [ -z "${member}" ]; then
        log_error "missing member (need to specify a string as first param)"
    fi
    # Iterate over all items on the list and compare with the member.
    # If they match, return, otherwise log error and exit.
    for item in ${list}
    do
        if [ "${item}" = "${member}" ]; then
            _inlist=1
            return
        fi
    done
}


### Functions that implement database initialization commands

memfile_init() {
    # Useless as Kea converts CSV versions at startup.
    log_error "NOT IMPLEMENTED"
    exit 1
}

# Validates that the MySQL db_users's permissions are sufficient to
# create the schema.
mysql_can_create() {

    # Let's grab the version for possible debugging issues.  It also
    # determines basic functional access to db.
    RESULT=$(mysql_execute "select @@global.version;")
    ERRCODE=$?
    if [ $ERRCODE -ne  0 ]
    then
        log_error "mysql_can_create: get MySQL version failed, mysql status = $ERRCODE"
        exit 1
    fi

    printf "MySQL Version is:\n" "$RESULT"

    # SQL to drop our test table and trigger
    cleanup_sql="DROP TABLE IF EXISTS kea_dummy_table; DROP PROCEDURE IF EXISTS kea_dummy_trigger;"

    # SQL to create our test table
    table_sql="CREATE TABLE kea_dummy_table(dummy INT UNSIGNED PRIMARY KEY NOT NULL);"

    # SQL to create our test trigger
    trigger_sql="\
CREATE TRIGGER kea_dummy_trigger BEFORE insert ON kea_dummy_table FOR EACH ROW\n \
BEGIN\n \
END;"

    # Let's clean up just in case.
    RESULT=$(mysql_execute "$cleanup_sql")
    ERRCODE=$?
    if [ $ERRCODE -ne  0 ]
    then
        log_error "mysql_can_create cannot run pre cleanup, mysql status = $ERRCODE"
        exit 1;
    fi

    # Now make the dummy table.
    perms_ok=1
    RESULT=$(mysql_execute "$table_sql")
    ERRCODE=$?
    if [ $ERRCODE -ne  0 ]
    then
        log_error "mysql_can_create cannot create table, check user permissions, mysql status = $ERRCODE"
        perms_ok=0;
    else
        # Now attempt to make trigger
        RESULT=$(mysql_execute "$trigger_sql")
        ERRCODE=$?
        if [ $ERRCODE -ne  0 ]
        then
            log_error "mysql_can_create cannot trigger, check user permissions, mysql status = $ERRCODE"
            perms_ok=0;
        fi
    fi

    # Try to cleanup no matter what happened above
    RESULT=$(mysql_execute "$cleanup_sql")
    ERRCODE=$?
    if [ $ERRCODE -ne  0 ]
    then
        log_error "mysql_can_create cannot run post cleanup, mysql status = $ERRCODE"
        exit 1;
    fi

    if [ $perms_ok -ne 1 ]
    then
        log_error "Create failed, the user, $db_user, has insufficient privileges."
        exit 1;
    fi
}

# Initializes a new, empty MySQL database.
# It essentially calls scripts/mysql/dhcpdb_create.mysql script, with
# some extra sanity checks. It will refuse to use it if there are any
# existing tables. It's better safe than sorry.
mysql_init() {
    printf "Checking if there is a database initialized already. Please ignore errors.\n"

    # Let's try to count the number of tables. Anything above 0 means that there
    # is some database in place. If there is anything, we abort. Note that
    # mysql may spit out connection or access errors to stderr, we ignore those.
    # We should not hide them as they may give hints to user what is wrong with
    # his setup.
    #
    RESULT=$(mysql_execute "SHOW TABLES;")
    ERRCODE=$?
    if [ $ERRCODE -ne  0 ]
    then
        log_error "mysql_init table query failed, mysql status = $ERRCODE"
        exit 1
    fi

    COUNT=$(echo "$RESULT" | wc -w)
    if [ "$COUNT" -gt 0 ]; then
        # Let's start with a new line. mysql could have printed something out.
        printf "\n"
        log_error "Expected empty database $db_name, but there are $COUNT tables: \n$RESULT. Aborting."
        exit 1
    fi

    # Beginning with MySQL 8.0, the db user needs additional settings or SUPER
    # privileges to create triggers and or functions. Call mysql_can_create to find
    # out if we're good to go.  If not, it will exit.
    printf "Verifying create permissions for %s\n" "$db_user"
    mysql_can_create

    printf "Initializing database using script %s\n" $scripts_dir/mysql/dhcpdb_create.mysql
    mysql -B --host=$db_host --user=$db_user --password=$db_password $db_name < $scripts_dir/mysql/dhcpdb_create.mysql
    ERRCODE=$?

    printf "mysql returned status code %s\n" "$ERRCODE"

    if [ "$ERRCODE" -eq 0 ]; then
        printf "Database version reported after initialization: "
        mysql_version
        printf "\n"
    fi

    exit $ERRCODE
}

pgsql_init() {
    printf "Checking if there is a database initialized already. Please ignore errors.\n"

    # Let's try to count the number of tables. Anything above 0 means that there
    # is some database in place. If there is anything, we abort.
    RESULT=$(pgsql_execute "\d")
    ERRCODE=$?
    if [ "$ERRCODE" -ne 0 ]; then
        log_error "pgsql_init: table query failed, status code: $ERRCODE?"
        exit 1
    fi

    COUNT=$(echo "$RESULT" | wc -w)
    if [ "$COUNT" -gt 0 ]; then
        printf "\n"
        log_error "Expected empty database $db_name, but the following tables are present \n$RESULT. Aborting."
        exit 2
    fi

    init_script="$scripts_dir/pgsql/dhcpdb_create.pgsql"
    printf "Initializing database using script %s\n" $init_script
    RESULT=$(pgsql_execute_script $init_script)
    ERRCODE=$?
    if [ "$ERRCODE" -ne 0 ]; then
        log_error "Database initialization failed, status code: $ERRCODE?"
        exit 1
    fi

    version=$(pgsql_version)
    printf "Database version reported after initialization: %s\n" "$version"
    exit 0
}

cql_init() {
    printf "Checking if there is a database initialized already... Please ignore errors.\n"

    result=$(cql_execute "DESCRIBE tables;")
    # Shellcheck complaints about missing quotes and word splitting here. There
    # is no problem here as wc -w always returns a single number.
    # shellcheck disable=SC2046
    if [ $(echo "$result" | grep -c "<empty>") -gt 0 ]; then
        printf "Creating and initializing tables using script %s...\n" $scripts_dir/cql/dhcpdb_create.cql
        cql_execute_script $scripts_dir/cql/dhcpdb_create.cql
    else
        log_error "Expected empty database $db_name, but the following tables are present \n$result. Aborting."
        exit 2
    fi

    version=$(cql_version)
    printf "Database version reported after initialization: %s\n" "$version"

    exit 0
}

### Functions that implement database version checking commands
memfile_version() {
    # @todo Implement this?
    log_error "NOT IMPLEMENTED"
    exit 1
}

### Functions used for upgrade
memfile_upgrade() {
    # Useless as Kea converts CSV versions at startup.
    log_error "NOT IMPLEMENTED"
    exit 1
}

# Upgrades existing MySQL database installation. The idea is that
# it will go over all upgrade scripts from (prefix)/share/kea/scripts/mysql
# and run them one by one. They will be named properly, so they will
# be run in order.
#
# This function prints version before and after upgrade.
mysql_upgrade() {

    printf "Database version reported before upgrade: "
    mysql_version
    printf "\n"

    # Check if the scripts directory exists at all.
    if [ ! -d ${scripts_dir}/mysql ]; then
        log_error "Invalid scripts directory: ${scripts_dir}/mysql"
        exit 1
    fi

    # Check if there are any files in it
    num_files=$(find "${scripts_dir}/mysql" -name 'upgrade*.sh' -type f | wc -l)
    if [ "$num_files" -eq 0 ]; then
        log_error "No scripts in ${scripts_dir}/mysql or the directory is not readable or does not have any upgrade* scripts."
        exit 1
    fi

    # Beginning with MySQL 8.0, the db user needs additional settings or SUPER
    # privileges to create triggers and or functions. Call mysql_can_create to find
    # out if we're good to go.  If not, it will exit.
    printf "Verifying upgrade permissions for %s\n" "$db_user"
    mysql_can_create

    for script in "${scripts_dir}"/mysql/upgrade*.sh
    do
        echo "Processing $script file..."
        sh "${script}" --host="${db_host}" --user="${db_user}" --password="${db_password}" "${db_name}"
    done

    printf "Database version reported after upgrade: "
    mysql_version
    printf "\n"
}

pgsql_upgrade() {
    version=$(pgsql_version)
    printf "Database version reported before upgrade: %s\n" "$version"

    # Check if the scripts directory exists at all.
    if [ ! -d ${scripts_dir}/pgsql ]; then
        log_error "Invalid scripts directory: ${scripts_dir}/pgsql"
        exit 1
    fi

    # Check if there are any files in it
    num_files=$(find "${scripts_dir}/pgsql" -name 'upgrade*.sh' -type f | wc -l)
    if [ "$num_files" -eq 0 ]; then
        log_error "No scripts in ${scripts_dir}/pgsql or the directory is not readable or does not have any upgrade* scripts."
        exit 1
    fi

    # Postgres psql does not accept pw on command line, but can do it
    # thru an env
    export PGPASSWORD=$db_password

    for script in "${scripts_dir}"/pgsql/upgrade*.sh
    do
        echo "Processing $script file..."
        sh "${script}" -U "${db_user}" -h "${db_host}" -d "${db_name}"
    done

    version=$(pgsql_version)
    printf "Database version reported after upgrade: %s\n" "$version"
    exit 0
}

cql_upgrade() {
    version=$(cql_version)
    printf "Database version reported before upgrade: %s\n" "$version"

    # Check if the scripts directory exists at all.
    if [ ! -d ${scripts_dir}/cql ]; then
        log_error "Invalid scripts directory: ${scripts_dir}/cql"
        exit 1
    fi

    # Check if directory is readable.
    if [ ! -r ${scripts_dir}/cql ]; then
        log_error "Directory is not readable: ${scripts_dir}/cql"
        exit 1
    fi

    # Check if there are upgrade scripts.
    find "${scripts_dir}/cql" -name 'upgrade*.sh' -type f
    retcode=$?
    if [ $retcode -eq 0 ]; then # Upgrade scripts are present.
        for script in "${scripts_dir}"/cql/upgrade*.sh
        do
            echo "Processing $script file..."
            sh "${script}" -u "${db_user}" -p "${db_password}" -k "${db_name}"
        done
    else
        echo "No upgrade script available."
    fi

    version=$(cql_version)
    printf "Database version reported after upgrade: %s\n" "$version"
    exit 0
}

# Utility function which tests if the given file exists and
# if so notifies the user and provides them the opportunity
# to abort the current command.
check_file_overwrite () {
    local file=$1
    if [ -e "${file}" ]
    then
        echo "Output file, $file, exists and will be overwritten."
        echo "Do you wish to continue? (y/n)"
        read -r ans
        if [ "${ans}" != "y" ]
        then
            echo "$command aborted by user."
            exit 1
        fi
    fi
}

### Functions used for dump

# Sets the global variable, dump_qry, to the schema-version specific
# SQL text needed to dump the lease data for the current backend
# and protocol
get_dump_query() {
    local version=$1

    case ${backend} in
    mysql)
        invoke="call"
        ;;
    pgsql)
        invoke="select * from"
        ;;
    *)
        log_error "unsupported backend ${backend}"
        usage
        exit 1
        ;;
    esac

    dump_qry="${invoke} lease${dump_type}DumpHeader();${invoke} lease${dump_type}DumpData();";
}

memfile_dump() {
    log_error "lease-dump is not supported for memfile"
    exit 1
}

mysql_dump() {

    # Check the lease type was given
    if [ $dump_type -eq 0 ]; then
        log_error "lease-dump: lease type ( -4 or -6 ) needs to be specified"
        usage
        exit 1
    fi

    # get the correct dump query
    version=$(mysql_version)
    retcode=$?
    if [ $retcode -ne 0 ]
    then
        log_error "lease-dump: mysql_version failed, exit code $retcode"
        exit 1;
    fi

    # Fetch the correct SQL text. Note this function will exit
    # if it fails.
    get_dump_query "$version"

    # Make sure they specified a file
    if [ "$dump_file" = "" ]; then
        log_error "you must specify an output file for lease-dump"
        usage
        exit 1

    fi

    # If output file exists, notify user, allow them a chance to bail
    check_file_overwrite $dump_file

    # Check the temp file too
    tmp_file="$dump_file.tmp"
    check_file_overwrite $tmp_file

    # Run the sql to output tab-delimited lease data to a temp file.
    # By using a temp file we can check for MySQL errors before using
    # 'tr' to translate tabs to commas.  We do not use MySQL's output
    # to file as that requires linux superuser privileges to execute
    # the select.
    mysql_execute "${dump_qry}" > $tmp_file
    retcode=$?
    if [ $retcode -ne 0 ]; then
        log_error "lease-dump: mysql_execute failed, exit code $retcode";
        exit 1
    fi

    # Now translate tabs to commas.
    tr '\t' ',' < $tmp_file >$dump_file
    retcode=$?
    if [ $retcode -ne 0 ]; then
        log_error "lease-dump: reformatting failed";
        exit 1
    fi

    # delete the tmp file on success
    rm $tmp_file
    echo lease$dump_type successfully dumped to $dump_file
    exit 0
}

### Functions used for dump
pgsql_dump() {
    # Check the lease type was given
    if [ $dump_type -eq 0 ]; then
        log_error "lease-dump: lease type ( -4 or -6 ) needs to be specified"
        usage
        exit 1
    fi

    version=$(pgsql_version)
    get_dump_query "$version"

    # Make sure they specified a file
    if [ "$dump_file" = "" ]; then
        log_error "you must specify an output file for lease-dump"
        usage
        exit 1

    fi

    # If output file exists, notify user, allow them a chance to bail
    check_file_overwrite $dump_file

    # psql does not accept password as a parameter but will look in the environment
    export PGPASSWORD=$db_password

    # Call psql and redirect output to the dump file. We don't use psql "to csv"
    # as it can only be run as db superuser.
    echo "$dump_qry" | psql --set ON_ERROR_STOP=1 -t -h $db_host -q --user=$db_user --dbname=$db_name -w --no-align --field-separator=',' >$dump_file
    retcode=$?

    # Check for errors.
    if [ $retcode -ne 0 ]; then
        log_error "lease-dump: psql call failed, exit code: $retcode";
        exit 1
    fi

    echo lease$dump_type successfully dumped to $dump_file
    exit 0
}

cql_dump() {
    # Get the query appropriate to lease version. Explicitly specify all columns
    # so that they are returned in expected order.
    if [ $dump_type -eq 4 ]; then
        dump_query="SELECT address,hwaddr,client_id,valid_lifetime,expire,subnet_id,fqdn_fwd,fqdn_rev,hostname,state,user_context FROM lease4"
    elif [ $dump_type -eq 6 ]; then
        dump_query="SELECT address,duid,valid_lifetime,expire,subnet_id,pref_lifetime,lease_type,iaid,prefix_len,fqdn_fwd,fqdn_rev,hostname,hwaddr,hwtype,hwaddr_source,state,user_context FROM lease6"
    else
        log_error "lease-dump: lease type ( -4 or -6 ) needs to be specified"
        usage
        exit 1
    fi

    # Check if file was specified.
    if [ "$dump_file" = "" ]; then
        log_error "lease-dump: output file needs to be specified with -o"
        usage
        exit 1
    fi

    # If output file exists, notify user, allow them a chance to bail.
    check_file_overwrite $dump_file

    # Run query, check for failure.
    result=$(cql_execute "$dump_query")
    return_code=$?
    if [ $return_code -ne 0 ]; then
        log_error "lease-dump: cql_execute failed, exit code $return_code";
        exit 1
    fi

    # Parse and display header.
    echo "$result" | head -n 2 | tail -n 1 | sed -e 's/[[:space:]]*//g' | sed -e 's/|/,/g' > $dump_file

    # Parse and display contents - done separately from header to allow sorting
    # by address. awk script replaces head -n -2 which is not portable.
    echo "$result" | tail -n +4 | awk 'n>=2 { print a[n%2] } { a[n%2]=$0; n=n+1 }' | sed -e 's/[[:space:]]*//g' | sed -e 's/|/,/g' | sort -r >> $dump_file

    echo lease$dump_type successfully dumped to $dump_file
    exit 0
}

### Script starts here ###

# First, find what the command is
command=${1}
if [ -z "${command}" ]; then
    log_error "missing command"
    usage
    exit 1
fi

# Check if this is a simple question about version.
if test "${command}" = "-v" || test "${command}" = "--version" ; then
    echo "${VERSION}"
    exit 0
fi

is_in_list "${command}" "db-init db-version db-upgrade lease-dump"
if [ ${_inlist} -eq 0 ]; then
    log_error "invalid command: ${command}"
    usage
    exit 1
fi
shift

# Second, check what's the backend
backend=${1}
if [ -z "${backend}" ]; then
    log_error "missing backend"
    usage
    exit 1
fi
is_in_list "${backend}" "memfile mysql pgsql cql"
if [ ${_inlist} -eq 0 ]; then
    log_error "invalid backend: ${backend}"
    exit 1
fi
shift

# Ok, let's process parameters (if there are any)
while [ -n "${1}" ]
do
    option=${1}
    case ${option} in
        # Specify database host
        -h|--host)
            shift
            db_host=${1}
            if [ -z "${db_host}" ]; then
                log_error "-h or --host requires a parameter"
                usage
                exit 1
            fi
            ;;
        # Specify database user
        -u|--user)
            shift
            db_user=${1}
            if [ -z "${db_user}" ]; then
                log_error "-u or --user requires a parameter"
                usage
                exit 1
            fi
            ;;
        # Specify database password
        -p|--password)
            shift
            db_password=${1}
            if [ -z "${db_password}" ]; then
                log_error "-p or --password requires a parameter"
                usage
                exit 1
            fi
            ;;
        # Specify database name
        -n|--name)
            shift
            db_name=${1}
            if [ -z "${db_name}" ]; then
                log_error "-n or --name requires a parameter"
                usage
                exit 1
            fi
            ;;
        -d|--directory)
            shift
            scripts_dir=${1}
            if [ -z "${scripts_dir}" ]; then
                log_error "-d or --directory requires a parameter"
                usage
                exit 1
            fi
            ;;
        # specify DHCPv4 lease type
        -4)
            if [ $dump_type -eq 6 ]; then
                log_error "you may not specify both -4 and -6"
                usage
                exit 1
            fi
            dump_type=4
            ;;
        # specify DHCPv6 lease type
        -6)
            if [ $dump_type -eq 4 ]; then
                log_error "you may not specify both -4 and -6"
                usage
                exit 1
            fi
            dump_type=6
            ;;
        # specify output file, currently only used by lease dump
        -o|--output)
            shift
            dump_file=${1}
            if [ -z "${dump_file}" ]; then
                log_error "-o or --output requires a parameter"
                usage
                exit 1
            fi
            ;;
        *)
            log_error "invalid option: ${option}"
            usage
            exit 1
    esac
    shift
done

case ${command} in
    # Initialize the database
    db-init)
        case ${backend} in
            memfile)
                memfile_init
                ;;
            mysql)
                mysql_init
                ;;
            pgsql)
                pgsql_init
                ;;
            cql)
                cql_init
                ;;
            esac
        ;;
    db-version)
        case ${backend} in
            memfile)
                memfile_version
                ;;
            mysql)
                mysql_version
                printf "\n"
                ;;
            pgsql)
                pgsql_version
                ;;
            cql)
                cql_version
                ;;
            esac
        ;;
    db-upgrade)
        case ${backend} in
            memfile)
                memfile_upgrade
                ;;
            mysql)
                mysql_upgrade
                ;;
            pgsql)
                pgsql_upgrade
                ;;
            cql)
                cql_upgrade
                ;;
            esac
        ;;
    lease-dump)
        case ${backend} in
            memfile)
                memfile_dump
                ;;
            mysql)
                mysql_dump
                ;;
            pgsql)
                pgsql_dump
                ;;
            cql)
                cql_dump
                ;;
            esac
        ;;
esac

exit 0
