#!/usr/bin/env bash :<<'__comments__' Name: show-host-info Usage: 'bash /path/to/show-host-info' or make this script executable and put it in your PATH. Synopsis: Collect, format and display information about the CPUs, memory, distro name, physical storage and network configuration of a GNU/Linux host; the info is required to perform an unattended kickstart installation of Alma/CentOS/Rocky Linux on the host. Notes: This refactors and enhances the script first released on 2014-07-13 (version 1.0) and last modified on 2019-03-24 (version 1.5); tested working on CentOS 5/6/7 and Debian 11 (Bullseye). CPU and Core counts may be inaccurate when run in a qemu guest. Release 4 adds distro/OS. Author: G. D. LaBossiere, Xview Solutions Inc. Release: 4 Date: 2024-08-14 RCS: $Id$ License: GNU GPL v3 __comments__ #------------------------------------------------------------------------------ # bash environment #------------------------------------------------------------------------------ # FUNCNAME[@] #------------------------------------------------------------------------------ # constants #------------------------------------------------------------------------------ # uses ANSI escape '\e' sequences to display bright white foreground '[1;37m' #+ on blue background '[44m' applied from cursor to the end of the line '[K' COLOR_SCHEME="\e[1;37m\e[44m\e[K" # clear screen; printf "$CLS" clears screen and enables COLOR_SCHEME CLS="%b \ec$COLOR_SCHEME" # end of script; printf "$EOS" prints an empty line with COLOR_SCHEME, then # disables COLOR_SCHEME, then prints an empty line EOS="%b\e[0m\n\n" # field separator; character that separates the fields of an array element in #+ localStorage[@] and networkInfo[@] FS="|" # new line; printf "$NL" prints a newline and enables COLOR_SCHEME NL="\n$COLOR_SCHEME" # these utilities must be installed for this script to work TOOL_LIST=(arch cat head hostname ip ls sort stty wc) #------------------------------------------------------------------------------ # error codes #------------------------------------------------------------------------------ ERROR_15="Missing required program(s)" ERROR_16="Missing required function(s)" #------------------------------------------------------------------------------ # variables #------------------------------------------------------------------------------ bootType="" cpuArch="" cpuCores="" cpuCount="" cpuKvm="" cpuModel="" distroName="" dnsInfo=() errFuncName="" errorNumber="" exitMessage="" localStorage=() fqdnDomain="" fqdnName="" netBootDevice="" netInfo=() ramSize="" #------------------------------------------------------------------------------ :<<'__function_calls__' main─┐ ├─exit_handler─┐ │ └─center_text ├─check_tools ├─get_distro_name ├─get_cpu_info ├─get_ram_size ├─get_boot_type ├─get_local_storage ├─get_fqdn_info ├─get_net_info ├─get_dns_info └─print_info─┐ └─center_text __function_calls__ #------------------------------------------------------------------------------ # functions #------------------------------------------------------------------------------ :<<'__comments__' Name: center_text Synopsis: Center quoted text $1 on screen; pad either side of $1 with optional single quoted char $2. Arguments: (required) $1="quoted text"; (optional) $2="quoted single char" Globals: NL Requires: stty Returns: 0 on success; $errFuncName and exit 15 on error. __comments__ center_text() { [[ $1 ]] || return 0 type -p stty &>/dev/null || exit 15 local char cols num pad cols=$(stty size < /dev/tty) cols="${cols#*[[:blank:]]}" num="$((((cols-${#1}))/2))" [[ $2 ]] && char="${2:0:1}" || char=" " pad="$(printf "%${num}s" "$char")" printf "%s%s%s$NL" "${pad// /$char}" "$1" "${pad// /$char}" return 0 } #------------------------------------------------------------------------------ :<<'__comments__' Name: check_tools Synopsis: For each program name in TOOL_LIST[@] not installed on this host add program name to missing[@]; return 0 if missing[@] is empty else set exitMessage and exit 15. Globals: FUNCNAME[@], TOOL_LIST[@], errFuncName, exitMessage Returns: 0 on success; $exitMessage, $errFuncName and exit 15 on error. __comments__ check_tools() { errFuncName="$FUNCNAME" local i missing for ((i=0;i<${#TOOL_LIST[@]};i++)); do type -p ${TOOL_LIST[$i]} &>/dev/null || missing+=("${TOOL_LIST[$i]}") done [[ ${missing[@]} ]] || return 0 exitMessage="The following required utilities were not found: ${missing[*]}" exit 15 } #------------------------------------------------------------------------------ :<<'__comments__' Name: exit_handler Synopsis: If errorNumber is non-zero display a message; the first statement to appear in main() must be 'trap exit_handler EXIT' Globals: CLS, EOS, NL, errFuncName, errorNumber, exitMessage Calls: center_text Returns: $errorNumber __comments__ exit_handler() { errorNumber="$?" [[ $errorNumber -eq 0 ]] && return 0 local errmsg errtxt errtxt="ERROR_${errorNumber}" errmsg="Error ${errorNumber}: No description available" [[ ${!errtxt} ]] && errmsg="Error ${errorNumber}: ${!errtxt}" [[ ${errFuncName} ]] && errmsg="${errFuncName}: ${errmsg}" errmsg="${0##*/}: ${errmsg}" printf "$CLS" printf "$NL$NL" declare -F center_text &>/dev/null && center_text " ${0##*/} " "-" printf "$NL" [[ $exitMessage ]] && printf "%s$NL" "${exitMessage}" printf "%s$NL$NL" "${errmsg}" declare -F center_text &>/dev/null && center_text "-" "-" printf "$EOS" return $errorNumber } #------------------------------------------------------------------------------ :<<'__comments__' Name: get_boot_type Synopsis: Determine if host boots from UEFI or from legacy BIOS or none if host is a container. Globals: FUNCNAME[@], bootType, errFuncName Reads: /sys/firmware/efi Returns: 0 in all cases. __comments__ get_boot_type() { errFuncName="$FUNCNAME" [[ -d /sys/firmware/efi ]] && bootType="UEFI" && return 0 [[ -d /boot/extlinux || -f /boot/grub/grub.conf || -f /etc/default/grub || \ -d /boot/syslinux ]] && bootType="Legacy BIOS" && return 0 bootType="None" return 0 } #------------------------------------------------------------------------------ :<<'__comments__' Name: get_cpu_info Synopsis: Query /proc/cpuinfo; extract data and assign values to globals. Globals: FUNCNAME[@], cpuArch, cpuCores, cpuCount, cpuKvm, cpuModel, errFuncName Requires: arch, sort, wc Reads: /proc/cpuinfo Returns: 0 on success; $errFuncName and exit 15 on error. __comments__ get_cpu_info() { errFuncName="$FUNCNAME" type -p arch sort wc &>/dev/null || exit 15 local core cpu i info readarray -t info < /proc/cpuinfo for ((i=0;i<${#info[@]};i++)); do [[ ${info[$i]} =~ 'core id' ]] && core+=("${info[$i]##*\:[[:blank:]]}") [[ ${info[$i]} =~ 'physical id' ]] && cpu+=("${info[$i]##*\:[[:blank:]]}") [[ $cpuModel ]] || { [[ ${info[$i]} =~ 'model name' ]] && \ cpuModel="${info[$i]##*\:[[:blank:]]}" } [[ $cpuKvm ]] || { [[ ${info[$i]} =~ 'flags' && ${info[$i]} =~ ( svm | vmx ) ]] && \ cpuKvm="Yes" } done # count unique core id values: 0, 1, 2, 3, etc.; N/A for old single-core CPUs [[ ${core[@]} ]] && { cpuCores="$(printf '%s\n' "${core[@]}" | sort -nu | wc -l)" [[ $cpuCores =~ [^0-9] ]] && cpuCores="Unknown" } # count unique physical id values: 0, 1, 2, 3, etc. [[ ${cpu[@]} ]] && { cpuCount="$(printf '%s\n' "${cpu[@]}" | sort -nu | wc -l)" [[ $cpuCount =~ [^0-9] ]] && cpuCount="Unknown" } # 'getconf LONG_BIT' returns the CPU instruction word width in bits; i.e. 64 #+ or 32 or whatever # 'arch' returns x86_64 or i686 or whatever; same as 'uname -m' cpuArch="$(arch)" || cpuArch="Unknown" # this will only happen for old, single-core CPUs [[ $cpuCores ]] || cpuCores="N/A" [[ $cpuCount ]] || cpuCount="1" [[ $cpuKvm ]] || cpuKvm="No" [[ $cpuModel ]] || cpuModel="Unknown Model" return 0 } #------------------------------------------------------------------------------ :<<'__comments__' Name: get_distro_name Synopsis: Set the value of distroName to the installed GNU/Linux distribution if it is found. Globals: FUNCNAME[@], distroName Reads: /etc/*-release Requires: cat, head Returns: 0 on success; errFuncName and exit 15 on error. __comments__ get_distro_name() { errFuncName="$FUNCNAME" type -p cat head &>/dev/null || exit 15 distroName="$(cat /etc/*-release | head -n1)" distroName="${distroName##*=}" distroName="${distroName//\"/}" [[ $distroName ]] || distroName="Unknown" return 0 } #------------------------------------------------------------------------------ :<<'__comments__' Name: get_dns_info Synopsis: Populate dnsInfo[@] with the name servers found in /etc/resolv.conf. Globals: FUNCNAME[@], dnsInfo[@], errFuncName Reads: /etc/resolv.conf Returns: 0 in all cases. __comments__ get_dns_info() { errFuncName="$FUNCNAME" local names [[ -f /etc/resolv.conf ]] || return 0 names="$(< /etc/resolv.conf)" readarray -t names <<< "${names}" for ((i=0;i<${#names[@]};i++)); do [[ ${names[$i]} =~ ^[[:blank:]]*'#'. ]] && continue 1 [[ ${names[$i]} =~ 'nameserver' ]] || continue 1 names[$i]="${names[$i]//nameserver/}" names[$i]="${names[$i]//[[:blank:]]/}" dnsInfo+=("${names[$i]}") done return 0 } #------------------------------------------------------------------------------ :<<'__comments__' Name: get_fqdn_info Synopsis: Get the host name and DNS domain name (if set); assign values to fqdnName and fqdnDomain; dnsdomainname is a symlink to hostname. Globals: FUNCNAME[@], errFuncName, fqdnDomain, fqdnName Requires: hostname Returns: 0 on success; $errFuncName and exit 15 on error. __comments__ get_fqdn_info() { errFuncName="$FUNCNAME" type -p hostname &>/dev/null || exit 15 fqdnDomain="$(dnsdomainname 2> /dev/null)" fqdnName="$(hostname -s 2> /dev/null)" [[ $fqdnName ]] || fqdnName="Hostname is not set." return 0 } #------------------------------------------------------------------------------ :<<'__comments__' Name: get_local_storage Synopsis: Look for host-attached SATA and ATA storage devices; populate each element of localStorage[@} with: device|size|UUID Globals: FS, FUNCNAME[@], localStorage[@] errFuncName Reads: /dev/disk/by-id, /proc/partitions Requires: ls Returns: 0 on success; $errFuncName and exit 15 on error. __comments__ get_local_storage() { errFuncName="$FUNCNAME" type -p ls &>/dev/null || exit 15 local device disk i id j list part size [[ -d /dev/disk/by-id ]] && { id="$(ls -gG --time-style=+'' /dev/disk/by-id/)" readarray -t id <<< "${id}" } || return 0 readarray -t part < /proc/partitions for ((i=0;i<${#part[@]};i++)); do [[ ${part[$i]} =~ [0-9] && ${part[$i]} =~ [a-z]$ ]] || continue 1 disk="${part[$i]##*[[:blank:]]}" part[$i]="${part[$i]//[[:blank:]]$disk/}" size="${part[$i]##*[[:blank:]]}" size="$((size/1048576))" device="" for ((j=0;j<${#id[@]};j++)); do [[ ${id[$j]} =~ "${disk}"$ && ${id[$j],,} =~ 'sata' ]] && { device="${id[$j]##*[0-9][[:blank:]][[:blank:]]}" device="${device%%[[:blank:]]*}" localStorage+=("${disk}${FS}${size}${FS}${device}") break 1 } done [[ $device ]] || { for ((j=0;j<${#id[@]};j++)); do [[ ${id[$j]} =~ "${disk}"$ && ${id[$j],,} =~ 'ata-' ]] && { device="${id[$j]##*[0-9][[:blank:]][[:blank:]]}" device="${device%%[[:blank:]]*}" localStorage+=("${disk}${FS}${size}${FS}${device}") break 1 } done } done return 0 } #------------------------------------------------------------------------------ :<<'__comments__' Name: get_net_info Synopsis: Extract network interface names from /proc/net/dev; use ip to get info for each; populate each element of netInfo[@] with: Device|State|MAC Address|ipv4 Address|CIDR|Netmask|Network|Gateway Notes: Credit to Janci for the ipv4/cidr --> network conversion code; https://stackoverflow.com/questions/15429420 Credit to Krzysztof Wilczyński for the cidr --> netmask code; https://gist.github.com/kwilczynski/5d37e1cced7e76c7c9ccfdf875ba6c5b Globals: FS, FUNCNAME[@], errFuncName, netBootDevice, netInfo[@] Reads: /proc/net/dev Requires: ip Calls: --- Returns: 0 on success; $errFuncName and exit 15 on error. __comments__ get_net_info() { errFuncName="$FUNCNAME" type -p ip &>/dev/null || exit 15 local address bits cidr dev gateway i ipv4 link mac mask network prefix state readarray -t dev < /proc/net/dev for ((i=0;i<${#dev[@]};i++)); do [[ ${dev[$i]} =~ ':' ]] && dev[$i]="${dev[$i]%\:*}" || continue 1 link+=("${dev[$i]//[[:blank:]]/}") done for ((i=0;i<${#link[@]};i++)); do ip link show up ${link[$i]} &>/dev/null && state="Up" || state="Down" mac="$(ip -f inet link show ${link[$i]})" mac="${mac##*link\/}" mac="${mac#*[[:blank:]]}" mac="${mac%%[[:blank:]]*}" ipv4="$(ip -f inet addr show ${link[$i]})" [[ $ipv4 ]] && { ipv4="${ipv4##*inet[[:blank:]]}" cidr="${ipv4#*\/}" cidr="${cidr%%[[:blank:]]*}" ipv4="${ipv4%%\/*}" for ((bits=1;bits<=32;bits++)); do [[ $(((bits-1)%8)) -eq 0 ]] && prefix="${prefix}." [[ $bits -le $cidr ]] && prefix="${prefix}1" || prefix="${prefix}0" done prefix="${prefix#*\.*}" readarray -t prefix <<< "${prefix//\./$'\n'}" readarray -t address <<< "${ipv4//\./$'\n'}" network=$(printf '%s.%s.%s.%s' \ "$((${address[0]}&(2#${prefix[0]})))" \ "$((${address[1]}&(2#${prefix[1]})))" \ "$((${address[2]}&(2#${prefix[2]})))" \ "$((${address[3]}&(2#${prefix[3]})))") bits=$((0xffffffff^((1<<(32-${cidr}))-1))) mask="$(((bits>>24)&0xff)).$(((bits>>16)&0xff)).$(((bits>>8)&0xff))" mask="${mask}.$((bits&0xff))" gateway="$(ip -f inet route show)" [[ $gateway =~ ${link[$i]} ]] && { gateway="${gateway##*default via}" gateway="${gateway%%dev*}" gateway="${gateway//[[:blank:]]/}" netBootDevice="${link[$i]}${FS}${ipv4}${FS}${mask}${FS}${gateway}" } || gateway="" } || { cidr="" mask="" network="" gateway="" } link[$i]="${link[$i]}${FS}${state}${FS}${mac}${FS}${ipv4}${FS}${cidr}" link[$i]="${link[$i]}${FS}${mask}${FS}${network}${FS}${gateway}" netInfo+=("${link[$i]}") done return 0 } #------------------------------------------------------------------------------ :<<'__comments__' Name: get_ram_size Synopsis: Extract MemTotal value in kB from /proc/meminfo, convert to GB and assign to ramSize. Globals: FUNCNAME[@], errFuncName, ramSize Reads: /proc/meminfo Returns: 0 in all cases. __comments__ get_ram_size() { errFuncName="$FUNCNAME" local dec i info int ram readarray -t info < /proc/meminfo for ((i=0;i<${#info[@]};i++)); do [[ ${info[$i],,} =~ memtotal ]] || continue 1 ram="${info[$i]//[[:blank:]]/}" ram="${ram##*\:}" ram="${ram%kB}" [[ $ram =~ ^[0-9]+$ ]] && ram="$((ram/1048))" [[ ${#ram} -eq 1 ]] && ramSize="0.00${ram}" [[ ${#ram} -eq 2 ]] && ramSize="0.0${ram}" [[ ${#ram} -eq 3 ]] && ramSize="0.${ram}" [[ ${#ram} -gt 3 ]] && { int="${ram%???}" dec="${ram#$int}" ramSize="${int}.${dec}" } break 1 done [[ ${ramSize//\./} =~ [1-9] ]] || ramSize="" return 0 } #------------------------------------------------------------------------------ :<<'__comments__' Name: print_info Synopsis: Format and print values of globals to tty. Globals: CLS, EOS, FS, FUNCNAME[@], NL, bootType, cpuArch, cpuCores, cpuCount, cpuKvm, cpuModel, distroName, dnsInfo[@], errFuncName, fqdnDomain, fqdnName, localStorage[@], netBootDevice, netInfo[@], ramSize Calls: center_text Returns: 0 on success; $errFuncName and exit 16 on error. __comments__ print_info() { errFuncName="$FUNCNAME" declare -F center_text &>/dev/null || exit 16 local disk gateway i ipv4 mask net size uuid printf "$CLS" printf "$NL$NL" center_text " Discovered the following host information " "-" printf "$NL" center_text "**** CPUs, memory, operating system & physical storage ****" printf "$NL %12s %s$NL %12s %s$NL %12s %s$NL %12s %s$NL" \ "CPU(s):" "${cpuCount} x ${cpuModel}" \ "Cores:" "${cpuCores} per CPU" \ "CPU type:" "${cpuArch}" \ "KVM capable:" "${cpuKvm}" printf "$NL %12s" "Memory:" [[ ${ramSize} ]] && printf " %s" "${ramSize} GB" || printf " %s" "Unknown" printf "$NL$NL %12s %s$NL %12s %s$NL$NL %11s %-8s %-s$NL" \ "Distro name:" "${distroName}" \ "Boot method:" "${bootType}" \ "Device" "Capacity" "/dev/disk/by-id" for ((i=0;i<${#localStorage[$i]};i++)); do disk="${localStorage[$i]%%$FS*}" size="${localStorage[$i]%$FS*}" size="${size##*$FS}" uuid="${localStorage[$i]##*$FS}" printf " %11s %8s %-s$NL" "${disk}" "${size} GB" "${uuid}" done [[ ${#localStorage[@]} -ge 1 ]] || printf " %11s %8s %-s$NL" \ "N/A" "N/A" "No disk partitions detected" printf "$NL" center_text "********* Network configuration *********" [[ $fqdnDomain ]] && \ printf "$NL %12s %s$NL" "FQDN:" "${fqdnName}.${fqdnDomain}" || \ printf "$NL %12s %s$NL" "Hostname:" "${fqdnName}" printf "$NL %-12s %-5s %-17s %15s%-5s %-15s %-15s %-15s$NL" \ "Device" "State" "MAC address" "IPv4 address" "/CIDR" "Netmask" "Network" \ "Gateway" for ((i=0;i<${#netInfo[@]};i++)); do readarray -t net <<< "${netInfo[$i]//$FS/$'\n'}" printf " %-12s" "${net[0]}" printf " %-5s" "${net[1]}" [[ ${net[2]} ]] && printf " %-17s" "${net[2]}" || printf " %-17s" "N/A" [[ ${net[3]} ]] && printf " %15s" "${net[3]}" || { printf "$NL" continue 1 } printf "%-5s" "/${net[4]}" printf " %-15s" "${net[5]}" printf " %-15s" "${net[6]}" printf " %-15s$NL" "${net[7]}" done ipv4="${netBootDevice#*$FS}" ipv4="${ipv4%%$FS*}" mask="${netBootDevice%$FS*}" mask="${mask##*$FS}" printf "$NL %s %s %s %s" \ "Network boot device: ${netBootDevice%%$FS*}," "address: ${ipv4}," \ "netmask: ${mask}," "gateway: ${netBootDevice##*$FS}" [[ ${dnsInfo[@]} ]] || printf "$NL %s" "There are no name servers." [[ ${#dnsInfo[@]} -eq 1 ]] && printf "$NL %s" \ "There is one name server: ${dnsInfo[*]}" [[ ${#dnsInfo[@]} -gt 1 ]] && printf "$NL %s" \ "There are ${#dnsInfo[@]} name servers:" [[ ${#dnsInfo[@]} -eq 2 ]] && printf " %s" " ${dnsInfo[0]}" [[ ${#dnsInfo[@]} -gt 2 ]] && { for ((i=0;i<((${#dnsInfo[@]}-2));i++)); do printf " %s," "${dnsInfo[$i]}" done printf " %s" "${dnsInfo[${#dnsInfo[@]}-2]}" } [[ ${#dnsInfo[@]} -gt 1 ]] && printf " %s" "and ${dnsInfo[${#dnsInfo[@]}-1]}" printf "$NL$NL" center_text "-" "-" printf "$NL$EOS" return 0 } #------------------------------------------------------------------------------ # main #------------------------------------------------------------------------------ main() { trap exit_handler EXIT check_tools get_distro_name get_cpu_info get_ram_size get_boot_type get_local_storage get_fqdn_info get_net_info get_dns_info print_info exit 0 } #------------------------------------------------------------------------------ # run program #------------------------------------------------------------------------------ main