#!/bin/sh
# SPDX-License-Identifier: MIT
set -e

GLOBAL_CONF=/etc/10mt.conf
LOCAL_CONF="${LOCAL_DATADIR}/10mt.conf"
QUIET=true

log() {
  _force="${2}"
  if [ -n "${_force}" ] || [ -z "${QUIET}" ]; then
    echo "${1}" >&2
  fi
  [ -z "${LOGFILE}" ] && return 0
  echo "[$(date)] ${1}" >> "${LOGFILE}"
}

die() {
  log "ERROR: ${1}" _force >&2
  exit 1
}

# Load configuration.
[ -r "${GLOBAL_CONF}" ] || die "Global configuration (${GLOBAL_CONF}) not available or not accessible"
. "${GLOBAL_CONF}"
[ -r "${LOCAL_CONF}" ] && . "${LOCAL_CONF}"

usage() {
  _ret="${1:-1}"
  _summary="10mt is a tool for provisioning virtual machines"
  _compose="http://mirror.stream.centos.org/9-stream"
  _repos="https://example.com/foo.repo https://example.com/bar.repo"
  _vars="CLEVIS_PIN=tpm2 CLEVIS_CONFIG={}"
  exec >&2
  printf 'Usage: 10mt -s SYSTEM [-a IP] [-b BRIDGE] [-c COMPOSE] [-d DIR] [-e EXTRA-ARGS] [-f EXTRA-FILES]'
  printf ' [-g NETWORK] [-i VM] [-j UPDATES-IMG] [-k KEY] [-m MEM] [-n NAME] [-p PACKAGES] [-r RELEASE]'
  printf ' [-v VAR1=VAL1 .. VARN=VALN] [-x SNIPPETS] [-C VCPUs] [-D DISK-SIZE-IN-GB] [-L LUKS-OPTS]'
  printf ' [-P PROFILER] [-R REPOFILE1 REPOFILEN] [-S STORAGE] [-T PCR-BANKS] [-Z KS-TEMPLATE]'
  printf ' [-h] [-l] [-t] [-w] [-z] [-A] [-B] [-E] [-F] [-K] [-M] [-N] [-O] [-Q] [-V] [-W]'
  echo
  echo "${_summary}:"
  echo
  echo "  -s SYSTEM       Specify the system to be provisioned; e.g.: -s c9s"
  echo "  -a IP           The IP address of the VM; e.g.: -a 192.168.1.64 or -a dhcp"
  echo "  -b BRIDGE       The network bridge to use; it will use virbr0 as default"
  echo "  -c COMPOSE      The compose to use when provisioning, or a local .iso path; e.g.: -c ${_compose}"
  echo "  -d IMAGES-DIR   Specify the directory where to store the disk of the VMs; e.g.: -d /var/lib/libvirt/images"
  echo "  -e EXTRA-ARGS   Specify extra kernel arguments for virt-install --extra-args; repeatable; e.g.: -e inst.sshd -e 'inst.lang=en_US'"
  echo "  -f EXTRA-FILES  Specify extra files from the host to be made available on the VM; the files should be specified by their absolute paths and will be available at the /data/shared-files directory on the VM; separate multiple files with semicolons (;)"
  echo "  -g NETWORK      Custom network configuration for virt-install --network, replacing the default bridge."
  echo "                  Use -g direct to auto-detect the primary NIC and create a bridge for direct LAN access."
  echo "                  Use -g direct:<iface> to specify the NIC explicitly. Requires DHCP on the network."
  echo "                  Set TENMT_FORCE=1 to skip interactive prompts (e.g. in scripts)."
  echo "                  Example: -g 'passt,portForward0=9091:80,portForward1=2201:22'"
  echo "  -j UPDATES-IMG  Serve a local updates.img via HTTP during provisioning; e.g.: -j ~/builds/updates.img"
  echo "  -i VM-ID        Displays information about a specific VM created by 10mt"
  echo "  -k SSH-KEY      Specify an SSH public key to be added to the VM for the root user"
  echo "  -m MEM          The amount of RAM for the VM, in MB"
  echo "  -n NAME         The name of the VM; e.g.: -n centos-stream-9"
  echo "  -p PACKAGES     Specify a list of packages to install/remove; e.g.: -p 'clevis clevis-dracut'"
  echo "  -r RELEASE      Specify a specific release to use; e.g: -s fedora -r 35"
  echo "  -v VAR=VAL      List of variables/values to be used with the extra kickstart snippets; e.g.: -v '${_vars}'"
  echo "  -x KS-SNIPPETS  Specify a list of extra kickstart %post snippets; e.g.: -x clevis-tpm2"
  echo "  -L LUKS-OPTS    LUKS options"
  echo "  -P PROFILER     Specify profiler to be used with some %post snippets; e.g.: -P 'valgrind --tool=massif'"
  echo "  -R REPOFILES    Specify a list of remote repo files; e.g.: -R '${_repos}'"
  echo "  -S STORAGE      Specify the storage layout to use; default is 'luks-simple'; other options include: "
  echo "                  'luks-non-root', 'luks-multiple' and 'simple'"

  echo "  -h              Displays this usage information"
  echo "  -l              Lists the VMs created by 10mt"
  echo "  -t              Enables a tpm2 device within the VM"
  echo "  -w              Enables legacy TPM 1.2"
  echo "  -A              Archive all VMs created by 10mt"
  echo "  -B              Enables Secure Boot

                  You can enroll a Machine Owner Key (MOK) certificate
                  by specifying it in the SB_MOK_CRT env variable

                  Additionally, there are other env variables of interest, such as:

                  SB_VARS - indicates the VARS file to use
                  SB_FW   - indicates the firmware file to use

                  For the next 4 variables, if any of them is used, they all must be all defined:

                  SB_PK_KEY - a custom Platform Key (PK) key to use
                  SB_PK_CRT - a custom Platform Key (PK) certificate to use
                  SB_KEK_KEY - a custom Key Exchange Key (KEK) key to use
                  SB_KEK_CRT - a custom Key Exchange Key (KEK) certificate to use
                  "
  echo "  -C              Number of VCPUs to use; default is 1"
  echo "  -D              Disk size in GB. Default is ${DEFAULT_VM_DISK_SIZE} if not defined"
  echo "  -E              Use external TPM management (requires -t)

                  Allows manual swtpm management with custom flags.
                  Use SWTPM_EXTRA_FLAGS env var for custom swtpm flags.
                  Use TPM_PCR_BANKS env var to specify PCR banks (default: sha1,sha256).

                  Example: SWTPM_EXTRA_FLAGS='--flags disable-auto-shutdown' 10mt -s c9s -t -E"
  echo "  -F              Use raw format instead of qcow2"
  echo "  -K              Perform a cleanup in all VMs created by 10mt"
  echo "  -M              Enable KVM nested virtualization"
  echo "  -N              Removes swap from autopart (not valid for RHEL7) and also disables swap in %post"
  echo "  -O              Force session mode (qemu:///session with passt networking); auto-enabled for non-root"
  echo "  -Q              Adds inst.nosave=all option to not save logs/kickstarts after install"
  echo "  -V              Verbose mode; increase output displayed"
  echo "  -W              Perform a stop in all VMs created by 10mt"
  echo "  -Z              Specify custom kickstart template; must be in the templates directory"
  echo
  echo "  -z              Dry-run mode; do not execute anything"
  echo
  exit "${_ret}"
}

parse_list() {
  _l=${1:-}
  [ -n "${_l}" ] || return 0

  _l=$(echo "${_l}" | tr '\n' ' ')
  _delims=",;:#@\t\r"
  echo "${_l}" | sed -e "s/[${_delims}]/ /g" \
               | sed -e 's/  \+/ /g' \
               | sed -e 's/^[[:blank:]]*//;s/[[:blank:]]*$//'
}

parse_extra_env_vars() {
  EXTRA_ENV_VARS="${EXTRA_VARS:-}"

  log "EXTRA_ENV_VARS: ${EXTRA_ENV_VARS}"
  eval "set -- ${EXTRA_ENV_VARS}"

  for _v in "${@}"; do
    if echo "${_v}" | grep -q '='; then
      _key=$(echo "${_v}" | cut -d'=' -f1)
      _value=$(echo "${_v}" | cut -d'=' -f2-)

      [ -z "${_key}" ] && die "Malformed variable ${_v}: key is empty"
      [ -z "${_value}" ] && die "Malformed variable ${_v}: value is empty"
      log "VAR(${_v}): ${_key} => ${_value}"
    else
      env | grep -wq "^${_v}" || die "Variable ${_v} is not defined"
      log "VAR(${_v})=$(env | grep -w "^${_v}" | cut -d'=' -f2-)"
    fi
  done

  if [ -n "${REQ_VARS}" ]; then
    for _v in ${REQ_VARS}; do
      if ! echo "${EXTRA_ENV_VARS}" | grep -qw "${_v}"; then
        die "Variable ${_v} required by one of the %post templates is missing; define it with -v '${_v}=<VAL>'"
      fi
    done
  fi


  export EXTRA_ENV_VARS
  return 0
}

parse_post_ks_snippets() {
  [ -z "${EXTRA_KS}" ] && return 0

  # Handle extra kickstart post- snippet.
  EXTRA_KS_FILES=

  log "EXTRA_KS: ${EXTRA_KS}"
  _ks_files="$(parse_list "${EXTRA_KS}")"
  for _f in ${_ks_files}; do
    _post=${_f}

    # Check if file exists in post/ templates.
    [ ! -e "${_post}" ] && [ -e "${TEMPLATE_DIR}"/post/"${_post}" ] \
                        && _post="${TEMPLATE_DIR}"/post/"${_post}"

    _valid_post="$(find "${TEMPLATE_DIR}"/post/ -maxdepth 1 \
                   -type f -exec basename {} \; | sort)"
    _valid_post="$(parse_list "${_valid_post}")"

    [ -d "${_post}" ] && die "Malformed parameters for extra kickstart post- snippet; (${_f}) is a directory; valid snippets are: ${_valid_post}"
    [ -r "${_post}" ] || die "Malformed parameters for extra kickstart post- snippet; unable to read file (${_post}); valid snippets are: ${_valid_post}"
    EXTRA_KS_FILES="${EXTRA_KS_FILES} ${_post}"
  done
  EXTRA_KS_FILES="$(echo "${EXTRA_KS_FILES}" \
                    | sed -e 's/^[[:blank:]]*//;s/[[:blank:]]*$//')"

  REQ_VARS=
  REQ_PKGS=
  for _f in ${EXTRA_KS_FILES}; do
    _reqs="$(grep '^#' "${_f}" | grep -E -- '#\s*-' | cut -d'-' -f2 | tr '\n' ' ' \
             | sed -e 's/^[[:blank:]]*//;s/[[:blank:]]*$//' \
             | sed -e 's/  / /g')"
    REQ_VARS="${REQ_VARS} ${_reqs}"
    _reqs="$(grep '^#' "${_f}" | grep -E -- '#\s*\*' | cut -d*'' -f2 | tr '\n' ' ' \
             | sed -e 's/^[[:blank:]]*//;s/[[:blank:]]*$//' \
             | sed -e 's/  / /g')"
    REQ_PKGS="${REQ_PKGS} ${_reqs}"
  done
  REQ_VARS="$(echo "${REQ_VARS}" \
              | sed -e 's/^[[:blank:]]*//;s/[[:blank:]]*$//')"
  REQ_PKGS="$(echo "${REQ_PKGS}" \
              | sed -e 's/^[[:blank:]]*//;s/[[:blank:]]*$//')"

  log "EXTRA_KS_FILES: ${EXTRA_KS_FILES}"
  log "REQ_VARS: ${REQ_VARS}"
  log "REQ_PKGS: ${REQ_PKGS}"

  export EXTRA_KS_FILES
  export REQ_VARS
  export REQ_PKGS
  return 0
}

get_info_by_name() {
  [ -e "${WORKDIR}/${INFO}"/log ] || die "No information found for ${INFO}"
  echo "## Info for ${INFO}" >&2
  echo >&2
  cat "${WORKDIR}/${INFO}"/log
  exit 0
}

_is_running() {
  _vm_path="${1}"
  _is_running='[ ]'

  if [ ! -e "${_vm_path}/id" ]; then
    echo "${_is_running}"
    return 0
  fi

  _running=" "$(virsh --connect ${LIBVIRT_URI} list --state-running --name \
                | tr '\n' ' ' \
                | sed -e 's/  / /g')

  _vm_id="$(cat "${_vm_path}/id" 2>/dev/null)"
  if printf '%s' "${_running}" | grep -q " ${_vm_id} "; then
    _is_running='[*]'
  fi
  echo "${_is_running}"
}

get_info() {
  echo "## List of 10mt VMs:" >&2
  [ -d  "${WORKDIR}" ] || return 0
  for _vm in $(find "${WORKDIR}" -maxdepth 1 -type d -printf '%T@ %p\n' \
               | sort -k 1nr \
               | sed 's/^[^ ]* //'); do
    [ "$(basename "${_vm}")" = "workspace" ] && continue
    printf "%s %s\n" "$(_is_running "${_vm}")" "$(basename "${_vm}")"
  done
  echo "##"
  exit 0
}

shebang() {
  printf '#!/bin/sh\n'
}

virsh_cmd() {
  _cmd="${1:-}"
  _vm="${2:-}"
  [ -n "${_cmd}" ] || die "virsh_cmd needs a command"
  [ -n "${_vm}" ] || die "virsh_cmd needs a vm"
  printf 'virsh --connect %s %s %s\n' "${LIBVIRT_URI}" "${_cmd}" "${_vm}"
}

prepare_cmds() {
  [ -n "${CLEAN_CMD}" ] || die "prepare_cmds needs CLEAN_CMD defined"
  [ -n "${START_CMD}" ] || die "prepare_cmds needs START_CMD defined"
  [ -n "${STOP_CMD}" ] || die "prepare_cmds needs STOP_CMD defined"
  [ -n "${CONSOLE_CMD}" ] || die "prepare_cmds needs CONSOLE_CMD defined"
  [ -n "${SNAPSHOT_LIST_CMD}" ] || die "prepare_cmds needs SNAPSHOT_LIST_CMD defined"
  [ -n "${SNAPSHOT_LIST_CMD}" ] || die "prepare_cmds needs SNAPSHOT_LIST_CMD defined"
  [ -n "${SNAPSHOT_CMD}" ] || die "prepare_cmds needs SNAPSHOT_CMD defined"
  [ -n "${SNAPSHOT_RESTORE_CMD}" ] || die "prepare_cmds needs SNAPSHOT_RESTORE_CMD defined"

  [ -n "${DISK}" ] || die "prepare_cmds needs DISK defined"
  [ -n "${NAME}" ] || die "prepare_cmds needs NAME defined"

  # clean.
    cat << EOF > "${CLEAN_CMD}"
#!/bin/sh

die() {
  echo "\${1}" >&2
  exit 1
}

# Disk for future removal.
DISK=${DISK}

# Checking whether domain exists.
if ! virsh --connect ${LIBVIRT_URI} list --all --name \\
    | grep -qw "^${NAME}\$"; then
  exit 0
fi

_target="\$(virsh --connect ${LIBVIRT_URI} domblklist \\
           ${NAME} --details \\
           | grep disk | awk '{ print \$3 }')"

[ -n "\${_target}" ] && \\
  virsh --connect ${LIBVIRT_URI} blockcommit \\
    ${NAME} \\
    "\${_target}" --verbose --pivot --active

virsh --connect ${LIBVIRT_URI} destroy \\
  ${NAME}

# Remove any snapshots.
if [ -n "\${DISK}" ]; then
  _sdir="\$(dirname \${DISK})"
  for _s in \$(${_session_sudo}find "\${_sdir}/" \\
              -name '${NAME}.snapshot-*' \\
              | tac); do
    _smeta="\$(echo "\${_s}" | cut -d'.' -f2)"
    virsh --connect ${LIBVIRT_URI} snapshot-delete \\
      --metadata ${NAME} "\${_smeta}"
    ${_session_sudo}rm -f "\${_s}"
  done
fi

_nvram=
USE_NVRAM=${USE_NVRAM}
[ -n "\${USE_NVRAM}" ] && _nvram="--nvram"

virsh --connect ${LIBVIRT_URI} undefine \${_nvram} \\
  ${NAME} \\
  --managed-save --snapshots-metadata --remove-all-storage
EOF

  # Add external TPM cleanup if needed
  if [ -n "${EXTERNAL_TPM}" ]; then
    _tpm_env=
    [ -n "${SWTPM_EXTRA_FLAGS}" ] && _tpm_env="${_tpm_env} SWTPM_EXTRA_FLAGS='${SWTPM_EXTRA_FLAGS}'"
    [ -n "${TPM_PCR_BANKS}" ] && _tpm_env="${_tpm_env} TPM_PCR_BANKS='${TPM_PCR_BANKS}'"
    printf '\n%s%s%s 10mt-tpm2 -v %s -w %s -k\n' "${_session_sudo}" "${_session_env}" "${_tpm_env}" "${VM_ID}" "${WORKDIR}" >> "${CLEAN_CMD}"
  fi

  chmod 755 "${CLEAN_CMD}"
  log "CLEANUP: ${CLEAN_CMD}"

  # start.
  shebang > "${START_CMD}"

  # Ensure direct-mode bridge is up before starting VM.
  if [ -n "${DIRECT_MODE}" ]; then
    cat << 'BREOF' >> "${START_CMD}"
if [ -f "WDIR_PLACEHOLDER/direct-bridge" ]; then
  _br="$(cat "WDIR_PLACEHOLDER/direct-bridge")"
  case "${_br}" in *[!a-zA-Z0-9._-]*|"")
    echo "ERROR: invalid bridge name in direct-bridge file" >&2
    exit 1
  esac
  nmcli --wait 30 con up ifname "${_br}" >/dev/null 2>&1 \
    || { echo "ERROR: failed to bring up bridge ${_br}" >&2; exit 1; }
fi
BREOF
    sed -i "s#WDIR_PLACEHOLDER#${WDIR}#g" "${START_CMD}"
  fi

  # Start external TPM before starting VM if needed
  if [ -n "${EXTERNAL_TPM}" ]; then
    _tpm_env=
    [ -n "${SWTPM_EXTRA_FLAGS}" ] && _tpm_env="${_tpm_env} SWTPM_EXTRA_FLAGS='${SWTPM_EXTRA_FLAGS}'"
    [ -n "${TPM_PCR_BANKS}" ] && _tpm_env="${_tpm_env} TPM_PCR_BANKS='${TPM_PCR_BANKS}'"
    printf '%s%s%s 10mt-tpm2 -v %s -w %s\n' "${_session_sudo}" "${_session_env}" "${_tpm_env}" "${VM_ID}" "${WORKDIR}" >> "${START_CMD}"
  fi

  virsh_cmd "start" "${NAME}" >> "${START_CMD}"
  chmod 755 "${START_CMD}"
  log "START: ${START_CMD}"

  # stop.
  shebang > "${STOP_CMD}"
  virsh_cmd "destroy --graceful" "${NAME}" >> "${STOP_CMD}"

  # Stop external TPM after stopping VM if needed
  if [ -n "${EXTERNAL_TPM}" ]; then
    _tpm_env=
    [ -n "${SWTPM_EXTRA_FLAGS}" ] && _tpm_env="${_tpm_env} SWTPM_EXTRA_FLAGS='${SWTPM_EXTRA_FLAGS}'"
    [ -n "${TPM_PCR_BANKS}" ] && _tpm_env="${_tpm_env} TPM_PCR_BANKS='${TPM_PCR_BANKS}'"
    printf '%s%s%s 10mt-tpm2 -v %s -w %s -k\n' "${_session_sudo}" "${_session_env}" "${_tpm_env}" "${VM_ID}" "${WORKDIR}" >> "${STOP_CMD}"
  fi

  chmod 755 "${STOP_CMD}"
  log "STOP: ${STOP_CMD}"

  # console.
  shebang > "${CONSOLE_CMD}"
  virsh_cmd "console" "${NAME}" >> "${CONSOLE_CMD}"
  chmod 755 "${CONSOLE_CMD}"
  log "CONSOLE: ${CONSOLE_CMD}"

  # snapshot.
  shebang > "${SNAPSHOT_LIST_CMD}"
  virsh_cmd "snapshot-list" "${NAME} --name | sed -e '/^[[:space:]]*$/d'" >> "${SNAPSHOT_LIST_CMD}"
  chmod 755 "${SNAPSHOT_LIST_CMD}"
  log "SNAPSHOT-LIST: ${SNAPSHOT_LIST_CMD}"

  cat << EOF > "${SNAPSHOT_CMD}"
#!/bin/sh

if ! _last_id="\$(virsh --connect ${LIBVIRT_URI} snapshot-list \\
                 ${NAME} --name \\
                 | sed -e '/^[[:space:]]*$/d' \\
                 | tail -1 | cut -d'-' -f2)"; then
  echo "Error trying to obtain current snapshots; exiting" >&2
  exit 1
fi

# Let's start from the ID of the last snapshot.
_sid=\${_last_id:-0}
# Remove any leading zeros.
_sid=\$(echo "\${_sid}" | sed 's/^0*//')
# Now we increment it.
_sid=\$((_sid+1))

# And our new snapshot can use the new ID.
_snap="\$(printf 'snapshot-%02d' "\${_sid}")"

virsh --connect ${LIBVIRT_URI} \\
  snapshot-create-as ${NAME} \\
  --name "\${_snap}" --disk-only
EOF
  chmod 755 "${SNAPSHOT_CMD}"
  log "SNAPSHOT: ${SNAPSHOT_CMD}"

  # restore-snapshot.
  cat << EOF > "${SNAPSHOT_RESTORE_CMD}"
#!/bin/sh

VMWD="${WDIR}"

if [ ! -e "${DISK}" ]; then
  echo "Disk ${DISK} not available" >&2
  exit 1
fi
EOF

  virsh_cmd "destroy --graceful" "${NAME}" >> "${SNAPSHOT_RESTORE_CMD}"

  cat << EOF >> "${SNAPSHOT_RESTORE_CMD}"
if ! ns="\$("\${VMWD}"/snapshot-list 2>/dev/null | wc -l)" || [ -z "\${ns}" ]; then
  echo "Unable to get existing snapshots for ${DISK}" >&2
  exit 1
fi

if [ "\${ns}" -eq 0 ]; then
  echo "There are no snapshots available; exiting" >&2
  exit 1
fi

EOF

cmd="$(virsh_cmd "snapshot-revert" "${NAME}")"
printf '%s "$("${VMWD}"/snapshot-list 2>/dev/null | tail -1)"\n' "${cmd}" >> "${SNAPSHOT_RESTORE_CMD}"

  chmod 755 "${SNAPSHOT_RESTORE_CMD}"
  log "RESTORE-SNAPSHOT:: ${SNAPSHOT_RESTORE_CMD}"
}

cleanup_all() {
  for _vm in $(find "${WORKDIR}" -maxdepth 1 -type d); do
    [ "${_vm}" = "${WORKDIR}" ] && continue
    [ -e "${_vm}/cleanup" ] && \
      sh "${_vm}/cleanup" >/dev/null 2>/dev/null
  done
  exit 0
}

archive_all() {
  _archive_dir="${LOCAL_DATADIR}/archived"
  mkdir -p "${_archive_dir}"
  [ -d "${WORKDIR}" ] || exit 0
  for _vm in $(find "${WORKDIR}" -maxdepth 1 -type d); do
    [ "${_vm}" = "${WORKDIR}" ] && continue
    log "Archiving $(basename ${_vm})..."
    [ -e "${_vm}/cleanup" ] && \
      sh "${_vm}/cleanup" >/dev/null 2>/dev/null
    mv -f "${_vm}" "${_archive_dir}"
  done
  exit 0
}

stop_all() {
  for _vm in $(find "${WORKDIR}" -maxdepth 1 -type d); do
    [ "${_vm}" = "${WORKDIR}" ] && continue
    [ -e "${_vm}/stop" ] && \
      sh "${_vm}/stop" >/dev/null 2>/dev/null
  done
  exit 0
}

# From Rich’s sh (POSIX shell) tricks - http://www.etalabs.net/sh_tricks.html
_array_save() {
  for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
  echo " "
}

_array_append() {
  _arr="${1:-}"
  _data="${2:-}"

  [ -n "${_data}" ] || return 1

  if [ -n "${_arr}" ]; then
    eval "set -- ${_arr}"
    _array_save "${@}" "${_data}"
  else
    _array_save "${_data}"
  fi
}

ARGS="${*}"

SYSTEM_TYPE=${SYSTEM_TYPE:-}
NAME=${NAME:-}
COMPOSE=${COMPOSE:-}
EXPLICIT_COMPOSE=
IP=${IP:-dhcp}
KEY=${KEY:-}
USER_KEY=${USER_KEY:-}
NEW_USER="${NEW_USER:-}"
USER_PW="${USER_PW:-}"
RELEASE=${RELEASE:-}
USE_OFFICIAL=${USE_OFFICIAL:-}
IMAGES_DIR=${IMAGES_DIR:-/var/lib/libvirt/images}
PACKAGES=
EXTRA_KS=
INFO=
EXTRA_VARS=
EXTRA_REPOFILES=
EXTRA_FILES=
USE_TPM=
EXTERNAL_TPM=
TPM_V=2.0
DRY_RUN=
VM_MEM=
NO_SWAP=
LUKS_OPTIONS=
PROFILER=
STORAGE_LAYOUT=luks-simple
USE_KVM=
PCR_BANKS=
VCPUS=1
SECUREBOOT=
USE_NVRAM=
USE_KS_TEMPLATE=
NOSAVE=
RAW_FORMAT=
USE_UEFI=
USE_BRIDGE=virbr0
VM_DISK_SIZE=
EXTRA_KERNEL_ARGS=
CUSTOM_NETWORK=
DIRECT_MODE=
DIRECT_IFACE=
UPDATES_IMG=
LIBVIRT_URI="${LIBVIRT_URI:-qemu:///system}"
EXPLICIT_IMAGES_DIR=

# Auto-detect session mode for non-root users.
SESSION_MODE="${SESSION_MODE:-}"
if [ "$(id -u)" -ne 0 ]; then
  SESSION_MODE=true
fi
if [ -n "${SESSION_MODE}" ]; then
  LIBVIRT_URI="qemu:///session"
fi

case "${1:-}" in --help) usage 0;; esac

while getopts ":a:b:c:d:e:f:g:i:j:k:m:n:p:r:s:v:x:C:D:L:P:R:S:T:K:U:Z:hlotuwzABEFKMNOQWV" o; do
    case "${o}" in
    a) IP="${OPTARG}";;
    b) USE_BRIDGE="${OPTARG}";;
    c) COMPOSE="${OPTARG}"
       EXPLICIT_COMPOSE=true;;
    d) IMAGES_DIR="${OPTARG}"
       EXPLICIT_IMAGES_DIR=true;;
    e) EXTRA_KERNEL_ARGS="${EXTRA_KERNEL_ARGS:+${EXTRA_KERNEL_ARGS} }${OPTARG}";;
    f) EXTRA_FILES="${OPTARG}";;
    g) CUSTOM_NETWORK="${OPTARG}";;
    i) INFO="${OPTARG}"
       get_info_by_name;;
    j) UPDATES_IMG="${OPTARG}";;
    k) KEY="${OPTARG}";;
    m) VM_MEM="${OPTARG}";;
    n) NAME="${OPTARG}";;
    p) PACKAGES="${PACKAGES:+${PACKAGES} }${OPTARG}";;
    r) RELEASE="${OPTARG}";;
    s) SYSTEM_TYPE="${OPTARG}";;
    v) EXTRA_VARS=$(_array_append "${EXTRA_VARS}" "${OPTARG}");;
    x) EXTRA_KS="${EXTRA_KS:+${EXTRA_KS} }${OPTARG}";;
    L) LUKS_OPTIONS="${OPTARG}";;
    P) PROFILER="${OPTARG}";;
    R) EXTRA_REPOFILES="${EXTRA_REPOFILES:+${EXTRA_REPOFILES} }${OPTARG}";;
    S) STORAGE_LAYOUT="${OPTARG}";;
    h) usage 0;;
    l) get_info;;
    o) USE_OFFICIAL=true;;
    t) USE_TPM=true;;
    u) USE_UEFI=true;;
    w) USE_TPM=true
      TPM_V=1.2
      ;;
    z) DRY_RUN=true;;
    A) archive_all;;
    B) SECUREBOOT=1;;
    C) VCPUS="${OPTARG}";;
    D) VM_DISK_SIZE="${OPTARG}";;
    E) EXTERNAL_TPM=true;;
    F) RAW_FORMAT=true;;
    K) USER_KEY="${OPTARG}";;
    U) NEW_USER="${OPTARG}";;
    M) USE_KVM=true;;
    N) NO_SWAP=true;;
    O) SESSION_MODE=true
       LIBVIRT_URI="qemu:///session";;
    Q) NOSAVE=true;;
    V) QUIET=;;
    W) stop_all;;
    Z) USE_KS_TEMPLATE="${OPTARG}";;
    T) PCR_BANKS="${OPTARG}";;
    *) echo "Invalid option ${o}" >&2
      usage 1;;
    esac
done
shift $((OPTIND - 1))

[ $# -gt 0 ] && die "Unexpected argument(s): $*"

[ -d "${TEMPLATE_DIR}"/system ] || die "Unable to locate systems template directory (${TEMPLATE_DIR}/system)"

# Parse -g direct[:<iface>].
case "${CUSTOM_NETWORK}" in
direct)
  DIRECT_MODE=1
  ;;
direct:?*)
  DIRECT_MODE=1
  DIRECT_IFACE="${CUSTOM_NETWORK#direct:}"
  ;;
direct:)
  die "Missing interface name in -g direct:<iface>"
  ;;
esac

# Validate direct mode constraints.
if [ -n "${DIRECT_MODE}" ]; then
  if [ -n "${SESSION_MODE}" ]; then
    die "Direct mode (-g direct) requires root (incompatible with session mode)"
  fi
  if [ "${IP}" != "dhcp" ]; then
    die "Static IP (-a) is not supported with -g direct; direct mode uses DHCP"
  fi
  if [ -n "${UPDATES_IMG}" ]; then
    die "Updates image (-j) is not yet supported with -g direct"
  fi
  if [ "${USE_BRIDGE}" != "virbr0" ]; then
    die "Cannot combine -b and -g direct"
  fi
  if ! command -v nmcli >/dev/null 2>&1; then
    die "Direct mode requires NetworkManager (nmcli not found)"
  fi
  if ! nmcli general status >/dev/null 2>&1; then
    die "Direct mode requires NetworkManager to be running"
  fi
fi

# Apply session-mode defaults.
if [ -n "${SESSION_MODE}" ]; then
  # Override IMAGES_DIR unless user explicitly set -d.
  if [ -z "${EXPLICIT_IMAGES_DIR}" ]; then
    IMAGES_DIR="${LOCAL_DATADIR}/images"
  fi
  mkdir -p "${IMAGES_DIR}"
  chmod 700 "${IMAGES_DIR}"

  # Default to passt networking unless user set -g.
  if [ -z "${CUSTOM_NETWORK}" ]; then
    CUSTOM_NETWORK="passt"
  fi

  # Check passt is available.
  if ! command -v passt >/dev/null 2>&1; then
    die "Session mode requires passt for networking (dnf install passt)"
  fi

  # Static IP not yet supported in session mode.
  if [ "${IP}" != "dhcp" ]; then
    die "Static IP (-a) is not yet supported in session mode"
  fi

  # MOK enrollment not yet supported in session mode.
  if [ -n "${SB_MOK_CRT}" ]; then
    die "MOK enrollment is not yet supported in session mode"
  fi
fi

# Sudo and env wrappers — empty in session mode.
_session_sudo="sudo "
_session_env=""
if [ -n "${SESSION_MODE}" ]; then
  _session_sudo=""
  _session_env="SESSION_MODE=true "
fi

# Validate updates.img if specified.
if [ -n "${UPDATES_IMG}" ]; then
  [ -f "${UPDATES_IMG}" ] || die "Updates image is not a regular file: ${UPDATES_IMG}"
  [ -r "${UPDATES_IMG}" ] || die "Updates image not readable: ${UPDATES_IMG}"
  if echo "${EXTRA_KERNEL_ARGS}" | grep -q 'inst\.updates='; then
    die "Cannot use -j and -e 'inst.updates=...' together"
  fi
fi

# Check if custom KS_TEMPLATE exists.
if [ -n "${USE_KS_TEMPLATE}" ]; then
  [ -e "${KS_DIR}/${USE_KS_TEMPLATE}" ] || die "Specified kickstart template (${KS_TEMPLATE}) does not seem to exist"
  KS_TEMPLATE="${KS_DIR}"/"${USE_KS_TEMPLATE}"
fi

# Direct mode: set up a bridge on the physical NIC.
if [ -n "${DIRECT_MODE}" ]; then
  _direct_bridge_name="10mt-br0"

  # Detect or validate NIC.
  if [ -z "${DIRECT_IFACE}" ]; then
    DIRECT_IFACE="$(ip route get 1 2>/dev/null \
                    | sed -n 's/.* dev \([^ ]*\).*/\1/p')"
    [ -n "${DIRECT_IFACE}" ] \
      || die "Unable to detect primary NIC (no default route)"
    if [ -n "${TENMT_FORCE}" ]; then
      log "Direct mode: auto-detected NIC ${DIRECT_IFACE} (TENMT_FORCE)" _force
    fi
  fi

  # Validate NIC name (injection prevention).
  case "${DIRECT_IFACE}" in
    *[!a-zA-Z0-9._-]*)
      die "Invalid interface name: ${DIRECT_IFACE}" ;;
  esac
  if [ "${#DIRECT_IFACE}" -gt 15 ]; then
    die "Interface name too long (max 15 chars): ${DIRECT_IFACE}"
  fi
  [ -d "/sys/class/net/${DIRECT_IFACE}" ] \
    || die "Interface ${DIRECT_IFACE} does not exist"

  # Classify the interface.
  if [ -d "/sys/class/net/${DIRECT_IFACE}/bridge" ]; then
    # Already a bridge — offer to reuse it.
    _direct_bridge_name="${DIRECT_IFACE}"
    _direct_need_create=
    _direct_prompt="reuse"
  elif [ -d "/sys/class/net/${DIRECT_IFACE}/bonding" ]; then
    die "Detected ${DIRECT_IFACE} (bond); specify the underlying physical NIC with -g direct:<nic>"
  elif [ -e "/sys/class/net/${DIRECT_IFACE}/wireless" ] \
    || [ -e "/sys/class/net/${DIRECT_IFACE}/phy80211" ]; then
    die "Wireless interfaces cannot be bridged; ${DIRECT_IFACE} is a wireless NIC"
  elif [ -e "/sys/class/net/${DIRECT_IFACE}/device" ]; then
    # Physical NIC — check if it's already part of a bridge.
    if [ -e "/sys/class/net/${DIRECT_IFACE}/master" ]; then
      _existing_br="$(basename "$(readlink "/sys/class/net/${DIRECT_IFACE}/master")")"
      _direct_bridge_name="${_existing_br}"
      _direct_need_create=
      _direct_prompt="slave"
    elif ip link show "${_direct_bridge_name}" >/dev/null 2>&1; then
      # 10mt-br0 exists — verify it has the right slave.
      _existing_slave="$(ls "/sys/class/net/${_direct_bridge_name}/brif/" 2>/dev/null | head -1)"
      if [ "${_existing_slave}" = "${DIRECT_IFACE}" ]; then
        _direct_need_create=
        _direct_prompt=
      else
        die "${_direct_bridge_name} exists but uses ${_existing_slave:-no slave}, not ${DIRECT_IFACE}"
      fi
    else
      _direct_need_create=1
      _direct_prompt="create"
    fi
  else
    die "${DIRECT_IFACE} is not a bridgeable interface (not physical, bond, or bridge)"
  fi

  # Interactive confirmation.
  _direct_confirmed=
  if [ -n "${TENMT_FORCE}" ]; then
    _direct_confirmed=1
  elif [ ! -t 0 ] 2>/dev/null; then
    die "Direct mode requires interactive confirmation (or set TENMT_FORCE=1)"
  fi

  if [ -z "${_direct_confirmed}" ]; then
    case "${_direct_prompt}" in
    reuse)
      printf '%s is already a bridge. Use it for the VM? [Y/n] ' \
        "${DIRECT_IFACE}" >/dev/tty
      read -r _answer </dev/tty
      case "${_answer}" in
        [Nn]*) echo "Aborted." >&2; exit 1 ;;
      esac
      ;;
    slave)
      printf '%s is already part of bridge %s. Use it? [Y/n] ' \
        "${DIRECT_IFACE}" "${_direct_bridge_name}" >/dev/tty
      read -r _answer </dev/tty
      case "${_answer}" in
        [Nn]*) echo "Aborted." >&2; exit 1 ;;
      esac
      ;;
    create)
      printf 'WARNING: This will bridge %s to the physical network.\n' \
        "${DIRECT_IFACE}" >/dev/tty
      printf 'The VM will have direct L2 access to your LAN.\n' >/dev/tty
      printf 'Active SSH sessions on %s may be briefly interrupted.\n' \
        "${DIRECT_IFACE}" >/dev/tty
      printf 'Create bridge %s on %s? [y/N] ' \
        "${_direct_bridge_name}" "${DIRECT_IFACE}" >/dev/tty
      read -r _answer </dev/tty
      case "${_answer}" in
        [Yy]*) : ;;
        *) echo "Aborted." >&2; exit 1 ;;
      esac
      ;;
    esac
  fi

  # Create the bridge if needed.
  if [ -n "${_direct_need_create}" ]; then
    (
      flock -n 9 || { echo "ERROR: Another 10mt instance is creating a bridge; try again" >&2; exit 1; }

      # Deactivate current NM connection on the NIC.
      _old_con="$(nmcli -t -f DEVICE,NAME con show --active \
                  | sed -n "s/^${DIRECT_IFACE}://p")"
      [ -n "${_old_con}" ] && nmcli con down "${_old_con}" >/dev/null 2>&1

      nmcli con add type bridge ifname "${_direct_bridge_name}" \
        con-name "${_direct_bridge_name}" \
        bridge.stp no connection.autoconnect no >/dev/null \
        || { echo "ERROR: Failed to create bridge ${_direct_bridge_name}" >&2; exit 1; }

      nmcli con add type bridge-slave \
        ifname "${DIRECT_IFACE}" master "${_direct_bridge_name}" \
        con-name "${_direct_bridge_name}-slave" \
        connection.autoconnect no >/dev/null \
        || {
          nmcli con delete "${_direct_bridge_name}" >/dev/null 2>&1
          echo "ERROR: Failed to enslave ${DIRECT_IFACE} to ${_direct_bridge_name}" >&2
          exit 1
        }

      nmcli --wait 30 con up "${_direct_bridge_name}" >/dev/null \
        || {
          nmcli con delete "${_direct_bridge_name}-slave" >/dev/null 2>&1
          nmcli con delete "${_direct_bridge_name}" >/dev/null 2>&1
          echo "ERROR: Failed to bring up ${_direct_bridge_name}" >&2
          exit 1
        }
    ) 9>/var/lock/10mt-bridge.lock
    [ $? -ne 0 ] && exit 1

    log "Direct mode: created bridge ${_direct_bridge_name} on ${DIRECT_IFACE}" _force
    log "To remove later: nmcli con delete ${_direct_bridge_name} ${_direct_bridge_name}-slave" _force
  else
    # Ensure existing bridge is up.
    nmcli --wait 30 con up ifname "${_direct_bridge_name}" >/dev/null 2>&1 || true
  fi

  CUSTOM_NETWORK="bridge=${_direct_bridge_name}"
  IP=dhcp
fi

if [ -n "${CUSTOM_NETWORK}" ]; then
  # No bridge detection needed for custom networking (e.g. passt).
  # Force DHCP if no IP was explicitly given, since there's no
  # bridge IP range to derive a static IP from.
  IP="${IP:-dhcp}"
  NET_RANGE=
else
  # Let's try to find the network range of USE_BRIDGE (default virbr0) interface.
  _viface=
  for _iface in /sys/class/net/*; do
    if echo "${_iface}" | grep -q "${USE_BRIDGE}"; then
      _viface="$(basename "${_iface}")"
      break
    fi
  done
  [ -z "${_viface}" ] && die "Unable to determine the IP range of libvirt network interface (bridge ${USE_BRIDGE})"

  if ! NET_RANGE="$(ip addr show "${_viface}" \
                    | grep inet \
                    | awk '{ print $2 }' \
                    | awk -F'.' '{ print $1,$2,$3 }' OFS='.')" \
                     || [ -z "${NET_RANGE}" ]; then
    die "Unable to determine default network range from interface ${_viface}"
  fi
fi

log "NET RANGE: ${NET_RANGE}"

VALID_SYSTEMS=
for _s in "${TEMPLATE_DIR}"/system/*; do
  [ -d "${_s}" ] || continue
  VALID_SYSTEMS="${VALID_SYSTEMS} $(basename "${_s}")"
done
VALID_SYSTEMS="$(echo "${VALID_SYSTEMS}" | sed -e 's/^ //')"

if ! echo "${VALID_SYSTEMS}" | grep -wq "${SYSTEM_TYPE}"; then
   echo "ERROR: Invalid or unsupported system type (SYSTEM TYPE=${SYSTEM_TYPE})" >&2
   echo "Valid system types are: ${VALID_SYSTEMS}" >&2
   usage 1
fi

# Variables related to secureboot.
# The next variables are for the Machine Owner Key certificate (MOK_CRT),
# as well as the Platform Key (PK) and Key Exchange Key (KEK), both
# the key and the certificate.
# If the MOK_CRT is specified, it will be enrolled to the shim.
SB_MOK_CRT=${SB_MOK_CRT:-}
# If we are enrolling the MOK certificate to the shim, we will need
# keys, the PK and KEK keys + their certs. If these are not specified
# (we need the 4 variables specified, if wanting to use pre-generated keys),
# 10mt will autogenerate those.
SB_PK_KEY="${SB_PK_KEY:-}"
SB_PK_CRT="${SB_PK_CRT:-}"
SB_KEK_KEY="${SB_KEK_KEY:-}"
SB_KEK_CRT="${SB_KEK_CRT:-}"
# The files for the variables and firmware.
# If not specified, we will try to use the default ones from edk2 ovmf.
SB_VARS="${SB_VARS:-}"
SB_FW="${SB_FW:-}"

# Timestamp.
TS="$(date '+%Y%m%d-%H%M%S')"
# Suffix.
SFX="$(shuf -i 1-65535 -n 1)"

# ISO detection — must run before system config sourcing, since
# some configs (e.g. Fedora) call resolve_compose() which curls
# the COMPOSE value and would fail on a local ISO path.
USE_ISO=
if echo "${COMPOSE}" | grep -qi '\.iso$'; then
  [ -r "${COMPOSE}" ] || die "ISO file not readable: ${COMPOSE}"
  USE_ISO=true
fi
export USE_ISO

# Load config.
. "${TEMPLATE_DIR}"/system/"${SYSTEM_TYPE}"/config

export TMPDIR="${WORKDIR}" && mkdir -p "${TMPDIR}"
mkdir -p "${IMAGES_DIR}" || die "IMAGES_DIR (${IMAGES_DIR}) does not exist"

WDIR_TEMPLATE="$(printf '10mt_%s_%s_%s_XXX' "${SYSTEM_TYPE}" "${NAME}" "${TS}")"
WDIR="$(mktemp -d -p "${WDIR}" "${WDIR_TEMPLATE}")"
chmod 755 "${WDIR}"
export WDIR

VM_ID="$(basename "${WDIR}")"

# Record direct-mode bridge for the start script.
if [ -n "${DIRECT_MODE}" ]; then
  echo "${_direct_bridge_name}" > "${WDIR}"/direct-bridge
fi

export LOGFILE="${WDIR}"/log

log "ARGS: ${ARGS}"
log "VCPUS: ${VCPUS}"

CMD="${WDIR}"/provision
TPM_CMD="${WDIR}"/setup-swtpm
COMPOSE_SAVE="${WDIR}"/compose

# Actions.
CLEAN_CMD="${WDIR}"/cleanup
START_CMD="${WDIR}"/start
STOP_CMD="${WDIR}"/stop
CONSOLE_CMD="${WDIR}"/console
SNAPSHOT_CMD="${WDIR}"/snapshot
SNAPSHOT_RESTORE_CMD="${WDIR}"/snapshot-restore
SNAPSHOT_LIST_CMD="${WDIR}"/snapshot-list

# Shared files.
SHARED_FILES="${WDIR}"/shared-files.tar

KS="${WDIR}"/ks.cfg

# Copy updates.img to a restricted serve directory.
if [ -n "${UPDATES_IMG}" ]; then
  mkdir -p "${WDIR}"/updates-srv
  cp "${UPDATES_IMG}" "${WDIR}"/updates-srv/updates.img
  UPDATES_IMG="${WDIR}"/updates-srv/updates.img
  log "UPDATES_IMG: ${UPDATES_IMG}"
fi
log "ARCH: ${ARCH}"
log "WDIR: ${WDIR}"
log "EL MAJOR: ${EL_MAJOR}"
log "RELEASE: ${RELEASE}"
log "KS_TEMPLATE: ${KS_TEMPLATE}"
log "HOST: ${HOST}"
log "COMPOSE: ${COMPOSE}"
log "KEY: ${KEY}"
echo "${NAME}" > "${WDIR}"/id
log "NAME: ${NAME}"

log "IP: ${IP}"
VM_NETWORK="$(printf -- \
  '--bootproto static --ip %s --netmask 255.255.255.0 --gateway %s.1 --nameserver %s.1' \
  "${IP}" "${NET_RANGE}" "${NET_RANGE}")"

[ "${IP}" = "dhcp" ] && VM_NETWORK="$(printf -- '--bootproto dhcp')"
log "VM_NETWORK: ${VM_NETWORK}"

LUKS2_OPTIONS_DEFAULT='--luks-version=luks2 --pbkdf=pbkdf2 --pbkdf-iterations=1000 --pbkdf-memory=64'
LUKS_OPTIONS=${LUKS_OPTIONS:-${LUKS2_OPTIONS_DEFAULT}}
log "LUKS_OPTIONS: ${LUKS_OPTIONS}"
[ "${LUKS_OPTIONS}" = "none" ] && LUKS_OPTIONS=

LUKS_PW=${LUKS_PW:-${DEFAULT_LUKS_PW}}
ROOT_PW=${ROOT_PW:-${DEFAULT_ROOT_PW}}
[ -n "${USER_PW}" ] || USER_PW="${ROOT_PW}"

VM_DISK_SIZE=${VM_DISK_SIZE:-${DEFAULT_VM_DISK_SIZE}}
VM_RAM=${VM_MEM:-${DEFAULT_VM_RAM}}
log "LUKS_PW: ${LUKS_PW}"
log "ROOT_PW: ${ROOT_PW}"
log "USER_PW: ${USER_PW}"
log "VM_DISK_SIZE(GB): ${VM_DISK_SIZE}"
log "VM_RAM(MB): ${VM_RAM}"

if [ -n "${KEY}" ]; then
  if [ -r "${KEY}" ]; then
    ssh-keygen -l -f "${KEY}" 2>/dev/null >/dev/null \
      || die "${KEY} is not a valid SSH public key"
    grep -qw PRIVATE "${KEY}" \
      && die "${KEY} is not a valid SSH public key; it looks like a private key instead"
    SSHKEY="$(cat "${KEY}")"
  else
    die "Unable to access SSH key file ${KEY}; please make sure it exists and is readable"
  fi
fi
log "SSHKEY: ${SSHKEY}"

if [ -n "${USER_KEY}" ]; then
  if [ -r "${USER_KEY}" ]; then
    ssh-keygen -l -f "${USER_KEY}" 2>/dev/null >/dev/null \
      || die "${USER_KEY} is not a valid SSH public key"
    grep -qw PRIVATE "${USER_KEY}" \
      && die "${USER_KEY} is not a valid SSH public key; it looks like a private key instead"
    USER_SSHKEY="$(cat "${USER_KEY}")"
  else
    die "Unable to access USER SSH key file ${USER_KEY}; please make sure it exists and is readable"
  fi
fi
log "USER_SSHKEY: ${USER_SSHKEY}"


DISK_FORMAT="qcow2"
[ -n "${RAW_FORMAT}" ] && DISK_FORMAT="raw"
DISK_BASENAME="${HOST}-${TS}-r${SFX}"
DISK="${IMAGES_DIR}"/"${DISK_BASENAME}"."${DISK_FORMAT}"
log "IMAGES_DIR: ${IMAGES_DIR}"
log "DISK: ${DISK}"

if [ -e "${DISK}" ]; then
  die "VM disk file (${DISK}) already exists"
fi

parse_post_ks_snippets
parse_extra_env_vars

# Prepare packages list.
PACKAGES="${PACKAGES} ${REQ_PKGS}"
[ -n "${PACKAGES}" ] && PACKAGES="$(parse_list "${PACKAGES}")"

log "PACKAGES: ${PACKAGES}"
PACKAGES="$(echo "${PACKAGES}" | sed -e 's/ /\\n/g')"

log "PROFILER: ${PROFILER}"

# Storage layout.
is_absolute_path() {
  case $1 in (/*) pathchk -- "$1";; (*) ! : ;; esac
}
# If provided STORAGE_LAYOUT is an absolute path, we can use it.
# Otherwise, let's check the templates we have.

_storage="${STORAGE_LAYOUT}"
if ! is_absolute_path "${STORAGE_LAYOUT}"; then
  _valid_storage="$(find "${TEMPLATE_DIR}"/storage/ -maxdepth 1 \
                    ! -name '.*' ! -name '*.swp' -type f \
                    -type f -exec basename {} \; | sort)"
  _valid_storage="$(parse_list "${_valid_storage}")"
  [ -e "${TEMPLATE_DIR}/storage/${STORAGE_LAYOUT}" ] \
    || die "Invalid storage layout; valid layouts are: ${_valid_storage}"
  _storage="${TEMPLATE_DIR}/storage/${STORAGE_LAYOUT}"
fi

# Update template to generate ks, in 2 steps.
_post_template="${TEMPLATE_DIR}/ks/ks.post.main.template"
cat "${KS_TEMPLATE}" "${_storage}" "${_post_template}" > "${KS}".stage1

if [ -n "${EXTRA_KS_FILES}" ]; then
  for _f in ${EXTRA_KS_FILES}; do
    cat "${_f}" >> "${KS}".stage1
  done
fi

update_template_env_vars() {
  [ -n "${EXTRA_ENV_VARS}" ] || return 0

  eval "set -- ${EXTRA_ENV_VARS}"
  for _v in "${@}"; do
    if echo "${_v}" | grep -q '='; then
      _key=$(echo "${_v}" | cut -d'=' -f1)
      _value=$(echo "${_v}" | cut -d'=' -f2-)
    else
      _key="${_v}"
      _value="$(env | grep -w "^${_key}" | cut -d'=' -f2-)"
    fi
    sed -e "s^@${_key}@^${_value}^" -i "${KS}".stage1
  done
}
update_template_env_vars

# Terminating the %post section.
printf '\n\n%%end\n' >> "${KS}".stage1

# Shared files.
SHARED_DIR=
shared_files_cleanup() {
  [ -d "${SHARED_DIR}" ] && rm -rf "${SHARED_DIR}"
  return
}
if [ -n "${EXTRA_FILES}" ]; then
  # Create temp directory where we will create the tarball.
  if ! SHARED_DIR="$(mktemp -d "/tmp/shared-files-XXXXXXX")"; then
    die "Unable to create temp directory to handle shared files"
  fi
  export SHARED_DIR
  trap shared_files_cleanup INT TERM EXIT

  # Create empty tarball.
  tar -cf "${SHARED_DIR}"/shared.tar -T /dev/null

  # Validate file list.
  for f in ${EXTRA_FILES//;/ }; do
    if ! is_absolute_path "${f}"; then
      die "Please specify the absolute path of the files to share (issue: ${f})"
    fi

    # Create symlink for the file.
    bn="$(basename "${f}")"
    ln -s "${f}" "${SHARED_DIR}/${bn}"
    tar -rf "${SHARED_DIR}"/shared.tar -h "${bn}"
  done
  mv -f "${SHARED_DIR}"/shared.tar "${SHARED_FILES}"
  rm -rf "${SHARED_DIR}"
  EXTRA_SHAREDFILES="$(base64 -w 0 "${SHARED_FILES}")"
fi

CDL=

PREP_BOOT_PART=
[ -z "${USE_UEFI}" ] && \
  PREP_BOOT_PART='part biosboot --size=2 --fstype=biosboot'

[ "${ARCH}" = "s390x" ] && CDL='--cdl'
[ "${ARCH}" = "ppc64le" ] \
  && PREP_BOOT_PART='part prepboot --fstype="PPC PReP Boot" --size=10'
AUTO_FS_TYPE=${AUTO_FS_TYPE:-lvm}

BASE_FS_TYPE=xfs
[ "${STORAGE_LAYOUT}" = "luks-multiple" ] && BASE_FS_TYPE=ext4
FS_TYPE=${FORCE_FS_TYPE:-${BASE_FS_TYPE}}

EXTRA_AUTOPART="--type=${AUTO_FS_TYPE} --nohome"
KS_SWAP=
if [ -n "${NO_SWAP}" ]; then
  EXTRA_AUTOPART='--noswap'
  KS_SWAP='swapoff -a'
fi

if [ "${SYSTEM_TYPE}" = "rhel6" ]; then
 EXTRA_AUTOPART=
 FS_TYPE=ext4
fi

log "NO_SWAP: ${NO_SWAP}"

cat "${KS}".stage1 \
  | sed -e "s#@ARCH@#${ARCH}#" \
  | sed -e "s#@CDL@#${CDL}#" \
  | sed -e "s#@FS_TYPE@#${FS_TYPE}#" \
  | sed -e "s#@PREP_BOOT_PART@#${PREP_BOOT_PART}#" \
  | sed -e "s#@EL_MAJOR@#${EL_MAJOR}#" \
  | sed -e "s#@HOST@#${HOST}#" \
  | sed -e "s#@COMPOSE@#${COMPOSE}#" \
  | sed -e "s#@APPSTREAM@#${APPSTREAM}#" \
  | sed -e "s#@BASEOS@#${BASEOS}#" \
  | sed -e "s#@REPOFILES@#${EXTRA_REPOFILES}#" \
  | sed -e "s#@VM_NETWORK@#${VM_NETWORK}#" \
  | sed -e "s#@IP@#${IP}#" \
  | sed -e "s#@PACKAGES@#${PACKAGES}#" \
  | sed -e "s#@LUKS_PW@#${LUKS_PW}#" \
  | sed -e "s#@ROOT_PW@#${ROOT_PW}#" \
  | sed -e "s#@USER_PW@#${USER_PW}#" \
  | sed -e "s#@SSHKEY@#${SSHKEY}#" \
  | sed -e "s#@USER_SSH_PUBKEY@#${USER_SSHKEY}#" \
  | sed -e "s#@EXTRA_AUTOPART@#${EXTRA_AUTOPART}#" \
  | sed -e "s#@KS_SWAP@#${KS_SWAP}#" \
  | sed -e "s#@NO_SAVE@#${NOSAVE}#" \
  | sed -e "s#@PROFILER@#${PROFILER}#" \
  | sed -e "s#@LUKS_OPTIONS@#${LUKS_OPTIONS}#" \
  > "${KS}"

# For ISO installs, replace 'url --url=...' with 'cdrom' and remove
# the AppStream repo line (only relevant for EL kickstart templates).
if [ -n "${USE_ISO}" ]; then
  sed -i -e 's#^url --url=.*$#cdrom#' \
         -e '/^repo --name="AppStream"/d' \
         "${KS}"
fi

if [ -n "${EXTRA_FILES}" ]; then
  sed '/@EXTRA_SHAREDFILES@/q' "${KS}" | head -n -1 > "${KS}".shared-1
  sed -n '/@EXTRA_SHAREDFILES@/,$p' "${KS}" | tail -n +2 > "${KS}".shared-2
  cat "${KS}".shared-1 <(printf "  SHARED_FILES='%s'\n\n" "${EXTRA_SHAREDFILES}") "${KS}".shared-2 > "${KS}".shared-3-full
  mv -f "${KS}".shared-3-full "${KS}"
  rm -f "${KS}".shared*
else
  sed -i 's/@EXTRA_SHAREDFILES@//' "${KS}"
fi

log "KS: ${KS}"

_kvm_args=
log "USE_KVM: ${USE_KVM}"
[ -n "${USE_KVM}" ] && _kvm_args="--cpu host"

log "USE_TPM: ${USE_TPM} / TPM VERSION ${TPM_V}"
log "EXTERNAL_TPM: ${EXTERNAL_TPM}"
log "PCR_BANKS: ${PCR_BANKS}"
_tpm_args=
_qemu_tpm_args=
pre_cmd=
if [ -n "${USE_TPM}" ] && [ -z "${EXTERNAL_TPM}" ]; then
  # Use virt-install's built-in TPM emulator (default behavior)
  _tpm_args="--tpm backend.type=emulator,backend.version=${TPM_V:-2.0}"
  if [ -n "${PCR_BANKS}" ]; then
    _banks="$(echo "${PCR_BANKS}" | tr ',' ' ')"
    for _pcr_bank in ${_banks}; do
      _tpm_args="$(printf '%s,active_pcr_banks.%s=on' \
                   "${_tpm_args}" "${_pcr_bank}")"
    done
  fi
elif [ -n "${EXTERNAL_TPM}" ]; then
  # External TPM mode - use custom swtpm instance with QEMU command-line args
  log "Using external TPM management"
  [ -z "${USE_TPM}" ] && die "External TPM mode (-E) requires -t flag to be set"

  # Calculate the TPM socket path (same logic as 10mt-tpm2)
  S1_ID="$(printf '%s' "$(realpath -m "${WORKDIR}/${VM_ID}")" | sha1sum | awk '{ print $1 }')"
  TPM_PATH="${LOCAL_DATADIR}/tpm2/${S1_ID}/swtpm.sock"
  log "TPM_PATH: ${TPM_PATH}"

  # Determine TPM device type based on architecture
  _tpm_device=
  case "${ARCH}" in
  x86_64)
    _tpm_device='-device tpm-tis,tpmdev=tpm0'
    ;;
  aarch64)
    _tpm_device='-device tpm-tis-device,tpmdev=tpm0'
    ;;
  ppc64le)
    _tpm_device='-device tpm-spapr,tpmdev=tpm0'
    ;;
  *)
    die "Unsupported architecture (${ARCH}) for the TPM2 emulator"
    ;;
  esac

  # Build QEMU command-line arguments for external TPM
  _qemu_tpm_args=$(printf -- \
    '-chardev socket,id=chrtpm,path="%s" -tpmdev emulator,id=tpm0,chardev=chrtpm %s' \
    "${TPM_PATH}" "${_tpm_device}")

  # Pre-command to start swtpm before VM creation
  _tpm_env_vars=
  [ -n "${SWTPM_EXTRA_FLAGS}" ] && _tpm_env_vars="${_tpm_env_vars} SWTPM_EXTRA_FLAGS='${SWTPM_EXTRA_FLAGS}'"
  [ -n "${TPM_PCR_BANKS}" ] && _tpm_env_vars="${_tpm_env_vars} TPM_PCR_BANKS='${TPM_PCR_BANKS}'"
  pre_cmd="${_session_sudo}${_session_env}${_tpm_env_vars} 10mt-tpm2 -v ${VM_ID} -w ${WORKDIR} && "

  touch "${WDIR}/.tpm2"
fi

OSINFO=
VIRT_INST_MAJOR_VER=$(virt-install --version | cut -d'.' -f1)
VIRT_INST_OSINFO_NEWER_VER=3
if [ "${VIRT_INST_MAJOR_VER=}" -ge "${VIRT_INST_OSINFO_NEWER_VER}" ]; then
  OSINFO='--osinfo detect=on,require=off'
fi

case "${SYSTEM_TYPE}" in
rhel*|alma*|rocky*|c9s|c8s)
  OSINFO="--os-variant rhel7-unknown"
esac
[ "${SYSTEM_TYPE}" = "rhel6" ] && OSINFO="--os-variant rhel6-unknown";

SBARGS=
if [ -n "${SECUREBOOT}" ]; then
  # We need the vars and fw files, that may have been provided
  # via the SB_VARS / SB_FW. If they were not, we will look
  # for the OVMF_CODE.secboot.fd, which might be located in
  # different places depending on the distro used. Similarly,
  # we will also look for the variables file.

  _ovmf_fw=
  _ovmf_vars=
  _ovmf_common_paths="/usr/share/edk2/ovmf /usr/share/edk2-ovmf/x64"

  [ -n "${SB_FW}" ] && [ ! -r "${SB_FW}" ] \
    && die "Unable to access specified SB_FW env variable (${SB_FW}); make sure it is correct"

  [ -n "${SB_VARS}" ] && [ ! -r "${SB_VARS}" ] \
    && die "Unable to access specified SB_VARS env variable (${SB_VARS}); make sure it is correct"

  [ -n "${SB_FW}" ] && _ovmf_fw="${SB_FW}"
  [ -n "${SB_VARS}" ] && _ovmf_vars="${SB_VARS}"

  _ovmf_fw=/usr/share/10mt/template/ovmf/OVMF_CODE_4M.secboot.fd
  _ovmf_vars=/usr/share/10mt/template/ovmf/OVMF_VARS_4M.secboot.fd

  if [ -z "${_ovmf_fw}" ] || [ -z "${_ovmf_vars}" ]; then
    die "Unable to enable Secure Boot because either the OVMF FW (${_ovmf_fw}) or VARs (${_ovmf_vars}) was not found; either fix this or remove the -B switch."
  fi

  SB_NVRAM=nvram_template

  # Now let's check if we need to enrol a MOK certificate.
  if [ -n "${SB_MOK_CRT}" ]; then
    # Not a template anymore.
    SB_NVRAM=nvram

    [ -r "${SB_MOK_CRT}" ] || die "Unable to read the specified Machine Owner Key (MOK) certificate (${SB_MOK_CRT})"

    if [ -n "${SB_PK_KEY}" ] || [ -n "${SB_PK_CRT}" ] || [ -n "${SB_KEK_KEY}" ] || [ -n "${SB_KEK_CRT}" ]; then
      _errmsg="If at least one of SB_PK_KEY/SB_PK_CRT/SB_KEK_KEY/SB_KEK_CRT is defined, they all need to be defined; make sure that is the case"
      [ -n "${SB_PK_KEY}" ] || die "${SB_PK_KEY} is not defined: ${_errmsg}"
      [ -n "${SB_PK_CRT}" ] || die "${SB_PK_CRT} is not defined: ${_errmsg}"
      [ -n "${SB_KEK_KEY}" ] || die "${SB_KEK_KEY} is not defined: ${_errmsg}"
      [ -n "${SB_KEK_CRT}" ] || die "${SB_KEK_CRT} is not defined: ${_errmsg}"

    else
      # We will need to creaet the keys for enrolling the MOK certificate.
      _keysdir="${WDIR}/keys"
      10mt-sb-keys "${_keysdir}" >>"${LOGFILE}" 2>>"${LOGFILE}" \
        || die "Unable to create the keys for enrolling the MOK certificate"
      # Keys created, let's adjust the variables to use later.
      SB_PK_KEY="${_keysdir}/PK.key"
      SB_PK_CRT="${_keysdir}/PK.crt"
      SB_KEK_KEY="${_keysdir}/KEK.key"
      SB_KEK_CRT="${_keysdir}/KEK.crt"
    fi

    # The SB_ variables should all exist not. Let's make
    # sure we can read them.
    [ -r "${SB_PK_KEY}" ] || die "Unable to read PK key ${SB_PK_KEY}"
    [ -r "${SB_PK_CRT}" ] || die "Unable to read PK cert ${SB_PK_CRT}"
    [ -r "${SB_KEK_KEY}" ] || die "Unable to read KEK key ${SB_KEK_KEY}"
    [ -r "${SB_KEK_CRT}" ] || die "Unable to read KEK cert ${SB_KEK_CRT}"

    # Let's do the actual enrolling.
    # We will start by creating a VARS file from the template we have,
    # which has simply the dbx variable, besides the CustomMode and
    # SecureBootEnable ones. This template is in full Secure Boot "User
    # Mode", and by default, it is 4M.
    _ovmf_vars="${WDIR}/ovmf.template"
#    cp -f /usr/share/10mt/template/ovmf/OVMF_VARS.fd "${_ovmf_vars}"a
    10mt-ovmf-vars -i -y /usr/share/10mt/template/ovmf/ovmf-vars.template \
                   -o "${_ovmf_vars}" >>"${LOGFILE}" 2>>"${LOGFILE}"
    # # 10mt-sb-mok-enroll will create a secureboot subdir here.
    _sbdir="${WDIR}"

    # 10mt-sb-mok-enroll usage:
    # 10mt-sb-mok-enroll <MOK crt> <PK crt> <PK key> <KEK crt> <KEK key> <OVMF vars file> <WORKDIR>

    10mt-sb-mok-enroll "${SB_MOK_CRT}" \
      "${SB_PK_CRT}" "${SB_PK_KEY}" \
      "${SB_KEK_CRT}" "${SB_KEK_KEY}" \
      "${_ovmf_vars}" \
      "${_sbdir}" >>"${LOGFILE}" 2>>"${LOGFILE}" || die "Unable to enrol the MOK certificate (${MOK_CRT})"

    # Let's use the qcow2 VARS file.
    _ovmf_vars="${_ovmf_vars}".qcow2

    # MOK enrolled, we need to use the proper vars file now.
    _ovmf_vars="${_sbdir}/secureboot/ovmf-vars.fd"
    log "New _ovmf_vars=${_ovmf_vars}"
    [ -r "${_ovmf_vars}" ] || die "Unable to read the VARS file (${_ovmf_vars}) after enrolling the MOK certificate"
  fi

  USE_NVRAM=1
  SBARGS="--features smm=on --boot loader=${_ovmf_fw},${SB_NVRAM}=${_ovmf_vars},loader.readonly=yes,loader.type=pflash,loader_secure=yes"
  log "Using Secure boot - SBARGS: ${SBARGS}"
fi

INST_PREFIX='inst.'
[ "${SYSTEM_TYPE}" = "rhel6" ] && INST_PREFIX=

EXTRA_INST_ARGS=
if [ -n "${NOSAVE}" ]; then
  EXTRA_INST_ARGS="${INST_PREFIX}"nosave=all
fi
[ -n "${EXTRA_KERNEL_ARGS}" ] && \
  EXTRA_INST_ARGS="${EXTRA_INST_ARGS:+${EXTRA_INST_ARGS} }${EXTRA_KERNEL_ARGS}"

UEFI_VM=
[ -n "${USE_UEFI}" ] && UEFI_VM="--boot uefi"
# Add features smm=on when secure boot is not enabled (secure boot adds it otherwise).
[ -n "${UEFI_VM}" ] && [ -z "${SBARGS}" ] && UEFI_VM="${UEFI_VM} --features smm=on"

# Save compose.
echo "${COMPOSE}" > "${COMPOSE_SAVE}"

cat << EOF > "${CMD}"
#!/bin/sh

# VARS for secureboot.
[ -e "${WDIR}/secureboot/ovmf-vars.fd.pre-installation" ] &&
  cp -af "${WDIR}/secureboot/ovmf-vars.fd.pre-installation" \
        "${WDIR}/secureboot/ovmf-vars.fd"

EOF

# Add pre-command if external TPM is used
[ -n "${pre_cmd}" ] && echo "${pre_cmd}" >> "${CMD}"

# Add HTTP server for updates.img to provision script.
if [ -n "${UPDATES_IMG}" ]; then
  cat << 'UPDEOF' >> "${CMD}"

# Serve updates.img via temporary HTTP server.
_updates_url="$(10mt-updates-server \
  "WDIR_PLACEHOLDER/updates-srv" \
  "WDIR_PLACEHOLDER" \
  "BRIDGE_PLACEHOLDER")" \
  || { echo "ERROR: failed to start updates.img server" >&2; exit 1; }
echo "updates.img HTTP server: ${_updates_url}"
_updates_bind_addr="$(cat "WDIR_PLACEHOLDER/updates-server.addr")"
_updates_port="$(cat "WDIR_PLACEHOLDER/updates-server.port")"
UPDEOF

  # Replace placeholders with actual values.
  _bridge_val=
  [ -z "${CUSTOM_NETWORK}" ] && _bridge_val="${USE_BRIDGE}"
  sed -i \
    -e "s#WDIR_PLACEHOLDER#${WDIR}#g" \
    -e "s#BRIDGE_PLACEHOLDER#${_bridge_val}#g" \
    "${CMD}"

  EXTRA_INST_ARGS="${EXTRA_INST_ARGS:+${EXTRA_INST_ARGS} }${INST_PREFIX}updates=http://\${_updates_bind_addr}:\${_updates_port}/updates.img"
  log "UPDATES_IMG: served via HTTP (address and port selected at runtime)"
fi

# Build virt-install command with optional QEMU commandline args
_qemu_cmd_args=
[ -n "${_qemu_tpm_args}" ] && _qemu_cmd_args="--qemu-commandline=\"${_qemu_tpm_args}\""

if [ -n "${CUSTOM_NETWORK}" ]; then
  _network_arg="--network ${CUSTOM_NETWORK}"
else
  _network_arg="--network bridge=${USE_BRIDGE}"
fi

# For ISO installs, omit inst.repo (the ISO is the source).
_repo_arg="${INST_PREFIX}repo=${LOCATION}"
[ -n "${USE_ISO}" ] && _repo_arg=

echo 'virt-install --connect '${LIBVIRT_URI} \
    '--name='${NAME} \
    ${OSINFO} '--ram='${VM_RAM} ${UEFI_VM} \
    '--vcpus='${VCPUS} ${_kvm_args} \
    '--rng /dev/urandom' \
    '--graphics vnc,listen=0.0.0.0' \
    "${_network_arg}" \
    '--noautoconsole '${_tpm_args} \
    '--disk=path="'${DISK}'",size='${VM_DISK_SIZE}',bus=virtio,cache=none,format='${DISK_FORMAT} \
    '--location="'${LOCATION}'",kernel=images/pxeboot/vmlinuz,initrd=images/pxeboot/initrd.img' \
    '--initrd-inject='${KS} ${SBARGS} \
    '--extra-args="'${INST_PREFIX}'ks=file:/ks.cfg' \
    "${_repo_arg} ${EXTRA_INST_ARGS}" \
    'disable_ipv6=1' \
    'console=tty0 console=ttyS0,115200 rhgb quiet"' \
    '--console pty,target_type=serial --noreboot' ${_qemu_cmd_args} >> "${CMD}"
chmod 755 "${CMD}"

prepare_cmds

# Create setup-swtpm helper script if using external TPM
if [ -n "${EXTERNAL_TPM}" ]; then
  _tpm_env=
  [ -n "${SWTPM_EXTRA_FLAGS}" ] && _tpm_env="${_tpm_env} SWTPM_EXTRA_FLAGS='${SWTPM_EXTRA_FLAGS}'"
  [ -n "${TPM_PCR_BANKS}" ] && _tpm_env="${_tpm_env} TPM_PCR_BANKS='${TPM_PCR_BANKS}'"
  printf '#!/bin/sh\n%s%s%s 10mt-tpm2 -v %s -w %s\n' "${_session_sudo}" "${_session_env}" "${_tpm_env}" "${VM_ID}" "${WORKDIR}" > "${TPM_CMD}"
  chmod 755 "${TPM_CMD}"
  log "SETUP-SWTPM: ${TPM_CMD}"
fi

cmd="$(cat "${CMD}")"

log "${cmd}"

[ -n "${DRY_RUN}" ] && echo "${VM_ID}" && exit 0

"${CMD}"

echo "START: ${START_CMD}" >&2
echo "CONSOLE: ${CONSOLE_CMD}" >&2
echo "CLEANUP: ${CLEAN_CMD}" >&2

echo "${VM_ID}"

# vim:set ts=2 sw=2 et:
