#!/bin/sh

# Copyright (C) 2019-2020 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/.

# shellcheck disable=SC1091
# SC1091: Not following: ... was not specified as input (see shellcheck -x).

# shellcheck disable=SC2039
# SC2039: In POSIX sh, arrays are undefined.

# Exit with error if commands exit with non-zero and if undefined variables are
# used.
set -eu

# shellcheck disable=SC2034
# SC2034: ... appears unused. Verify use (or export if used externally).
prefix="/usr"

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

# Need a path for temporary files created during upgrade data migration
# Use the state directory in the install path directory if it exists, otherwise
# use the build tree
if [ -e "/var/lib/kea" ]; then
    temp_file_dir="/var/lib/kea"
else
    temp_file_dir="/build/kea/src/kea-1.9.3/src/share/database/scripts/cql"
fi

# Ensures the current schema version is 3.0. If not it exits.
check_version() {
    version=$(cql_version "${@}")

    if [ "${version}" != "3.0" ]; then
        printf "This script upgrades 3.0 to 4.0. Reported version is %s. Skipping upgrade.\n" "${version}"
        exit 0
    fi
}

# Peforms the schema changes from 3.0 to 4.0
update_schema() {
    cqlsh "${@}" <<EOF
-- This line starts database upgrade to version 4.0

-- -----------------------------------------------------
-- Table \`hosts\`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS hosts (
    key BIGINT,
    id BIGINT,
    host_identifier BLOB,
    host_identifier_type INT,
    host_ipv4_subnet_id INT,
    host_ipv6_subnet_id INT,
    host_ipv4_address INT,
    host_ipv4_next_server INT,
    host_ipv4_server_hostname VARCHAR,
    host_ipv4_boot_file_name VARCHAR,
    hostname VARCHAR,
    auth_key VARCHAR,
    user_context VARCHAR,
    host_ipv4_client_classes VARCHAR,
    host_ipv6_client_classes VARCHAR,
    -- reservation
    reserved_ipv6_prefix_address VARCHAR,
    reserved_ipv6_prefix_length INT,
    reserved_ipv6_prefix_address_type INT,
    iaid INT,
    -- option
    option_universe INT,
    option_code INT,
    option_value BLOB,
    option_formatted_value VARCHAR,
    option_space VARCHAR,
    option_is_persistent BOOLEAN,
    option_client_class VARCHAR,
    option_subnet_id INT,
    option_user_context VARCHAR,
    option_scope_id INT,
    PRIMARY KEY ((key), id)
);

CREATE INDEX IF NOT EXISTS hostsindex1 ON hosts (host_identifier);
CREATE INDEX IF NOT EXISTS hostsindex2 ON hosts (host_identifier_type);
CREATE INDEX IF NOT EXISTS hostsindex3 ON hosts (host_ipv4_subnet_id);
CREATE INDEX IF NOT EXISTS hostsindex4 ON hosts (host_ipv6_subnet_id);
CREATE INDEX IF NOT EXISTS hostsindex5 ON hosts (host_ipv4_address);
CREATE INDEX IF NOT EXISTS hostsindex6 ON hosts (reserved_ipv6_prefix_address);
CREATE INDEX IF NOT EXISTS hostsindex7 ON hosts (reserved_ipv6_prefix_length);
EOF
}

# Peforms the clean up schema changes from 3.0 to 4.0
clean_up_schema() {
    cqlsh "${@}" <<EOF
DROP TABLE IF EXISTS host_reservations;

DROP INDEX IF EXISTS host_reservationsindex1;
DROP INDEX IF EXISTS host_reservationsindex2;
DROP INDEX IF EXISTS host_reservationsindex3;
DROP INDEX IF EXISTS host_reservationsindex4;
DROP INDEX IF EXISTS host_reservationsindex5;
DROP INDEX IF EXISTS host_reservationsindex6;
DROP INDEX IF EXISTS host_reservationsindex7;

-- Cql requires primary keys in the WHERE here.
DELETE FROM schema_version WHERE version=3;
INSERT INTO schema_version (version, minor) VALUES(4, 0);

-- This line concludes database upgrade to version 4.0
EOF
}

# Function to delete temporary migration files
clean_up() {
    # clean up the files
    if [ -e "$export_file" ]
    then
        rm "$export_file"
    fi

    if [ -e "$update_file" ]
    then
        rm "$update_file"
    fi
}

# Function to clean up and exit the script gracefully
#
# Called by migrate_host_data()
#
# Parameters:
# status - integer value to pass to sh:exit
# explanation - "quoted" text message to emit to stdout
exit_now() {
    status=$1; shift
    explanation=$1; shift

    clean_up
    if [ "$status" -eq 0 ]
    then
        clean_up_schema "${@}"
        echo "Data Migration SUCCESS! $explanation"
    else
        echo "Data Migration FAILURE! $explanation"
    fi

    exit "$status"
}

# Prefix ${string} with ${count} characters like the one spciefied in
# ${fill_char}.
fill() {
    string=$1;shift
    count=$1;shift
    fill_char=$1;shift
    value="$(printf "%${count}s" | sed "s/ /${fill_char}/g")${string}"
}

identifier_text() {
    string=$1;shift
    length="${#string}"
    # skip 0x from 0xabcdef
    string=$(echo "$string" | cut -c "3-$length")
    identifier=""
    # add starting 0: 0xabc->0x0abc
    mod=$((length % 2))
    if [ $mod -ne 0 ]; then
        string="0"$string
    fi
    length="${#string}"
    i=1
    while [ "$i" -le "$length" ]; do
        char=$(echo "$string" | cut -c $i-$i)
        mod=$((i % 2))
        if test $mod -ne 0 && test $i -ne 1; then
            char=":"$char
        fi
        identifier=$identifier$char
        i=$((i+1))
    done
}

key_hash() {
    string=$1;shift
    length="${#string}"
    FNV_prime=1099511628211
    FNV_offset_basis=-3750763034362895579 # signed value for 14695981039346656037
    hash=$FNV_offset_basis
    i=1
    while [ "$i" -le "$length" ]; do
        char=$(echo "$string" | cut -c $i-$i)
        data=$(echo "$char" | tr -d "\n" | od -An -t uC | tr -d ' ')
        hash=$((hash ^ data))
        hash=$((hash * FNV_prime))
        i=$((i+1))
    done
}

generate_key() {
    host_id=$1;shift
    host_identifier=$1;shift
    host_identifier_type=$1;shift
    host_ipv4_subnet_id=$1;shift
    host_ipv6_subnet_id=$1;shift
    host_ipv4_address=$1;shift
    key=""
    identifier_text "$host_identifier"
    local_host_identifier=$identifier
    if [ -n "$host_ipv4_address" ] && [ "$host_ipv4_address" -eq 0 ]; then
        fill "$local_host_identifier" 383 "-"
        key="$key$value"
        fill "$host_identifier_type" 10 "-"
        key="$key$value"
    else
        fill "" 383 "-"
        key="$key$value"
        fill "" 10 "-"
        key="$key$value"
    fi
    fill "$host_ipv4_subnet_id" 10 "-"
    key="$key$value"
    fill "$host_ipv6_subnet_id" 10 "-"
    key="$key$value"
    fill "$host_ipv4_address" 15 "-"
    key="$key$value"
    key_hash "$key"
    key="$hash"
}

# This function adds host 'key' column which is the partition key
# of the 'hosts' table.
#
# After exhausting the export file, the update file is submitted to
# cqlsh for execution.
#
# No parameters.
migrate_host_data() {
    export_file="$temp_file_dir/cql_export.csv"
    update_file="$temp_file_dir/cql_update.cql"

    clean_up

    # Fetch host_reservation data so we have host_identifier,
    # host_identifier_type, host_ipv4_subnet_id, host_ipv6_subnet_id and
    # host_ipv4_address to generate host key
    echo "Exporting host_reservation data to $export_file ..."
    query="COPY host_reservations \
        (id, host_identifier, host_identifier_type, host_ipv4_subnet_id, \
         host_ipv6_subnet_id, host_ipv4_address, host_ipv4_next_server, \
         host_ipv4_server_hostname, host_ipv4_boot_file_name, hostname, \
         auth_key, user_context, host_ipv4_client_classes, \
         host_ipv6_client_classes, reserved_ipv6_prefix_address, \
         reserved_ipv6_prefix_length, reserved_ipv6_prefix_address_type, \
         iaid, option_universe, option_code, option_value, \
         option_formatted_value, option_space, option_is_persistent, \
         option_client_class, option_subnet_id, option_user_context, \
         option_scope_id) \
        TO '$export_file'"


    if ! cqlsh "${@}" -e "$query"
    then
        exit_now 1 "Cassandra export failed! Could not migrate data!" "${@}"
    fi

    # Strip the carriage returns that CQL insists on adding.
    if [ -e "$export_file" ]
    then
        tr -d '\015' < "${export_file}" > $export_file.2
        mv $export_file.2 $export_file
    else
        # Shouldn't happen but then again we're talking about CQL here
        exit_now 1 "Cassandra export file $export_file is missing?" "${@}"
    fi

    # Iterate through the exported data, accumulating update statements,
    # one for each reservation that needs updating.  We should have one
    # host per line.
    line_cnt=0
    update_cnt=0
    while read -r line
    do
        line_cnt=$((line_cnt + 1))
        xIFS="$IFS"
        IFS=','
        i=1
        # Parse the column values
        for val in $line
        do
            case $i in
            1)
                host_id=$val
                ;;
            2)
                host_identifier=$val
                ;;
            3)
                host_identifier_type=$val
                ;;
            4)
                host_ipv4_subnet_id=$val
                ;;
            5)
                host_ipv6_subnet_id=$val
                ;;
            6)
                host_ipv4_address=$val
                ;;
            *)
                ;;
            esac
            i=$((i + 1))
        done

        generate_key "$host_id" "$host_identifier" "$host_identifier_type" "$host_ipv4_subnet_id" "$host_ipv6_subnet_id" "$host_ipv4_address"
        key_data="$key"
        update_cnt=$((update_cnt + 1))

        IFS="$xIFS"
        echo "${line}" | sed -e "s/$host_id/$host_id,$key_data/" >> "${update_file}"
    done < "${export_file}"

    # If we didn't record any updates, then hey, we're good to go!
    if [ "$update_cnt" -eq 0 ]
    then
        exit_now 0 "Completed successfully: No updates were needed" "${@}"
    fi

    # We have at least one update in the update file, so submit it # to cqlsh.
    echo "$update_cnt update statements written to $update_file"
    echo "Running the updates..."
    query="COPY hosts \
        (id, key, host_identifier, host_identifier_type, host_ipv4_subnet_id, \
         host_ipv6_subnet_id, host_ipv4_address, host_ipv4_next_server, \
         host_ipv4_server_hostname, host_ipv4_boot_file_name, hostname, \
         auth_key, user_context, host_ipv4_client_classes, \
         host_ipv6_client_classes, reserved_ipv6_prefix_address, \
         reserved_ipv6_prefix_length, reserved_ipv6_prefix_address_type, \
         iaid, option_universe, option_code, option_value, \
         option_formatted_value, option_space, option_is_persistent, \
         option_client_class, option_subnet_id, option_user_context, \
         option_scope_id) \
        FROM '$update_file'"

    if ! cqlsh "${@}" -e "$query"
    then
        exit_now 1 "Cassandra updates failed" "${@}"
    fi

    exit_now 0 "Updated $update_cnt of $line_cnt records" "${@}"
}

check_version "${@}"
if ! update_schema "${@}"; then
    printf 'Schema update FAILED!\n'
    exit 1
fi
migrate_host_data "${@}"
