#!/bin/bash
# License: GPL 
# Author: Steven Shiau <steven _at_ nchc org tw>, Ceasar Sun <ceasar _at_ nchc ogr tw>
# Description: Program to prepare grub uEFI network booting image

#
DRBL_SCRIPT_PATH="${DRBL_SCRIPT_PATH:-/usr/share/drbl}"
. $DRBL_SCRIPT_PATH/sbin/drbl-conf-functions
. /etc/drbl/drbl-ocs.conf
. $DRBL_SCRIPT_PATH/sbin/ocs-functions

# Settings
grub_mkimg_compress_opt="-C xz"
#efi_required_mod="part_gpt part_msdos hfsplus fat ext2 ntfs btrfs normal chain boot configfile linux appleldr minicmd multiboot iso9660 acpi efi_gop efi_uga elf loadbios loopback video_fb usb search fixvideo png gfxterm gfxterm_background gfxterm_menu echo videotest video reboot halt gfxmenu gettext tftp efinet net gzio xzio"
# Rerfer to the file systems list in /usr/lib/grub/x86_64-efi/fs.lst. This is for client's local disk boot.
efi_required_mod="normal tftp efinet chain echo net gzio xzio linux \
efi_gop efi_uga png gfxterm gfxterm_background gfxterm_menu \
serial part_gpt part_msdos boot multiboot progress search \
$GRUB_FS_MOD_drbl_ocs configfile test sleep tr reboot halt"
# Default values
# secure_boot_client is from drbl.conf.

prog="$0"

#
USAGE() {
  echo "Generate the Grub EFI network boot loader for DRBL clients."
  echo "Usage: $prog [OPTION]"
  echo "Options:"
  echo "-s, --secure-boot-client: Enabled secure boot for uEFI network boot client. This is still in testing."
  echo "-t, --tftp-server IPADD:  Assign the IP address IPADD for the TFTP server. If not assigned, automatically obtain from network boot server. Howerver, grub <= 2.0.2 does not support receiving that. Not sure if newer version will support it or not. Hence this is a workaround."
  echo "Ex:"
  echo "To create Grub EFI network boot loader for DRBL clients with TFTP server IP address 192.168.67.254:"
  echo "$prog -t 192.168.67.254"
}

################
##### Main #####
################
check_if_root
ask_and_load_lang_set

# Parse command-line options
while [ $# -gt 0 ]; do
  case "$1" in
    -t|--tftp-server)
            shift
            if [ -z "$(echo $1 |grep ^-.)" ]; then
              # skip the -xx option, in case 
              tftp_server_ip="$1"
              shift
            fi
	    [ -z "$tftp_server_ip" ] && USAGE && exit 1
	    ;;
    -s|--secure-boot-client) secure_boot_client="yes"; shift;;
    -*)     echo "${0}: ${1}: invalid option" >&2
            USAGE >& 2
            exit 2 ;;
    *)      break ;;
  esac
done

# Decide if it's something like (tftp,192.168.22.254) or (tftp) in grub config.
if [ -n "$tftp_server_ip" ]; then
  tftp_srv_opt="tftp,$tftp_server_ip"
else
  tftp_srv_opt="tftp"
fi

# Find the grub2 commands: grub-mkimage, grub-mknetdir
if type grub2-mkimage >/dev/null 2>&1; then
  GRUB_MKIMG=grub2-mkimage
elif type grub-mkimage >/dev/null 2>&1; then
  GRUB_MKIMG=grub-mkimage
else
  [ "$BOOTUP" = "color" ] && $SETCOLOR_WARNING
  echo "Command grub2-mkimage or grub-mkimage not found!"
  [ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
  echo "Skip creating grub2 uEFI network boot image."
  exit 1
fi
if type grub2-mknetdir >/dev/null 2>&1; then
  GRUB_MKNETD=grub2-mknetdir
elif type grub-mknetdir >/dev/null 2>&1; then
  GRUB_MKNETD=grub-mknetdir
else
  [ "$BOOTUP" = "color" ] && $SETCOLOR_WARNING
  echo "Command grub2-mknetdir or grub-mknetdir not found!"
  [ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
  echo "Skip creating grub2 uEFI network boot modules."
  exit 1
fi
#
grub_efi_tmpd="$(mktemp -d /tmp/grub-efi.XXXXXX)"
# Prepare a grub.cfg so that the eltorito image could find it.
# Because if/elif/else/fi is only supported in normal module, and in this embedded, preconfig only the command in rescue mode is supported, here we use this unique, subtle way. i.e.
# Quoted from Andrei Borzenkov:
# "if" is not external command - it must be recognized and processed by
# script parser and at this point rescue script parser is still active.
# OTOH "configfile" is more or less standard external command. So first
# "if" results in error, then "configfile" succeeds. configfile
# implicitly loads and calls normal.mod so it appears to work. Although
# not exactly as envisioned :)
# //NOTE// So when configfile command succeeds, it won't return to this
# preconfig file. Therefore there is no need to have if/elif/else/fi
# commands.
# For more details, check:
# http://lists.gnu.org/archive/html/help-grub/2015-09/msg00035.html

# Also check grub 2.02 manual (not 2.00, since the variables are very different):
# http://dev.gentoo.org/~floppym/grub.html

cat <<-GRUB_END > $grub_efi_tmpd/grub-header.cfg
set prefix=($tftp_srv_opt)/grub-efi.cfg
echo "Grub CPU and platform: \$grub_cpu, \$grub_platform"
echo 'Network status: '
net_ls_cards
net_ls_addr
net_ls_routes

tr --set pretty_mac x: x- \$net_default_mac

echo "Loading config file \$prefix/grub.cfg-01-\$pretty_mac..."
configfile \$prefix/grub.cfg-01-\$pretty_mac

echo "Loading config file \$prefix/grub.cfg-\$net_default_ip..."
configfile \$prefix/grub.cfg-\$net_default_ip

echo "Loading config file: \$prefix/grub.cfg"
configfile \$prefix/grub.cfg

echo "Could not find config file \$prefix/grub.cfg-\$pretty_mac, \$prefix/grub.cfg-\$net_default_ip or \$prefix/grub.cfg!"
sleep 15

GRUB_END

if [ -z "$GRUB_EFINB_DIR" ]; then
  [ "$BOOTUP" = "color" ] && $SETCOLOR_FAILURE
  echo "\"\$GRUB_EFINB_DIR\" _NOT_ defined!"
  [ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
  echo "$msg_program_stop!"
  exit 1
fi
echo "Removing the old $GRUB_EFINB_DIR if it exists..."
if [ -d "$GRUB_EFINB_DIR" ]; then
  rm -rf $GRUB_EFINB_DIR
fi
mkdir -p $GRUB_EFINB_DIR
# link dir grub-efi.cfg as grub in /tftpboot/nbi_img/. This is to be compatible for secure booot for Ubuntu's grubnetx64.efi.signed
if [ ! -h "$(dirname $GRUB_EFINB_DIR)"/grub ]; then
  ln -fs -r "$GRUB_EFINB_DIR" "$(dirname $GRUB_EFINB_DIR)"/grub
fi

# Get the grub version
grub_v="$(LC_ALL=C $GRUB_MKIMG --version | awk -F" " '{print $NF}')"
if [ -n "$grub_v" ]; then
  echo "$grub_v" > $pxecfg_pd/GRUB_VERSION
else
  [ "$BOOTUP" = "color" ] && $SETCOLOR_WARNING
  echo "Warngin! Grub version _NOT_ found!"
  [ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
fi

# Prepare some files, e.g. background img and fonts
# Unicode is located at:
# Debian: /usr/share/grub/unicode.pf2 
# CetnOS: /boot/efi/EFI/centos/fonts/unicode.pf2
echo "Preparing background img and font..."
unicode_font="$(LC_ALL=C find /usr/share/grub/ /boot/efi/EFI/ -name "unicode.pf2" -print 2>/dev/null | uniq | head -n 1)"
if [ -n "$unicode_font" ]; then
  cp -fv $unicode_font $pxecfg_pd/
fi
cp -fv $grub_bg_img $pxecfg_pd/

# Generate the template grub.cfg
gen-grub-efi-nb-menu

# For x86-64 arch, we create bootx64.efi and bootia32.efi.
# For i386 arch, we only create bootia32.efi.
# First we find the installed OS arch
# //NOTE// This could be in the chroot environment, therefore we have to use "getconf LONG_BIT" instead of "uname -m"
# E.g., if on X86_64 OS, the chroot env is i386: "getconf LONG_BIT" will give 32, while "uname -m" will give x86_64
sys_cputype="$(LC_ALL=C uname -m)"
sys_arch="$(LC_ALL=C getconf LONG_BIT)"
dest_bootloader=""
case "$sys_cputype" in
  i686|x86_64)
    case "$sys_arch" in 
      32)  echo "System architecture is 32-bit."
           dest_bootloader="i386"
           ;;
      64)  echo "System architecture is 64-bit."
           dest_bootloader="i386 x86_64";;
       *) 
           [ "$BOOTUP" = "color" ] && $SETCOLOR_FAILURE
           echo "Unknown system architecture in drbl-gen-grub-efi-nb!"
           [ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
           echo "$msg_program_stop!"
           exit 1
           ;;
    esac
    ;;
  aarch64)
    dest_bootloader="arm64"
    ;;
esac

for i in $dest_bootloader; do
  export EFI_ARCH=$i
  case "$EFI_ARCH" in
    i386)    if [ ! -e /usr/lib/grub/i386-efi/moddep.lst ]; then
	       # On some system, like CentOS 7, there is no i386 efi from grub2.
	       continue
	     fi 
	     SB_EFI_NAME="ia32"
             out_f="$pxecfg_pd/bootia32.efi"
	     ;;
    x86_64)  SB_EFI_NAME="x64"
	     out_f="$pxecfg_pd/bootx64.efi"
	     ;;
    arm64)   SB_EFI_NAME="aa64"
	     out_f="$pxecfg_pd/bootaa64.efi"
	     ;;
  esac
  # For amd64 arch, the files for secure boot:
  # /usr/lib/shim/shimx64.efi.signed -> /tftpboot/nbi_img/bootx64.efi
  # /usr/lib/grub/x86_64-efi-signed/grubnetx64.efi.signed -> /tftpboot/nbi_img/grubx64.efi
  # For arm64 arch,
  # /usr/lib/shim/shimaa64.efi.signed -> /tftpboot/nbi_img/bootaa64.efi
  # /usr/lib/grub/arm64-efi-signed/grubnetaa64.efi.signed -> /tftpboot/nbi_img/grubaa64.efi
  if [ "$secure_boot_client" = "yes" -a \
       -e /usr/lib/shim/shim${SB_EFI_NAME}.efi.signed -a \
       -e /usr/lib/grub/${EFI_ARCH}-efi-signed/grubnet${SB_EFI_NAME}.efi.signed ]; then
    # Secure boot enabled. Use the one from upstream
    [ "$BOOTUP" = "color" ] && $SETCOLOR_WARNING
    echo "UEFI Secure Boot support enabled for ${EFI_ARCH} clients."
    [ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
    cp -fv /usr/lib/shim/shim${SB_EFI_NAME}.efi.signed $pxecfg_pd/boot${SB_EFI_NAME}.efi
    cp -fv /usr/lib/grub/${EFI_ARCH}-efi-signed/grubnet${SB_EFI_NAME}.efi.signed $pxecfg_pd/grub${SB_EFI_NAME}.efi
    echo "Copied from /usr/lib/shim/shim${SB_EFI_NAME}.efi.signed" > ${out_f}.info
  else
    [ "$BOOTUP" = "color" ] && $SETCOLOR_WARNING
    echo "UEFI Secure Boot support disabled for ${EFI_ARCH} clients becasue there is no /usr/lib/shim/shim${SB_EFI_NAME}.efi.signed and /usr/lib/grub/${EFI_ARCH}-efi-signed/grubnet${SB_EFI_NAME}.efi.signed installed, or it is disabled in /etc/drbl/drbl.conf (\"secure_boot_client\")."
    [ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
    # Non-secure boot
    # Checking the required modules
    for im in $efi_required_mod; do
      if [ ! -e "/usr/lib/grub/${EFI_ARCH}-efi/${im}.mod" ]; then
        [ "$BOOTUP" = "color" ] && $SETCOLOR_WARNING
        echo "Missing grub2 module ${im}.mod... Exluding ${im}.mod..."
        [ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
        efi_required_mod="$(echo $efi_required_mod | sed -r -e "s/$im //g")"
      fi
    done
    echo "Creating the uEFI network booting bootable image $out_f..."
    $GRUB_MKIMG $grub_mkimg_compress_opt -O ${EFI_ARCH}-efi -o $out_f --prefix="($tftp_srv_opt)/grub-efi.cfg/" -c $grub_efi_tmpd/grub-header.cfg $efi_required_mod
    rc=$?
    if [ "$rc" -ne 0 ]; then
      # When grub-mkimage fails, an empty output file will still be created. We'd better to clean it.
      rm -f $out_f
      [ "$BOOTUP" = "color" ] && $SETCOLOR_WARNING
      echo "Warning! Failed to create $out_f!"
      echo "This server won't be able to serve uEFI network boot clients."
      [ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
      sleep 5
    else
      # Create a tag file
      echo "Created by program \"$prog\" with command:" > ${out_f}.info
      echo "$GRUB_MKIMG $grub_mkimg_compress_opt -O ${EFI_ARCH}-efi -o $out_f --prefix='(tftp)/grub-efi.cfg/' -c $grub_efi_tmpd/grub-header.cfg $efi_required_mod" >> ${out_f}.info
      echo "The contents of $grub_efi_tmpd/grub-header.cfg:" >> ${out_f}.info
      echo "$msg_delimiter_star_line" >> ${out_f}.info
      cat $grub_efi_tmpd/grub-header.cfg >> ${out_f}.info
      echo "$msg_delimiter_star_line" >> ${out_f}.info
    fi
  fi
done

# Suppress the output to avoid confusion.
echo "Preparing the grub modules in $GRUB_EFINB_DIR..."
$GRUB_MKNETD --net-directory=$GRUB_EFINB_DIR --subdir=/ >/dev/null
echo "The uEFI network booting is ready."

# Clean the temp dir
if [ -e "$grub_efi_tmpd" -a -n "$(echo $grub_efi_tmpd | grep -E "grub-efi")" ]; then
  rm -rf $grub_efi_tmpd
fi
