#!/usr/local/bin/cbsd
#v12.0.9
CBSDMODULE="node"
MYARG="mode"
MYOPTARG="allinfo cbsdkeyfile display header ip node port pw rootkeyfile"
MYDESC="Manipulate or show information for remote nodes"
ADDHELP="
${H3_COLOR}Description${N0_COLOR}:

This script forces two CBSD nodes to exchange private keys and thus provide each other 
with password-less access from the 'cbsd' user (who, in fact, has root privileges!) 
through the OpenSSH service.

This is one of the mechanisms to build a cluster (a group of interconnected computers 
performing a common function), as opposed to the API-based method.

Connectivity via SSH (peer-to-peer network) and interaction via API 
(single point of failure/controller one-to-many) are opposite solutions in architecture. 

Each method has its own pros and cons. By default, script use ssh public/private key 
from the ~cbsd/.ssh directory.

Before adding remote nodes, make sure you copy the public key as 'authorized_keys' on
remote host:

  cp -a ~cbsd/.ssh/id_rsa.pub ~cbsd/.ssh/authorized_keys

Also, the private key of the remote node will be saved in the ~cbsd/.rssh directory.

${H3_COLOR}Options${N0_COLOR}:

 ${N2_COLOR}allinfo=${N0_COLOR}      - 1 (default) show all info for nodelist, 0 - only nodename
 ${N2_COLOR}cbsdkeyfile=${N0_COLOR}  - alternative path private key.
 ${N2_COLOR}display=${N0_COLOR}      - list by comma for column. 
                 default: nodename,ip,port,keyfile,status,idle
 ${N2_COLOR}header=${N0_COLOR}       - print header in node list
 ${N2_COLOR}ip=${N0_COLOR}           - IP address for update
 ${N2_COLOR}mode=${N0_COLOR}         - add,remove,list,show,update,status - show or modify details about node
 ${N2_COLOR}node=${N0_COLOR}         - remote node ip or fqdn
 ${N2_COLOR}port=${N0_COLOR}         - 22 (default), alternative ssh port of remote node
 ${N2_COLOR}pw=${N0_COLOR}           - password of cbsd user from remote node (when cbsdkeyfile= is not used)
 ${N2_COLOR}rootkeyfile=${N0_COLOR}  - path to id_rsa for root access

${H3_COLOR}Examples${N0_COLOR}:

 # cbsd node mode=add node=10.0.0.7

${H3_COLOR}See also${N0_COLOR}:

 cbsd rexe --help

"

EXTHELP="wf_node"

. ${subrdir}/nc.subr

readconf node.conf

. ${cbsdinit}

. ${nodes}

[ -z "${display}" ] && display="nodename,ip,port,keyfile,status,idle"

#remove commas for loop action on header
mydisplay=$( echo ${display} | ${TR_CMD} ',' '  ' )

# upper for header
myheader=$( echo ${mydisplay} | ${TR_CMD} '[:lower:]' '[:upper:]' )

cluster_nodes_show()
{
	# todo: figure out why output is different like this
	cmd node ls && return 0
}

show_header()
{
	local _header="${H1_COLOR}${BOLD}${myheader}${N0_COLOR}"
	[ ${header} -eq 1 ] && ${ECHO} "${_header}"
}

getpw()
{
	local oldmodes=$( ${STTY_CMD} -g )
	pw=""

	trap "${STTY_CMD} ${oldmodes}" HUP INT ABRT BUS TERM EXIT

	printf "${BOLD}Enter password of cbsd user on ${N2_COLOR}${node}${N0_COLOR}${BOLD}:${N0_COLOR} "
	while [ -z "${pw}" ]; do
		${STTY_CMD} -echo
		set -e
		read pw
		set +e
	done

	${STTY_CMD} ${oldmodes}
	echo
}

nodeadd()
{
	local _res= _ret=
	local _myip=

	[ -z "${node}" ] && err 1 "${N1_COLOR}Empty node${N0_COLOR}"
	[ -z "${port}" ] && port=22
	if [ -z "${cbsdkeyfile}" ]; then
		[ -z "${pw}" ] && getpw
	else
		[ ! -r "${cbsdkeyfile}" ] && err 1 "${N1_COLOR}node: no such file here: ${N2_COLOR}${cbsdkeyfile}${N0_COLOR}"
	fi
	[ -z "${rootkeyfile}" ] && rootkeyfile="/root/.ssh/id_rsa"

	iptype ${node} >/dev/null 2>&1
	_ret=$?

	case ${_ret} in
		1|2)
			# ip4 or ip6
			_myip="${node}"
			;;
		*)
			_myip=$( resolvhost ${node} )
			[ -z "${_myip}" ] && err 1 "${N1_COLOR}Can't resolv IP for ${node} hostname. Use IP address.${N0_COLOR}"
			node=${_myip}
			;;
	esac

	iptype ${_myip} >/dev/null 2>&1
	_ret=$?

	# if natip is not valid IPv4, assume it is NIC variable.
	# so try to find out first IPv4 for aliasing
	case ${_ret} in
		1)
			if [ -n "${cbsdkeyfile}" ]; then
				# ssh key based
				CBSD_SSH_CMD="${SSH_CMD} -i ${cbsdkeyfile} -oStrictHostKeyChecking=no -oUserKnownHostsFile=/dev/null -oBatchMode=yes -oPort=${port} -l ${cbsduser} ${node}"
			else
				# pw based auth
				CBSD_SSH_CMD="cbsdssh ${node} ${port} ${cbsduser} ${pw}"
			fi
			proto="IPv4"
			;;
		2)
			if [ -n "${cbsdkeyfile}" ]; then
				# ssh key based
				CBSD_SSH_CMD="${SSH_CMD} -6 -i ${cbsdkeyfile} -oStrictHostKeyChecking=no -oUserKnownHostsFile=/dev/null -oBatchMode=yes -oPort=${port} -l ${cbsduser} ${node}"
			else
				CBSD_SSH_CMD="cbsdssh6 ${node} ${port} ${cbsduser} ${pw}"
			fi
			proto="IPv6"
			;;
		*)
			err 1 "${N1_COLOR}Unknown IP type: ${N2_COLOR}${_myip}${N0_COLOR}"
		;;
	esac

	${ECHO} "${N1_COLOR}Connecting to ${node} via ${proto}...${N0_COLOR}"
	NODENAME=$( ${CBSD_SSH_CMD} /usr/local/bin/cbsd getinfo -q nodename )
	_ret=$?

	cbsdlogger NOTICE ${CBSD_APP}: remote node getinfo: ${CBSD_SSH_CMD}

	case ${_ret} in
		0)
			${ECHO} "${N1_COLOR}${node} has nodename: ${N2_COLOR}${NODENAME}${N0_COLOR}"
			;;
		2)
			log_err 1 "${N1_COLOR}Bad password or system user: ${NODENAME}${N0_COLOR}"
			;;
		*)
			log_err 1 "${N1_COLOR}Connection problem (_ret ${_ret}): ${N2_COLOR}${NODENAME}${N0_COLOR}"
			;;
	esac

	[ -z "${NODENAME}" ] && err 1 "${N1_COLOR}No nodename found. Check remote cbsd settings${N0_COLOR}"

	MD5NAME=$( ${miscdir}/cbsd_md5 "${NODENAME}" )
	if [ -n "${cbsdkeyfile}" ]; then
		nodeaddkey md5name=${MD5NAME} ip=${node} port=${port} cbsdkeyfile=${cbsdkeyfile} > ${DEBLOG} 2>&1
	else
		nodeaddkey md5name=${MD5NAME} ip=${node} port=${port} pw=${pw} > ${DEBLOG} 2>&1
	fi
	_res=$?

	case ${_res} in
		0)
			true
			;;
		1)
			if [ -r ${DEBLOG} ]; then
				${CAT_CMD} ${DEBLOG}
				${RM_CMD} ${DEBLOG}
			fi
			err ${_res} "${N1_COLOR}Error: Bad password${N0_COLOR}"
			;;
		2)
			if [ -r ${DEBLOG} ]; then
				${CAT_CMD} ${DEBLOG}
				${RM_CMD} ${DEBLOG}
			fi
			err ${_res} "${N1_COLOR}Error: No key found or wrong hostname. Please run: 'cbsd initenv' on remote machine${N0_COLOR}"
			;;
		*)
			if [ -r ${DEBLOG} ]; then
				${CAT_CMD} ${DEBLOG}
				${RM_CMD} ${DEBLOG}
			fi
			err ${_res} "${N1_COLOR}Error: Unkown error${N0_COLOR}"
			;;
	esac

	[ -r ${DEBLOG} ] && ${RM_CMD} ${DEBLOG}

	# Copy remote .pub into authorized_keys
	printf "${N1_COLOR}Update ~cbsd/.ssh/authorized_keys...${N0_COLOR}"
	${CBSD_SSH_CMD} ${CP_CMD} -a .ssh/id_rsa.pub .ssh/authorized_keys >/dev/null 2>&1
	_ret=$?

	if [ ${_ret} -ne 0 ]; then
		err 1 "failed: ${CP_CMD} -a .ssh/id_rsa.pub authorized_keys"
	fi

	${CAT_CMD} ${DEBLOG}
	${ECHO} "${W1_COLOR}>>>   !Warning! Please note! !Warning!${N0_COLOR}"
	${ECHO} "${H5_COLOR}Having a ~cbsd/.ssh/authorized_keys file on ${NODENAME}, anyone who gets private key (~cbsd/.ssh/id_rsa) will have the access from cbsd user!${N0_COLOR}"
	${ECHO} "${H5_COLOR}In a multi-node configuration your commands can be run on remote servers.${N0_COLOR}"
	${ECHO} "${H5_COLOR}Be careful with [xjb]remove-like command.${N0_COLOR}"
	${ECHO} "${W1_COLOR}<<<   !Warning! Please note! !Warning!${N0_COLOR}"
	LOCALKEY="${rsshdir}/${MD5NAME}.id_rsa"
	${TOUCH_CMD} ${rsshdir}/${NODENAME}.node
	/usr/local/cbsd/misc/cbsdsysrc -qf ${rsshdir}/${NODENAME}.node SSHKEY=${LOCALKEY} IP=${node} PORT=${port} > /dev/null
	${CHOWN_CMD} ${cbsduser} ${rsshdir}/${NODENAME}.node
	IP=$( cbsdsqlro nodes "SELECT ip FROM nodelist WHERE nodename='${NODENAME}'" )
	external_exec_master_node_script -a add.d -n ${NODENAME} -i ${node} -p ${port} -k ${LOCALKEY}
	if [ -z "${IP}" ]; then
		cbsdsqlrw nodes "INSERT INTO nodelist ( nodename, ip, port, keyfile, rootkeyfile, invfile ) VALUES ( '${NODENAME}', '${node}', '${port}', '${LOCALKEY}', '${rootkeyfile}', 'inv.${NODENAME}.sqlite' )"
	else
		${ECHO} "${N1_COLOR}Already exist in database, updating: ${N2_COLOR}${node}${N0_COLOR}"
		cbsdsqlrw nodes "DELETE FROM nodelist WHERE nodename='${NODENAME}'"
		cbsdsqlrw nodes "INSERT INTO nodelist ( nodename, ip, port, keyfile, rootkeyfile, invfile ) VALUES ( '${NODENAME}', '${node}', '${port}', '${LOCALKEY}', '${rootkeyfile}', 'inv.${NODENAME}.sqlite' )"
	fi
	idle_update ${NODENAME}
	retrinv node=${NODENAME} tryoffline=1
	return ${_res}
}

nodedel()
{
	local _descext="descr role domain notes location" _res

	[ -z "${node}" ] && err 1 "${N1_COLOR}Empty node${N0_COLOR}"
	NODECONF="${rsshdir}/${node}.node"

	if [ -f "${NODECONF}" ]; then
		. ${NODECONF}
		[ -f ${SSHKEY} ] && ${RM_CMD} -f ${SSHKEY}
		${RM_CMD} -f ${NODECONF}
	else
		${ECHO} "${N1_COLOR}No such node config: ${N2_COLOR}${NODECONF}${N0_COLOR}"
	fi

	[ -f "${dbdir}/${node}.sqlite" ] && ${RM_CMD} -f "${dbdir}/${node}.sqlite"
	_res=$( cbsdsqlrw nodes DELETE FROM nodelist WHERE nodename=\"${node}\" )

	#descriptions die too
	${FIND_CMD} ${dbdir}/nodedescr -mindepth 1 -maxdepth 1 -name ${node}.\*.descr -type f -delete
	for i in ${_descext}; do
		[ -f "${dbdir}/nodedescr/${node}.${i}" ] && ${RM_CMD} -f "${dbdir}/nodedescr/${node}.${i}"
	done
	err 0 "${N1_COLOR}Removed${N0_COLOR}"
}

show_nodes()
{
	if [ "${mod_cbsd_cluster_enabled}" = "YES" -a -z "${MOD_CBSD_CLUSTER_DISABLED}" ]; then
		cluster_nodes_show && return
	fi

	OIFS=${IFS}
	local sqldelimer IFS


	if [ ${allinfo} -eq 0 ]; then
		cbsdsqlro nodes SELECT nodename FROM nodelist
	else
		show_header
		sqldelimer="|"
		IFS="|"
		cbsdsqlro nodes SELECT nodename,ip,port,keyfile,idle FROM nodelist | while read nodename ip port keyfile idle; do
			IFS=${OIFS}

			conv_idle "${idle}"

			for _i in ${mydisplay}; do
				_val=""

				eval _val="\$$_i"
				[ -z "${_val}" ] && _val="-"

				if [ -z "${_status}" ]; then
					_status="${N0_COLOR}${N2_COLOR}${_val}"
				else
					_status="${_status} ${_val}"
				fi
			done

			${ECHO} "${_status}${N0_COLOR}"
			IFS="|"
			_status=
		done
	fi
	unset sqldelimer
}

shownode() {
	local _res

	local sqldelimer=" "
	_res=$( cbsdsqlro nodes "SELECT nodename FROM nodelist WHERE nodename='${node}'" )

	[ -z "${_res}" ] && err 1 "${N1_COLOR}No such node in databases: ${N2_COLOR}${node}${N0_COLOR}"

	${ECHO} "${BOLD}  Summary statistics for ${node}  ${N0_COLOR}"
	${ECHO} "${BOLD}  ====================================  ${N0_COLOR}"
	echo

	if [ ! -f "${dbdir}/${node}.sqlite" ]; then
		${ECHO} "${N1_COLOR}No such inventory for: ${N2_COLOR}${node}${N0_COLOR}. ${N1_COLOR}Try to obtain it...${N0_COLOR}"
		retrinv node=${node} tryoffline=1
		[ ! -f "${dbdir}/${node}.sqlite" ] && err 1 "${N1_COLOR}No such inventory for: ${N2_COLOR}${node}${N0_COLOR}"
	fi

	local _nodeinv="hostname,nodeip,fs,ncpu,physmem,freemem,disks,cpumode,cpufreq,nics,osrelease,hostarch"

	sqldelimer="|"

	eval $( cbsdsqlro ${node} SELECT ${_nodeinv} FROM local | while read hostname nodeip fs ncpu physmem freemem disks cpumodel cpufreq nics osrelease hostarch; do
		echo "local hostname=\"\${hostname}\""
		echo "local nodeip=\"\${nodeip}\""
		echo "local fs=\"${fs}\""
		echo "local ncpu=\"${ncpu}\""
		echo "local physmem=\"${physmem}\""
		echo "local freemem=\"${freemem}\""
		echo "local disks=\"${disks}\""
		echo "local cpumodel=\"${cpumodel}\""
		echo "local cpufreq=\"${cpufreq}\""
		echo "local nics=\"${nics}\""
		echo "local osrelease=\"${osrelease}\""
		echo "local hostarch=\"${hostarch}\""
	done )

	sqldelimer=" "

	local jailsnum=$( cbsdsqlro ${node} "SELECT count\(jname\) FROM jails WHERE emulator != 'bhyve'" )
	local vmsnum=$( cbsdsqlro ${node} "SELECT count\(jname\) FROM jails WHERE emulator = 'bhyve'" )

	[ -z "${jailsnum}" ] && jailsnum="0"
	[ -z "${vmsnum}" ] && vmsnum="0"

	${ECHO} "${BOLD}Nodename: ${N2_COLOR}${node}${N0_COLOR}"
	${ECHO} "${BOLD}Hostname: ${N2_COLOR}${hostname}${N0_COLOR}"
	${ECHO} "${BOLD}Node IP: ${N2_COLOR}${nodeip}${N0_COLOR}"
	${ECHO} "${BOLD}FS: ${N2_COLOR}${fs}${N0_COLOR}"
	${ECHO} "${BOLD}Total core's: ${N2_COLOR}${ncpu}${N0_COLOR}"
	${ECHO} "${BOLD}CPU Model: ${N2_COLOR}${cpumode}${N0_COLOR}"
	${ECHO} "${BOLD}CPU Frequency: ${N2_COLOR}${cpufrq}${N0_COLOR}"
	${ECHO} "${BOLD}Total physmem: ${N2_COLOR}${physmem}${N0_COLOR}"
	${ECHO} "${BOLD}Free mem: ${N2_COLOR}${freemem}${N0_COLOR}"
	${ECHO} "${BOLD}NICs: ${N2_COLOR}${nics}${N0_COLOR}"
	echo
	${ECHO} "${BOLD}OS Release: ${N2_COLOR}${osrelease}${N0_COLOR}"
	${ECHO} "${BOLD}OS arch: ${N2_COLOR}${hostarch}${N0_COLOR}"
	${ECHO} "${BOLD}Total VMs: ${N2_COLOR}${vmsnum}${N0_COLOR}"
	${ECHO} "${BOLD}Total Jails: ${N2_COLOR}${jailsnum}${N0_COLOR}"
	echo
	[ ${jailsnum} -ne 0 ] && ${ECHO} "${BOLD}List of ${node} jails:${N0_COLOR}" && jls node=${node} display=jname,host_hostname,status && echo
	[ ${vmsnum} -ne 0 ] && ${ECHO} "${BOLD}List of ${node} VMs:${N0_COLOR}" && bls node=${node} display=jname,status && echo
}

update_node()
{
	local _port _ip

	while getopts "i:p:" opt; do
		case "${opt}" in
			p)
				_port="${OPTARG}"
				cbsdsqlrw nodes UPDATE nodelist SET port=\"${_port}\" WHERE nodename=\"${node}\"
			;;
			i)
				_ip="${OPTARG}"
				cbsdsqlrw nodes UPDATE nodelist SET ip=\"${_ip}\" WHERE nodename=\"${node}\"
			;;
		esac
		shift $(($OPTIND - 1))
	done

	return 0
}

# MAIN
curtime=$( ${DATE_CMD} +%s )

[ -z "${allinfo}" ] && allinfo=1
[ -z "${header}" ] && header=1

case "${mode}" in
	"add")
		nodeadd
		;;
	"list")
		show_nodes | ${COLUMN_CMD} -t
		;;
	"remove")
		nodedel
		;;
	"show")
		[ -z "${node}" ] && err 1 "${N1_COLOR}Please specify: ${N2_COLOR}node=${N0_COLOR}"
		shownode
		;;
	"update")
		[ -z "${node}" ] && err 1 "${N1_COLOR}Please specify ${N2_COLOR}node=${N0_COLOR}"
		modified=0
		if [ -n "${port}" ]; then
			update_node -p "${port}"
			modified=1
		fi
		if [ -n "${ip}" ]; then
			update_node -i "${ip}"
			modified=1
		fi
		if [ ${modified} -eq 1 ]; then
			idle_update ${node} > /dev/null 2>&1
			retrinv node=${node} tryoffline=1
		else
			err 1 "${N1_COLOR}Please specify ${N2_COLOR}ip=${N1_COLOR} or ${N2_COLOR}port=${N1_COLOR} to update${N0_COLOR}"
		fi
		;;
	*)
		err 1 "${N1_COLOR}Unknown mode${N0_COLOR}"
		;;
esac

exit 0
