Repository
Munin (contrib)
Last change
2021-07-19
Graph Categories
Capabilities
Keywords
Language
Shell
License
GPL-3.0-or-later
Authors

internode_usage

Example graph: month

Name

internode_usage - Plugin to monitor quota usage of an Internode service

The ideal usage is also used as an updated warning limit.

Configuration

[internode_usage]
  env.internode_api_login LOGIN
  env.internode_api_password PASSWORD

You can display the graph on another host (e.g., the actual router) than the one running munin. To do so, first configure the plugin to use a different hostname.

env.host_name router

Then configure munin (in /etc/munin/munin-conf or /etc/munin/munin-conf.d), to support a new host.

[example.net;router]
  address 127.0.0.1
  use_node_name no

An optional ‘env.internode_api_url’ can be used, but should not be needed. It will default to https://customer-webtools-api.internode.on.net/api/v1.5.

If multiple services are available, the plugin will automatically pick the first service from the list. To monitor other services, the plugin can be used multiple times, by symlinking it as ‘internode_usage_SERVICEID’.

Caching

As the API is sometimes flakey, the initial service information is cached locally, with a day’s lifetime, before hitting the base API again. However, if hitting the API to refresh the cache fails, the stale cache is used anyway, to have a better chance of getting the data out nonetheless.

Caveats

* The hourly rate are a bit spikey in the -day view, as the API seems to update every 20 to 30 minutes; it is fine in the -month and more aggregated views * The daily rate is the _previous_ day, and does always lag by 24h. * Due to the way the API seems to update the data, values for the daily rate are missing for a short period every day. This may not play very well with spoolfetch.

Author

Olivier Mehani

Copyright (C) 2019–2021 Olivier Mehani <shtrom+munin@ssji.net>

License

SPDX-License-Identifier: GPL-3.0-or-later

#!/bin/sh -eu
# -*- sh -*-

: << =cut

=head1 NAME

internode_usage - Plugin to monitor quota usage of an Internode service

The ideal usage is also used as an updated warning limit.

=head1 CONFIGURATION

  [internode_usage]
    env.internode_api_login LOGIN
    env.internode_api_password PASSWORD

You can display the graph on another host (e.g., the actual router) than the
one running munin. To do so, first configure the plugin to use a different
hostname.

    env.host_name router

Then configure munin (in /etc/munin/munin-conf or /etc/munin/munin-conf.d), to
support a new host.

  [example.net;router]
    address 127.0.0.1
    use_node_name no

An optional 'env.internode_api_url' can be used, but should not be needed.  It
will default to  https://customer-webtools-api.internode.on.net/api/v1.5.

If multiple services are available, the plugin will automatically pick the first
service from the list. To monitor other services, the plugin can be used
multiple times, by symlinking it as 'internode_usage_SERVICEID'.

=head1 CACHING

As the API is sometimes flakey, the initial service information is cached
locally, with a day's lifetime, before hitting the base API again. However,
if hitting the API to refresh the cache fails, the stale cache is used anyway,
to have a better chance of getting the data out nonetheless.

=head1 CAVEATS

* The hourly rate are a bit spikey in the -day view, as the API seems to update
  every 20 to 30 minutes; it is fine in the -month and more aggregated views
* The daily rate is the _previous_ day, and does always lag by 24h.
* Due to the way the API seems to update the data, values for the daily rate
  are missing for a short period every day. This may not play very well with
	spoolfetch.

=head1 AUTHOR

Olivier Mehani

Copyright (C) 2019--2021 Olivier Mehani <shtrom+munin@ssji.net>

=head1 LICENSE

SPDX-License-Identifier: GPL-3.0-or-later

=cut

# shellcheck disable=SC1090
. "${MUNIN_LIBDIR:-.}/plugins/plugin.sh"

CURL_ARGS='-s'
if [ "${MUNIN_DEBUG:-0}" = 1 ]; then
    CURL_ARGS='-v'
    set -x
fi

if ! command -v curl >/dev/null; then
	echo "curl not found" >&2
	exit 1
fi
if ! command -v xpath >/dev/null; then
	echo "xpath (Perl XML::LibXML) not found" >&2
	exit 1
fi
if ! command -v bc >/dev/null; then
	echo "bc not found" >&2
	exit 1
fi

if [ -z "${internode_api_url:-}" ]; then
    internode_api_url="https://customer-webtools-api.internode.on.net/api/v1.5"
fi

xpath_extract() {
	# shellcheck disable=SC2039
	local xpath="$1"
	# shellcheck disable=SC2039
	local node="$(xpath -q -n -e "${xpath}")" \
		|| { echo "error extracting ${xpath}" >&2; false; }
	echo "${node}" | sed 's/<\([^>]*\)>\([^<]*\)<[^>]*>/\2/;s^N/A^U^'
}

xpath_extract_attribute() {
	# shellcheck disable=SC2039
	local xpath="$1"
	# shellcheck disable=SC2039
	local node="$(xpath -q -n -e "${xpath}")" \
		|| { echo "error extracting attribute at ${xpath}" >&2; false; }
	echo "${node}" | sed 's/.*="\([^"]\+\)".*/\1/'
}

fetch() {
	# shellcheck disable=SC2154
	curl -u "${internode_api_login}:${internode_api_password}" -f ${CURL_ARGS} "$@" \
		|| { echo "error fetching ${*} for user ${internode_api_login}" >&2; false; }
}

get_cached_api() {
	# shellcheck disable=SC2039
	local url=${1}
	# shellcheck disable=SC2039
	local name=${2}
	# shellcheck disable=SC2039
	local api_data=''
	# shellcheck disable=SC2039
	local cachefile="${MUNIN_PLUGSTATE}/$(basename "${0}").${name}.cache"
	if [ -n "$(find "${cachefile}" -mmin -1440 2>/dev/null)" ]; then
		api_data=$(cat "${cachefile}")
	else
		api_data="$(fetch "${url}" \
			|| true)"

		if [ -n "${api_data}" ]; then
			echo "${api_data}" > ${cachefile}
		else
			echo "using ${name} info from stale cache ${cachefile}" >&2
			api_data=$(cat "${cachefile}")
		fi
	fi
	echo "${api_data}"
}

get_service_data() {
	# Determine the service ID from the name of the symlink
	SERVICE_ID="$(echo "${0}" | sed -n 's/^.*internode_usage_//p')"
	if [ -z "${SERVICE_ID}" ]; then
		# Otherwise, get the first service in the list
		API_XML="$(get_cached_api ${internode_api_url} API_XML)"
		if [ -z "${API_XML}" ]; then
			echo "unable to determine service ID for user ${internode_api_login}" >&2
			exit 1
		fi
		SERVICE_ID="$(echo "${API_XML}" | xpath_extract "internode/api/services/service")"
	fi


	CURRENT_TIMESTAMP="$(date +%s)"
	SERVICE_USERNAME='n/a'
	SERVICE_QUOTA='n/a'
	SERVICE_PLAN='n/a'
	SERVICE_ROLLOVER='n/a'
	IDEAL_USAGE=''
	USAGE_CRITICAL=''
	SERVICE_XML="$(get_cached_api "${internode_api_url}/${SERVICE_ID}/service" SERVICE_XML \
		|| true)"
	if [ -n "${SERVICE_XML}" ]; then
		SERVICE_USERNAME="$(echo "${SERVICE_XML}" | xpath_extract "internode/api/service/username")"
		SERVICE_QUOTA="$(echo "${SERVICE_XML}" | xpath_extract "internode/api/service/quota")"
		SERVICE_PLAN="$(echo "${SERVICE_XML}" | xpath_extract "internode/api/service/plan")"
		SERVICE_ROLLOVER="$(echo "${SERVICE_XML}" | xpath_extract "internode/api/service/rollover")"
		SERVICE_INTERVAL="$(echo "${SERVICE_XML}" | xpath_extract "internode/api/service/plan-interval" | sed 's/ly$//')"

		FIRST_DAY="$(date +%s --date "${SERVICE_ROLLOVER} -1 ${SERVICE_INTERVAL}")"
		LAST_DAY="$(date +%s --date "${SERVICE_ROLLOVER}")"
		BILLING_PERIOD="(${LAST_DAY}-${FIRST_DAY})"
		IDEAL_USAGE="$(echo "${SERVICE_QUOTA}-(${SERVICE_QUOTA}*(${LAST_DAY}-${CURRENT_TIMESTAMP})/${BILLING_PERIOD})" | bc -q)"

		USAGE_CRITICAL="${SERVICE_QUOTA}"
	fi

}

get_data() {
	DAILY_TIMESTAMP=N
	DAILY_USAGE=U
	HISTORY_XML="$(fetch "${internode_api_url}/${SERVICE_ID}/history" \
		|| true)"
	if [ -n "${HISTORY_XML}" ]; then
		DAILY_USAGE="$(echo "${HISTORY_XML}" | xpath_extract "internode/api/usagelist/usage[last()-1]/traffic")"
		DAILY_DATE="$(echo "${HISTORY_XML}" | xpath_extract_attribute "internode/api/usagelist/usage[last()-1]/@day")"
		DAILY_TIMESTAMP="$(date -d "${DAILY_DATE} $(date +%H:%M:%S)" +%s \
			|| echo N)"
	fi

	SERVICE_USAGE='U'
	USAGE_XML="$(fetch "${internode_api_url}/${SERVICE_ID}/usage" \
		|| true)"
	if [ -n "${USAGE_XML}" ]; then
		SERVICE_USAGE="$(echo "${USAGE_XML}" | xpath_extract "internode/api/traffic")"


	fi
}

graph_config() {
	graph=""
	if [ -n "${1:-}" ]; then
		graph=".$1"
	fi

	echo "multigraph internode_usage_${SERVICE_ID}${graph}"

	case "$graph" in
		.current)
			echo "graph_title Uplink usage rate (hourly)"
			echo "graph_info Username: ${SERVICE_USERNAME}; Service ID: ${SERVICE_ID}; Plan: ${SERVICE_PLAN}"
			echo 'graph_category network'
			# ${graph_period} is not a shell variable
			# shellcheck disable=SC2016
			echo 'graph_vlabel bytes per ${graph_period}'
			# XXX: this seems to be updated twice per hour;
			# the data from this graph may be nonsense
			echo 'graph_period hour'

			echo "hourly_rate.label Hourly usage"
			echo "hourly_rate.type DERIVE"
			echo "hourly_rate.min 0"

			;;
		.daily)
			echo "graph_title Uplink usage rate (daily)"
			echo "graph_info Username: ${SERVICE_USERNAME}; Service ID: ${SERVICE_ID}; Plan: ${SERVICE_PLAN}"
			echo "graph_info Uplink usage rate (daily)"
			echo 'graph_category network'
			# ${graph_period} is not a shell variable
			# shellcheck disable=SC2016
			echo 'graph_vlabel bytes per ${graph_period}'
			echo 'graph_period day'

			echo "daily_rate.label Previous-day usage"
			echo "daily_rate.type GAUGE"

			;;
		'')
			echo "graph_title Uplink usage"
			echo "graph_info Username: ${SERVICE_USERNAME}; Service ID: ${SERVICE_ID}; Plan: ${SERVICE_PLAN}"
			echo 'graph_category network'
			echo 'graph_vlabel bytes'
			echo 'graph_period hour'
			echo 'graph_order root_usage=usage.usage root_ideal=usage.ideal'

			echo "root_usage.label Total usage"
			echo "root_usage.draw AREA"
			echo "root_ideal.extinfo Quota rollover: ${SERVICE_ROLLOVER}"
			echo "root_ideal.label Ideal usage"
			echo "root_ideal.draw LINE2"
			echo "root_ideal.colour FFA500"
			;;
		*)
			echo "graph_title Uplink usage"
			echo "graph_info Username: ${SERVICE_USERNAME}; Service ID: ${SERVICE_ID}; Plan: ${SERVICE_PLAN}"
			echo 'graph_category network'
			echo 'graph_vlabel bytes'
			echo 'graph_period hour'

			echo "usage.label Total usage"
			echo "usage.draw AREA"
			echo "ideal.extinfo Quota rollover: ${SERVICE_ROLLOVER}"
			echo "ideal.label Ideal usage"
			echo "ideal.draw LINE2"
			echo "ideal.colour FFA500"

			echo "usage.critical ${USAGE_CRITICAL}"
			echo "usage.warning ${IDEAL_USAGE}"
			;;
	esac
	echo
}

graph_data() {
	graph=""
	if [ -n "${1:-}" ]; then
		graph=".${1}"
	fi

	echo "multigraph internode_usage_${SERVICE_ID}${graph}"
	case "${graph}" in
		.current)
			echo "hourly_rate.value ${CURRENT_TIMESTAMP}:${SERVICE_USAGE:-U}"
			;;
		.daily)
			echo "daily_rate.value ${DAILY_TIMESTAMP}:${DAILY_USAGE:-U}"
			;;
		'')
			# Nothing to do: all values loaned from the traffic graph
			;;
		*)
			echo "usage.value ${CURRENT_TIMESTAMP}:${SERVICE_USAGE:-U}"
			echo "ideal.value ${CURRENT_TIMESTAMP}:${IDEAL_USAGE:-U}"
			;;
	esac
	echo
}

main() {
	case ${1:-} in
		config)
			if [ -n "${host_name:-}" ]; then
				echo "host_name ${host_name}"
			fi
			graph_config ''
			graph_config usage
			graph_config daily
			graph_config current
			;;
		*)
			get_data
			graph_data ''
			graph_data usage
			graph_data daily
			graph_data current
			;;
	esac
}

get_service_data

main "${1:-}"