#!/bin/bash
#
#      This confidential and proprietary software  may be used only as
#      authorized by a licensing agreement from Maia EDA Ltd.
# 
#                   (C) COPYRIGHT 2024-2025 Maia EDA Ltd.
#                            ALL RIGHTS RESERVED
#
#   The entire notice above must be reproduced on all authorized
#   copies and any such reproduction must be pursuant to a licensing
#   agreement from Maia EDA Ltd.
# 
# File          : $HeadURL: http://peregrine/repos/vserver-iso/docs/vs-install.sh $
# Date          : $Date: 2025-04-14 19:40:28 +0100 (Mon, 14 Apr 2025) $
# File Revision : $Revision: 15 $
# Author        : E.M. Lavelle
#
# Server123 network download to disk. To test, run with '--dry-run', and set
# 'ufile' to a small-ish file on the server (50-100MB). The checksums file on
# the server must also be updated for the download file. '--dry-run' will
# download the file, and check the sha256sum, but will not write the file to
# disk. Make sure that you also invalidate the checksums file, and confirm
# that the download fails.
#
# -----------------------------------------------------------------------------

if [ -z "$STY" ]; then exec screen -S netinst /bin/bash "$0"; fi

set -o pipefail

imgfile=https://www.server123.io/data/vserver-latest.img.gz
csmfile=https://www.server123.io/data/checksums-latest
support=support@server123.io

scriptname=$(basename "$0")
dryrun=0
ipaddr=

filesize=                                 # size of unzipped image
checksum1=                                # sha256sum, unzipped image
checksum2=                                # sha256sum, zipped image

support_msg="Please report this to $support."
errmsg1="It was not possible to download the vServer image. "
errmsg2="It was not possible to determine the size of the vServer image. "
errmsg3="It was not possible to download a checksums file. "
errmsg4="It was not possible to determine the IP address on the default route on this system. "
errmsg5="It was not possible to retrieve the server image file. "

errmsg1+=$support_msg
errmsg2+=$support_msg
errmsg3+=$support_msg
errmsg4+=$support_msg
errmsg5+=$support_msg

usage() {
    echo "Usage: sudo $scriptname [--dry-run]"
    echo ""
    echo "Attempt a Server123 network install on this system. You will"
    echo "be prompted for  confirmation before any destructive actions"
    echo "are carried out."
    echo "'--dry-run' can be  used to test the installation procedure;"
    echo "the server image will not be written to disk."
    exit 1
}

# -----------------------------------------------------------------------------
# Compare the SHA256 value recorded in the file named by the first parameter
# ($1) against the value returned from the server's checksums file.
#
# We return 0 for a successful comparison, and 1 otherwise.
# -----------------------------------------------------------------------------
compare_sha256sum() {
    if [ -z "$checksum2" ] || [ ! -f "$1" ]; then
        return 1
    fi

    actual=$(grep -oE '^[0-9a-fA-F]+' <"$1")
    [[ "$checksum2" = "$actual" ]]        # comparison result is returned
}

# -----------------------------------------------------------------------------
# Get the IP address of the adapter on the default route. This system could
# have multiple adapters on different default routes; this doesn't really
# matter, since the IP address is only used in a message to the user, with
# instructions for Stage 1 configuration. In principle, we should choose the
# default with lowest metric, but this code currently just uses the first one
# which appears in the 'ip route show' output (because 'read' reads a single
# line from stdin). In practice, I think this is probably the one with the
# lowest metric anyway.
# -----------------------------------------------------------------------------
get_ipaddr() {
    route=$(ip route show | grep default)

    read -ra fields <<< "$route"

    device=${fields[4]}

    locals=$(ip route show table local | grep "$device.*scope host")
    num_ip=$(echo -n "$locals" | grep -c '^')

    # we should only have one IP address on the default route
    if [[ "$num_ip" != 1 ]]; then
        error_exit "$errmsg3"
        exit 2
    fi

    # get that IP address
    read -ra fields <<< "$locals"
    ipaddr=${fields[1]}
}

# -----------------------------------------------------------------------------
# get the wget progress by checking the log file. A percentage is extracted
# and written to fd 3; anything written on fd 3 will be displayed in a dialog
# gauge widget. this function runs in the background.
#
# note that the wget logfile output is unbuffered, so the last line isn't
# generally complete. this code extracts and checks the second from last line,
# and misses the 100% output, which isn't required.
# -----------------------------------------------------------------------------
progress() {
    while :
    do
        sleep 1
        if [ ! -f "$pfile" ]; then
            continue
        fi
        status=$(tail -2 "$pfile" | head -1)
        if ! grep -E '[\. ]+[0-9]+%' <<<"$status" 2>&1 >/dev/null; then
            continue
        fi
        progress=$(echo "$status" | perl -ne "s/.*[\. ]+([0-9]+)%.*/\1/ && print")
        if ((progress > 100)); then
            progress=100
        fi
        echo "$progress" >&3
    done
}

# -----------------------------------------------------------------------------
# Error handling: pop up a message box, wait for the user Ok, and then
# exit. The error message must be the first parameter
# -----------------------------------------------------------------------------
error_exit() {
    popup "$1"
    terminate
    clear
    exit 1
}

# popup a message, wait for Ok
popup() {
    dialog --msgbox "$1" 12 75
}

# -----------------------------------------------------------------------------
# Terminate: close the pipe and remove any temporary files. Closing the pipe
# kills the backgrounded cat process, but not dialog, which continues running
# and intercepting stdin
# -----------------------------------------------------------------------------
terminate() {
    exec 3>&-
    rm -f "$tmppipe" "$pfile" "$tfile"

    if kill -0 "$cpid" >& /dev/null; then
        kill "$cpid"
    fi

    if kill -0 "$dpid" >& /dev/null; then
        kill "$dpid"
    fi
}

# CONTINUE HERE: check the args
if (( EUID > 0 || $# > 1 )); then
    usage
fi

if (($# == 1)); then
    case "$1" in
        --dry-run)
            dryrun=1
            ;;
        *)
            usage
            ;;
    esac
fi

# if we're on LVM, disable event_activation
lvmconf=/etc/lvm/lvm.conf
if [[ -f $lvmconf ]]; then
    perl -pi -e 's/^([ \t]*)#?[ \t]*event_activation.*/$1event_activation = 0/'  "$lvmconf"
fi

# get the drives on this system
if ! devlist=$(lsblk -Jbpo TYPE,NAME,SIZE,MODEL,MOUNTPOINTS | jq '
    .blockdevices[] | select(.type == "disk") | [ 
    .name, .size, .model,
    (.. | (.mountpoints? // []) | flatten[] | values)]' |\
    tr -d '\n' |\
    perl -pe 's/"\[([^\]]*)\]"/"\1"/g' |\
    sed 's/]/]\n/g' |\
    sed 's/[] []//g')
then
    printf "It was not possible to find any drives which were suitable for installation."
    exit 1
fi

# parse the lsblk output into these arrays
declare -a drives
declare -a sizes
declare -a models
declare -a mounts

while read -r line; do
    IFS=, read -ra fields <<< "$line"
    drives+=("${fields[0]}")
    sizes+=("${fields[1]}")
    models+=("${fields[2]}")
    if [ -z "${fields[3]}" ]; then
        mounts+=("0")
    else
        mounts+=("1")
    fi
done <<< "$devlist"

# remove the quotes around the CSV fields
for ((i=0; i<${#drives[@]}; i++)); do
    text="${drives[$i]}"
    text="${text%\"}"
    text="${text#\"}"
    drives[$i]=$text

    text="${sizes[$i]}"
    text="${text%\"}"
    text="${text#\"}"
    sizes[$i]=$text

    text="${models[$i]}"
    text="${text%\"}"
    text="${text#\"}"
    models[$i]=$text
done

# if this system is virtualised, replace all the drive names (which will be
# 'null')
if vtype=$(systemd-detect-virt 2>&1); then
    if [ "$vtype" == "none" ]; then
        vtype="Unknown"
    fi
    for ((i=0; i<${#drives[@]}; i++)); do
        models[$i]="$vtype"
    done
fi

height=0       # 20
width=0        # 73
menu_height=0  # 5
backtitle="Server123"

menu="
Select the drive which will be used for Server123 installation.

THIS DRIVE WILL BE  COMPLETELY  OVER-WRITTEN.  DO NOT PROCEED UNLESS
YOU ARE SURE THAT THIS DRIVE IS NOT REQUIRED, OR HAS BEEN BACKED UP.

You can move between the drive selections with the Up and Down arrow
keys.  You can move between  'Ok' and  'Cancel'  (or 'Yes' and 'No')
with the tab key.
"

declare -a options
for ((i=0; i<${#drives[@]}; i++)); do
    (( j=i+1 ))
    descr=${drives[$i]}
    descr+=" (model: "
    descr+="${models[$i]}"
    descr+="; size: "

    # produce a human-readable size
    # descr+=$(numfmt --to=si --suffix=B "${sizes[$i]}")
    descr+=$(numfmt --to=iec-i --suffix=B "${sizes[$i]}")

    descr+=")"
    options+=("$j")
    options+=("$descr")
done

# -----------------------------------------------------------------------------
# Ask the user to select a drive, and loop until we have an acceptable
# selection
# -----------------------------------------------------------------------------
while :
do
    failed=0

    if ! choice=$(dialog --clear  \
                    --no-collapse \
                    --backtitle "$backtitle" \
                    --title "Select drive" \
                    --menu "$menu" \
                    $height $width $menu_height \
                    "${options[@]}" \
                    2>&1 >/dev/tty)
    then
        continue
    fi

    (( drv_select=choice-1 ))
    (( opt_select=choice*2 -1 ))

    confirm="
You have selected:

 \Zb${options[$opt_select]}\ZB
"

    if(( ${mounts[$drv_select]} == 1 )); then
        failed=1
        confirm+="
This drive is currently mounted and cannot be used
for installation. Please select 'No' to try again."
    elif(( ${sizes[$drv_select]} < (20 * 2**30) )); then
        failed=1
        confirm+="
This drive is too small for a Server123 installation 
(a minimum of 20GiB is required).  Please select 'No' 
to try again.
"
    else
        confirm+="
If you confirm your selection, this drive will be
over-written. Do you want to proceed?"
    fi

    # this one writes no text to the output
    dialog --clear       \
           --colors      \
           --no-collapse \
           --backtitle "$backtitle" \
           --title "Confirm"        \
           --yesno "$confirm" $height $width \
           2> /dev/null
    excode=$?
    if(( excode == 0 && failed == 0)); then
      break
    fi
done

# -----------------------------------------------------------------------------
# make sure that we can connect to the server and find the download files
# -----------------------------------------------------------------------------

if ! output=$(wget --spider $imgfile 2>&1 >/dev/null); then
    error_exit "$errmsg1"
fi

# Get the download size. This isn't actually necessary, since wget produces a
# percentage indicator, but it might be useful in future
size=$(echo "$output" | perl -ne "s/^Length:[ ]*([0-9]+).*/\1/ && print")
if [ -z "$size" ]; then
    error_exit "$errmsg2"
fi
size=$(numfmt --to=iec-i --suffix=B "$size")

# get the checksums file
if ! checksums=$(wget -qO - $csmfile); then
    error_exit "$errmsg4"
fi

# extract the 3 lines of the checksums file
filesize=$( echo "$checksums" | sed '1!d' | grep -oE '^[0-9]+')
checksum1=$(echo "$checksums" | sed '2!d' | grep -oE '^[0-9a-fA-F]+')
checksum2=$(echo "$checksums" | sed '3!d' | grep -oE '^[0-9a-fA-F]+')

if [ -z "$filesize" ] || [ -z "$checksum1" ] || [ -z "$checksum2" ]; then
    error_exit "The server's checksums file is invalid, or has been downloaded incorrectly. Please try again."
fi

# -----------------------------------------------------------------------------
# get the selected drive name and our IP address
# -----------------------------------------------------------------------------

drive=${drives[$drv_select]}
get_ipaddr
if [ -z "$ipaddr" ]; then
    error_exit "$errmsg4"
fi

# -----------------------------------------------------------------------------
# Set up a gauge (progress bar) widget, running in the background. we write to
# fd 3 to send data to the widget's stdin
# -----------------------------------------------------------------------------

# get a message for the widget
insmsg=$(cat <<EOF
$drive installation...

The Server123 image will be downloaded  and written to your drive.
The image will be checked after the download completes, which will
take an additional minute or so.  When the image has been verified
you will be asked to confirm a reboot.
Following the  reboot you can  carry out configuration by browsing
to http://$ipaddr/configure.
Note that  configuration must be carried out over http, not https.
EOF
)

tmppipe=$(mktemp -u)
mkfifo "$tmppipe"

# start a writer to prevent an EOF being sent; preserve the writer's pid to
# make sure that it terminates
cat > "$tmppipe" &
cpid=$!

# connect the pipe to fd 3; we can then write data to the widget on this fd
exec 3<>"$tmppipe"

# start the widget in the background; keep the new pid so that we can kill it
dialog --no-collapse --gauge "$insmsg" 15 75 <&3 &
dpid=$!

# run wget in the background
pfile=$(mktemp)                           # progress file
if ((dryrun != 0)); then
    tfile=$(mktemp)                       # checksum file
    (wget -o "$pfile" -O - "$imgfile" | sha256sum > "$tfile" 2>/dev/null) &
    wpid=$!
else
    (wget -o "$pfile" -O - "$imgfile" | \
         gunzip -c | \
         cat > "$drive" 2>/dev/null) &
    wpid=$!
fi

# start the progress function in the background
progress &
ppid=$!

# wait for the wget pipe to finish
if wait $wpid; then
    kill $ppid
    if ((dryrun != 0)); then
        if compare_sha256sum "$tfile"; then
            popup "'$imgfile' has been downloaded with a valid checksum."
        else
            popup "There was a checksum error in the image file download. Please try again."
        fi
    else
        # check the image
        if ! csuma=$(head -c "$filesize" "$drive" | sha256sum 2>/dev/null); then
            error_exit "The decompressed image did not pass checksum test 1. Please try again."
        fi

        # remove the trailing '-' and compare
        csumb=$(grep -oE '^[0-9a-fA-F]+' <<<"$csuma")
        if [[ "$csumb" != "$checksum1" ]]; then
            error_exit "The decompressed image did not pass checksum test 2. Please try again."
        fi

        popup "Installation has completed. Select 'Ok' to reboot..."
        /sbin/shutdown -r now
    fi
else
    sleep 3
    kill $ppid
    popup "$errmsg5"
fi

terminate
clear

# ------------------------------------ EOF ------------------------------------
