#! /bin/sh

# Copyright (c) 2023-2025 Slawomir Wojciech Wojtczak (vermaden)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that following conditions are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS 'AS IS' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

# ------------------------------
# LIST AND MGMT FreeBSD jail(8)
# ------------------------------
# vermaden [AT] interia [DOT] pl
# https://vermaden.wordpress.com

# DISPLAY USAGE INFORMATION
__usage() {
  local NAME=${0##*/}
  echo "usage:"
  echo "  ${NAME}              list jail(8) containers"
  echo "  ${NAME} -a           list jail(8) containers with all IP(s)"
  echo "  ${NAME} -d INTERVAL  run '${NAME}' every INTERVAL seconds like top(1)"
  echo "  ${NAME} -h           show help"
  echo "  ${NAME} --help       show help"
  echo "  ${NAME} help         show help"
  echo "  ${NAME} version      show version"
  echo
  echo "manage:"
  echo "  ${NAME} <JAILNAME> start"
  echo "  ${NAME} <JAILNAME> restart"
  echo "  ${NAME} <JAILNAME> stop"
  echo "  ${NAME} <JAILNAME> status"
  echo "  ${NAME} <JAILNAME> console"
  echo "  ${NAME} <JAILNAME> shell"
  echo "  ${NAME} <JAILNAME> jexec"
  echo
  echo "shorts:"
  echo "  ${NAME} <JAILNAME> u   UP ------> alias for start"
  echo "  ${NAME} <JAILNAME> d   DOWN ----> alias for stop"
  echo "  ${NAME} <JAILNAME> r   RESTART -> alias for restart"
  echo "  ${NAME} <JAILNAME> c   CONSOLE -> alias for console|shell|jexec"
  echo "  ${NAME} <JAILNAME> s   STATUS --> alias for status"
  echo

  exit 1
}

# DISPLAY ONLY 3 FIRST INTERFACES
__interfaces_limit() {
  if [ $( echo "${IFACE}" | wc -l | tr -d ' ' ) -gt 3 ]
  then
    IFACE=$( echo "${IFACE}" | head -3 | tr '\n' ' ' | tr ' ' '/' )
    IFACE="${IFACE}(...)"
  else
    IFACE=$( echo "${IFACE}" | tr '\n' '/' | sed '$s/.$//' )
  fi
}

__buffer() {
  # GET ALL OUTPUT INTO VARIABLE FOR FAST DISPLAY
  BUFFER="INFO: running '${NAME}' in INTERVAL mode every '${1}' seconds
EXIT: press and hold [CTRL]+[C] keys to end

$( ${0} )"
  echo "${BUFFER}"
  BACK=$( echo "${BUFFER}" | wc -l )

  # MOVE CURSOR BACK TO BEGINING INSTEAD clear(1) BUILTIN
  seq ${BACK} | xargs -I- tput cuu1
}

# INTERVAL RUN
if [ "${1}" = "-d" ]
then
  # CHECK IF INTERVAL IS NATURAL NUMBER
  REGEX_NUMBER=$( echo ${2} | grep -E -o "[0-9]+" )
  if [ "${2}" != "${REGEX_NUMBER}" ]
  then
    echo "NOPE: the INTERVAL must be natural number"
    exit 1
  fi

  # UNHIDE CURSOR ON EXIT
  trap 'tput cnorm' SIGINT SIGQUIT SIGHUP SIGTRAP SIGABRT SIGTERM

  # HIDE CURSOR
  tput civis

  # CLEAR SCREEN ONE TIME
  clear

  # GET basename(1) NAME OF PROGRAM
  NAME=${0##*/}

  # RUN 'INTERVAL' LOOP
  __buffer ${2}
  while sleep ${2}
  do
    __buffer ${2}
  done
  exit 0
fi

# CHECK doas(1) AND/OR sudo(8) EXISTENCE
unalias doas
unalias sudo
if which doas 1> /dev/null 2> /dev/null
then
  DOAS=doas
elif which sudo 1> /dev/null 2> /dev/null
then
  DOAS=sudo
else
  # CHECK USER WITH whoami(1)
  if [ "$( whoami )" != "root" ]
  then
    echo "NOPE: you need to be 'root' to use jmore(8) tool"
    exit 1
  fi
fi

# CHECK PASSED ARGUMENTS
case ${#} in
  (0)
    # DO NOTHING AND JUST DISPLAY LIST OF JAILS
    :
    ;;

  (1)
    # DISPLAY VERSION
    if [ "${1}" = "--version" -o \
         "${1}" =  "-version" -o \
         "${1}" =   "version" ]
    then
      echo "                                          __ ____ __"
      echo "        ___                              / //    \\\\ \\"
      echo "       /__/__ __ _   ____   __ __ ____  / //  /  / \\ \\"
      echo "      /  //       \\ /    \\ /  /_//  _ \\/ / \     \\ / /"
      echo "     /  //  /  /  //  /  //  /  /  ___/\\ \\ /  /  // /"
      echo "  __/  //__/__/__/ \\____//__/   \\____/  \\_\\\\____//_/"
      echo " /____/"
      echo
      echo "jmore(8) 0.5 2025/10/30"
      echo
      exit 0
    fi

    # DISPLAY HELP IF NEEDED
    case ${1} in
      (-h|--h|help|-help|--help) __usage ;;
    esac

    # DO FULL LISTING WITH ALL INTERFACES AND IP ADDRESSES
    [ "${1}" = "-a" ] && FULL_LISTING=1
    ;;

  (2)
    # MANAGE/OPERATE JAILS
    case ${2} in
      (u|start)  ${DOAS} service jail onestart   ${1} ;;
      (d|stop)   ${DOAS} service jail onestop    ${1} ;;
      (s|status) ${DOAS} service jail onestatus  ${1} ;;
      (c|console|shell|jexec)
        if ! jls -j ${1} 1> /dev/null 2> /dev/null
        then
          echo "NOPE: jail '${1}' is not running"
          exit 1
        fi
        env PS1="${1} # " PS2="> " ${DOAS} jexec ${1}
        ;;
    esac
    exit 0
    ;;

  (*)
    # DISPLAY USAGE INFORMATION
    __usage
    ;;
esac

# SETTINGS AND INITIAL INFORMATION GATHERING
JLS=$( jls 2> /dev/null )
NETSTAT=$( netstat -Win -f inet )
IFCONFIG=$( echo "${NETSTAT}" | sed 1d | awk '{print $1}' | uniq | tr '\n' ' ' )

# FIND AND SET bastille_prefix FOR BastilleBSD JAILS
eval $( grep '^[^#]' /usr/local/etc/bastille/bastille.conf 2> /dev/null \
          | grep -m 1 bastille_prefix \
          | awk '{print $1}' )

if [ "${bastille_prefix}" != "" ]
then
  BAST_DIR="${bastille_prefix}"
  unset bastille_prefix
fi

# GET LIST OF JAILS NAMES
JAILS=$( grep -h '^[^#]' \
           /etc/jail.conf \
           /etc/jail.conf.d/*.conf \
           "${BAST_DIR}"/jails/*/jail.conf 2> /dev/null \
           | grep -h -E '[[:space:]]*[[:alnum:]][[:space:]]*\{' \
           | tr -d '\ \t{' )

# MAYBE THERE ARE NO JAILS AT ALL
if [ "${JAILS}" = "" ]
then
  echo "No configured Jails detected."
  exit 0
fi

# OUTPUT START
(

# PRINT HEADER
echo JAIL JID "CPU/%" "RAM/mb" TYPE VER DIR IFACE "IP(s)"

# PARSE JAILS INFORMATION
while read JAIL
do

  CONFIG=$(
    (
      cat /etc/jail.conf                  2> /dev/null
      cat /etc/jail.conf.d/*.conf         2> /dev/null
      cat "${BAST_DIR}"/jails/*/jail.conf 2> /dev/null
    ) | grep '^[^#]' \
      | grep -A 512 -E "[[:space:]]*${JAIL}*[[:space:]]*\{" \
      | grep -B 512 -m 1 ".*[[:space:]]*\}[[:space:]]*$"
  )

  # FIND DIR FOR RUNNING JAIL
  DIR=$( echo "${JLS}" | awk '{print $NF}' | grep -E -e "/${JAIL}$" -e "/${JAIL}/root" )

  # FIND DIR FOR STOPPED JAIL
  if [ "${DIR}" = "" ]
  then
    DIR=$( grep -h '^[^#]' \
              /etc/jail.conf \
              /etc/jail.conf.d/*.conf \
              "${BAST_DIR}"/jails/*/jail.conf 2> /dev/null \
              | grep -A 512 -h -E "${JAIL}[[:space:]]\{" \
              | grep -m 1 path \
              | awk '{print $NF}' \
              | tr -d ';' \
              | sed -e "s.\${name}.${JAIL}.g" \
                    -e "s.\$name.${JAIL}.g" \
              | tr -d '"' )
  fi

  # FIND JAIL VERSION
  VER=$( ${DIR}/bin/freebsd-version -u 2> /dev/null \
            | sed -e s.RELEASE.R.g \
                  -e s.CURRENT.C.g \
                  -e s.STABLE.S.g \
                  -e s.BETA.B.g )

  # IF DIR IS ABSENT THEN SET IT TO [GONE]
  if [ ! -d "${DIR}" ]
  then
    DIR="[GONE]${DIR}"
  fi

  NETSTAT_JAIL=$( ${DOAS} jexec ${JAIL} netstat -Win -f inet 2> /dev/null )
  # FIND JAIL IP(s)
  IPS=$( echo "${NETSTAT_JAIL}" \
           | sed 1d \
           | awk '{print $4}' \
           | grep -v '127.0.0.1' \
           | tr '\n' '/' \
           | sed '$s/.$//' )

  # FIND JAIL TYPE
  TYPE=$( ${DOAS} jexec ${JAIL} sysctl -n security.jail.vnet 2> /dev/null )

  case ${TYPE} in

    # VNET JAIL
    (1)
      TYPE=vnet
      IFACE=$( echo "${NETSTAT_JAIL}" | sed 1d | grep -v '127.0.0.0/8' | awk '{print $1}'  )

      case ${FULL_LISTING} in

        (1)
          IFACE=$( echo "${IFACE}" | tr '\n' '/' | sed '$s/.$//' )
          ;;

        (*)
          IPS=$( echo "${IPS}" | tr '/' '\n' | head -3 | tr '\n' '/' | sed '$s/.$//' )
          __interfaces_limit
          ;;

      esac
      ;;

    # CLASSIC JAIL
    (0)
      TYPE=std
      while read IP
      do
        while read INTERFACE
        do
          if echo "${NETSTAT}" | grep -v '127.0.0.0/8' | grep -q -E -- "${IP}/"
          then
            IFACE=${INTERFACE}
            break
          fi
        done << EOF_INTERFACE
  $( echo "${IFCONFIG}" | tr ' ' '\n' )
EOF_INTERFACE
      done << EOF_IP
  $( echo "${IPS}" | tr '/' '\n' )
EOF_IP
      ;;

    # MORE DETECTION NEEDED
    (*)
      # MORE VNET JAIL
      if echo "${CONFIG}" | grep -q 'vnet.interface'
      then
        TYPE=vnet
        IFACE=$( echo "${CONFIG}" | grep 'vnet.interface' | awk '{print $NF}' | tr -d ';"' | tr ',' '\n' )
        __interfaces_limit
      else
      # MORE CLASSIC JAIL
        TYPE=std
        IFTYPE=$( echo "${CONFIG}" | grep 'ip4.addr' )
        case ${IFTYPE} in
          # PART FOR THIS FORMAT BELOW
          # ip4.addr = em0|10.0.0.1;
          (*\|*)
            IFACE=$( echo "${IFTYPE}" | awk -F '=' '{print $2}' )
            IPS=$(   echo "${IFACE}"  | awk -F '|' '{print $2}' | tr -c -d '0-9.' )
            IFACE=$( echo "${IFACE}"  | awk -F '|' '{print $1}' )
            ;;
          # PART FOR THIS FORMAT BELOW
          # interface = em0;
          # ip4.addr = 10.0.0.1;
          (*)
            IFACE=$( echo "${CONFIG}" | grep 'interface' | awk '{print $NF}' | tr -d ';' | tr ',' '\n' )
            __interfaces_limit
            IPS=$( echo "${CONFIG}" | grep '\.addr' | awk '{print $NF}' | tr -d ';' )
            ;;
        esac
      fi
      ;;

  esac

  # GET JAIL JID
  JID=$( echo "${JLS}" | grep -E -e "/${JAIL}$" -e "/${JAIL}/root" | awk '{print $1}' )

  # IF JAIL IS NOT RUNNING THEN SET JID TO '-' VALUE
  if [ "${JID}" = "" ]
  then
    if ! ${DOAS} jexec ${JAIL} echo / 1> /dev/null 2> /dev/null
    then
      JID=-
    fi
  fi

  # CALCULATE CPU/RAM USAGE
  RAM=0
  CPU=0
  RESOURCES=$( ${DOAS} ps -J ${JID} -o 'rss,%cpu' 2> /dev/null | sed 1d )
  if [ "${RESOURCES}" != "" ]
  then
    while read RSS PER
    do
      RAM=$( echo "${RAM} + ${RSS}" | bc -l )
      CPU=$( echo "${CPU} + ${PER}" | bc -l )
    done << CPU_RAM
  $( echo "${RESOURCES}" )
CPU_RAM
  fi

  # CONVERT KILOBYTES TO MEGABYTES
  RAM=$( echo "scale=1; ${RAM} / 1024" | bc -l )
  RAM=$( printf "%3.1f" "${RAM}" )
  CPU=$( printf "%3.1f" "${CPU}" )

  # LAST CHECKS BEFORE DISPLAY
  [ "${JID}"   = "" ] && JID='-'
  [ "${TYPE}"  = "" ] && TYPE='-'
  [ "${DIR}"   = "" ] && DIR='-'
  [ "${IFACE}" = "" ] && IFACE='-'
  [ "${IPS}"   = "" ] && IPS='-'
  [ "${VER}"   = "" ] && VER='-'
  [ "${CPU}"   = "" ] && CPU='-'
  [ "${RAM}"   = "" ] && RAM='-'

  # PRING GATHERED JAIL INFORMATION
  echo ${JAIL} ${JID} ${CPU} ${RAM} ${TYPE} ${VER} ${DIR} ${IFACE} ${IPS}

  # UNSET VARIABLES FOR NEXT LOOP
  unset JAIL JID TYPE VER DIR IFACE IPS CPU RAM

done << JAILS
  ${JAILS}
JAILS

# OUTPUT END
# OUTPUT FORMATTING BEGIN
) | awk \
'{
  # FIND LENGTH FOR EACH COLUMN
  for(L = 1; L <= NF; L++) {
    if (length($L) > MAX[L]) {
      MAX[L] = length($L)
    }
  }

  # STORE EACH COLUMN LENGTH
  for(R = 1; R <= NF; R++) {
    ROW[NR, R] = $R
  }
  COLS = NF
  ROWS = NR
}

END {
  # BUILD FORMAT STRING
  FORMAT = ""
  for(C = 1; C <= COLS; C++) {
    if (C <= 4) {
      FORMAT = FORMAT "%" MAX[C] + 1 "s"
    } else {
      # ADD SPACE BETWEEN 4TH AND 5TH COLUMNS
      if (C == 5) {
        FORMAT = FORMAT " "
      }
      FORMAT = FORMAT "%-" MAX[C] + 1 "s"
    }
  }
  FORMAT = FORMAT "\n"

  # PRINT OUTPUT
  for(R = 1; R <= ROWS; R++) {
    ARGS = ""
    for(C = 1; C <= COLS; C++) {
      ARGS = ARGS sprintf("%s ", ROW[R, C])
    }
    N = split(ARGS, O)
    printf FORMAT, O[1], O[2], O[3], O[4], O[5], O[6], O[7], O[8], O[9]
  }
}'
# OUTPUT FORMATTING END
