munin-contrib/plugins/isp/internode_usage

327 lines
9.0 KiB
Bash
Executable File

#!/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:-}"