#!/bin/bash
# Description: Displays basic system information
# Modified:    2021 Mar 18

##############################################################################
#  Copyright (C) 2018-2021 SUSE LLC
##############################################################################
#
#  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; version 2 of the License.
#
#  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 <http://www.gnu.org/licenses/>.
#
#  Authors/Contributors:
#     Jason Record <jason.record@suse.com>
#
##############################################################################

##############################################################################
# Variables
##############################################################################
SVER='1.8.7'
PAD=25
QUIET=0
FORMAT_OUT="%-${PAD}s %s\n"
CONFIG_FILE='/etc/hostinfo.conf'
ISSUE_BASE='/etc/issue.d'
FILE_HOSTINFO=''
FILE_BASE="80-hostinfo-"
FILE_MOTD="/etc/motd"
HOSTINFO_PID="/run/hostinfo.pid"
ERR_MISSING_CONFIG=2
ERR_CUSTOM_LIMIT_EXCEEDED=3
ERR_HOSTINFO_ACTIVE=4
SCRIPT_NAME="${0##*/}"
PRODUCT_FILE="/etc/products.d/baseproduct"
RELEASE_FILE='/etc/os-release'


trap deConfigure SIGTERM

if [[ -s $CONFIG_FILE ]]
then
	. $CONFIG_FILE
else
	echo "Error: Missing $CONFIG_FILE"
	cleanUp
	exit $ERR_MISSING_CONFIG
fi

##############################################################################
# Functions
##############################################################################
getHelp()
{
	echo "DESCRIPTION: Gathers host information for issue generator and motd"
	echo
	echo "USAGE:       hostinfo [OPTION]"
	echo
	echo "OPTIONS"
	echo " help     This help screen"
	echo " all      All commands run (default)"
	echo " dist     Gathers distribution information"
	echo " host     Gathers the server hostname"
	echo " kernel   Gathers current kernel information"
	echo " updates  Gather current updates from zypper"
	echo " net      Update network information"
	echo " mem      Update memory information"
	echo " cpu      Show current CPU load information"
	echo " disk     Show list is storage devices and their size"
	echo ' custom <label> <value> Creates a custom entry'
	echo
}

title() {
	echo "#######################################################################"
	echo "# hostinfo v${SVER}"
	echo "#######################################################################"
}

getHostname()
{
	FILE_HOSTINFO="${ISSUE_BASE}/${FILE_BASE}03-host" 
	printf "$FORMAT_OUT" "Hostname:" "$HOSTNAME" > $FILE_HOSTINFO
}

getDate()
{
	FILE_HOSTINFO="${ISSUE_BASE}/${FILE_BASE}02-date" 
	printf "$FORMAT_OUT" "Current As Of:" "$(date '+%c')" > $FILE_HOSTINFO
}

getDistribution()
{
	FILE_HOSTINFO="${ISSUE_BASE}/${FILE_BASE}01-distro" 
	> $FILE_HOSTINFO
	if [[ -s $PRODUCT_FILE ]]; then
		SUMMARY=$(grep '<summary>' $PRODUCT_FILE 2>/dev/null)
		PATCHLVL=$(grep '<patchlevel>' $PRODUCT_FILE 2>/dev/null)

		SUMMARY=${SUMMARY#"${SUMMARY%%[![:space:]]*}"} # trim leading white space
		PATCHLVL=${PATCHLVL#"${PATCHLVL%%[![:space:]]*}"} 
		SUMMARY=${SUMMARY//\<summary\>/} # search and remove xml tags
		DIST=${SUMMARY//\<\/summary\>/}
		PATCHLVL=${PATCHLVL//\<patchlevel\>/}
		PATCHLVL=${PATCHLVL//\<\/patchlevel\>/}
		if (( PATCHLVL > 0 )); then
			printf "$FORMAT_OUT" "Distribution:" "$DIST" >> $FILE_HOSTINFO
		else
			printf "$FORMAT_OUT" "Distribution:" "$DIST SP${PATCHLVL}" >> $FILE_HOSTINFO
		fi
	else
		PRETTY_NAME=$(cat ${RELEASE_FILE} | grep PRETTY_NAME | cut -d\= -f2 | tr -d '"')
		DIST=$(sed -e "s/ SP.//g" <<< $PRETTY_NAME)
		SPSTR="0"
		if echo $PRETTY_NAME | grep SP &>/dev/null; then
			SPTMP=$(echo $PRETTY_NAME | awk '{print $NF}')
			SPSTR=${SPTMP##SP}
		fi
		printf "$FORMAT_OUT" "Distribution:" "$DIST SP${SPSTR}" >> $FILE_HOSTINFO
	fi
}

getKernel()
{
	FILE_HOSTINFO="${ISSUE_BASE}/${FILE_BASE}04-kernel"
	> $FILE_HOSTINFO
	KERNEL_STR=$(uname -r)
	KERNEL_TYPE=$(awk -F- '{print $NF}' <<< $KERNEL_STR)
	KERN_RPM="kernel-${KERNEL_TYPE}"
	KERN_RPM_VERSION="${KERN_RPM}-${KERNEL_STR%%-$KERNEL_TYPE}*"
	KERN_DATE=$(rpm -q --queryformat "%{INSTALLTIME:date}" $KERN_RPM_VERSION 2>/dev/null)
	if [[ -n "$KERN_DATE" ]]; then
		KERN_DATE_STR="$KERN_DATE"
	else
		KERN_DATE_STR="Unknown"
	fi
	TAINT=$(cat /proc/sys/kernel/tainted)
	TAINTED=0
	if (( ${#TAINT} > 1 )); then
		TAINTED=1
	elif (( ${TAINT} > 0 )); then
		TAINTED=1
	fi
	if (( TAINTED )); then
		KERN_TAINT_STR="Tainted: ${TAINT}"
	else
		KERN_TAINT_STR="Not Tainted"
	fi

	printf "$FORMAT_OUT" "Kernel Version:" "$(uname -r)" >> $FILE_HOSTINFO
	printf "$FORMAT_OUT" " Architecture:" "$(uname -m)" >> $FILE_HOSTINFO
	printf "$FORMAT_OUT" " Installed:" "$KERN_DATE_STR" >> $FILE_HOSTINFO
	printf "$FORMAT_OUT" " Status:" "$KERN_TAINT_STR" >> $FILE_HOSTINFO
}

getNetAddr() {
	FILE_HOSTINFO="${ISSUE_BASE}/${FILE_BASE}06-network" 
	if (( IGNORE_NET )); then
		rm -f $FILE_HOSTINFO
#		echo "Network ignored in $CONFIG_FILE"
		return 0
	else
		rm -f ${ISSUE_BASE}/70-[be]*conf
		> $FILE_HOSTINFO
	fi

	NET_LIST="$(ip address)
"
	MASTER=''
	IFACE=''
	ADDR4=''
	ADDR6=''
	STR_LABEL=''
	STR_ADDR=''
	RE_START='^[0-9]'
	RE_IPV4='inet '
	RE_IPV6='inet6 '
	echo "Network Interfaces" > $FILE_HOSTINFO
	echo "$NET_LIST" | while read LINE
	do
#		echo "Pre:    MASTER=$MASTER IFACE=$IFACE #ADDR4=${#ADDR4} #ADDR6=${#ADDR6} #LINE=${#LINE} : $LINE"
		if (( ${#LINE} < 1 )); then
#			echo "Line0:  MASTER=$MASTER IFACE=$IFACE #ADDR4=${#ADDR4} #ADDR6=${#ADDR6} #LINE=${#LINE} : $LINE"
			STR_ADDR=''
			if (( ${#ADDR4} > 0 )); then
				STR_LABEL=" ${IFACE}:"
				STR_ADDR="$ADDR4"
			fi
			if (( ${#ADDR6} > 0 )); then
				if (( ${#STR_ADDR} > 0 )); then
					STR_ADDR="$STR_ADDR $ADDR6"
				else
					STR_LABEL=" ${IFACE}:"
					STR_ADDR="$ADDR6"
				fi
			fi
			if (( ${#STR_ADDR} < 1 )); then
				if (( ${#MASTER} > 0 )); then
					STR_LABEL=" ${IFACE}:"
					STR_ADDR="(Slave to $MASTER)"
				else
					STR_LABEL=" ${IFACE}:"
					STR_ADDR="(Unconfigured)"
				fi
			fi
			printf "$FORMAT_OUT" "$STR_LABEL" "$STR_ADDR" >> $FILE_HOSTINFO
		elif [[ "$LINE" =~ $RE_START ]]; then
			if (( ${#IFACE} > 0 )); then
				STR_ADDR=''
				if (( ${#ADDR4} > 0 )); then
					STR_LABEL=" ${IFACE}:"
					STR_ADDR="$ADDR4"
				fi
				if (( ${#ADDR6} > 0 )); then
					if (( ${#STR_ADDR} > 0 )); then
						STR_ADDR="$STR_ADDR $ADDR6"
					else
						STR_LABEL=" ${IFACE}:"
						STR_ADDR="$ADDR6"
					fi
				fi
				if (( ${#STR_ADDR} < 1 )); then
					if (( ${#MASTER} > 0 )); then
						STR_LABEL=" ${IFACE}:"
						STR_ADDR="(Slave to $MASTER)"
					else
						STR_LABEL=" ${IFACE}:"
						STR_ADDR="(Unconfigured)"
					fi
				fi
				printf "$FORMAT_OUT" "$STR_LABEL" "$STR_ADDR" >> $FILE_HOSTINFO
			fi
			MASTER=''
			ADDR4=''
			ADDR6=''
			IFACE=''
			LINE_ARRAY=($LINE)
			IFACE=${LINE_ARRAY[1]//:/}
			if [[ "$IFACE" == "lo" ]]; then
				IFACE=''
			else
				MASTER_IDX=$(echo ${LINE_ARRAY[@]/master//} | cut -d/ -f1 | wc -w | tr -d ' ')
				if (( $MASTER_IDX != ${#LINE_ARRAY[@]} )); then
					(( MASTER_IDX++ ))
					MASTER=${LINE_ARRAY[$MASTER_IDX]}
				fi
			fi
		elif [[ "$LINE" =~ $RE_IPV4 ]]; then
			LINE_ARRAY=($LINE)
			ADDR4=${LINE_ARRAY[1]}
		elif [[ "$LINE" =~ $RE_IPV6 ]]; then
			LINE_ARRAY=($LINE)
			ADDR6=${LINE_ARRAY[1]}
		fi
#		echo "Post:   MASTER=$MASTER IFACE=$IFACE #ADDR4=${#ADDR4} #ADDR6=${#ADDR6} #LINE=${#LINE} : $LINE"
	done
}

getUpdates()
{
	FILE_HOSTINFO="${ISSUE_BASE}/${FILE_BASE}05-updates" 
	if (( IGNORE_UPDATES )); then
		rm -f $FILE_HOSTINFO
#		echo "System updates ignored in $CONFIG_FILE"
		return 0
	fi
	> $FILE_HOSTINFO
	UPDATES_TMP=$(mktemp /var/tmp/hostinfo-tmp.XXXXXXXXXX)
	rpm -qa --queryformat "%{INSTALLTIME}|%{VENDOR}|%{NAME}\n" | sort -nr > $UPDATES_TMP
	if [[ -s $RELEASE_FILE ]]; then
		CPEID=$(grep 'CPE_NAME' $RELEASE_FILE 2>/dev/null | cut -d\: -f3 2>/dev/null)
	else
		CPEID=''
	fi

	PATCH_STRING=$(zypper --non-interactive --no-gpg-checks patch-check 2>/dev/null | tail -1 | grep -i patches 2>/dev/null)
	LIP_EPOC=$(head -1 $UPDATES_TMP | cut -d\| -f1)
	LIP_DATE=$(date '+%c' -d @${LIP_EPOC})

	REPOS=$(zypper --non-interactive --no-gpg-checks repos 2>/dev/null | grep '^[ ]\{0,1\}[[:digit:]]' | wc -l)
	if (( REPOS > 1 ))
	then
		if [[ -n "$PATCH_STRING" ]]
		then
			PATCH_COUNT_NEEDED=$(echo $PATCH_STRING | cut -d' ' -f1)
			PATCH_COUNT_SECURITY=$(echo $PATCH_STRING | cut -d\( -f2 | cut -d' ' -f1)
		else
			PATCH_COUNT_NEEDED='Pending'
			PATCH_COUNT_SECURITY='Pending'
		fi
	else
		PATCH_COUNT_NEEDED='Not Registered'
		PATCH_COUNT_SECURITY='Not Registered'
	fi

	printf "$FORMAT_OUT" "Last Installed Package:" "$LIP_DATE" >> $FILE_HOSTINFO
	printf "$FORMAT_OUT" " Patches Needed:" "$PATCH_COUNT_NEEDED" >> $FILE_HOSTINFO
	printf "$FORMAT_OUT" " Security:" "$PATCH_COUNT_SECURITY" >> $FILE_HOSTINFO
	case $CPEID in
	suse)
		COUNT_NVR=$(awk -F\| '{print $2}' $UPDATES_TMP | egrep -iv "^SUSE LLC|^SUSE Linux Enterprise|gpg-pubkey$" | wc -l)
		;;
	opensuse)
		COUNT_NVR=$(awk -F\| '{print $2}' $UPDATES_TMP | egrep -iv "^opensuse|gpg-pubkey$" | wc -l)
		;;
	*)	COUNT_NVR="Unknown"
		;;
	esac
	printf "$FORMAT_OUT" " 3rd Party Packages:" "$COUNT_NVR" >> $FILE_HOSTINFO
}

getMemory()
{
	FILE_HOSTINFO="${ISSUE_BASE}/${FILE_BASE}07-memory" 
	if (( IGNORE_MEM )); then
		rm -f $FILE_HOSTINFO
#		echo "Memory ignored in $CONFIG_FILE"
		return 0
	fi
	if [[ -x /usr/bin/free ]]
	then
		FREE=$(free -h | grep "^Mem")
		FREE_ARRAY=($FREE)
		MEM_TOTAL=${FREE_ARRAY[1]}
		MEM_FREE=${FREE_ARRAY[3]}
		MEM_AVAIL=${FREE_ARRAY[-1]}
		FREE=$(free -b | grep "^Mem")
		FREE_ARRAY=($FREE)
		CALC_TOTAL=${FREE_ARRAY[1]}
		CALC_AVAIL=${FREE_ARRAY[-1]}
		MEM_PERCENT=$(printf "%i" $(( CALC_AVAIL * 100 / CALC_TOTAL )))

		printf "$FORMAT_OUT" "Memory" "${MEM_PERCENT}% Available" > $FILE_HOSTINFO
		printf "$FORMAT_OUT" " Total/Free/Avail:" "${MEM_TOTAL}/${MEM_FREE}/${MEM_AVAIL}" >> $FILE_HOSTINFO
	else
		MEM_TOTAL=$(cat /proc/meminfo | grep MemTotal: | awk '{printf "%i", $2/1024}')
		MEM_FREE=$(cat /proc/meminfo | grep MemFree: | awk '{printf "%i", $2/1024}')

		echo "Memory" > $FILE_HOSTINFO
		printf "$FORMAT_OUT" " Total/Free:" "${MEM_TOTAL}/${MEM_FREE} MB" >> $FILE_HOSTINFO
	fi
}

getDisks()
{
	FILE_HOSTINFO="${ISSUE_BASE}/${FILE_BASE}10-disks" 
	if (( IGNORE_DISK )); then
		rm -f $FILE_HOSTINFO
#		echo "Disk information ignored in $CONFIG_FILE"
		return 0
	fi
	echo "Storage Devices" > $FILE_HOSTINFO
	DISK_LIST=$(fdisk -l | grep 'Disk /' | awk '{print $2,$3,$4}' | sed -e 's/://g;s/,//g')
	echo "$DISK_LIST" | while read DISK_INFO
	do
		THIS_DISK=($DISK_INFO)
		printf "$FORMAT_OUT" " ${THIS_DISK[0]}:" "${THIS_DISK[1]} ${THIS_DISK[2]}" >> $FILE_HOSTINFO
	done
}

getCPULoad() {
	FILE_HOSTINFO="${ISSUE_BASE}/${FILE_BASE}08-cpuload" 
	if (( IGNORE_CPULOAD )); then
		rm -f $FILE_HOSTINFO
#		echo "CPU load ignored in $CONFIG_FILE"
		return 0
	fi
	CPU_COUNT=$(grep '^processor' /proc/cpuinfo 2>/dev/null | wc -l)
	UP_TIME_CNT=3
	SUM=0
	for TIME in $(uptime | awk '{print $(NF-2), $(NF-1), $NF}' | tr -d ',')
	do
		THIS_TIME=$(cut -d\. -f1 <<< $TIME)
		SUM=$(( $SUM + $THIS_TIME ))
	done
	AVG=$(( $SUM / $UP_TIME_CNT ))
	PERCENT=$(( $AVG * 100 / $CPU_COUNT ))
	printf "$FORMAT_OUT" "CPU Load Average:" "$AVG (${PERCENT}%) with $CPU_COUNT CPUs" > $FILE_HOSTINFO
}

getSSHKeys() {
	FILE_HOSTINFO="${ISSUE_BASE}/${FILE_BASE}09-sshkeys" 
	if (( IGNORE_SSHKEYS )); then
		rm -f $FILE_HOSTINFO
#		echo "SSH keys ignored in $CONFIG_FILE"
		return 0
	else
		rm -f ${ISSUE_BASE}/60-ssh_host_keys.conf
		echo "SSH Host Keys" > $FILE_HOSTINFO
	fi
	for KEY_FILE in $(find /etc/ssh -name 'ssh_host_*_key' 2>/dev/null)
	do
		KEY=$(ssh-keygen -l -f ${KEY_FILE} | awk '{print $2}')
		TYPE=$(ssh-keygen -l -f ${KEY_FILE} | awk '{print $4}')
		printf "$FORMAT_OUT" " $TYPE:" "$KEY" >> $FILE_HOSTINFO
	done
}

getCustom() {
	BASE="${FILE_BASE}99-custom"
	CUSTOM_LIMIT=9
	LAST=$(find $ISSUE_BASE -type f | sort | grep $BASE | tail -1)
	[[ -n "$LAST" ]] && NUM=$(awk -F- '{print $NF}' <<< $LAST) || NUM=0
	if (( NUM < CUSTOM_LIMIT )); then
		(( NUM++ ))
		FILE_HOSTINFO="${ISSUE_BASE}/${BASE}-${NUM}"
		LABEL="$1"
		VALUE="$2"
		printf "$FORMAT_OUT" "${LABEL}:" "${VALUE}" > $FILE_HOSTINFO
	else
		echo "Error: Custom label exceeds limit of $CUSTOM_LIMIT"
		cleanUp
		exit $ERR_CUSTOM_LIMIT_EXCEEDED
	fi
}

cleanUp() {
	rm -f $UPDATES_TMP
	/usr/sbin/issue-generator
	rm -f $HOSTINFO_PID
}

putMOTD() {
	> $FILE_MOTD
	for FILE in $(ls -1 ${ISSUE_BASE}/${FILE_BASE}* | sort)
	do
		cat $FILE >> $FILE_MOTD
	done
	echo >> $FILE_MOTD
}

deConfigure() {
	rm -f ${ISSUE_BASE}/80-hostinfo-*
	rm -f ${ISSUE_BASE}/00-OS
	rm -f ${ISSUE_BASE}/90-OS
	/usr/sbin/issue-generator
	> $FILE_MOTD
	exit 0
}

##############################################################################
# main
##############################################################################
[[ -s $HOSTINFO_PID ]] && exit $ERR_HOSTINFO_ACTIVE || echo $$ > $HOSTINFO_PID
OPT="$1"
[[ -z $OPT ]] && OPT="all"
[[ -d ${ISSUE_BASE} ]] || mkdir -p ${ISSUE_BASE}
SPACER="${FILE_BASE}00-space"
echo > ${ISSUE_BASE}/${SPACER}
cd ${ISSUE_BASE}
[[ -h 00-OS ]] || ln -sf ${SPACER} 00-OS
[[ -h 90-OS ]] || ln -sf ${SPACER} 90-OS
case $OPT in
	-h|--h*|help) title; getHelp; cleanUp; exit ;;
	dist*) getDate; getDistribution ;;
	date) getDate ;;
	host*) getDate; getHostname ;;
	kern*) getDate; getKernel ;;
	up*) getDate; getUpdates ;;
	net*) getDate; getNetAddr ;;
	mem*) getDate; getMemory ;;
	disk*) getDate; getDisks ;;
	cpu*) getDate; getCPULoad ;;
	ssh*) getDate; getSSHKeys ;;
	Uninstalled) cleanUp; deConfigure ;;
	cust*) getCustom "$2" "$3";;
	all)
		getDistribution
		getDate
		getHostname
		getKernel
		getUpdates
		getNetAddr
		getMemory
		getCPULoad
		getSSHKeys
		getDisks
		;;
	*) title; getHelp; cleanUp; exit ;;
esac
cleanUp
(( INCLUDE_MOTD )) && putMOTD || :

