1#!/bin/sh -eu 2# -*- sh -*- 3 4: << =cut 5 6=head1 NAME 7 8internode_usage - Plugin to monitor quota usage of an Internode service 9 10The ideal usage is also used as an updated warning limit. 11 12=head1 CONFIGURATION 13 14 [internode_usage] 15 env.internode_api_login LOGIN 16 env.internode_api_password PASSWORD 17 18You can display the graph on another host (e.g., the actual router) than the 19one running munin. To do so, first configure the plugin to use a different 20hostname. 21 22 env.host_name router 23 24Then configure munin (in /etc/munin/munin-conf or /etc/munin/munin-conf.d), to 25support a new host. 26 27 [example.net;router] 28 address 127.0.0.1 29 use_node_name no 30 31An optional 'env.internode_api_url' can be used, but should not be needed. It 32will default to https://customer-webtools-api.internode.on.net/api/v1.5. 33 34If multiple services are available, the plugin will automatically pick the first 35service from the list. To monitor other services, the plugin can be used 36multiple times, by symlinking it as 'internode_usage_SERVICEID'. 37 38=head1 CACHING 39 40As the API is sometimes flakey, the initial service information is cached 41locally, with a day's lifetime, before hitting the base API again. However, 42if hitting the API to refresh the cache fails, the stale cache is used anyway, 43to have a better chance of getting the data out nonetheless. 44 45=head1 CAVEATS 46 47* The hourly rate are a bit spikey in the -day view, as the API seems to update 48 every 20 to 30 minutes; it is fine in the -month and more aggregated views 49* The daily rate is the _previous_ day, and does always lag by 24h 50* Due to the way the API seems to update the data, values for the daily rate 51 are missing for a short period every day 52 53=head1 AUTHOR 54 55Olivier Mehani 56 57Copyright (C) 2019 Olivier Mehani <shtrom+munin@ssji.net> 58 59=head1 LICENSE 60 61SPDX-License-Identifier: GPL-3.0-or-later 62 63=cut 64 65# shellcheck disable=SC1090 66. "${MUNIN_LIBDIR:-.}/plugins/plugin.sh" 67 68CURL_ARGS='-s' 69if [ "${MUNIN_DEBUG:-0}" = 1 ]; then 70 CURL_ARGS='-v' 71 set -x 72fi 73 74if ! command -v curl >/dev/null; then 75 echo "curl not found" >&2 76 exit 1 77fi 78if ! command -v xpath >/dev/null; then 79 echo "xpath (Perl XML::LibXML) not found" >&2 80 exit 1 81fi 82if ! command -v bc >/dev/null; then 83 echo "bc not found" >&2 84 exit 1 85fi 86 87if [ -z "${internode_api_url:-}" ]; then 88 internode_api_url="https://customer-webtools-api.internode.on.net/api/v1.5" 89fi 90 91xpath_extract() { 92 # shellcheck disable=SC2039 93 local xpath="$1" 94 # shellcheck disable=SC2039 95 local node="$(xpath -q -n -e "${xpath}")" \ 96 || { echo "error extracting ${xpath}" >&2; false; } 97 echo "${node}" | sed 's/<\([^>]*\)>\([^<]*\)<[^>]*>/\2/;s^N/A^U^' 98} 99 100xpath_extract_attribute() { 101 # shellcheck disable=SC2039 102 local xpath="$1" 103 # shellcheck disable=SC2039 104 local node="$(xpath -q -n -e "${xpath}")" \ 105 || { echo "error extracting attribute at ${xpath}" >&2; false; } 106 echo "${node}" | sed 's/.*="\([^"]\+\)".*/\1/' 107} 108 109fetch() { 110 # shellcheck disable=SC2154 111 curl -u "${internode_api_login}:${internode_api_password}" -f ${CURL_ARGS} "$@" \ 112 || { echo "error fetching ${*} for user ${internode_api_login}" >&2; false; } 113} 114 115get_cached_api() { 116 # shellcheck disable=SC2039 117 local url=${1} 118 # shellcheck disable=SC2039 119 local name=${2} 120 # shellcheck disable=SC2039 121 local api_data='' 122 # shellcheck disable=SC2039 123 local cachefile="${MUNIN_PLUGSTATE}/$(basename "${0}").${name}.cache" 124 if [ -n "$(find "${cachefile}" -mmin -1440 2>/dev/null)" ]; then 125 api_data=$(cat "${cachefile}") 126 else 127 api_data="$(fetch "${url}" \ 128 || true)" 129 130 if [ -n "${api_data}" ]; then 131 echo "${api_data}" > ${cachefile} 132 else 133 echo "using ${name} info from stale cache ${cachefile}" >&2 134 api_data=$(cat "${cachefile}") 135 fi 136 fi 137 echo "${api_data}" 138} 139 140get_service_data() { 141 # Determine the service ID from the name of the symlink 142 SERVICE_ID="$(echo "${0}" | sed -n 's/^.*internode_usage_//p')" 143 if [ -z "${SERVICE_ID}" ]; then 144 # Otherwise, get the first service in the list 145 API_XML="$(get_cached_api ${internode_api_url} API_XML)" 146 if [ -z "${API_XML}" ]; then 147 echo "unable to determine service ID for user ${internode_api_login}" >&2 148 exit 1 149 fi 150 SERVICE_ID="$(echo "${API_XML}" | xpath_extract "internode/api/services/service")" 151 fi 152 153 154 CURRENT_TIMESTAMP="$(date +%s)" 155 SERVICE_USERNAME='n/a' 156 SERVICE_QUOTA='n/a' 157 SERVICE_PLAN='n/a' 158 SERVICE_ROLLOVER='n/a' 159 IDEAL_USAGE='' 160 USAGE_CRITICAL='' 161 SERVICE_XML="$(get_cached_api "${internode_api_url}/${SERVICE_ID}/service" SERVICE_XML \ 162 || true)" 163 if [ -n "${SERVICE_XML}" ]; then 164 SERVICE_USERNAME="$(echo "${SERVICE_XML}" | xpath_extract "internode/api/service/username")" 165 SERVICE_QUOTA="$(echo "${SERVICE_XML}" | xpath_extract "internode/api/service/quota")" 166 SERVICE_PLAN="$(echo "${SERVICE_XML}" | xpath_extract "internode/api/service/plan")" 167 SERVICE_ROLLOVER="$(echo "${SERVICE_XML}" | xpath_extract "internode/api/service/rollover")" 168 SERVICE_INTERVAL="$(echo "${SERVICE_XML}" | xpath_extract "internode/api/service/plan-interval" | sed 's/ly$//')" 169 170 FIRST_DAY="$(date +%s --date "${SERVICE_ROLLOVER} -1 ${SERVICE_INTERVAL}")" 171 LAST_DAY="$(date +%s --date "${SERVICE_ROLLOVER}")" 172 BILLING_PERIOD="(${LAST_DAY}-${FIRST_DAY})" 173 IDEAL_USAGE="$(echo "${SERVICE_QUOTA}-(${SERVICE_QUOTA}*(${LAST_DAY}-${CURRENT_TIMESTAMP})/${BILLING_PERIOD})" | bc -ql)" 174 175 USAGE_CRITICAL="${SERVICE_QUOTA}" 176 fi 177 178} 179 180get_data() { 181 DAILY_TIMESTAMP=N 182 DAILY_USAGE=U 183 HISTORY_XML="$(fetch "${internode_api_url}/${SERVICE_ID}/history" \ 184 || true)" 185 if [ -n "${HISTORY_XML}" ]; then 186 DAILY_USAGE="$(echo "${HISTORY_XML}" | xpath_extract "internode/api/usagelist/usage[last()-1]/traffic")" 187 DAILY_DATE="$(echo "${HISTORY_XML}" | xpath_extract_attribute "internode/api/usagelist/usage[last()-1]/@day")" 188 DAILY_TIMESTAMP="$(date -d "${DAILY_DATE} $(date +%H:%M:%S)" +%s \ 189 || echo N)" 190 fi 191 192 SERVICE_USAGE='U' 193 USAGE_XML="$(fetch "${internode_api_url}/${SERVICE_ID}/usage" \ 194 || true)" 195 if [ -n "${USAGE_XML}" ]; then 196 SERVICE_USAGE="$(echo "${USAGE_XML}" | xpath_extract "internode/api/traffic")" 197 198 199 fi 200} 201 202graph_config() { 203 graph="" 204 if [ -n "${1:-}" ]; then 205 graph=".$1" 206 fi 207 208 echo "multigraph internode_usage_${SERVICE_ID}${graph}" 209 210 case "$graph" in 211 .current) 212 echo "graph_title Uplink usage rate (hourly)" 213 echo "graph_info Username: ${SERVICE_USERNAME}; Service ID: ${SERVICE_ID}; Plan: ${SERVICE_PLAN}" 214 echo 'graph_category network' 215 # ${graph_period} is not a shell variable 216 # shellcheck disable=SC2016 217 echo 'graph_vlabel bytes per ${graph_period}' 218 # XXX: this seems to be updated twice per hour; 219 # the data from this graph may be nonsense 220 echo 'graph_period hour' 221 222 echo "hourly_rate.label Hourly usage" 223 echo "hourly_rate.type DERIVE" 224 echo "hourly_rate.min 0" 225 226 ;; 227 .daily) 228 echo "graph_title Uplink usage rate (daily)" 229 echo "graph_info Username: ${SERVICE_USERNAME}; Service ID: ${SERVICE_ID}; Plan: ${SERVICE_PLAN}" 230 echo "graph_info Uplink usage rate (daily)" 231 echo 'graph_category network' 232 # ${graph_period} is not a shell variable 233 # shellcheck disable=SC2016 234 echo 'graph_vlabel bytes per ${graph_period}' 235 echo 'graph_period day' 236 237 echo "daily_rate.label Previous-day usage" 238 echo "daily_rate.type GAUGE" 239 240 ;; 241 *) 242 #.usage) 243 echo "graph_title Uplink usage" 244 echo "graph_info Username: ${SERVICE_USERNAME}; Service ID: ${SERVICE_ID}; Plan: ${SERVICE_PLAN}" 245 echo 'graph_category network' 246 echo 'graph_vlabel bytes' 247 echo 'graph_period hour' 248 249 echo "usage.label Total usage" 250 echo "usage.draw AREA" 251 echo "ideal.extinfo Quota rollover: ${SERVICE_ROLLOVER}" 252 echo "ideal.label Ideal usage" 253 echo "ideal.draw LINE" 254 echo "ideal.colour FFA500" 255 256 echo "usage.critical ${USAGE_CRITICAL}" 257 echo "usage.warning ${IDEAL_USAGE}" 258 echo "ideal.critical 0:" 259 echo "ideal.warning 0:" 260 ;; 261 esac 262 echo 263} 264 265graph_data() { 266 graph="" 267 if [ -n "${1:-}" ]; then 268 graph=".${1}" 269 fi 270 271 echo "multigraph internode_usage_${SERVICE_ID}${graph}" 272 case "${graph}" in 273 .current) 274 echo "hourly_rate.value ${CURRENT_TIMESTAMP}:${SERVICE_USAGE:-U}" 275 ;; 276 .daily) 277 echo "daily_rate.value ${DAILY_TIMESTAMP}:${DAILY_USAGE:-U}" 278 ;; 279 *) 280 echo "usage.value ${CURRENT_TIMESTAMP}:${SERVICE_USAGE:-U}" 281 echo "ideal.value ${CURRENT_TIMESTAMP}:${IDEAL_USAGE:-U}" 282 ;; 283 esac 284 echo 285} 286 287main() { 288 case ${1:-} in 289 config) 290 if [ -n "${host_name:-}" ]; then 291 echo "host_name ${host_name}" 292 fi 293 graph_config 294 graph_config usage 295 graph_config daily 296 graph_config current 297 ;; 298 *) 299 get_data 300 graph_data 301 graph_data usage 302 graph_data daily 303 graph_data current 304 ;; 305 esac 306} 307 308get_service_data 309 310main "${1:-}" 311