#!/usr/bin/env bash
#
# hpe-ams-jbod.sh
#
# Install HPE AMS + storage CLIs and OPTIONALLY set HPE storage controllers to
# JBOD / HBA (pass-through) mode on Proxmox VE / Debian.
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
# Copyright (C) 2026  VMsources Group Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <https://www.gnu.org/licenses/>.
#
# ===========================================================================
#  NO WARRANTY - USE AT YOUR OWN RISK
#
#  This script performs DESTRUCTIVE storage operations: it can enable HBA/JBOD
#  mode and delete logical/virtual drives, which renders existing arrays and
#  their data inaccessible. It is provided "AS IS", WITHOUT WARRANTY OF ANY
#  KIND, express or implied. The author(s) and contributors accept NO LIABILITY
#  for data loss, downtime, hardware damage, or any other harm arising from its
#  use. Read the code, test on non-production hardware, and confirm you have
#  working backups before running it. By using this script you accept full
#  responsibility for the outcome.
# ===========================================================================
#
# ---------------------------------------------------------------------------
# TRADEMARKS / AFFILIATION
#   This is an independent project and is NOT affiliated with, authorized,
#   sponsored, or endorsed by Hewlett Packard Enterprise (HPE) or Broadcom Inc.
#   "ProLiant", "iLO", "Smart Array", and "SmartRAID" are trademarks of Hewlett
#   Packard Enterprise. "MegaRAID" and "StorCLI" are trademarks of Broadcom Inc.
#   All trademarks are the property of their respective owners and are used
#   here for identification purposes only.
#
# CONTRIBUTING
#   Contributions are welcome under GPL-2.0-or-later. By submitting a patch you
#   certify the Developer Certificate of Origin (https://developercertificate.org)
#   and sign off your commits:  git commit -s
# ---------------------------------------------------------------------------
#
# Installs the HPE Management Component Pack (Agentless Management Service +
# storage CLIs) on Proxmox VE / Debian, and OPTIONALLY switches HPE storage
# controllers into JBOD / HBA (pass-through) mode for ZFS.
#
# Supported generations & controllers:
#   Gen9  (iLO4) : AMS = hp-ams ; Smart Array P-series / Smart HBA H240/H241 -> ssacli (hbamode)
#   Gen10 (iLO5) : AMS = amsd   ; Smart Array SR P-series (P408i-a, P816i-a ...) -> ssacli (hbamode)
#   Gen11 (iLO6) : AMS = amsd   ; SmartRAID SR (SR416i-a ...) -> ssacli (hbamode)
#                                 MegaRAID  MR (MR216i/MR416i/MR408i ...) -> storcli (set jbod)
#
# JBOD is OPT-IN. A plain run only installs AMS + the CLIs and lists controllers.
# Add --jbod to actually flip controllers to pass-through mode.
#
# WARNING: --jbod is DESTRUCTIVE. Existing logical drives / virtual drives (and
#          their data) are removed or made inaccessible. Back up first. The
#          script refuses to touch a controller that still holds arrays unless
#          you pass --force.
#
# NOTE: You generally cannot boot the OS from a controller that is in HBA/JBOD
#       mode. Keep PVE boot disks on a separate controller/RAID1/M.2, or install
#       PVE fresh with the controller already in HBA mode (ZFS-on-root).
#
# Usage:
#   sudo ./hpe-ams-jbod.sh                 # install AMS + CLIs, list controllers
#   sudo ./hpe-ams-jbod.sh --list          # only detect + list controllers, no install
#   sudo ./hpe-ams-jbod.sh --jbod          # install, then enable JBOD on all controllers (asks first)
#   sudo ./hpe-ams-jbod.sh --jbod --force  # ... even if arrays exist (DESTROYS them)
#   sudo ./hpe-ams-jbod.sh --jbod --controller=0     # only ssacli slot 0
#   sudo ./hpe-ams-jbod.sh --jbod --controller=c0    # only storcli controller /c0
#   sudo ./hpe-ams-jbod.sh --jbod-off      # revert JBOD/HBA back to RAID mode
#   sudo ./hpe-ams-jbod.sh --no-install --jbod       # skip install, only flip mode
#   sudo ./hpe-ams-jbod.sh --ams=none      # skip AMS package (still installs CLIs)
#   sudo ./hpe-ams-jbod.sh --disable-repo  # disable the HPE repo after install (quiets PVE UI on trixie)
#   sudo ./hpe-ams-jbod.sh --yes           # non-interactive (assume "yes")
#
set -euo pipefail

# ------------------------------- options -----------------------------------
SCRIPT_VERSION="1.0.0"
DO_INSTALL=1
DO_JBOD=0            # 0=none, 1=enable, -1=disable
FORCE=0
ASSUME_YES=0
LIST_ONLY=0
AMS_MODE="auto"     # auto | amsd | hp-ams | none
ONLY_CTRL=""        # "" = all ; "0"/"1"... = ssacli slot ; "c0"/"c1"... = storcli controller
DISABLE_REPO=0      # 1 = disable the HPE repo after a successful install

for arg in "$@"; do
  case "$arg" in
    --jbod)         DO_JBOD=1 ;;
    --jbod-off)     DO_JBOD=-1 ;;
    --no-install)   DO_INSTALL=0 ;;
    --list)         LIST_ONLY=1; DO_INSTALL=0 ;;
    --force)        FORCE=1 ;;
    --yes|-y)       ASSUME_YES=1 ;;
    --ams=*)        AMS_MODE="${arg#*=}" ;;
    --controller=*) ONLY_CTRL="${arg#*=}" ;;
    --disable-repo) DISABLE_REPO=1 ;;
    --version|-V)
      echo "hpe-ams-jbod.sh ${SCRIPT_VERSION}"
      echo "Copyright (C) 2026  VMsources Group Inc."
      echo "License GPLv2+: GNU GPL version 2 or later <https://gnu.org/licenses/gpl-2.0.html>."
      echo "This is free software: you are free to change and redistribute it."
      echo "There is NO WARRANTY, to the extent permitted by law."
      exit 0 ;;
    -h|--help)      grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
    *) echo "Unknown option: $arg" >&2; exit 2 ;;
  esac
done

log()  { echo -e "\e[1;34m[*]\e[0m $*"; }
ok()   { echo -e "\e[1;32m[+]\e[0m $*"; }
warn() { echo -e "\e[1;33m[!]\e[0m $*"; }
die()  { echo -e "\e[1;31m[x]\e[0m $*" >&2; exit 1; }

confirm() {
  [ "$ASSUME_YES" -eq 1 ] && return 0
  local reply
  # Read from the controlling terminal explicitly. Without this, a read inside a
  # loop fed by process substitution consumes the loop's stdin (EOF) and the
  # prompt is silently auto-answered "no".
  if [ -r /dev/tty ]; then
    read -r -p "$1 [y/N] " reply </dev/tty || return 1
  else
    warn "No terminal available for confirmation; assuming NO (use --yes to auto-confirm)."
    return 1
  fi
  [[ "$reply" =~ ^[Yy]$ ]]
}

[ "$(id -u)" -eq 0 ] || die "This script must be run as root (use sudo)."

# --------------------------- generation detect -----------------------------
SERVER_MODEL="unknown"
SERVER_GEN=""
detect_generation() {
  if command -v dmidecode >/dev/null 2>&1; then
    SERVER_MODEL="$(dmidecode -s system-product-name 2>/dev/null | head -n1 || true)"
  fi
  SERVER_GEN="$(printf '%s' "$SERVER_MODEL" | grep -oiP 'Gen\K[0-9]+' | head -n1 || true)"
  if [ -n "$SERVER_GEN" ]; then
    ok "Detected server: ${SERVER_MODEL} (Gen${SERVER_GEN})"
  else
    warn "Could not determine HPE generation from '${SERVER_MODEL}'."
  fi
}

pick_ams_package() {
  # Echoes the AMS package name to install, or empty for none.
  case "$AMS_MODE" in
    amsd)   echo "amsd"; return ;;
    hp-ams) echo "hp-ams"; return ;;
    none)   echo ""; return ;;
  esac
  # auto: hp-ams for Gen<=9, amsd for Gen>=10
  if [ -z "$SERVER_GEN" ]; then
    warn "Unknown generation; defaulting AMS to 'amsd' (override with --ams=hp-ams|none)." >&2
    echo "amsd"; return
  fi
  if [ "$SERVER_GEN" -le 9 ]; then echo "hp-ams"; else echo "amsd"; fi
}

# --------------------- 1. Install repo + tools + AMS -----------------------
install_hpe_stack() {
  log "Preparing HPE Management Component Pack repository..."
  # shellcheck disable=SC1091
  . /etc/os-release
  local codename="${VERSION_CODENAME:-}" repo_suite
  case "$codename" in
    trixie)   repo_suite="bookworm"
              warn "Debian 13 (trixie/PVE9) detected; HPE has no trixie suite yet, using bookworm packages." ;;
    bookworm) repo_suite="bookworm" ;;
    bullseye) repo_suite="bullseye" ;;
    buster)   repo_suite="buster" ;;
    "")       repo_suite="bookworm"; warn "No codename detected; defaulting to bookworm." ;;
    *)        repo_suite="bookworm"; warn "Unrecognized codename '$codename'; trying bookworm." ;;
  esac
  ok "HPE MCP suite: ${repo_suite}/current"

  apt-get update -qq
  apt-get install -y -qq curl gnupg ca-certificates dmidecode >/dev/null

  local keyring=/usr/share/keyrings/hpePublicKey2048-archive-keyring.gpg
  install -d -m 0755 /usr/share/keyrings
  : > "${keyring}.tmp"
  for key in hpePublicKey2048_key1.pub hpePublicKey2048_key2.pub; do
    curl -fsSL "https://downloads.linux.hpe.com/SDR/${key}" | gpg --dearmor >> "${keyring}.tmp" \
      || warn "Could not fetch ${key} (continuing)."
  done
  [ -s "${keyring}.tmp" ] || die "Failed to import any HPE signing key."
  mv "${keyring}.tmp" "$keyring"; chmod 0644 "$keyring"

  cat > /etc/apt/sources.list.d/hpe-mcp.list <<EOF
# HPE Management Component Pack (AMS + storage CLIs)
deb [signed-by=${keyring}] https://downloads.linux.hpe.com/SDR/repo/mcp ${repo_suite}/current non-free
EOF
  apt-get update -qq || true

  log "Installing storage CLIs (ssacli, ssaducli, storcli)..."
  # ssacli -> Smart Array / SmartRAID (SR) ; storcli -> MegaRAID (MR)
  apt-get install -y ssacli ssaducli storcli || warn "One or more CLIs failed to install; check repo output."

  # HPE/Broadcom storcli installs to /opt/MegaRAID/storcli/storcli64 and is NOT on
  # PATH. Symlink it so 'storcli ...' works from anywhere.
  if ! command -v storcli >/dev/null 2>&1; then
    local sc; sc="$(dpkg -L storcli 2>/dev/null | grep -E '/storcli64$' | head -n1 || true)"
    if [ -n "$sc" ] && [ -x "$sc" ]; then
      ln -sf "$sc" /usr/local/sbin/storcli && ok "Linked storcli -> ${sc}"
    fi
  fi

  local ams_pkg; ams_pkg="$(pick_ams_package)"
  if [ -n "$ams_pkg" ]; then
    log "Installing Agentless Management Service package: ${ams_pkg}"
    if apt-get install -y "$ams_pkg"; then
      systemctl enable --now "$ams_pkg" 2>/dev/null || systemctl enable --now amsd 2>/dev/null || true
      ok "${ams_pkg} installed. AMS should report OK in iLO > System Information (may take a few minutes)."
    else
      warn "'${ams_pkg}' not available in ${repo_suite}/current."
      if [ "$ams_pkg" = "hp-ams" ]; then
        warn "Gen9/earlier hp-ams is often only in older suites. Browse the pool and dpkg -i the matching .deb:"
        warn "  https://downloads.linux.hpe.com/SDR/repo/mcp/pool/non-free/"
      fi
    fi
  else
    log "Skipping AMS package (--ams=none)."
  fi

  # On PVE 9 / trixie we intentionally use the bookworm suite (no trixie suite from
  # HPE yet). The PVE web UI Repositories panel compares each repo's suite against
  # the running release and will flag this as "some suites are misconfigured".
  # That warning is cosmetic; apt update still works. Two ways to keep it quiet:
  if [ "$repo_suite" != "$codename" ] && [ -n "$codename" ]; then
    warn "Note: the PVE UI will flag the HPE repo (suite '${repo_suite}' on '${codename}')"
    warn "      as 'misconfigured'. This is expected and harmless until HPE ships a"
    warn "      '${codename}' suite. Use --disable-repo to silence it (tools stay installed)."
  fi
  if [ "$DISABLE_REPO" -eq 1 ]; then
    if [ -f /etc/apt/sources.list.d/hpe-mcp.list ]; then
      mv /etc/apt/sources.list.d/hpe-mcp.list /etc/apt/sources.list.d/hpe-mcp.list.disabled
      apt-get update -qq || true
      ok "HPE repo disabled (renamed to hpe-mcp.list.disabled). Re-enable by renaming it back."
    fi
  fi
}

# ============================ ssacli (Smart Array / SR) =====================
sa_present() { command -v ssacli >/dev/null 2>&1; }

sa_slots() {   # prints slot numbers, one per line
  ssacli ctrl all show 2>/dev/null | grep -oiP 'in Slot \K[0-9]+' || true
}
sa_desc() {    # sa_desc <slot> -> controller description line
  ssacli ctrl all show 2>/dev/null | grep -iP "in Slot $1\b" | sed 's/^[[:space:]]*//' | head -n1
}
sa_has_ld() {  # sa_has_ld <slot> -> 0 if logical drives exist
  local out; out="$(ssacli ctrl slot="$1" ld all show 2>&1 || true)"
  ! grep -qi 'does not have any' <<<"$out"
}
sa_show_ld() { ssacli ctrl slot="$1" ld all show 2>&1 || true; }
sa_mode() {    # sa_mode <slot> -> current controller mode string (e.g. Mixed / RAID / HBA)
  ssacli ctrl slot="$1" show detail 2>/dev/null \
    | grep -iP 'Controller Mode' | sed 's/.*:[[:space:]]*//' | head -n1
}

sa_jbod() {    # sa_jbod <slot> <on|off>
  local slot="$1" state="$2" mode
  mode="$(sa_mode "$slot")"
  log "Slot ${slot}: current Controller Mode = ${mode:-unknown}"

  # SR Gen10/Gen11 P/E-class controllers run in "Mixed" mode, where any drive not
  # in a logical drive is ALREADY passed through raw to the OS. In that case there
  # is nothing to do for ZFS (and a full hbamode toggle is refused from this state).
  if [ "$state" = "on" ] && printf '%s' "$mode" | grep -qi 'mixed' && ! sa_has_ld "$slot"; then
    ok "Slot ${slot} is in Mixed mode with no logical drives."
    ok "Unassigned drives are already exposed raw to the OS - ready for ZFS. No change needed."
    warn "Verify with:  lsblk -d -o NAME,SIZE,MODEL   and   ls -l /dev/disk/by-id/"
    return 0
  fi

  log "ssacli: setting hbamode=${state} on slot ${slot} ..."
  if ssacli ctrl slot="$slot" modify hbamode="$state" forced 2>&1; then
    ok "Slot ${slot}: hbamode=${state} accepted. A REBOOT is required to take effect."
  else
    warn "Slot ${slot}: controller did not accept hbamode=${state}. Current config:"
    ssacli ctrl slot="$slot" show config 2>&1 | sed 's/^/    /' || true
    if printf '%s' "$mode" | grep -qi 'mixed'; then
      warn "  Controller is in Mixed mode; unassigned drives already pass through raw."
      warn "  Full HBA mode (cache removed from path) must be set in the offline UEFI"
      warn "  Smart Storage configuration, not via ssacli. For ZFS, Mixed mode is fine."
    else
      warn "  Some legacy cards (P410i/P420i) never support HBA mode; use an IT-mode HBA."
    fi
  fi
}

# ============================ storcli (MegaRAID / MR) =======================
MR_BIN=""
mr_find() {
  local b
  for b in storcli storcli64 /usr/local/sbin/storcli /opt/MegaRAID/storcli/storcli64 /usr/sbin/storcli /usr/bin/storcli; do
    if command -v "$b" >/dev/null 2>&1; then MR_BIN="$(command -v "$b")"; return 0; fi
    [ -x "$b" ] && { MR_BIN="$b"; return 0; }
  done
  # Fallback: ask dpkg where the storcli package put its binary.
  if command -v dpkg >/dev/null 2>&1; then
    local p; p="$(dpkg -L storcli 2>/dev/null | grep -E '/storcli(64)?$' | head -n1 || true)"
    [ -n "$p" ] && [ -x "$p" ] && { MR_BIN="$p"; return 0; }
  fi
  return 1
}
mr_count() {   # number of MegaRAID controllers
  "$MR_BIN" show ctrlcount 2>/dev/null | grep -oiP 'Controller Count = \K[0-9]+' | head -n1 || echo 0
}
mr_desc() { "$MR_BIN" /c"$1" show 2>/dev/null | grep -iP 'Product Name' | sed 's/^[[:space:]]*//' | head -n1; }
mr_has_vd() {  # 0 if virtual drives exist on /c<idx>
  local out; out="$("$MR_BIN" /c"$1"/vall show 2>&1 || true)"
  grep -qiE '^[0-9]+/[0-9]+ ' <<<"$out" || grep -qi 'VD LIST' <<<"$out"
}
mr_show_vd() { "$MR_BIN" /c"$1"/vall show 2>&1 || true; }

mr_jbod() {    # mr_jbod <idx> <on|off>
  local c="$1" state="$2"
  if [ "$state" = "on" ]; then
    log "storcli: enabling JBOD personality on /c${c} ..."
    "$MR_BIN" /c"$c" set jbod=on 2>&1 || warn "/c${c}: could not set jbod=on (may already be enabled)."
    log "storcli: marking all unconfigured drives on /c${c} as JBOD ..."
    "$MR_BIN" /c"$c"/eall/sall set jbod 2>&1 \
      || warn "/c${c}: some drives not converted (must be Unconfigured-Good; delete VDs first)."
    ok "/c${c}: JBOD enabled. Drives appear as raw /dev/sd* (a reboot may be needed)."
  else
    log "storcli: reverting JBOD on /c${c} ..."
    "$MR_BIN" /c"$c"/eall/sall set good force 2>&1 || true
    "$MR_BIN" /c"$c" set jbod=off 2>&1 || warn "/c${c}: could not set jbod=off."
    ok "/c${c}: JBOD disabled (drives returned to Unconfigured-Good)."
  fi
}

# ------------------------------ enumerate ----------------------------------
list_controllers() {
  local found=0
  if sa_present; then
    local s
    while read -r s; do
      [ -n "$s" ] || continue
      found=1
      echo "  [ssacli]  slot=${s}   $(sa_desc "$s")   [mode: $(sa_mode "$s")]"
    done < <(sa_slots)
  fi
  if mr_find; then
    local n i; n="$(mr_count)"
    for ((i=0; i<n; i++)); do
      found=1
      echo "  [storcli] /c${i}       $(mr_desc "$i")"
    done
  fi
  [ "$found" -eq 1 ] || warn "No HPE Smart Array/SR or MR controllers detected."
}

# --------------------------- JBOD orchestration ----------------------------
want_sa_slot() { [ -z "$ONLY_CTRL" ] || [ "$ONLY_CTRL" = "$1" ]; }
want_mr_idx()  { [ -z "$ONLY_CTRL" ] || [ "$ONLY_CTRL" = "c$1" ]; }

do_jbod() {
  local enable="$1"   # 1=on, -1=off
  local state="on"; [ "$enable" -eq -1 ] && state="off"
  local acted=0

  # --- Smart Array / SR via ssacli ---
  if sa_present; then
    local s; local -a sa_list=()
    mapfile -t sa_list < <(sa_slots)
    for s in "${sa_list[@]}"; do
      [ -n "$s" ] || continue
      want_sa_slot "$s" || continue
      acted=1
      echo; ok "ssacli controller: slot=${s}  $(sa_desc "$s")"
      if [ "$state" = "on" ] && sa_has_ld "$s"; then
        warn "Slot ${s} has existing logical drives:"; sa_show_ld "$s"
        warn "Enabling HBA mode makes these arrays and their DATA inaccessible."
        [ "$FORCE" -eq 1 ] || { warn "Skipping slot ${s} (re-run with --force to proceed)."; continue; }
        confirm "Destroy the above arrays and enable HBA on slot ${s}?" || { warn "Skipped slot ${s}."; continue; }
        log "Deleting all logical drives on slot ${s} ..."
        ssacli ctrl slot="$s" ld all delete forced 2>&1 | sed 's/^/    /' \
          || warn "Slot ${s}: logical-drive delete reported an issue."
      elif [ "$state" = "on" ]; then
        [ "$FORCE" -eq 1 ] || confirm "Enable HBA (JBOD) mode on slot ${s}?" || { warn "Skipped slot ${s}."; continue; }
      fi
      sa_jbod "$s" "$state"
    done
  fi

  # --- MegaRAID / MR via storcli ---
  if mr_find; then
    local n i; n="$(mr_count)"
    for ((i=0; i<n; i++)); do
      want_mr_idx "$i" || continue
      acted=1
      echo; ok "storcli controller: /c${i}  $(mr_desc "$i")"
      if [ "$state" = "on" ] && mr_has_vd "$i"; then
        warn "/c${i} has existing virtual drives:"; mr_show_vd "$i"
        warn "Converting to JBOD requires the drives to be unconfigured; VDs would be deleted."
        [ "$FORCE" -eq 1 ] || { warn "Skipping /c${i} (re-run with --force to delete VDs first)."; continue; }
        confirm "Delete all VDs on /c${i} and enable JBOD?" || { warn "Skipped /c${i}."; continue; }
        "$MR_BIN" /c"$i"/vall delete force 2>&1 || warn "/c${i}: VD delete reported an issue."
      elif [ "$state" = "on" ]; then
        [ "$FORCE" -eq 1 ] || confirm "Enable JBOD mode on /c${i}?" || { warn "Skipped /c${i}."; continue; }
      fi
      mr_jbod "$i" "$state"
    done
  fi

  [ "$acted" -eq 1 ] || warn "No matching controllers acted on (check --controller / detection)."
}

# ------------------------------- main --------------------------------------
detect_generation
[ "$DO_INSTALL" -eq 1 ] && install_hpe_stack

echo; log "Detected storage controllers:"
list_controllers
[ "$LIST_ONLY" -eq 1 ] && { echo; ok "List complete."; exit 0; }

if [ "$DO_JBOD" -ne 0 ]; then
  echo
  if [ "$DO_JBOD" -eq 1 ]; then
    warn "About to enable JBOD/HBA (pass-through) mode. This is destructive to existing arrays."
  else
    warn "About to DISABLE JBOD/HBA and return controllers to RAID mode."
  fi
  do_jbod "$DO_JBOD"
  echo; warn "Reboot the server so controller mode changes take effect, then verify:"
  warn "  ssacli ctrl slot=<N> show | grep -i 'HBA Mode'"
  if mr_find; then
    warn "  ${MR_BIN} /cx show | grep -iE 'JBOD|Personality'"
  else
    warn "  storcli /cx show | grep -i 'JBOD'   (storcli binary: /opt/MegaRAID/storcli/storcli64)"
  fi
else
  echo; ok "Install/list complete. Re-run with --jbod to switch controllers to pass-through mode."
fi

ok "Done."
