#!/bin/bash
#
# rbln-pciehp-hook.sh -- keep pciehp off the PCIe downstream ports that host
# Rebellions NPUs.
#
# Why this exists
# ---------------
# On a downstream port that hosts an NPU, pciehp turns a transient link event
# (DLLSC/PDC) into a device removal: a link that would have retrained takes the
# card out of the kernel mid-workload instead, and the teardown that follows
# drags in an IOMMU invalidation storm and a full re-enumeration.  Unbinding
# only the hotplug port service leaves AER/DPC/PME on that same port alive,
# which the global `pcie_ports=compat` cmdline switch would not.
#
# On a Broadcom PEX89xxx in synthetic mode the host-facing downstream port is
# emulated, and the switch only reports the link change to a host that has
# hotplug notification enabled.  Unbinding pciehp clears those enables, so the
# switch stops presenting the transition altogether.
#
# Ordering against the driver
# ---------------------------
# The udev rule alone cannot beat the driver: `80-drivers.rules` sorts before
# `99-`, the module load is synchronous inside the udev worker, and this rule
# defers its work to a systemd unit.  So the driver's own probe-time reset happens
# while pciehp is still bound.  install therefore also lays down a modprobe
# `install` line by default, which is honoured by every load path -- the
# initramfs (systemd-modules-load or udev modalias), udev in the real root, a
# manual modprobe, and the runfile installer's `modprobe rebellions`.
# `--no-initramfs` opts out and leaves that window open.
#
# The unbind path deliberately uses bash builtins only (no basename/dirname/
# readlink/cat/id), because an initramfs carries none of those reliably.
#
# What it does NOT cover
# ----------------------
# Concurrent resets of several NPUs behind the same switch upstream port remain
# the underlying trigger.  This hook hides the symptom from the host; it does
# not make the switch re-assign endpoints any faster.  Serialising resets per
# switch upstream port is the complementary fix and belongs in the driver.
#
# Hosts whose configuration is managed for them
# ---------------------------------------------
# Do not run install/uninstall on a host that reconciles /etc and /usr/local
# against a declared desired state -- RHCOS under the Machine Config Operator
# is the case at hand.  Creating or deleting a managed file trips its drift
# detector and the node is marked degraded.  Ship the same three files there
# through that mechanism instead: the helper, the udev rule and the unit, with
# RequiresMountsFor= spelled out (install resolves it at run time, a static
# config cannot).
#
# Tested on RHEL 10.2.

set -uo pipefail

VENDOR="0x1eff"
MODULE="rebellions"
TAG="rbln-pciehp-hook"

INSTALL_PATH="/usr/local/sbin/rbln-pciehp-hook.sh"
UDEV_RULE="/etc/udev/rules.d/99-rbln-pciehp.rules"
UNIT_FILE="/etc/systemd/system/rbln-pciehp-unbind@.service"
MODPROBE_CONF="/etc/modprobe.d/rbln-pciehp.conf"
DRACUT_CONF="/etc/dracut.conf.d/rbln-pciehp.conf"
INITRAMFS_HOOK="/etc/initramfs-tools/hooks/rbln-pciehp"

# RBLN_PCIEHP_SYSROOT relocates every sysfs path onto a fake tree so the walk
# can be exercised without touching real hardware.  Production leaves it unset.
SYSROOT="${RBLN_PCIEHP_SYSROOT:-}"
DRV_DIR="${SYSROOT}/sys/bus/pci_express/drivers/pciehp"
SVC_DIR="${SYSROOT}/sys/bus/pci_express/devices"
PCI_DIR="${SYSROOT}/sys/bus/pci/devices"
LOCK="${SYSROOT}/run/lock/rbln-pciehp-hook.lock"

log() {
  logger -t "${TAG}" -- "$*" 2>/dev/null || true
  echo "${TAG}: $*" >&2
}

die() {
  log "$*"
  exit 1
}

require_root() {
  [ "${EUID:-1}" -eq 0 ] || die "must run as root"
}

# A file freshly created under /usr/local/sbin carries a label that blocks
# execution from udev on SELinux-enforcing hosts.  The rule and the unit need
# it for the same reason: install may run from a context whose default for
# /etc is not the one udev and systemd expect, which is exactly what a
# `chroot /host` from a debug container gives you.
relabel() {
  command -v restorecon >/dev/null 2>&1 || return 0
  restorecon -F "$@" >/dev/null 2>&1 || true
}

# ---------------------------------------------------------------------------
# Port discovery -- builtins only, so this also runs inside an initramfs
# ---------------------------------------------------------------------------

# Echo the distinct downstream ports hosting a Rebellions endpoint.  With no
# argument every endpoint on the machine is considered; with BDFs, only those
# (the path udev takes, which passes the endpoint that just appeared).
collect_ports() {
  local candidates=() ep bdf vendor real parent ports=""

  if [ "$#" -eq 0 ]; then
    candidates=("${PCI_DIR}"/*)
  else
    for bdf in "$@"; do
      candidates+=("${PCI_DIR}/${bdf}")
    done
  fi

  for ep in "${candidates[@]}"; do
    [ -r "${ep}/vendor" ] || continue
    read -r vendor < "${ep}/vendor" || continue
    [ "${vendor}" = "${VENDOR}" ] || continue

    # `cd -P` resolves the sysfs symlink with no external readlink; the parent
    # of the physical path is the downstream port hosting this endpoint.
    real=$(cd -P "${ep}" 2>/dev/null && pwd -P) || continue
    [ -n "${real}" ] || continue
    parent="${real%/*}"
    parent="${parent##*/}"

    # A root-complex-integrated endpoint's parent reads as "pci0000:00" rather
    # than a BDF, and has no switch port underneath it to act on.
    case "${parent}" in
      [0-9a-f][0-9a-f][0-9a-f][0-9a-f]:[0-9a-f][0-9a-f]:[0-9a-f][0-9a-f].[0-9a-f]) ;;
      *) continue ;;
    esac

    # Multi-function cards land on the same parent; act on each port once.
    case " ${ports} " in
      *" ${parent} "*) ;;
      *) ports="${ports} ${parent}" ;;
    esac
  done

  echo "${ports# }"
}

# ---------------------------------------------------------------------------
# unbind / rebind
# ---------------------------------------------------------------------------

act_on_ports() {
  local action="$1"; shift
  local ports port services=() svc name matched rc=0
  local acted=0 quiet=0 detail=() d

  if [ ! -d "${DRV_DIR}" ]; then
    log "pciehp is not present on this kernel; nothing to do"
    return 0
  fi

  ports=$(collect_ports "$@")
  if [ -z "${ports}" ]; then
    log "no Rebellions endpoint sits behind a PCIe downstream port"
    return 0
  fi

  # Endpoints under one port race on the same sysfs write and the loser sees
  # ENODEV.  flock is util-linux; the guard keeps the script usable where it is
  # absent, which includes some initramfs images.
  if command -v flock >/dev/null 2>&1; then
    mkdir -p "${LOCK%/*}" 2>/dev/null || true
    exec 9>"${LOCK}"
    flock 9
  fi

  for port in ${ports}; do
    if [ "${action}" = "unbind" ]; then
      services=("${DRV_DIR}/${port}:pcie"*)
    else
      # The driver link is gone after an unbind, so go by service-device name.
      # The low byte of the descriptor id is the port-service bit, and
      # PCIE_PORT_SERVICE_HP is 4 (PME=1, AER=2, DPC=8), so hotplug is the
      # entry ending in 04 whatever the port type puts in the high byte:
      # 004 on a root port, 204 on a downstream port.
      services=("${SVC_DIR}/${port}:pcie"*04)
    fi

    matched=0
    for svc in "${services[@]}"; do
      [ -e "${svc}" ] || continue
      matched=1
      name="${svc##*/}"
      if [ "${action}" = "bind" ] && [ -e "${DRV_DIR}/${name}" ]; then
        quiet=$((quiet + 1))
        continue
      fi
      if echo "${name}" > "${DRV_DIR}/${action}" 2>/dev/null; then
        acted=$((acted + 1))
        detail+=("${action}: ${name}")
      else
        detail+=("failed to ${action} ${name}")
        rc=1
      fi
    done
    [ "${matched}" -eq 1 ] || quiet=$((quiet + 1))
  done

  # A no-op run gets one line, not one per port.  udev fires this once per
  # endpoint and modprobe can re-run the install line just as often, so a
  # 32-card host would otherwise write a thousand no-op lines every boot.
  if [ "${acted}" -eq 0 ] && [ "${rc}" -eq 0 ]; then
    log "nothing to ${action}: ${quiet} port(s) already in the desired state"
  else
    for d in ${detail[@]+"${detail[@]}"}; do
      log "${d}"
    done
  fi

  return ${rc}
}

# ---------------------------------------------------------------------------
# initramfs integration
# ---------------------------------------------------------------------------

detect_initramfs_tool() {
  if command -v dracut >/dev/null 2>&1; then
    echo dracut
  elif command -v update-initramfs >/dev/null 2>&1; then
    echo initramfs-tools
  else
    echo none
  fi
}

write_modprobe_conf() {
  cat > "${MODPROBE_CONF}" <<RBLN_MODPROBE_EOF
# Managed by ${TAG}; reinstalling overwrites this file.
#
# Run the unbind before the module is inserted, whichever path loads it.  The
# [ -x ] guard is load-bearing: without it a stale conf whose helper has gone
# missing would make the module -- and therefore every NPU -- unloadable.
#
# The /sys/module test keeps this to one run: udev calls modprobe once per
# endpoint, and modprobe executes an install line every time even for a module
# that is already up, so a 32-card host would otherwise fork the helper 32
# times per boot.  Concurrent callers may still race through it; the unbind is
# idempotent and flock-serialised, so that is harmless.
#
# Output is deliberately NOT discarded.  In an initramfs there is no logger, so
# stderr is the only channel this run has, and silencing it leaves the one
# invocation that actually matters -- the one before the driver loads -- with
# no trace in the journal at all.
install ${MODULE} if [ ! -d /sys/module/${MODULE} ] && [ -x ${INSTALL_PATH} ]; then ${INSTALL_PATH} unbind; fi; /sbin/modprobe --ignore-install ${MODULE} \$CMDLINE_OPTS
RBLN_MODPROBE_EOF
  chmod 0644 "${MODPROBE_CONF}"
}

# Confirm modprobe actually picked the install line up, before anything is
# baked into an initramfs where a mistake is far harder to undo.
verify_modprobe_line() {
  local out rc
  command -v modprobe >/dev/null 2>&1 || {
    log "modprobe not found; skipping pre-flight check"
    return 0
  }
  # --show-depends resolves the install command whether or not the module is
  # already loaded.  `-n -v` prints nothing at all for a module that is up,
  # which is the normal state on a running host and made it useless here.
  out=$(modprobe --show-depends "${MODULE}" 2>&1)
  rc=$?
  if [ "${rc}" -ne 0 ]; then
    case "${out}" in
      *"not found"*|*"not currently installed"*)
        log "pre-flight: ${MODULE} not installed yet; install line written but unverified"
        return 0
        ;;
    esac
    log "pre-flight FAILED: modprobe --show-depends ${MODULE} exited ${rc}"
    printf '%s\n' "${out}" >&2
    return 1
  fi
  case "${out}" in
    *"${INSTALL_PATH}"*)
      log "pre-flight ok: modprobe ${MODULE} would run the hook first"
      return 0
      ;;
  esac
  log "pre-flight FAILED: modprobe did not pick up the install line"
  printf '%s\n' "${out}" >&2
  return 1
}

# The initramfs only needs the helper if the driver itself is baked in; when it
# is not (Ubuntu ships no out-of-tree DKMS module there), the module loads in
# the real root and the modprobe install line alone already orders it.  Ubuntu's
# initramfs also carries busybox sh rather than bash, so shipping this script
# there would not even run.
# The listing is matched with a case glob rather than `| grep -q`: under
# `set -o pipefail` grep's early exit SIGPIPEs the lister, and the pipeline
# then reports failure even on a match.
initramfs_contains() {
  local needle="$1" img listing
  for img in "/boot/initramfs-$(uname -r).img" "/boot/initrd.img-$(uname -r)"; do
    [ -r "${img}" ] || continue
    case "$(detect_initramfs_tool)" in
      dracut)          listing=$(lsinitrd "${img}" 2>/dev/null) ;;
      initramfs-tools) listing=$(lsinitramfs "${img}" 2>/dev/null) ;;
      *) return 1 ;;
    esac
    case "${listing}" in
      *"${needle}"*) return 0 ;;
    esac
    return 1
  done
  return 1
}

module_in_initramfs() {
  initramfs_contains "/${MODULE}.ko"
}

# Matched on the basename, not on the install path: where /usr/local is a
# symlink the generator resolves it, and the helper then lands under the
# link target rather than under the path it was installed to.
helper_in_initramfs() {
  initramfs_contains "${INSTALL_PATH##*/}"
}

write_initramfs_glue() {
  case "$(detect_initramfs_tool)" in
    dracut)
      mkdir -p "${DRACUT_CONF%/*}" || return 1
      cat > "${DRACUT_CONF}" <<RBLN_DRACUT_EOF
# Managed by ${TAG}; /etc/modprobe.d is pulled in automatically, the helper is not.
install_items+=" ${INSTALL_PATH} "
RBLN_DRACUT_EOF
      chmod 0644 "${DRACUT_CONF}"
      log "wrote ${DRACUT_CONF}"
      ;;
    initramfs-tools)
      mkdir -p "${INITRAMFS_HOOK%/*}" || return 1
      cat > "${INITRAMFS_HOOK}" <<RBLN_HOOK_EOF
#!/bin/sh
# Managed by ${TAG}
[ "\$1" = prereqs ] && { echo; exit 0; }
# Stay quiet if the helper is gone, so a stale hook cannot fail an image build.
[ -x ${INSTALL_PATH} ] || exit 0
. /usr/share/initramfs-tools/hook-functions
# copy_exec's second argument is the destination *path*, not a directory: it is
# only treated as one when it already exists inside DESTDIR.  Passing the
# directory landed the script at usr/local/sbin as a file.  Omitting it keeps
# the source path, which is what the modprobe install line looks for.
copy_exec ${INSTALL_PATH}
RBLN_HOOK_EOF
      chmod 0755 "${INITRAMFS_HOOK}"
      log "wrote ${INITRAMFS_HOOK}"
      ;;
    *)
      log "no supported initramfs tool found (dracut / update-initramfs)"
      return 1
      ;;
  esac
}

# The image inspected above is the running kernel's, so the rebuild has to name
# that same kernel.  Bare `update-initramfs -u` regenerates the newest kernel's
# image instead, which on a host that has not rebooted into its latest kernel is
# a different file than the one that was just checked.
rebuild_initramfs() {
  local kver
  kver=$(uname -r)
  case "$(detect_initramfs_tool)" in
    dracut)
      log "rebuilding initramfs: dracut -f --kver ${kver}"
      dracut -f --kver "${kver}"
      ;;
    initramfs-tools)
      log "rebuilding initramfs: update-initramfs -u -k ${kver}"
      update-initramfs -u -k "${kver}"
      ;;
    *)
      return 1
      ;;
  esac
}

remove_initramfs_glue() {
  local touched=0
  for f in "${MODPROBE_CONF}" "${DRACUT_CONF}" "${INITRAMFS_HOOK}"; do
    if [ -e "${f}" ]; then
      rm -f "${f}" && log "removed ${f}"
      touched=1
    fi
  done
  return $(( 1 - touched ))
}

# ---------------------------------------------------------------------------
# install / uninstall
# ---------------------------------------------------------------------------

cmd_install() {
  local with_initramfs=1

  while [ "$#" -gt 0 ]; do
    case "$1" in
      --with-initramfs) with_initramfs=1 ;;
      --no-initramfs)   with_initramfs=0 ;;
      *) die "unknown install option: $1" ;;
    esac
    shift
  done

  require_root

  local self src="${BASH_SOURCE[0]:-$0}" selfdir
  selfdir="${src%/*}"
  [ "${selfdir}" = "${src}" ] && selfdir="."
  self=$(cd -P "${selfdir}" 2>/dev/null && pwd -P)/${src##*/}
  [ -r "${self}" ] || die "cannot locate this script to install"

  command -v systemctl >/dev/null 2>&1 || die "systemctl not found; the udev hook needs systemd"

  mkdir -p "${INSTALL_PATH%/*}" "${UDEV_RULE%/*}" "${UNIT_FILE%/*}" \
    || die "failed to create target directories"

  if [ "${self}" -ef "${INSTALL_PATH}" ]; then
    log "already running from ${INSTALL_PATH}; keeping it"
  else
    install -m 0755 "${self}" "${INSTALL_PATH}" || die "failed to install ${INSTALL_PATH}"
    log "installed ${INSTALL_PATH}"
  fi

  relabel "${INSTALL_PATH}"

  # The unit needs the helper's *resolved* directory: on an ostree host
  # /usr/local is a symlink into /var, and RequiresMountsFor= does not chase
  # symlinks, so passing the literal path would miss the var.mount dependency.
  local realdir
  realdir=$(cd -P "${INSTALL_PATH%/*}" 2>/dev/null && pwd -P) || realdir="${INSTALL_PATH%/*}"

  cat > "${UNIT_FILE}" <<RBLN_UNIT_EOF
# Managed by ${TAG}; reinstalling overwrites this file.
[Unit]
Description=Unbind pciehp from the PCIe downstream port hosting %I
DefaultDependencies=no
After=systemd-udevd.service
# udev coldplug for PCI runs before local-fs.target.  On an ostree host the
# helper lives under /var, so without this the unit would fail to exec and the
# boot-time unbind would silently never happen.
RequiresMountsFor=${realdir}
Conflicts=shutdown.target
Before=shutdown.target

[Service]
Type=oneshot
ExecStart=${INSTALL_PATH} unbind %I
RBLN_UNIT_EOF
  chmod 0644 "${UNIT_FILE}"
  log "installed ${UNIT_FILE}"

  cat > "${UDEV_RULE}" <<RBLN_RULE_EOF
# Managed by ${TAG}; reinstalling overwrites this file.
#
# Matched on the endpoint, not on its downstream port: udev cannot test a
# device's children, and the port is enumerated before the endpoint anyway.
#
# The work is handed to a systemd unit rather than run here: the unbind emits
# pci_express remove uevents, and handling those from inside a udev worker
# deadlocks against the event queue.  SYSTEMD_WANTS is used in place of
# \`systemd-run\` so systemd owns the ordering -- notably the mount the helper
# lives on, which udev otherwise beats to the punch.
ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="${VENDOR}", TAG+="systemd", ENV{SYSTEMD_WANTS}+="rbln-pciehp-unbind@\$kernel.service"
RBLN_RULE_EOF
  chmod 0644 "${UDEV_RULE}"
  log "installed ${UDEV_RULE}"

  systemctl daemon-reload >/dev/null 2>&1 || log "systemctl daemon-reload failed"

  relabel "${UNIT_FILE}" "${UDEV_RULE}"

  if ! udevadm control --reload-rules >/dev/null 2>&1; then
    log "udevadm reload failed; the rule takes effect from the next boot"
  fi

  if [ "${with_initramfs}" -eq 1 ]; then
    write_modprobe_conf || die "failed to write ${MODPROBE_CONF}"
    log "installed ${MODPROBE_CONF}"
    if ! verify_modprobe_line; then
      rm -f "${MODPROBE_CONF}"
      die "rolled back ${MODPROBE_CONF}; initramfs left untouched"
    fi
    # The glue is written unconditionally, even with no driver installed yet:
    # whatever regeneration later puts the module into the initramfs reads the
    # same config and picks the helper up in the same pass, so installing this
    # before the driver needs no second run afterwards.  Only the immediate
    # rebuild is conditional -- it would change nothing while the module is not
    # in the image.
    #
    # The install line already covers every load path in the real root, so a
    # host with no initramfs generator is degraded rather than broken: only the
    # initramfs load path stays unprotected.
    if write_initramfs_glue; then
      if module_in_initramfs; then
        rebuild_initramfs || die "initramfs rebuild failed"
      else
        log "${MODULE} is not in the initramfs; skipping the rebuild"
        log "(the glue is in place for whenever the image is next regenerated)"
      fi
    else
      log "WARNING: probe-time reset inside the initramfs is NOT covered"
      log "         (${MODPROBE_CONF} still covers the real-root load paths)"
    fi
  else
    log "--no-initramfs: the driver's probe-time reset is NOT covered"
  fi

  # Apply to the running boot so no reboot is needed; the rule covers every
  # boot and re-enumeration after this one.
  act_on_ports unbind
  local rc=$?

  echo
  cmd_status
  return ${rc}
}

cmd_uninstall() {
  require_root

  local had_initramfs=0
  remove_initramfs_glue && had_initramfs=1

  if [ -e "${UDEV_RULE}" ]; then
    rm -f "${UDEV_RULE}" && log "removed ${UDEV_RULE}"
    udevadm control --reload-rules >/dev/null 2>&1 || true
  fi

  if [ -e "${UNIT_FILE}" ]; then
    rm -f "${UNIT_FILE}" && log "removed ${UNIT_FILE}"
    systemctl daemon-reload >/dev/null 2>&1 || true
  fi

  # Rebind before dropping anything else: this is what knows which ports were
  # taken from pciehp in the first place.
  act_on_ports bind
  local rc=$?

  if [ -e "${INSTALL_PATH}" ]; then
    rm -f "${INSTALL_PATH}" && log "removed ${INSTALL_PATH}"
  fi
  rm -f "${LOCK}" 2>/dev/null || true

  # install only rebuilds when the module is in the image, so gating on the glue
  # alone would regenerate an image that install never touched.  The accurate
  # test is whether the helper actually made it in: the dracut/initramfs-tools
  # glue copies it on any regeneration, module or not.
  if [ "${had_initramfs}" -eq 1 ] && helper_in_initramfs; then
    rebuild_initramfs || log "initramfs rebuild failed; rerun it manually before rebooting"
  elif [ "${had_initramfs}" -eq 1 ]; then
    log "the helper is not in the initramfs; skipping the rebuild"
  fi

  echo
  cmd_status
  return ${rc}
}

# ---------------------------------------------------------------------------
# status
# ---------------------------------------------------------------------------

cmd_status() {
  local ports port ep e v real bound svc tool total=0 unbound=0

  echo "host:      $(uname -n)"
  if [ -r /etc/os-release ]; then
    # shellcheck disable=SC1091
    echo "os:        $(. /etc/os-release && echo "${PRETTY_NAME}")  kernel $(uname -r)"
  fi

  if [ -d "${DRV_DIR}" ]; then
    echo "pciehp:    present"
  else
    echo "pciehp:    ABSENT (CONFIG_HOTPLUG_PCI_PCIE off or module not loaded)"
  fi

  [ -e "${INSTALL_PATH}" ]  && echo "script:    ${INSTALL_PATH}"  || echo "script:    not installed"
  [ -e "${UDEV_RULE}" ]     && echo "udev rule: ${UDEV_RULE}"     || echo "udev rule: not installed"
  [ -e "${UNIT_FILE}" ]     && echo "unit:      ${UNIT_FILE}"     || echo "unit:      not installed"
  [ -e "${MODPROBE_CONF}" ] && echo "modprobe:  ${MODPROBE_CONF}" || echo "modprobe:  not installed"

  tool=$(detect_initramfs_tool)
  case "${tool}" in
    dracut)          [ -e "${DRACUT_CONF}" ]    && echo "initramfs: ${DRACUT_CONF} (dracut)"           || echo "initramfs: not integrated (dracut available)" ;;
    initramfs-tools) [ -e "${INITRAMFS_HOOK}" ] && echo "initramfs: ${INITRAMFS_HOOK} (initramfs-tools)" || echo "initramfs: not integrated (initramfs-tools available)" ;;
    *)               echo "initramfs: no supported tool" ;;
  esac

  ports=$(collect_ports)
  if [ -z "${ports}" ]; then
    echo "ports:     no Rebellions endpoint behind a downstream port"
    return 0
  fi

  echo "ports:"
  for port in ${ports}; do
    total=$((total + 1))
    if [ -d "${DRV_DIR}" ]; then
      bound="unbound"
      for svc in "${DRV_DIR}/${port}:pcie"*; do
        [ -e "${svc}" ] && { bound="BOUND"; break; }
      done
      [ "${bound}" = "unbound" ] && unbound=$((unbound + 1))
    else
      bound="n/a"
    fi
    ep=""
    for e in "${PCI_DIR}"/*; do
      [ -r "${e}/vendor" ] || continue
      read -r v < "${e}/vendor" || continue
      [ "${v}" = "${VENDOR}" ] || continue
      real=$(cd -P "${e}" 2>/dev/null && pwd -P) || continue
      real="${real%/*}"
      [ "${real##*/}" = "${port}" ] && ep="${ep}${e##*/} "
    done
    printf "  %-14s %-8s  ep: %s\n" "${port}" "${bound}" "${ep% }"
  done

  if [ -d "${DRV_DIR}" ]; then
    echo "summary:   ${unbound}/${total} port(s) unbound from pciehp"
  else
    echo "summary:   pciehp absent -- nothing to unbind, and nothing at risk"
  fi
}

usage() {
  cat <<'RBLN_USAGE_EOF'
rbln-pciehp-hook.sh -- keep pciehp off the downstream ports hosting Rebellions NPUs

  install [--no-initramfs]   deploy + apply now (no reboot needed)
  uninstall                  hand the ports back to pciehp and remove everything
  status                     show what is deployed and which ports are bound
  unbind [BDF...]            one-shot, no deployment
  rebind [BDF...]            one-shot, no deployment

install deploys, by default:
  /usr/local/sbin/rbln-pciehp-hook.sh
  /etc/udev/rules.d/99-rbln-pciehp.rules   every boot and re-enumeration
  /etc/systemd/system/rbln-pciehp-unbind@.service   what the rule starts
  /etc/modprobe.d/rbln-pciehp.conf         install line, ordered before insmod
  /etc/dracut.conf.d/rbln-pciehp.conf      (RHEL)   ships the helper in the initramfs
  /etc/initramfs-tools/hooks/rbln-pciehp   (Ubuntu) same, via copy_exec
and rebuilds the initramfs when the driver is already in it. The install line
is checked with `modprobe --show-depends` first; if modprobe does not pick it
up, the conf is rolled back and the initramfs is left untouched.

Installing before the driver exists is fine and is in fact the better order:
the modprobe install line then covers the very first load. The initramfs glue
is written even with no driver present, so the regeneration that later puts
the module into the image picks the helper up in the same pass -- no second
run of this script is needed.

--no-initramfs stops after the udev rule. That still covers every boot and
re-enumeration, but NOT the driver's own probe-time reset: udev loads the
module before the rule's (asynchronous) unbind lands.

WARNING: do not run this on a host whose /etc and /usr/local are reconciled
against a declared state (RHCOS under the Machine Config Operator). Deploy the
helper, the udev rule and the unit through that mechanism instead.

WARNING: the default install changes how the driver is loaded. Try it on one
host with console/BMC access first, and keep a copy of the current initramfs.
Recovery if the module will not load: rm /etc/modprobe.d/rbln-pciehp.conf, or
boot and run `modprobe --ignore-install rebellions`.
RBLN_USAGE_EOF
}

# ---------------------------------------------------------------------------

case "${1:-}" in
  install)   shift; cmd_install "$@" ;;
  uninstall) shift; cmd_uninstall "$@" ;;
  status)    shift; cmd_status "$@" ;;
  unbind)    shift; require_root; act_on_ports unbind "$@" ;;
  rebind)    shift; require_root; act_on_ports bind "$@" ;;
  -h|--help|help|"") usage ;;
  *) echo "unknown command: $1" >&2; echo >&2; usage >&2; exit 2 ;;
esac
