#!/usr/bin/env bash

# netdata
# real-time performance and health monitoring, done right!
# (C) 2023 Netdata Inc.
# SPDX-License-Identifier: GPL-3.0-or-later
#
# This script is a helper to allow netdata collect tc data.
# tc output parsing has been implemented in C, inside netdata
# This script allows setting names to dimensions.

export PATH="${PATH}:/sbin:/usr/sbin:/usr/local/sbin:/usr/sbin"
export LC_ALL=C

cmd_line="'${0}' $(printf "'%s' " "${@}")"

# -----------------------------------------------------------------------------
# logging

PROGRAM_NAME="$(basename "${0}")"
PROGRAM_NAME="${PROGRAM_NAME/.plugin/}"

# these should be the same with syslog() priorities
NDLP_EMERG=0   # system is unusable
NDLP_ALERT=1   # action must be taken immediately
NDLP_CRIT=2    # critical conditions
NDLP_ERR=3     # error conditions
NDLP_WARN=4    # warning conditions
NDLP_NOTICE=5  # normal but significant condition
NDLP_INFO=6    # informational
NDLP_DEBUG=7   # debug-level messages

# the max (numerically) log level we will log
LOG_LEVEL=$NDLP_INFO

set_log_min_priority() {
  case "${NETDATA_LOG_LEVEL,,}" in
    "emerg" | "emergency")
      LOG_LEVEL=$NDLP_EMERG
      ;;

    "alert")
      LOG_LEVEL=$NDLP_ALERT
      ;;

    "crit" | "critical")
      LOG_LEVEL=$NDLP_CRIT
      ;;

    "err" | "error")
      LOG_LEVEL=$NDLP_ERR
      ;;

    "warn" | "warning")
      LOG_LEVEL=$NDLP_WARN
      ;;

    "notice")
      LOG_LEVEL=$NDLP_NOTICE
      ;;

    "info")
      LOG_LEVEL=$NDLP_INFO
      ;;

    "debug")
      LOG_LEVEL=$NDLP_DEBUG
      ;;
  esac
}

set_log_min_priority

log() {
  local level="${1}"
  shift 1

  [[ -n "$level" && -n "$LOG_LEVEL" && "$level" -gt "$LOG_LEVEL" ]] && return

  systemd-cat-native --log-as-netdata --newline="--NEWLINE--" <<EOFLOG
INVOCATION_ID=${NETDATA_INVOCATION_ID}
SYSLOG_IDENTIFIER=${PROGRAM_NAME}
PRIORITY=${level}
THREAD_TAG=tc-qos-helper
ND_LOG_SOURCE=collector
ND_REQUEST=${cmd_line}
MESSAGE=${*//\\n/--NEWLINE--}

EOFLOG
  # AN EMPTY LINE IS NEEDED ABOVE
}

info() {
  log "$NDLP_INFO" "${@}"
}

warning() {
  log "$NDLP_WARN" "${@}"
}

error() {
  log "$NDLP_ERR" "${@}"
}

fatal() {
  log "$NDLP_ALERT" "${@}"
  exit 1
}

debug() {
  log "$NDLP_DEBUG" "${@}"
}

# -----------------------------------------------------------------------------
# find /var/run/fireqos

# the default
fireqos_run_dir="/var/run/fireqos"

function realdir() {
	local r
	local t
	r="$1"
	t="$(readlink "$r")"

	while [ "$t" ]; do
		r=$(cd "$(dirname "$r")" && cd "$(dirname "$t")" && pwd -P)/$(basename "$t")
		t=$(readlink "$r")
	done

	dirname "$r"
}

if [ ! -d "${fireqos_run_dir}" ]; then

	# the fireqos executable - we will use it to find its config
	fireqos="$(command -v fireqos 2>/dev/null)"

	if [ -n "${fireqos}" ]; then

		fireqos_exec_dir="$(realdir "${fireqos}")"

		if [ -n "${fireqos_exec_dir}" ] && [ "${fireqos_exec_dir}" != "." ] && [ -f "${fireqos_exec_dir}/install.config" ]; then
			LOCALSTATEDIR=
			#shellcheck source=/dev/null
			source "${fireqos_exec_dir}/install.config"

			if [ -d "${LOCALSTATEDIR}/run/fireqos" ]; then
				fireqos_run_dir="${LOCALSTATEDIR}/run/fireqos"
			else
				warning "FireQOS is installed as '${fireqos}', its installation config at '${fireqos_exec_dir}/install.config' specifies local state data at '${LOCALSTATEDIR}/run/fireqos', but this directory is not found or is not readable (check the permissions of its parents)."
			fi
		else
			warning "Although FireQOS is installed on this system as '${fireqos}', I cannot find/read its installation configuration at '${fireqos_exec_dir}/install.config'."
		fi
	else
		warning "FireQOS is not installed on this system. Use FireQOS to apply traffic QoS and expose the class names to netdata. Check https://github.com/netdata/netdata/tree/master/collectors/tc.plugin#tcplugin"
	fi
fi

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

[ -z "${NETDATA_PLUGINS_DIR}" ] && NETDATA_PLUGINS_DIR="$(dirname "${0}")"
[ -z "${NETDATA_USER_CONFIG_DIR}" ] && NETDATA_USER_CONFIG_DIR="/etc/netdata"
[ -z "${NETDATA_STOCK_CONFIG_DIR}" ] && NETDATA_STOCK_CONFIG_DIR="/usr/lib/netdata/conf.d"

plugins_dir="${NETDATA_PLUGINS_DIR}"
tc="$(command -v tc 2>/dev/null)"

# -----------------------------------------------------------------------------
# user configuration

# time in seconds to refresh QoS class/qdisc names
qos_get_class_names_every=120

# time in seconds to exit - netdata will restart the script
qos_exit_every=3600

# what to use? classes or qdiscs?
tc_show="qdisc" # can also be "class"

# -----------------------------------------------------------------------------
# check if we have a valid number for interval

t=${1}
update_every=$((t))
[ $((update_every)) -lt 1 ] && update_every=${NETDATA_UPDATE_EVERY}
[ $((update_every)) -lt 1 ] && update_every=1

# -----------------------------------------------------------------------------
# allow the user to override our defaults

for CONFIG in "${NETDATA_STOCK_CONFIG_DIR}/tc-qos-helper.conf" "${NETDATA_USER_CONFIG_DIR}/tc-qos-helper.conf"; do
	if [ -f "${CONFIG}" ]; then
		info "Loading config file '${CONFIG}'..."
		#shellcheck source=/dev/null
		source "${CONFIG}" || error "Failed to load config file '${CONFIG}'."
	else
		warning "Cannot find file '${CONFIG}'."
	fi
done

case "${tc_show}" in
qdisc | class) ;;

*)
	error "tc_show variable can be either 'qdisc' or 'class' but is set to '${tc_show}'. Assuming it is 'qdisc'."
	tc_show="qdisc"
	;;
esac

# -----------------------------------------------------------------------------
# default sleep function

LOOPSLEEPMS_LASTWORK=0
loopsleepms() {
	sleep "$1"
}

# if found and included, this file overwrites loopsleepms()
# with a high resolution timer function for precise looping.
#shellcheck source=/dev/null
. "${plugins_dir}/loopsleepms.sh.inc"

# -----------------------------------------------------------------------------
# final checks we can run

if [ -z "${tc}" ] || [ ! -x "${tc}" ]; then
	fatal "cannot find command 'tc' in this system."
fi

tc_devices=
fix_names=

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

setclassname() {
	if [ "${tc_show}" = "qdisc" ]; then
		echo "SETCLASSNAME $4 $2"
	else
		echo "SETCLASSNAME $3 $2"
	fi
}

show_tc_cls() {
	[ "${tc_show}" = "qdisc" ] && return 1

	local x="${1}"

	if [ -f /etc/iproute2/tc_cls ]; then
		local classid name rest
		while read -r classid name rest; do
			if [ -z "${classid}" ] ||
				[ -z "${name}" ] ||
				[ "${classid}" = "#" ] ||
				[ "${name}" = "#" ] ||
				[ "${classid:0:1}" = "#" ] ||
				[ "${name:0:1}" = "#" ]; then
				continue
			fi
			setclassname "" "${name}" "${classid}"
		done </etc/iproute2/tc_cls
		return 0
	fi
	return 1
}

show_fireqos_names() {
	local x="${1}" name n interface_dev interface_classes_monitor

	if [ -f "${fireqos_run_dir}/ifaces/${x}" ]; then
		name="$(<"${fireqos_run_dir}/ifaces/${x}")"
		echo "SETDEVICENAME ${name}" || exit

		#shellcheck source=/dev/null
		source "${fireqos_run_dir}/${name}.conf"
		for n in ${interface_classes_monitor}; do
			# shellcheck disable=SC2086
			setclassname ${n//|/ }
		done
		[ -n "${interface_dev}" ] && echo "SETDEVICEGROUP ${interface_dev}" || exit

		return 0
	fi

	return 1
}

show_tc() {
	local x="${1}"

	echo "BEGIN ${x}" || exit

	# netdata can parse the output of tc
	${tc} -s ${tc_show} show dev "${x}"

	# check FireQOS names for classes
	if [ -n "${fix_names}" ]; then
		show_fireqos_names "${x}" || show_tc_cls "${x}"
	fi

	echo "END ${x}" || exit
}

find_tc_devices() {
	local count=0 devs dev rest l

	# find all the devices in the system
	# without forking
	while IFS=":| " read -r dev rest; do
		count=$((count + 1))
		[ ${count} -le 2 ] && continue
		devs="${devs} ${dev}"
	done </proc/net/dev

	# from all the devices find the ones
	# that have QoS defined
	# unfortunately, one fork per device cannot be avoided
	tc_devices=
	for dev in ${devs}; do
		l="$(${tc} class show dev "${dev}" 2>/dev/null)"
		[ -n "${l}" ] && tc_devices="${tc_devices} ${dev}"
	done
}

# update devices and class names
# once every 2 minutes
names_every=$((qos_get_class_names_every / update_every))

# exit this script every hour
# it will be restarted automatically
exit_after=$((qos_exit_every / update_every))

c=0
gc=0
while true; do
	fix_names=
	c=$((c + 1))
	gc=$((gc + 1))

	if [ ${c} -le 1 ] || [ ${c} -ge ${names_every} ]; then
		c=1
		fix_names="YES"
		find_tc_devices
	fi

	for d in ${tc_devices}; do
		show_tc "${d}"
	done

	echo "WORKTIME ${LOOPSLEEPMS_LASTWORK}" || exit

	loopsleepms "${update_every}"

	[ ${gc} -gt ${exit_after} ] && exit 0
done
