327 lines
9.0 KiB
Bash
Executable File
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:-}"
|