#!/usr/bin/env bash

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

username=root
userDataServerPort=8080
configDriveLabel=config-2

function findPrimaryNetwork(){
    outputLog "Detecting primary network"
    if command -v ip &> /dev/null
    then
        primaryNet=$(ip -o -4 route show to default | awk '{print $5}')
    elif command -v netstat &> /dev/null
    then
        primaryNet=$(netstat -r4 | grep default | awk '{print $(NF)}')
    elif command -v route &> /dev/null
    then
        primaryNet=$(route -4 2> /dev/null | grep default | awk '{print $(NF)}')
        if [ -z "$primaryNet" ]
        then
            primaryNet=$(route get default 2> /dev/null | grep interface | tr -d ' ' | awk '{split($0,a,":"); print a[2]}')
        fi
    fi
    if [ -z "$primaryNet" ]
    then
        outputLog "Could not find primary network"
        return 1
    fi
    echo "$primaryNet"
    return 0
}

function findUserDataServer(){
    primaryNet=$1
    outputLog "Trying to find userdata server"
    if [ -z "$primaryNet" ]
    then
        outputLog "Unable to determine the userdata server, falling back to data-server"
        echo "data-server"
        return 0
    fi

    if command -v netplan &> /dev/null
    then
        outputLog "Operating System is using netplan"

        userDataServer=$(netplan ip leases "$primaryNet" | grep SERVER_ADDRESS | awk '{split($0,a,"="); print a[2]}')

        if [ -n "$userDataServer" ]
        then
            outputLog "Found userdata server IP $userDataServer in netplan config"
            echo "$userDataServer"
            return 0
        fi
    fi

    if command -v nmcli &> /dev/null
    then
        outputLog "Operating System is using NetworkManager"

        userDataServer=$(nmcli -t connection show "$(nmcli -t -f UUID,DEVICE connection | grep "$primaryNet" | awk '{split($0,a,":"); print a[1]}')" | grep next_server | tr -d ' ' |awk '{split($0,a,"="); print a[2]}')

        if [ -n "$userDataServer" ]
        then
            outputLog "Found userdata server IP $userDataServer in NetworkManager config"
            echo "$userDataServer"
            return 0
        fi
    fi

    if command -v wicked &> /dev/null
    then
        outputLog "Operating System is using wicked"

        userDataServer=$(grep SERVERID /run/wicked/leaseinfo."$primaryNet"* | tr -d "'" | awk '{split($0,a,"="); print a[2]}')

        if [ -n "$userDataServer" ]
        then
            outputLog "Found userdata server IP $userDataServer in wicked config"
            echo "$userDataServer"
            return 0
        fi
    fi

    if command -v udhcpc &> /dev/null
    then
        outputLog "Operating System is using udhcpc"

        userDataServer=$(< /run/dhcp-server-ip."$primaryNet")

        if [ -n "$userDataServer" ]
        then
            outputLog "Found userdata server IP $userDataServer in udhcpc"
            echo "$userDataServer"
            return 0
        fi
    fi

    outputLog "Searching for DHCP server in lease files"

    primaryLease=$(
        dhcpFolders="/var/lib/dhclient/* /var/lib/dhcp3/* /var/lib/dhcp/* /var/lib/NetworkManager/* /var/db/dhclient*"
        for files in $dhcpFolders
        do
            if [ -e "$files" ]
            then
                < "$files" tr -d '\n' | sed 's/  //g ; s/lease {//g ; s/}/\n/g' | grep 'option routers'
            fi
        done
    )

    serverList=$(
        IFS=$'\n'
        for line in $(echo -e "$primaryLease")
        do
            splitLine=$(echo "$line" | sed -e 's/;/\n/g')
            if date -j &> /dev/null
            then
                timestamp=$(date -j -f "%Y/%m/%d %H:%M:%S" "$(echo "$splitLine" | grep 'expire' | sed -r 's/.*expire [0-9]+ (.*)/\1/')" +"%s")
            else
                timestamp=$(date -d "$(echo "$splitLine" | grep 'expire' | sed -e 's/.*expire [0-9]\+ \(.*\)/\1/')" +"%s")
            fi
            interface=$(echo "$splitLine" | grep 'interface' | sed -e 's/.*interface "\(.*\)"/\1/')
            server=$(echo "$splitLine" | grep 'dhcp-server-identifier' | sed -e 's/.*dhcp-server-identifier \(.*\)/\1/')
            echo "$timestamp","$interface","$server"
        done
    )

    userDataServer=$(echo "$serverList" | grep "$primaryNet" | sort -n | tail -1 | awk '{split($0,a,","); print a[3]}')

    if [ -n "$userDataServer" ]
    then
        outputLog "Userdata server found: $userDataServer"
        echo "$userDataServer"
        return 0
    fi

    outputLog "Unable to determine the userdata server, falling back to data-server"
    echo "data-server"
    return 0
}

function getPasswordFromUserDataServer(){
    userDataServer=$1
    userDataServerPort=$2
    outputLog "Sending request to userdata server at $userDataServer to get the password"
    if ! response=$(curl --fail --silent --connect-timeout 20 --retry 3 --header "DomU_Request: send_my_password" http://"$userDataServer":"$userDataServerPort")
    then
        outputLog "Failed to send request to userdata server at $userDataServer"
        return 4
    fi
    outputLog "Got response from userdata server at $userDataServer"
    response=$(echo "$response" | tr -d '\r')
    case $response in
        "")
            outputLog "Userdata server at $userDataServer did not have any password for the VM"
            return 2
        ;;
        "bad_request")
            outputLog "VM sent an invalid request to userdata server at $userDataServer"
            return 3
        ;;
        "saved_password")
            outputLog "VM has already saved a password from the userdata server at $userDataServer"
            return 1
        ;;
        *)
            outputLog "VM got a valid password from server at $userDataServer"
            echo "$response"
            return 0
    esac
}

function findHomeDirectory(){
    username=$1
    getent passwd "$username"|awk -F ":" '{print $6}'
}

function setPassword(){
    username=$1
    homeDir=$2
    password=$3
    if command -v md5sum &> /dev/null
    then
        newMd5=$(echo "$password" | md5sum | awk '{print $1}')
    elif command -v md5 &> /dev/null
    then
        newMd5=$(echo "$password" | md5)
    else
        newMd5='N/A'
    fi
    if [ $newMd5 != 'N/A' ]
    then
        if [ -f "$homeDir"/.password.md5 ]
        then
            oldMd5=$(cat "$homeDir"/.password.md5)
        fi
        if [ "$newMd5" ==  "$oldMd5" ]
        then
            outputLog  "There is no update of VM password"
            return 0
        fi
    else
        outputLog "Cannot determine change of password"
    fi
    outputLog "Changing password for user $username"
    if command -v chpasswd &> /dev/null
    then
        echo "$username":"$password" | chpasswd
    elif command -v usermod &> /dev/null && command -v mkpasswd &> /dev/null
    then
        usermod -p "$(mkpasswd -m SHA-512 "$password")" "$username"
    elif command -v pw &> /dev/null
    then
        echo "$password" | pw mod user "$username" -h 0
    else
        outputLog "Failed to change password for user $username"
        return 1
    fi
    outputLog "Successfully changed password for user $username"
    if [ $newMd5 != 'N/A' ]
    then
        echo "$newMd5" > "$homeDir"/.password.md5
        chmod 600 "$homeDir"/.password.md5
        chown "$username": "$homeDir"/.password.md5
    fi
    return 0
}

function sendAckToUserDataServer(){
    userDataServer=$1
    userDataServerPort=$2
    outputLog "Sending acknowledgment to userdata server at $userDataServer"
    if ! curl --fail --silent --connect-timeout 20 --retry 3 --header "DomU_Request: saved_password" "$userDataServer":"$userDataServerPort" &> /dev/null
    then
        outputLog "Failed to sent acknowledgment to userdata server at $userDataServer"
        return 1
    fi
    outputLog "Successfully sent acknowledgment to userdata server at $userDataServer"
    return 0
}

function getPublicKeyFromUserDataServer(){
    userDataServer=$1
    outputLog "Sending request to userdata server at $userDataServer to get public key"
    if ! response=$(curl --fail --silent --connect-timeout 20 --retry 3 http://"$userDataServer"/latest/public-keys)
    then
        outputLog "Failed to get public key from userdata server"
        return 2
    fi
    outputLog "Got response from userdata server at $userDataServer"
    if [ -z "$response" ]
    then
        outputLog "Did not receive any public keys from userdata server"
        return 1
    fi
    outputLog "Successfully get public key from userdata server"
    echo "$response"
    return 0
}

function setPublicKey(){
    username=$1
    homeDir=$2
    publicKey=$3
    outputLog "Applying public key for $username"
    sshDir=$homeDir/.ssh
    authorizedKeysFile=$sshDir/authorized_keys

    if [ ! -d "$sshDir" ]
    then
        outputLog ".ssh directory for $username not found, creating .ssh directory"
        mkdir "$sshDir"
    fi

    if [ ! -f "$authorizedKeysFile" ]
    then
        outputLog "authorized_keys file for $username not found, creating authorized_keys file"
        touch "$authorizedKeysFile"
    fi
    if grep "$(echo "$publicKey" | awk '{print $2}')" "$authorizedKeysFile" > /dev/null
    then
        outputLog "No need to update authorized_keys file"
        return 0
    fi
    outputLog "Writing public key in authorized_keys file"
    sed -i "/ cloudstack@apache.org$/d" "$authorizedKeysFile"
    echo "$publicKey cloudstack@apache.org" >> "$authorizedKeysFile"
    chmod 600 "$authorizedKeysFile"
    chmod 700 "$sshDir"
    chown -R "$username": "$sshDir"
    which restorecon &> /dev/null && restorecon -R -v "$sshDir"
    return 0
}

function findConfigDrive(){
    configDriveLabel=$1
    outputLog "Searching for ConfigDrive"

    if [ -e /dev/disk/by-label/"$configDriveLabel" ]
    then
        outputLog "ConfigDrive found at /dev/disk/by-label/$configDriveLabel"
        echo "/dev/disk/by-label/$configDriveLabel"
        return 0
    fi

    if [ -e /dev/iso9660/"$configDriveLabel" ]
    then
        outputLog "ConfigDrive found at /dev/iso9660/$configDriveLabel"
        echo "/dev/iso9660/$configDriveLabel"
        return 0
    fi

    blockDevice=$(blkid -t LABEL="$configDriveLabel" /dev/hd? /dev/sd? /dev/xvd? /dev/vd? /dev/sr? -o device 2> /dev/null)
    if [ -n "$blockDevice" ]
    then
        outputLog "ConfigDrive found at $blockDevice"
        echo "$blockDevice"
        return 0
    fi
    outputLog "ConfigDrive not found"
    return 1
}

function mountConfigDrive(){
    disk=$1
    outputLog "Mounting ConfigDrive"
    mountDir=$(mktemp -d)
    if [ ! -e "$mountDir" ]
    then
        mkdir "$mountDir"
        chmod 700 "$mountDir"
    fi

    mounted=0
    if [ $mounted == 0 ] && mount -r "$disk" "$mountDir" &> /dev/null
    then
        mounted=1
    fi
    if [ $mounted == 0 ] && mount -r -t cd9660 "$disk" "$mountDir" &> /dev/null
    then
        mounted=1
    fi
    if [ $mounted == 0 ] && mount -r -t iso9660 "$disk" "$mountDir" &> /dev/null
    then
        mounted=1
    fi

    if [ $mounted == 1 ]
    then
        outputLog "$disk successfully mounted on $mountDir"
        echo "$mountDir"
        return 0
    fi

    outputLog "Failed mounting $disk on $mountDir"
    rm -rf "$mountDir"
    return 1
}

function unmountConfigDrive(){
    mountDir=$1
    outputLog "Unmounting ConfigDrive"
    if ! umount "$mountDir"
    then
        outputLog "Failed unmounting $mountDir"
        return 1
    fi
    rm -rf "$mountDir"
    outputLog "Successfully unmount $mountDir"
    return 0
}

function getPasswordFromConfigDrive(){
    mountDir=$1
    passwordFile=$mountDir/cloudstack/password/vm_password.txt
    if [ ! -f "$passwordFile" ]
    then
        outputLog "Password file not found in ConfigDrivee"
        return 3
    fi
    outputLog "Password file found in ConfigDrive"
    content=$(< "$passwordFile" tr -d '\r')

    case $content in

        "")
            outputLog "ConfigDrive did not have any password for the VM"
            return 2
        ;;

        "saved_password")
            outputLog "VM has already saved a password"
            return 1
        ;;

        *)
            outputLog "VM got a valid password"
            echo "$content"
            return 0
    esac
}

function getPublicKeyFromConfigDrive() {
    mountDir=$1
    publicKeyFile=$mountDir/cloudstack/metadata/public-keys.txt

    if [ ! -f "$publicKeyFile" ]
    then
        outputLog "Public key file not found in ConfigDrive"
        return 2
    fi
    content=$(< "$publicKeyFile" tr -d '\r')

    if [ -z "$content" ]
    then
        outputLog "Did not receive any public keys"
        return 1
    fi
    echo "$content"
    outputLog "Public key successfully received."
    return 0
}

function outputLog() {
    stderr=1
    logger=1
    message=$1
    if [ $stderr == 1 ]
    then
        echo "Cloud Password Manager: $message" 1>&2
    fi
    if [ $logger == 1 ]
    then
        logger -t "Cloud Password Manager" "$message"
    fi
}

publicKeyReceived=0
passwordReceived=0
dataSource=''

if disk=$(findConfigDrive "$configDriveLabel")
then
    if mountDir=$(mountConfigDrive "$disk")
    then
        dataSource='ConfigDrive'
        if publicKey=$(getPublicKeyFromConfigDrive "$mountDir")
        then
            publicKeyReceived=1
        fi
        if password=$(getPasswordFromConfigDrive "$mountDir")
        then
            passwordReceived=1
        fi
        unmountConfigDrive "$mountDir"
    fi
fi
if [ $publicKeyReceived == 0 ] || [ $passwordReceived == 0 ]
then
    primaryNet=$(findPrimaryNetwork)
    userDataServer=$(findUserDataServer "$primaryNet")
    if [ $publicKeyReceived == 0 ]
    then
        if publicKey=$(getPublicKeyFromUserDataServer "$userDataServer")
        then
            dataSource='UserDataServer'
            publicKeyReceived=1
        fi
    fi
    if [ $passwordReceived == 0 ]
    then
        if password=$(getPasswordFromUserDataServer "$userDataServer" "$userDataServerPort")
        then
            dataSource='UserDataServer'
            passwordReceived=1
        fi
    fi
fi
homeDir=$(findHomeDirectory "$username")
if [ $passwordReceived == 1 ]
then
    setPassword "$username" "$homeDir" "$password"
    if [ $dataSource == 'UserDataServer' ]
    then
        sendAckToUserDataServer "$userDataServer" "$userDataServerPort"
    fi
fi
if [ $publicKeyReceived == 1 ]
then
    setPublicKey "$username" "$homeDir" "$publicKey"
fi
