444 lines
15 KiB
Python
444 lines
15 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
bgw210-700_prom_collector.py
|
|
=============================
|
|
|
|
Prometheus collector script to pull stats from AT&T / Arris BGW210-700 and
|
|
expose them as Prometheus metrics.
|
|
|
|
Tested With
|
|
-----------
|
|
|
|
* AT&T/Arris BGW210-700 (Software 1.5.12 / Hardware 02001C0046004D)
|
|
|
|
Requirements
|
|
------------
|
|
|
|
- requests
|
|
- lxml
|
|
- prometheus-client
|
|
|
|
Usage
|
|
-----
|
|
|
|
Run as a standalone HTTP server:
|
|
python3 bgw210-700_prom_collector.py --port 8000 --ip 192.168.1.254
|
|
|
|
Then scrape metrics from http://localhost:8000/metrics
|
|
|
|
License
|
|
-------
|
|
|
|
Copyright 2025 Jason Antman <[email protected]> <http://www.jasonantman.com>
|
|
Free for any use provided that patches are submitted back to me.
|
|
|
|
The latest version of this script can be found at:
|
|
<https://github.com/jantman/misc-scripts/blob/master/bgw210-700_prom_collector.py>
|
|
|
|
CHANGELOG
|
|
---------
|
|
|
|
2025-05-29 Jason Antman <[email protected]>:
|
|
- initial version of script based on bgw210-700_to_graphite.py
|
|
"""
|
|
|
|
import logging
|
|
import sys
|
|
import argparse
|
|
import re
|
|
import time
|
|
from urllib.parse import urlparse
|
|
|
|
import requests
|
|
from lxml import etree
|
|
from prometheus_client import start_http_server, Gauge, Counter, REGISTRY
|
|
from prometheus_client.core import CounterMetricFamily, GaugeMetricFamily
|
|
|
|
FORMAT = "[%(asctime)s %(levelname)s] %(message)s"
|
|
logging.basicConfig(level=logging.WARNING, format=FORMAT)
|
|
logger = logging.getLogger()
|
|
|
|
|
|
class BGW210700Collector:
|
|
"""
|
|
Prometheus collector for BGW210-700 modem metrics
|
|
"""
|
|
|
|
def __init__(self, modem_ip):
|
|
"""
|
|
Initialize the collector
|
|
|
|
:param modem_ip: modem IP address
|
|
:type modem_ip: str
|
|
"""
|
|
self.modem_ip = modem_ip
|
|
self.stats_getter = BGW210700StatsGetter(modem_ip)
|
|
|
|
def collect(self):
|
|
"""
|
|
Collect metrics from the modem and yield them
|
|
"""
|
|
try:
|
|
stats = self.stats_getter.get_stats()
|
|
|
|
# Create gauge metrics
|
|
connection_status = GaugeMetricFamily(
|
|
'bgw210_connection_status',
|
|
'Broadband connection status (1=up, 0=down)'
|
|
)
|
|
connection_status.add_metric([], stats.get('connection', 0))
|
|
yield connection_status
|
|
|
|
uptime_seconds = GaugeMetricFamily(
|
|
'bgw210_uptime_seconds',
|
|
'Time since last reboot in seconds'
|
|
)
|
|
uptime_seconds.add_metric([], stats.get('uptime_sec', 0))
|
|
yield uptime_seconds
|
|
|
|
line_speed_mbps = GaugeMetricFamily(
|
|
'bgw210_line_speed_mbps',
|
|
'Current line speed in Mbps'
|
|
)
|
|
line_speed_mbps.add_metric([], stats.get('line_speed', 0))
|
|
yield line_speed_mbps
|
|
|
|
line_state = GaugeMetricFamily(
|
|
'bgw210_line_state',
|
|
'Line state (1=up, 0=down)'
|
|
)
|
|
line_state.add_metric([], stats.get('line_state', 0))
|
|
yield line_state
|
|
|
|
duplex_status = GaugeMetricFamily(
|
|
'bgw210_duplex_status',
|
|
'Duplex status (1=full, 0.5=half)'
|
|
)
|
|
duplex_status.add_metric([], stats.get('duplex', 0))
|
|
yield duplex_status
|
|
|
|
# Create counter metrics for IPv4
|
|
ipv4_rx_bytes = CounterMetricFamily(
|
|
'bgw210_ipv4_rx_bytes_total',
|
|
'IPv4 received bytes total'
|
|
)
|
|
ipv4_rx_bytes.add_metric([], stats.get('ipv4.rx_bytes', 0))
|
|
yield ipv4_rx_bytes
|
|
|
|
ipv4_tx_bytes = CounterMetricFamily(
|
|
'bgw210_ipv4_tx_bytes_total',
|
|
'IPv4 transmitted bytes total'
|
|
)
|
|
ipv4_tx_bytes.add_metric([], stats.get('ipv4.tx_bytes', 0))
|
|
yield ipv4_tx_bytes
|
|
|
|
ipv4_rx_packets = CounterMetricFamily(
|
|
'bgw210_ipv4_rx_packets_total',
|
|
'IPv4 received packets total'
|
|
)
|
|
ipv4_rx_packets.add_metric([], stats.get('ipv4.rx_packets', 0))
|
|
yield ipv4_rx_packets
|
|
|
|
ipv4_tx_packets = CounterMetricFamily(
|
|
'bgw210_ipv4_tx_packets_total',
|
|
'IPv4 transmitted packets total'
|
|
)
|
|
ipv4_tx_packets.add_metric([], stats.get('ipv4.tx_packets', 0))
|
|
yield ipv4_tx_packets
|
|
|
|
ipv4_rx_unicast = CounterMetricFamily(
|
|
'bgw210_ipv4_rx_unicast_total',
|
|
'IPv4 received unicast packets total'
|
|
)
|
|
ipv4_rx_unicast.add_metric([], stats.get('ipv4.rx_unicast', 0))
|
|
yield ipv4_rx_unicast
|
|
|
|
ipv4_tx_unicast = CounterMetricFamily(
|
|
'bgw210_ipv4_tx_unicast_total',
|
|
'IPv4 transmitted unicast packets total'
|
|
)
|
|
ipv4_tx_unicast.add_metric([], stats.get('ipv4.tx_unicast', 0))
|
|
yield ipv4_tx_unicast
|
|
|
|
ipv4_rx_multicast = CounterMetricFamily(
|
|
'bgw210_ipv4_rx_multicast_total',
|
|
'IPv4 received multicast packets total'
|
|
)
|
|
ipv4_rx_multicast.add_metric([], stats.get('ipv4.rx_multicast', 0))
|
|
yield ipv4_rx_multicast
|
|
|
|
ipv4_tx_multicast = CounterMetricFamily(
|
|
'bgw210_ipv4_tx_multicast_total',
|
|
'IPv4 transmitted multicast packets total'
|
|
)
|
|
ipv4_tx_multicast.add_metric([], stats.get('ipv4.tx_multicast', 0))
|
|
yield ipv4_tx_multicast
|
|
|
|
ipv4_rx_errors = CounterMetricFamily(
|
|
'bgw210_ipv4_rx_errors_total',
|
|
'IPv4 receive errors total'
|
|
)
|
|
ipv4_rx_errors.add_metric([], stats.get('ipv4.rx_errors', 0))
|
|
yield ipv4_rx_errors
|
|
|
|
ipv4_tx_errors = CounterMetricFamily(
|
|
'bgw210_ipv4_tx_errors_total',
|
|
'IPv4 transmit errors total'
|
|
)
|
|
ipv4_tx_errors.add_metric([], stats.get('ipv4.tx_errors', 0))
|
|
yield ipv4_tx_errors
|
|
|
|
ipv4_rx_drops = CounterMetricFamily(
|
|
'bgw210_ipv4_rx_drops_total',
|
|
'IPv4 receive drops total'
|
|
)
|
|
ipv4_rx_drops.add_metric([], stats.get('ipv4.rx_drops', 0))
|
|
yield ipv4_rx_drops
|
|
|
|
ipv4_tx_drops = CounterMetricFamily(
|
|
'bgw210_ipv4_tx_drops_total',
|
|
'IPv4 transmit drops total'
|
|
)
|
|
ipv4_tx_drops.add_metric([], stats.get('ipv4.tx_drops', 0))
|
|
yield ipv4_tx_drops
|
|
|
|
ipv4_collisions = CounterMetricFamily(
|
|
'bgw210_ipv4_collisions_total',
|
|
'IPv4 collisions total'
|
|
)
|
|
ipv4_collisions.add_metric([], stats.get('ipv4.collisions', 0))
|
|
yield ipv4_collisions
|
|
|
|
# Create counter metrics for IPv6
|
|
ipv6_tx_packets = CounterMetricFamily(
|
|
'bgw210_ipv6_tx_packets_total',
|
|
'IPv6 transmitted packets total'
|
|
)
|
|
ipv6_tx_packets.add_metric([], stats.get('ipv6.tx_packets', 0))
|
|
yield ipv6_tx_packets
|
|
|
|
ipv6_tx_errors = CounterMetricFamily(
|
|
'bgw210_ipv6_tx_errors_total',
|
|
'IPv6 transmit errors total'
|
|
)
|
|
ipv6_tx_errors.add_metric([], stats.get('ipv6.tx_errors', 0))
|
|
yield ipv6_tx_errors
|
|
|
|
ipv6_tx_discards = CounterMetricFamily(
|
|
'bgw210_ipv6_tx_discards_total',
|
|
'IPv6 transmit discards total'
|
|
)
|
|
ipv6_tx_discards.add_metric([], stats.get('ipv6.tx_discards', 0))
|
|
yield ipv6_tx_discards
|
|
|
|
logger.debug('Successfully collected metrics from modem')
|
|
|
|
except Exception as e:
|
|
logger.error('Error collecting metrics from modem: %s', e)
|
|
# Yield a connection status metric set to 0 on error
|
|
connection_status = GaugeMetricFamily(
|
|
'bgw210_connection_status',
|
|
'Broadband connection status (1=up, 0=down)'
|
|
)
|
|
connection_status.add_metric([], 0)
|
|
yield connection_status
|
|
|
|
|
|
class BGW210700StatsGetter:
|
|
"""
|
|
Class to get statistics from BGW210-700 modem
|
|
"""
|
|
|
|
def __init__(self, ip):
|
|
"""
|
|
:param ip: modem IP address
|
|
:type ip: str
|
|
"""
|
|
logger.debug('Initializing BGW210700StatsGetter ip=%s', ip)
|
|
self.ip = ip
|
|
|
|
def get_stats(self):
|
|
"""
|
|
Get statistics from modem; return a dict
|
|
|
|
:return: stats
|
|
:rtype: dict
|
|
"""
|
|
res = {}
|
|
res.update(self._sysinfo())
|
|
res.update(self._broadband_stats())
|
|
return res
|
|
|
|
def _get_page(self, url):
|
|
logger.debug('GET: %s', url)
|
|
r = requests.get(url, timeout=10)
|
|
r.raise_for_status()
|
|
tree = etree.HTML(r.text)
|
|
return tree
|
|
|
|
def _do_kv_table(self, table):
|
|
res = {}
|
|
for tr in table.iter('tr'):
|
|
if len(tr) != 2 or tr[0].tag != 'th' or tr[1].tag != 'td':
|
|
continue
|
|
if tr[1].text is not None:
|
|
res[tr[0].text.strip()] = tr[1].text.strip()
|
|
return res
|
|
|
|
def _broadband_stats(self):
|
|
tree = self._get_page(
|
|
'http://%s/cgi-bin/broadbandstatistics.ha' % self.ip
|
|
)
|
|
data = {}
|
|
for tbl in tree.iter('table'):
|
|
summary = tbl.get('summary', None)
|
|
if summary is None:
|
|
continue
|
|
data[summary] = self._do_kv_table(tbl)
|
|
|
|
bs = 'Summary of the most important WAN information'
|
|
|
|
# Parse line state correctly
|
|
line_state_text = data.get('Ethernet Statistics Table', {}).get('Current Status', 'Down')
|
|
line_state = 1 if line_state_text == 'Up' else 0
|
|
|
|
return {
|
|
'connection': 1 if data[bs]['Broadband Connection'] == 'Up' else 0,
|
|
'ipv6.tx_discards': int(
|
|
data['IPv6 Statistics Table']['Transmit Discards']
|
|
),
|
|
'ipv6.tx_errors': int(
|
|
data['IPv6 Statistics Table']['Transmit Errors']
|
|
),
|
|
'ipv6.tx_packets': int(
|
|
data['IPv6 Statistics Table']['Transmit Packets']
|
|
),
|
|
'ipv4.rx_drops': int(
|
|
data['Ethernet IPv4 Statistics Table']['Receive Drops']
|
|
),
|
|
'ipv4.tx_drops': int(
|
|
data['Ethernet IPv4 Statistics Table']['Transmit Drops']
|
|
),
|
|
'ipv4.rx_unicast': int(
|
|
data['Ethernet IPv4 Statistics Table']['Receive Unicast']
|
|
),
|
|
'ipv4.tx_bytes': int(
|
|
data['Ethernet IPv4 Statistics Table']['Transmit Bytes']
|
|
),
|
|
'ipv4.rx_errors': int(
|
|
data['Ethernet IPv4 Statistics Table']['Receive Errors']
|
|
),
|
|
'ipv4.rx_bytes': int(
|
|
data['Ethernet IPv4 Statistics Table']['Receive Bytes']
|
|
),
|
|
'ipv4.tx_packets': int(
|
|
data['Ethernet IPv4 Statistics Table']['Transmit Packets']
|
|
),
|
|
'ipv4.rx_packets': int(
|
|
data['Ethernet IPv4 Statistics Table']['Receive Packets']
|
|
),
|
|
'ipv4.collisions': int(
|
|
data['Ethernet IPv4 Statistics Table']['Collisions']
|
|
),
|
|
'ipv4.rx_multicast': int(
|
|
data['Ethernet IPv4 Statistics Table']['Receive Multicast']
|
|
),
|
|
'ipv4.tx_multicast': int(
|
|
data['Ethernet IPv4 Statistics Table']['Transmit Multicast']
|
|
),
|
|
'ipv4.tx_unicast': int(
|
|
data['Ethernet IPv4 Statistics Table']['Transmit Unicast']
|
|
),
|
|
'ipv4.tx_errors': int(
|
|
data['Ethernet IPv4 Statistics Table']['Transmit Errors']
|
|
),
|
|
'line_speed': int(
|
|
data['Ethernet Statistics Table']['Current Speed (Mbps)']
|
|
),
|
|
'line_state': line_state,
|
|
'duplex': 1 if data[
|
|
'Ethernet Statistics Table'
|
|
]['Current Duplex'] == 'full' else 0.5
|
|
}
|
|
|
|
def _sysinfo(self):
|
|
tree = self._get_page('http://%s/cgi-bin/sysinfo.ha' % self.ip)
|
|
info = self._do_kv_table(tree.xpath('//table')[0])
|
|
return {
|
|
'uptime_sec': int(info['Time Since Last Reboot'])
|
|
}
|
|
|
|
|
|
def set_log_info():
|
|
"""set logger level to INFO"""
|
|
set_log_level_format(logging.INFO,
|
|
'%(asctime)s %(levelname)s:%(name)s:%(message)s')
|
|
|
|
|
|
def set_log_debug():
|
|
"""set logger level to DEBUG, and debug-level output format"""
|
|
set_log_level_format(
|
|
logging.DEBUG,
|
|
"%(asctime)s [%(levelname)s %(filename)s:%(lineno)s - "
|
|
"%(name)s.%(funcName)s() ] %(message)s"
|
|
)
|
|
|
|
|
|
def set_log_level_format(level, format):
|
|
"""
|
|
Set logger level and format.
|
|
|
|
:param level: logging level; see the :py:mod:`logging` constants.
|
|
:type level: int
|
|
:param format: logging formatter format string
|
|
:type format: str
|
|
"""
|
|
formatter = logging.Formatter(fmt=format)
|
|
logger.handlers[0].setFormatter(formatter)
|
|
logger.setLevel(level)
|
|
|
|
|
|
def parse_args(argv):
|
|
p = argparse.ArgumentParser(
|
|
description='BGW210-700 Prometheus metrics collector'
|
|
)
|
|
p.add_argument('-v', '--verbose', dest='verbose', action='count', default=0,
|
|
help='verbose output. specify twice for debug-level output.')
|
|
p.add_argument('-p', '--port', dest='port', type=int,
|
|
action='store', default=8000,
|
|
help='HTTP server port (default: 8000)')
|
|
p.add_argument('-i', '--ip', dest='modem_ip', action='store', type=str,
|
|
default='192.168.1.254',
|
|
help='Modem IP address (default: 192.168.1.254)')
|
|
p.add_argument('--host', dest='host', action='store', type=str,
|
|
default='0.0.0.0',
|
|
help='HTTP server bind address (default: 0.0.0.0)')
|
|
args = p.parse_args(argv)
|
|
return args
|
|
|
|
|
|
if __name__ == "__main__":
|
|
args = parse_args(sys.argv[1:])
|
|
|
|
# set logging level
|
|
if args.verbose > 1:
|
|
set_log_debug()
|
|
elif args.verbose > 0:
|
|
set_log_info()
|
|
|
|
# Create and register the collector
|
|
collector = BGW210700Collector(args.modem_ip)
|
|
REGISTRY.register(collector)
|
|
|
|
# Start the HTTP server
|
|
logger.info('Starting HTTP server on %s:%d', args.host, args.port)
|
|
start_http_server(args.port, addr=args.host)
|
|
|
|
logger.info('Metrics available at http://%s:%d/metrics', args.host, args.port)
|
|
|
|
try:
|
|
while True:
|
|
time.sleep(60)
|
|
except KeyboardInterrupt:
|
|
logger.info('Shutting down...')
|
|
sys.exit(0) |