#!/bin/bash
#
# exec linbo commands remote per ssh
#
# thomas@linuxmuster.net
# 20251111
# GPL V3
#

# read linuxmuster environment
source /usr/share/linuxmuster/helperfunctions.sh || exit 1

KNOWNCMDS="label partition format initcache new sync postsync start prestart create_image create_qdiff upload_image upload_qdiff reboot halt"
DLTYPES="multicast rsync torrent"
SSH="/usr/sbin/linbo-ssh -o BatchMode=yes -o StrictHostKeyChecking=no"
SCP=/usr/sbin/linbo-scp
WRAPPER=/usr/bin/linbo_wrapper
WOL="$(which wakeonlan)"
TMPDIR=/var/tmp
SCHOOL="default-school"

# usage info
usage(){
  msg="$1"
  echo
  echo "Usage: `basename $0` <options>"
  echo
  echo "Options:"
  echo
  echo " -h                 Show this help."
  echo " -a <hostname>      Attach the running tmux session for this hostname."
  echo " -b <sec>           Wait <sec> second(s) between sending wake-on-lan magic"
  echo "                    packets to the particular hosts. Must be used in"
  echo "                    conjunction with \"-w\"."
  echo " -c <cmd1,cmd2,...> Comma separated list of linbo commands transfered"
  echo "                    per ssh direct to the client(s). Gui will be disabled"
  echo "                    during execution."
  echo " -d                 Disables gui on next boot."
  echo " -g <group>         All hosts of this hostgroup will be processed."
  echo " -i <i1,i2,...>     Single ip or hostname or comma separated list of ips"
  echo "                    or hostnames of clients to be processed."
  echo " -l                 List current linbo-remote tmux sessions."
  echo " -n                 Bypasses start.conf configured auto functions"
  echo "                    (partition, format, initcache, start) on next boot."
  echo " -r <room>          All hosts of this room will be processed."
  echo " -s <school>        Select a school other than default-school"
  echo " -p <cmd1,cmd2,...> Create an onboot command file executed automatically"
  echo "                    once next time the client boots."
  echo " -u                 Use broadcast address for wol additionally."
  echo " -w <sec>           Send wake-on-lan magic packets to the client(s)"
  echo "                    and wait <sec> seconds before executing the"
  echo "                    commands given with \"-c\" or in case of \"-p\" after"
  echo "                    the creation of the pxe boot files."
  echo
  echo "Important: * Options \"-r\", \"-g\" and \"-i\" exclude each other, \"-c\" and"
  echo "             \"-p\" as well."
  echo
  echo "Supported commands for -c or -p options are:"
  echo
  echo "partition                : Writes the partition table."
  echo "label                    : Labels all partitions defined in start.conf."
  echo "                           Note: Partitions have to be formatted."
  echo "format                   : Writes the partition table and formats all"
  echo "                           partitions."
  echo "format:<#>               : Writes the partition table and formats only"
  echo "                           partition nr <#>."
  echo "initcache:<dltype>       : Updates local cache. <dltype> is one of"
  echo "                           rsync|multicast|torrent."
  echo "                           If dltype is not specified it is read from"
  echo "                           start.conf."
  echo "sync:<#>                 : Syncs the operating system on position nr <#>."
  echo "new:<#>                  : Clean sync of the operating system on position nr <#>"
  echo "                           (formats the according partition before)."
  echo "postsync:<#>             : Invokes postsync script of the os on position nr <#>."
  echo "start:<#>                : Starts the operating system on pos. nr <#>."
  echo "prestart:<#>             : Invokes prestart script of the os on position nr <#>."
  echo "create_image:<#>:<\"msg\"> : Creates a full image from operating system nr <#>."
  echo "upload_image:<#>         : Uploads a full image from operating system nr <#>."
  echo "create_qdiff:<#>:<\"msg\"> : Creates a differential image from operating system nr <#>."
  echo "upload_qdiff:<#>         : Uploads a differential image from operating system nr <#>."
  echo "reboot                   : Reboots the client."
  echo "halt                     : Shuts the client down."
  echo
  echo "<\"msg\"> is an optional image comment."
  echo "The position numbers are related to the position in start.conf."
  echo "The commands were sent per ssh to the linbo_wrapper on the client and processed"
  echo "in the order given on the commandline."
  echo "create_* and upload_* commands cannot be used with hostlists, -r and -g options."
  if [ -n "$msg" ]; then
    echo
    echo "$msg"
  fi
  exit 1
}

# list linbo-remote tmux sessions
list(){
  tmux list-sessions | grep .linbo-remote
}

# attach a host's linbo-remote tmux session
attach(){
  local session="${1}_linbo-remote"
  list | grep -q ^"$session" || usage "There is no session for host $1."
  tmux attach -t "$session" || exit 1
}

# process cmdline
while getopts ":a:b:c:dg:hi:lnp:r:uw:s:" opt; do

  # debug
  #echo "### opt: $opt $OPTARG"

  case $opt in
    a) attach $OPTARG ; exit 0 ;;
    l) list ; exit 0 ;;
    b) BETWEEN=$OPTARG ;;
    c) DIRECT=$OPTARG ;;
    d) DISABLEGUI=yes ;;
    i)
      # create a list of hosts
      for i in ${OPTARG//,/ }; do
        if validhostname "$i"; then
          HOSTNAME="$i"
        else
          validip "$i" && IP="$i"
          HOSTNAME=""
        fi
        [ -n "$IP" ] && HOSTNAME="$(nslookup "$IP" 2> /dev/null | head -1 | awk '{ print $4 }' | awk -F\. '{ print $1 }' | sed "s/^$SCHOOL-//g")"
        if [ -n "$HOSTNAME" ]; then
          # check for pxe flag, only use linbo related pxe flags 1 & 2
          pxe="$(grep -i ^[a-z0-9] $WIMPORTDATA | grep -i ";$HOSTNAME;" | awk -F\; '{ print $11 }')"
          if [ "$pxe" != "1" -a "$pxe" != "2" ]; then
            echo "Skipping $i, not a pxe host!"
            continue
          fi
          if [ -n "$HOSTS" ]; then
            HOSTS="$HOSTS $HOSTNAME"
          else
            HOSTS="$HOSTNAME"
          fi
        else
          echo "Host $i not found!"
        fi
      done
      [ -z "$HOSTS" ] && usage "No valid hosts in list!"
      ;;
    g) GROUP=$OPTARG ;;
    p) ONBOOT=$OPTARG  ;;
    r) ROOM=$OPTARG ;;
    u) USEBCADDR=yes;;
    w) WAIT=$OPTARG
      isinteger "$WAIT" || usage ;;
    n) NOAUTO=yes ;;
    s) SCHOOL=$OPTARG
      # calculate path of devices.csv
      if [ "$SCHOOL" != "default-school" ]; then
        WIMPORTDATA="$SOPHOSYSDIR/$SCHOOL/$SCHOOL.devices.csv"
      else
        WIMPORTDATA="$SOPHOSYSDIR/$SCHOOL/devices.csv"
      fi
      ;;
    h) usage ;;
    \?) echo "Invalid option: -$OPTARG" >&2
      usage ;;
    :) echo "Option -$OPTARG requires an argument." >&2
      usage ;;
  esac
done


# check options
[ -z "$GROUP" -a -z "$HOSTS" -a -z "$ROOM" ] && usage "No hosts, no group, no room defined!"
[ -n "$GROUP" -a -n "$HOSTS" ] && usage "Group and hosts defined!"
[ -n "$GROUP" -a -n "$ROOM" ] && usage "Group and room defined!"
[ -n "$DIRECT" -a -n "$ONBOOT" ] && usage "Direct and onboot commands defined!"
[ -z "$DIRECT" -a -z "$ONBOOT" -a -z "$WAIT" -a -z "$DISABLEGUI" -a -z "$NOAUTO" ] && usage "No commands or wakeonlan defined!"
if [ -n "$WAIT" ]; then
  if [ ! -x "$WOL" ]; then
    echo "$WOL not found!"
    exit 1
  fi
  [ -n "$DIRECT" -a "$WAIT" = "0" ] && WAIT=""
fi
if [ -n "$BETWEEN" ]; then
  [ -z "$WAIT" ] && usage "Option -b can only be used with -w!"
  isinteger "$BETWEEN" || usage "$BETWEEN is not an integer variable!"
fi

if [ -n "$DIRECT" ]; then
  CMDS="$DIRECT"
  DIRECT="yes"
elif [ -n "$ONBOOT" ]; then
  CMDS="$ONBOOT"
  ONBOOT="yes"
fi

# no upload or create commands for list of hosts
if [ -n "$CMDS" ]; then
  pattern=" |'"
  [[ $HOSTS =~ $pattern ]] && LIST="yes"
  [ -n "$GROUP" -o -n "$ROOM" ] && LIST="yes"
  case "$CMDS" in *upload*|*create*) [ -n "$LIST" ] && usage "Upload or create cannot be used with lists!" ;; esac

  # provide secrets for upload
  case "$CMDS" in *upload*) SECRETS=/etc/rsyncd.secrets ;; esac
fi


# common functions
# test if linbo-client is online
is_online(){
  $SSH -o ConnectTimeout=1 "$1" /bin/ls /start.conf &> /dev/null && return 0
  return 1
}

# waiting routine
do_wait(){
  local type="$1"
  local msg
  if [ "$type" = "wol" ]; then
    msg="Waiting $WAIT second(s) for client(s) to boot"
    secs="$WAIT"
    echo
  elif [ "$type" = "between" ]; then
    msg="  "
    secs="$BETWEEN"
  fi
  [ -z "$secs" -o "$secs" = "0" ] && return
  local c=0
  echo -n "$msg "
  while [ $c -lt $secs ]; do
    sleep 1
    echo -n "."
    c=$(( $c + 1 ))
  done
  echo
}

# print onboot linbocmd filename
onbootcmdfile(){
  echo "$LINBODIR/linbocmd/$1.cmd"
}


## evaluate commands string - begin
# strip from beginning of commands string
strip_cmds(){
  local tostrip="$1"
  CMDS="$(echo "$CMDS" | sed -e "s|^$tostrip||")"
}

# extract number parameter
extract_nr(){
  local nr="$(echo "$CMDS" | awk -F\: '{ print $2 }' | awk -F\, '{ print $1 }')"
  isinteger "$nr" || usage "$nr is not an integer variable!"
  strip_cmds ":$nr"
  command[$c]="$cmd:$nr"
}

# extract comment
extract_comment(){
  local comment="$(echo "$CMDS" | awk -F\: '{ print $2 }')"
  # count commas in comment string
  local nrofc="$(echo "$CMDS" | grep -o "," | wc -l)"
  # if more than zero commas exist
  if [ $nrofc -gt 0 ]; then
    # strip next command
    local i
    for i in $KNOWNCMDS; do
      stringinstring ",$i" "$comment" && comment="$(echo "$comment" | sed -e "s|\,${i}.*||")"
    done
  fi
  strip_cmds ":$comment"
  command[$c]="${command[$c]}:\\\"$comment\\\""
}

# iterate over command string and split the commands
c=0
while [ -n "$CMDS" ]; do

  # extract command from string
  cmd="$(echo "$CMDS" | awk -F\: '{ print $1 }' | awk -F\, '{ print $1 }')"
  # check if command is known
  stringinstring "$cmd" "$KNOWNCMDS" || usage "Command \"$cmd\" is not known!"
  # build array of commands
  command[$c]="$cmd"
  # strip command from beginning of string
  strip_cmds "$cmd"

  # evaluate commands and parameters
  case "$cmd" in

    format)
      [ "${CMDS:0:1}" = ":" ] && extract_nr
      ;;

    new|*sync|*start|upload_image|upload_qdiff)
      [ "${CMDS:0:1}" = ":" ] || usage "Command string \"$CMDS\" is not valid!"
      extract_nr
      ;;

    initcache)
      if [ "${CMDS:0:1}" = ":" ]; then
        dltype="$(echo "$CMDS" | awk -F\: '{ print $2 }' | awk -F\, '{ print $1 }')"
        stringinstring "$dltype" "$DLTYPES" || usage "$dltype is not known!"
        strip_cmds ":$dltype"
        command[$c]="$cmd:$dltype"
      fi
      ;;

    create_image|create_qdiff)
      extract_nr
      [ "${CMDS:0:1}" = ":" ] && extract_comment
      ;;

    label|partition|reboot|halt) ;;

    *) usage "Unknown command: $cmd!" ;;

  esac

  # remove preceding comma
  strip_cmds ","
  c=$(( $c + 1 ))

done
NR_OF_CMDS=$c
## evaluate commands string - end


# get ips of group or room if given on cl
if [ -n "$GROUP" ]; then # hosts in group with pxe flag set
  HOSTS="$(grep -i ^[a-z0-9] $WIMPORTDATA | awk -F\; '{ print $3, $2, $11 }' | grep ^"$GROUP " | grep " [1-2]" | awk '{ print $2 }')"
  msg="group $GROUP"
elif [ -n "$ROOM" ]; then # hosts in room with pxe flag set
  HOSTS="$(grep -i ^[a-z0-9] $WIMPORTDATA | awk -F\; '{ print $1, $2, $11 }' | grep ^"$ROOM " | grep " [1-2]" | awk '{ print $2 }')"
  msg="room $ROOM"
fi
[ -z "$HOSTS" ] && usage "No hosts in $msg!"

# add prefix to hosts if necessary
if [ "$SCHOOL" != "default-school" ]; then
  for i in $HOSTS; do
    PREFIXED_HOSTNAME="$SCHOOL-$i"
    if [ -n "$PREFIXED_HOSTS" ]; then
      PREFIXED_HOSTS="$PREFIXED_HOSTS $PREFIXED_HOSTNAME"
    else
      PREFIXED_HOSTS="$PREFIXED_HOSTNAME"
    fi
  done
  HOSTS=$PREFIXED_HOSTS
fi

# script header info
echo "###"
echo "### linbo-remote ($$) start: $(date)"
echo "###"


# create onboot command string, if -p is given
if [ -n "$ONBOOT" ]; then

  # add upload secrets
  [ -n "$SECRETS" ] && onbootcmds="$(grep ^linbo: "$SECRETS")"

  # collect commands
  c=0
  while [ $c -lt $NR_OF_CMDS ]; do
    if [ -n "$onbootcmds" ]; then
      onbootcmds="${onbootcmds},${command[$c]}"
    else
      onbootcmds="${command[$c]}"
    fi
    c=$(( $c + 1 ))
  done

fi # onboot command string

# add noauto triggers to onbootcmds
[ -n "$NOAUTO" ] && onbootcmds="$onbootcmds,noauto"

# add disablegui triggers to onbootcmds
[ -n "$DISABLEGUI" ] && onbootcmds="$onbootcmds,disablegui"


# create linbocmd files for onboot tasks
if [ -n "$onbootcmds" ]; then

  echo
  echo "Preparing onboot linbo tasks:"
  # strip leading and trailing space
  onbootcmds="$(echo "$onbootcmds" | awk '{$1=$1};1')"
  for i in $HOSTS; do
    echo -n " $i ... "
    echo "$onbootcmds" > "$(onbootcmdfile "$i")"
    echo "Done."
  done

  chown nobody:root $LINBODIR/linbocmd/*
  chmod 660 $LINBODIR/linbocmd/*

fi


# wake-on-lan, if -w is given
if [ -n "$WAIT" ]; then
  echo
  echo "Trying to wake up:"
  c=0
  for i in $HOSTS; do
    [ -n "$BETWEEN" -a "$c" != "0" ] && do_wait between
    echo -n " $i ... "
    # strip school from hostname
    host="$(echo "$i" | sed "s|^$SCHOOL-||")"
    # get mac address of client from devices.csv
    macaddr="$(get_mac "$host")"
    # get ip address of host
    ipaddr="$(get_ip "$host")"
    validip "$ipaddr" || ipaddr="$(arp -a "$host" | awk -F\( '{print $2}' | awk -F\) '{print $1}')"
    # check mac address
    validmac "$macaddr" || macaddr="$(get_mac_dhcp "$ipaddr")"
    if [ -n "$USEBCADDR" ]; then
      # get broadcast address
      validip "$ipaddr" && bcaddr=$(get_bcaddress "$ipaddr")
      # create wol command if broadcast address is valid
      validip "$bcaddr" && WOL="$WOL -i $bcaddr"
    fi

    if [ -n "$DIRECT" ]; then
      if validmac "$macaddr"; then
        $WOL "$macaddr"
      else
        echo "$macaddr is no valid mac address!"
        continue
      fi
    fi
    if [ -n "$ONBOOT" ]; then
      # reboot linbo-clients which are already online
      if is_online "$host"; then
        echo "Client is already online, rebooting ..."
        $SSH "$host" reboot &> /dev/null
      else
        $WOL "$macaddr"
      fi
    fi
    [ -z "$DIRECT" -a -z "$ONBOOT" ] && $WOL "$macaddr"
    c=$(( $c + 1 ))
  done
fi


# send commands directly per linbo-ssh, with -c
send_cmds(){

  # wait for clients to come up
  [ -n "$WAIT" ] && do_wait wol

  echo
  echo "Sending command(s) to:"
  for i in $HOSTS; do
    echo -n " $i ... "

    # look for not fetched old onboot file and delete it
    [ -z "$onbootcmds" -a -e "$(onbootcmdfile "$i")" ] && rm -f "$(onbootcmdfile "$i")"

    # test if client is online
    if ! is_online "$i"; then
      echo "Not online, host skipped."
      continue
    fi

    # provide secrets for image upload
    if [ -n "$SECRETS" ]; then
      echo -n "Uploading secrets ... "
      $SCP $SECRETS ${i}:/tmp
    fi

    # create a temporary script with linbo remote commands
    HOSTNAME="$i"
    SESSIONNAME="$HOSTNAME.linbo-remote"
    LOGFILE="$LINBOLOGDIR/$SESSIONNAME"
    REMOTESCRIPT=$TMPDIR/$$.$HOSTNAME.sh
    echo "#!/bin/bash" > $REMOTESCRIPT
    echo "$SSH $i gui_ctl disable" >> $REMOTESCRIPT
    echo "RC=0" >> $REMOTESCRIPT
    local c=0
    while [ $c -lt $NR_OF_CMDS ]; do
      # pause between commands
      [ $c -gt 0 ] && echo "sleep 3" >> $REMOTESCRIPT
      case ${command[$c]} in
        start*|reboot|halt|poweroff)
          START=yes
          echo "[ \$RC = 0 ] && $SSH $i $WRAPPER ${command[$c]} &" >> $REMOTESCRIPT
          echo "sleep 10" >> $REMOTESCRIPT ;;
        *) echo "[ \$RC = 0 ] && $SSH $i $WRAPPER ${command[$c]} || RC=1" >> $REMOTESCRIPT ;;
      esac
      c=$(( $c + 1 ))
    done
    [ -n "$SECRETS" -a -z "$START" ] && echo "$SSH $i /bin/rm -f /tmp/rsyncd.secrets" >> $REMOTESCRIPT
    echo "$SSH $i gui_ctl restore" >> $REMOTESCRIPT
    echo "rm -f $REMOTESCRIPT" >> $REMOTESCRIPT
    echo "exit \$RC" >> $REMOTESCRIPT
    chmod 755 $REMOTESCRIPT

    # start script in tmux session
    tmux new -Ads "$SESSIONNAME" "$REMOTESCRIPT" \; pipe-pane "cat > $LOGFILE"
    PID="$(ps ax | grep -v grep | grep -w "$LOGFILE" | awk '{print $1}')"
    [ -z "$PID" ] && PID="unknown"
    echo "Started with PID $PID. Log see $LOGFILE."
  done
}


# test if waked up clients have done their onboot tasks, with -p
test_onboot(){

  # wait for clients to come up
  do_wait wol

  # verifying if clients have done their onboot tasks
  echo
  echo "Verifying onboot tasks:"
  for i in $HOSTS; do
    echo -n " $i ... "
    if [ -e "$(onbootcmdfile "$i")" ]; then
      rm -f "$(onbootcmdfile "$i")"
      echo "Not done, host skipped!"
    else
      echo "Ok!"
    fi
  done
}


# test if waked up clienst are online
test_online(){

  # wait for clients to come up
  do_wait wol

  # testing if clients are online
  echo
  echo "Testing if clients have booted:"
  for i in $HOSTS; do
    echo -n " $i ... "
    if is_online "$i"; then
      echo "Online!"
    else
      echo "Not online!"
    fi
  done
}


# send commands live (-c)
[ -n "$DIRECT" ] && send_cmds

# test onboot tasks (-p)
[ -n "$ONBOOT" -a -n "$WAIT" -a "$WAIT" != "0" ] && test_onboot

# test online (-w only)
[ -z "$ONBOOT" -a -z "$DIRECT" -a -n "$WAIT" -a "$WAIT" != "0" ] && test_online


# script footer info
echo
echo "###"
echo "### linbo-remote ($$) end: $(date)"
echo "###"

exit 0
