root / plugins / router / arris-sb6183 @ 77e53059
Historique | Voir | Annoter | Télécharger (10,2 ko)
| 1 |
#!/usr/bin/env python3 |
|---|---|
| 2 |
|
| 3 |
# Copyright 2020 Nathaniel Clark <nathaniel.clark@misrule.us> |
| 4 |
# |
| 5 |
# This program is free software; you can redistribute it and/or modify |
| 6 |
# it under the terms of the GNU Library General Public License as published by |
| 7 |
# the Free Software Foundation; version 2 only |
| 8 |
# |
| 9 |
# This program is distributed in the hope that it will be useful, |
| 10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 |
# GNU Library General Public License for more details. |
| 13 |
# |
| 14 |
# You should have received a copy of the GNU Library General Public License |
| 15 |
# along with this program; if not, write to the Free Software |
| 16 |
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. |
| 17 |
# |
| 18 |
|
| 19 |
""" |
| 20 |
=head1 NAME |
| 21 |
|
| 22 |
arris-sb6183 - Health monitoring plugin for Arris SB6183 Cable Modem |
| 23 |
|
| 24 |
=head1 DESCRIPTION |
| 25 |
|
| 26 |
This provides the following multigraphs: |
| 27 |
* upstream and downstream power levels |
| 28 |
* downstream signal to noise ratio |
| 29 |
* downstream error counts |
| 30 |
* uptime |
| 31 |
|
| 32 |
The values are retrieved from the cable modem's status web pages at |
| 33 |
192.168.100.1. So, this plugin must be installed on a munin node |
| 34 |
which can access those pages. |
| 35 |
|
| 36 |
=head1 CONFIGURATION |
| 37 |
|
| 38 |
Make sure 192.168.100.1 is accessible through your firewall. |
| 39 |
|
| 40 |
To have this register with munin as it's own host set the "env.hostname" in config. |
| 41 |
Also ensure that the hostname set is listed in munin.conf. |
| 42 |
|
| 43 |
[arris*] |
| 44 |
env.hostname modem |
| 45 |
|
| 46 |
=head1 TESTING |
| 47 |
|
| 48 |
Developed and tested with: |
| 49 |
firmware: D30CM-OSPREY-2.4.0.1-GA-02-NOSH |
| 50 |
hardware version: 1 |
| 51 |
|
| 52 |
=head1 VERSION |
| 53 |
|
| 54 |
0.0.1 |
| 55 |
|
| 56 |
=head1 AUTHOR |
| 57 |
|
| 58 |
Nathaniel Clark <nathaniel.clark@misrule.us> |
| 59 |
|
| 60 |
=head1 LICENSE |
| 61 |
|
| 62 |
GPLv2 |
| 63 |
|
| 64 |
=head1 MAGIC MARKERS |
| 65 |
|
| 66 |
#%# family=contrib |
| 67 |
#%# capabilities=autoconf |
| 68 |
|
| 69 |
=cut |
| 70 |
""" |
| 71 |
|
| 72 |
import re |
| 73 |
import os |
| 74 |
import sys |
| 75 |
from urllib import request |
| 76 |
|
| 77 |
HOSTNAME = os.getenv("hostname", None)
|
| 78 |
STATUS_URL = "http://192.168.100.1/RgConnect.asp" |
| 79 |
INFO_URL = "http://192.168.100.1/RgSwInfo.asp" |
| 80 |
UPCOUNT = 4 |
| 81 |
DOWNCOUNT = 16 |
| 82 |
|
| 83 |
if len(sys.argv) == 2: |
| 84 |
if sys.argv[1] == "config": |
| 85 |
if HOSTNAME: |
| 86 |
print("host_name {0}\n".format(HOSTNAME))
|
| 87 |
|
| 88 |
# UPTIME |
| 89 |
print( |
| 90 |
"""multigraph arris_uptime |
| 91 |
graph_title Modem Uptime |
| 92 |
graph_category system |
| 93 |
graph_args --base 1000 -l 0 |
| 94 |
graph_vlabel uptime in days |
| 95 |
graph_scale no |
| 96 |
graph_category system |
| 97 |
graph_info This graph shows the number of days that the the host is up and running so far. |
| 98 |
uptime.label uptime |
| 99 |
uptime.info The system uptime itself in days. |
| 100 |
uptime.draw AREA |
| 101 |
""" |
| 102 |
) |
| 103 |
|
| 104 |
# POWER |
| 105 |
print("multigraph arris_power")
|
| 106 |
print("graph_title Arris Power (dBmV)")
|
| 107 |
print("graph_vlabel Power (dBmV)")
|
| 108 |
print("graph_category network")
|
| 109 |
for i in range(1, DOWNCOUNT + 1): |
| 110 |
print("down_{0}.label Down Ch {1}".format(i, i))
|
| 111 |
print("down_{0}.type GAUGE".format(i))
|
| 112 |
print("down_{0}.draw LINE1".format(i))
|
| 113 |
for i in range(1, UPCOUNT + 1): |
| 114 |
print("up_{0}.label Up Ch {1}".format(i, i))
|
| 115 |
print("up_{0}.type GAUGE".format(i))
|
| 116 |
print("up_{0}.draw LINE1".format(i))
|
| 117 |
|
| 118 |
for i in range(1, DOWNCOUNT + 1): |
| 119 |
name = "down_{0}".format(i)
|
| 120 |
print("\nmultigraph arris_power.{0}".format(name))
|
| 121 |
print("graph_title Downstream Power for Channel {0} (dBmV)".format(i))
|
| 122 |
print("graph_category network")
|
| 123 |
print("power.label dBmV")
|
| 124 |
print("power.type GAUGE")
|
| 125 |
print("power.draw LINE1")
|
| 126 |
for i in range(1, UPCOUNT + 1): |
| 127 |
name = "up_{0}".format(i)
|
| 128 |
print("\nmultigraph arris_power.{0}".format(name))
|
| 129 |
print("graph_title Upstream Power for Channel {0} (dBmV)".format(i))
|
| 130 |
print("graph_category network")
|
| 131 |
print("power.label dBmV")
|
| 132 |
print("power.type GAUGE")
|
| 133 |
print("power.draw LINE1")
|
| 134 |
|
| 135 |
# SNR |
| 136 |
print("\nmultigraph arris_snr")
|
| 137 |
print("graph_title Arris Signal-to-Noise Ratio (dB)")
|
| 138 |
print("graph_vlabel SNR (dB)")
|
| 139 |
print("graph_category network")
|
| 140 |
for i in range(1, DOWNCOUNT + 1): |
| 141 |
print("down_{0}.label Ch {1}".format(i, i))
|
| 142 |
print("down_{0}.type GAUGE".format(i))
|
| 143 |
print("down_{0}.draw LINE1".format(i))
|
| 144 |
|
| 145 |
for i in range(1, DOWNCOUNT + 1): |
| 146 |
name = "down_{0}".format(i)
|
| 147 |
print("\nmultigraph arris_snr.{0}".format(name))
|
| 148 |
print("graph_title SNR on Channel {0} (dB)".format(i))
|
| 149 |
print("graph_vlabel SNR (dB)")
|
| 150 |
print("graph_category network")
|
| 151 |
print("snr.label dB")
|
| 152 |
print("snr.type GAUGE")
|
| 153 |
print("snr.draw LINE1")
|
| 154 |
|
| 155 |
# ERRORS |
| 156 |
print("\nmultigraph arris_error")
|
| 157 |
print("graph_title Arris Channel Errors")
|
| 158 |
print("graph_category network")
|
| 159 |
print("graph_args --base 1000")
|
| 160 |
print("graph_vlabel errors/sec")
|
| 161 |
print("graph_category network")
|
| 162 |
print("corr.label Corrected")
|
| 163 |
print("corr.type DERIVE")
|
| 164 |
print("corr.min 0")
|
| 165 |
print("uncr.label Uncorrectable")
|
| 166 |
print("uncr.type DERIVE")
|
| 167 |
print("uncr.min 0")
|
| 168 |
print("uncr.warning 1")
|
| 169 |
|
| 170 |
for i in range(1, DOWNCOUNT + 1): |
| 171 |
name = "down_{0}".format(i)
|
| 172 |
print("\nmultigraph arris_error.{0}".format(name))
|
| 173 |
print("graph_title Channel {0} Errors".format(i))
|
| 174 |
print("graph_args --base 1000")
|
| 175 |
print("graph_vlabel errors/sec")
|
| 176 |
print("graph_category network")
|
| 177 |
print("corr.label Correctable")
|
| 178 |
print("corr.type DERIVE")
|
| 179 |
print("corr.min 0")
|
| 180 |
print("uncr.label Uncorrectable")
|
| 181 |
print("uncr.type DERIVE")
|
| 182 |
print("uncr.min 0")
|
| 183 |
print("uncr.warning 1")
|
| 184 |
|
| 185 |
sys.exit(0) |
| 186 |
|
| 187 |
if sys.argv[1] == "autoconfig": |
| 188 |
try: |
| 189 |
from lxml import html |
| 190 |
|
| 191 |
resp = request.urlopen(STATUS_URL) |
| 192 |
except ImportError: |
| 193 |
print("no (missing lxml module)")
|
| 194 |
except OSError: |
| 195 |
print("no (no router)")
|
| 196 |
else: |
| 197 |
if resp.status == 200: |
| 198 |
print("yes")
|
| 199 |
else: |
| 200 |
print("no (Bad status code: %d)" % page.status_code)
|
| 201 |
sys.exit(0) |
| 202 |
|
| 203 |
from lxml import html |
| 204 |
|
| 205 |
rxblank = re.compile(r"[\x00\n\r\t ]+", re.MULTILINE) |
| 206 |
rxcomment = re.compile(r"<!--.*?-->") |
| 207 |
rxscript = re.compile(r"<script.*?</script>", re.MULTILINE) |
| 208 |
|
| 209 |
|
| 210 |
def process_url(url): |
| 211 |
""" |
| 212 |
Extract simpleTables from page at URL |
| 213 |
""" |
| 214 |
try: |
| 215 |
resp = request.urlopen(url) |
| 216 |
except OSError: |
| 217 |
print("failed to contact router", file=sys.stderr)
|
| 218 |
return [] |
| 219 |
if resp.status != 200: |
| 220 |
print( |
| 221 |
"failed to get status page %d: %s" % (resp.status, resp.reason), |
| 222 |
file=sys.stderr, |
| 223 |
) |
| 224 |
return [] |
| 225 |
data = rxscript.sub( |
| 226 |
"", |
| 227 |
rxcomment.sub( |
| 228 |
"", |
| 229 |
rxblank.sub( |
| 230 |
" ", "".join(map(lambda x: x.decode("utf-8"), resp.readlines()))
|
| 231 |
), |
| 232 |
), |
| 233 |
) |
| 234 |
dom = html.fromstring(data) |
| 235 |
|
| 236 |
return dom.xpath('//table[contains(@class, "simpleTable")]')
|
| 237 |
|
| 238 |
|
| 239 |
print("multi_graph arris_uptime")
|
| 240 |
arr = process_url(INFO_URL) |
| 241 |
if arr: |
| 242 |
trs = arr[1].findall("tr")
|
| 243 |
# drop title |
| 244 |
trs.pop(0) |
| 245 |
|
| 246 |
date = "".join(trs[0].findall("td")[1].itertext()).strip()
|
| 247 |
|
| 248 |
arr = date.split(" ")
|
| 249 |
rx = re.compile(r"[hms]") |
| 250 |
days = int(arr[0]) |
| 251 |
hms = rx.sub("", arr[2]).split(":")
|
| 252 |
|
| 253 |
seconds = ((days * 24 + int(hms[0])) * 60 + int(hms[1])) * 60 + int(hms[2]) |
| 254 |
print("uptime.value {0}".format(seconds / 86400.0))
|
| 255 |
else: |
| 256 |
print("uptime.value U")
|
| 257 |
|
| 258 |
|
| 259 |
arr = process_url(STATUS_URL) |
| 260 |
if arr: |
| 261 |
downstream = arr[1] |
| 262 |
upstream = arr[2] |
| 263 |
|
| 264 |
trs = downstream.findall("tr")
|
| 265 |
# drop title |
| 266 |
trs.pop(0) |
| 267 |
|
| 268 |
headings = ["".join(x.itertext()).strip() for x in trs.pop(0).findall("td")]
|
| 269 |
# ['Channel', 'Lock Status', 'Modulation', 'Channel ID', 'Frequency', 'Power', 'SNR', 'Corrected', 'Uncorrectables'] |
| 270 |
else: |
| 271 |
trs = [] |
| 272 |
headings = [] |
| 273 |
|
| 274 |
# Summation Graphs |
| 275 |
correct = 0 |
| 276 |
uncorr = 0 |
| 277 |
power = {"up": ["U"] * UPCOUNT, "down": ["U"] * DOWNCOUNT}
|
| 278 |
snr = ["U"] * DOWNCOUNT |
| 279 |
for row in trs: |
| 280 |
data = dict( |
| 281 |
zip(headings, ["".join(x.itertext()).strip() for x in row.findall("td")])
|
| 282 |
) |
| 283 |
uncorr += int(data["Uncorrectables"]) |
| 284 |
correct += int(data["Corrected"]) |
| 285 |
|
| 286 |
channel = int(data["Channel"]) |
| 287 |
|
| 288 |
print("\nmultigraph arris_power.down_{0}".format(channel))
|
| 289 |
value = data["Power"].split(" ")[0]
|
| 290 |
print("power.value {0}".format(value))
|
| 291 |
power["down"][channel - 1] = value |
| 292 |
|
| 293 |
print("multigraph arris_snr.down_{0}".format(channel))
|
| 294 |
value = data["SNR"].split(" ")[0]
|
| 295 |
print("snr.value {0}".format(value))
|
| 296 |
snr[channel - 1] = value |
| 297 |
|
| 298 |
print("multigraph arris_error.down_{0}".format(channel))
|
| 299 |
print("corr.value {0}".format(data["Corrected"]))
|
| 300 |
print("uncr.value {0}".format(data["Uncorrectables"]))
|
| 301 |
|
| 302 |
# Fill missing |
| 303 |
for i in range(len(trs), DOWNCOUNT): |
| 304 |
print("\nmultigraph arris_power.down_{0}".format(i + 1))
|
| 305 |
print("power.value U")
|
| 306 |
|
| 307 |
print("multigraph arris_snr.down_{0}".format(i + 1))
|
| 308 |
print("snr.value U")
|
| 309 |
|
| 310 |
print("multigraph arris_error.down_{0}".format(i + 1))
|
| 311 |
print("corr.value U")
|
| 312 |
print("uncr.value U")
|
| 313 |
|
| 314 |
print("multigraph arris_error")
|
| 315 |
if arr: |
| 316 |
print("corr.value {0}".format(correct))
|
| 317 |
print("uncr.value {0}".format(uncorr))
|
| 318 |
else: |
| 319 |
print("corr.value U")
|
| 320 |
print("uncr.value U")
|
| 321 |
|
| 322 |
print("multigraph arris_snr")
|
| 323 |
for i in range(0, DOWNCOUNT): |
| 324 |
print("down_{0}.value {1}".format(i + 1, snr[i]))
|
| 325 |
|
| 326 |
if arr: |
| 327 |
trs = upstream.findall("tr")
|
| 328 |
# drop title |
| 329 |
trs.pop(0) |
| 330 |
|
| 331 |
headings = ["".join(x.itertext()).strip() for x in trs.pop(0).findall("td")]
|
| 332 |
# ['Channel', 'Lock Status', 'US Channel Type', 'Channel ID', 'Symbol Rate', 'Frequency', 'Power'] |
| 333 |
|
| 334 |
for row in trs: |
| 335 |
data = dict( |
| 336 |
zip(headings, ["".join(x.itertext()).strip() for x in row.findall("td")])
|
| 337 |
) |
| 338 |
channel = int(data["Channel"]) |
| 339 |
print("multigraph arris_power.up_{0}".format(channel))
|
| 340 |
value = data["Power"].split(" ")[0]
|
| 341 |
print("power.value {0}".format(value))
|
| 342 |
power["up"][channel - 1] = value |
| 343 |
|
| 344 |
# Fill missing |
| 345 |
for i in range(len(trs), UPCOUNT): |
| 346 |
print("multigraph arris_power.up_{0}".format(i + 1))
|
| 347 |
print("power.value U")
|
| 348 |
|
| 349 |
print("multigraph arris_power")
|
| 350 |
for i in range(0, DOWNCOUNT): |
| 351 |
print("down_{0}.value {1}".format(i + 1, power["down"][i]))
|
| 352 |
for i in range(0, UPCOUNT): |
| 353 |
print("up_{0}.value {1}".format(i + 1, power["up"][i]))
|
