Created
July 7, 2021 20:14
-
-
Save rus-kilian/996b2f1a1c1ad3f6f9e1a4847eebb278 to your computer and use it in GitHub Desktop.
Pull platform stats (like CoPP) from various Cisco platforms
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
import time | |
import re | |
import yaml | |
import json | |
import os.path | |
import logging | |
import argparse | |
import socket | |
import pprint | |
from netmiko import ConnectHandler | |
from prometheus_client import Gauge, Summary | |
from threading import Thread | |
# systemd socket activation | |
from prometheus_client import start_http_server | |
from prometheus_client.exposition import MetricsHandler | |
from prometheus_client.registry import REGISTRY | |
# Debian bullseye has _ThreadingSimpleServer renamed to ThreadingWSGIServer | |
try: | |
from prometheus_client.exposition import ( | |
ThreadingWSGIServer as _ThreadingSimpleServer, | |
) | |
except ImportError: | |
from prometheus_client.exposition import _ThreadingSimpleServer | |
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) | |
parser.add_argument( | |
"--debug", | |
"-d", | |
dest="debug", | |
action="store_true", | |
help="Run debug mode", | |
default=False, | |
) | |
parser.add_argument( | |
"--log", | |
"-l", | |
dest="ssh_log", | |
action="store_true", | |
help="Run in SSH debug mode with log", | |
default=False, | |
) | |
parser.add_argument( | |
"--listen-port", type=int, help="The port the exporter will listen on", default=9424 | |
) | |
parser.add_argument( | |
"--delay", | |
type=int, | |
help="The reconnect delay the poller will wait if SSH connection died", | |
default=300, | |
) | |
parser.add_argument( | |
"--asa-delay", | |
type=int, | |
help="The refresh delay the exporter will wait between ASA runs", | |
default=30, | |
) | |
parser.add_argument( | |
"--iosxe-delay", | |
type=int, | |
help="The refresh delay the exporter will wait between IOSXE runs", | |
default=30, | |
) | |
parser.add_argument( | |
"--nxos-delay", | |
type=int, | |
help="The refresh delay the exporter will wait between NX-OS runs", | |
default=30, | |
) | |
args = parser.parse_args() | |
config = {} | |
if os.path.isfile("/etc/net_exporter.yaml"): | |
with open("/etc/net_exporter.yaml", "r") as stream: | |
try: | |
config = yaml.safe_load(stream) | |
except yaml.YAMLError as exc: | |
print(exc) | |
exit(1) | |
elif os.path.isfile(os.environ["HOME"] + "/.config.yaml"): | |
with open(os.environ["HOME"] + "/.config.yaml", "r") as stream: | |
try: | |
config = yaml.safe_load(stream) | |
except yaml.YAMLError as exc: | |
print(exc) | |
exit(1) | |
else: | |
print("No config.yaml") | |
exit(1) | |
if "login" not in config: | |
print("config.yaml does not contain login!") | |
exit(1) | |
if "password" not in config: | |
print("config.yaml does not contain password!") | |
exit(1) | |
SYSTEMD_FIRST_SOCKET_FD = 3 | |
CONTENT_TYPE_LATEST = str("text/plain; version=0.0.1; charset=utf-8") | |
"""Content type of the latest text format""" | |
logger = logging.getLogger(__name__) | |
pp = pprint.PrettyPrinter(indent=4) | |
debug = args.debug | |
# create console handler and set level to debug | |
ch = logging.StreamHandler() | |
if debug: | |
logger.setLevel(logging.DEBUG) | |
ch.setLevel(logging.DEBUG) | |
# create formatter | |
formatter = logging.Formatter( | |
"%(asctime)s - %(name)s/%(threadName)s - %(levelname)s - %(message)s" | |
) | |
# add formatter to ch | |
ch.setFormatter(formatter) | |
# add ch to logger | |
logger.addHandler(ch) | |
# ASA regex | |
asa_ip_local_pool = re.compile(r"^ip local pool\s*([a-zA-Z0-9]+)\s*") | |
asa_ip_local_pool_usage = re.compile( | |
r"^[0-9\.]+\s+[0-9\.]+\s+[0-9\.]+\s+([0-9]+)\s+([0-9]+)\s+([0-9]+)\s*" | |
) | |
# IOS-XE regex | |
class_match = re.compile(r"^\s+Class-map:\s+([^\s]+)\s+") | |
by_match = re.compile(r"^\s+(\d+)\s+packets[^\d]+(\d+)\s+bytes") | |
conf_match = re.compile(r"^\s+conformed\s+(\d+)\s+bytes") | |
exc_match = re.compile(r"^\s+exceeded\s+(\d+)\s+bytes") | |
tcam_util_single_val = re.compile(r"^\s*([^\s].*[^\s])\s+(\d+)\s+(\d+)") | |
tcam_util_dual_val = re.compile(r"^\s*([^\s].*[^\s])\s+(\d+)/(\d+)\s+(\d+)/(\d+)") | |
ios_section_re = re.compile(r"^[=-]+$") | |
cpu_q_stat_re = re.compile( | |
r"^(\d+)\s+(\d+)\s+(\S.+\S)\s+(Yes|No)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s*" | |
) | |
cpu_q_policer_stat_re = re.compile(r"^(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s*") | |
cpp_classes_re = re.compile(r"^(\d+)\s+(\S+)\s+:\s+(\S.*\S)\s*") | |
# NX-OS regex | |
service_policy_re = re.compile(r"^\s*service-policy\s+input\s+(.*)$") | |
class_map_re = re.compile(r"^\s+class-map\s+([A-Za-z0-9-]+)\s?.*$") | |
module_re = re.compile(r"^\s+module\s(\d+):$") | |
value_re = re.compile(r"^\s+(conformed|violated)\s+(\d+)\s+bytes") | |
section_re = re.compile( | |
r"^\s*(RMON counters|Throttle statistics|NAPI statistics|qdisc stats)[\s:]?.*" | |
) | |
pkt_counters_re = re.compile(r"^\s*(\S+)\s+packets\s+(\d+)\s+(\d+)") | |
throttle_re = re.compile(r"^\s*([RT]x)\spacket\s+rate\s+[^\d]+(\d+)\s+/\s+(\d+)\s+(.+)") | |
napi_re = re.compile(r"^\s*([RT]x)\s+([a-z]+)[\s\.]+(\d+)") | |
qdisc_re = re.compile(r"^\s*([a-zA-Z][a-z\ ]+[a-z])[\s\.]+(\d+)") | |
# ASA metrics | |
ASA_collect = Summary( | |
"asa_collect", "ASA poller details collecting IP pool stats", labelnames=["target"] | |
) | |
ASA_pool_stats = Gauge( | |
"asa_pool_stats", | |
"ASA pool usage [total usage count]", | |
labelnames=["target", "pool", "status"], | |
) | |
# IOSXE metrics | |
IOSXE_collect = Summary( | |
"iosxe_collect", | |
"IOSXE poller details collecting IP pool stats", | |
labelnames=["target"], | |
) | |
IOSXE_copp_stats = Gauge( | |
"iosxe_copp_stats", "IOSXE CoPP stats", labelnames=["target", "chain", "status"] | |
) | |
IOSXE_tcam_utilization = Gauge( | |
"iosxe_tcam_utilization", | |
"IOSXE TCAM utilization", | |
labelnames=["target", "asic", "table", "ref"], | |
) | |
IOSXE_CPU_queue_stats = Gauge( | |
"iosxe_cpu_queue_stats", | |
"IOSXE CPU queue stats", | |
labelnames=["target", "queue", "match"], | |
) | |
IOSXE_CPU_policer_queue_accept_frames = Gauge( | |
"iosxe_cpu_policer_queue_accept_frames", | |
"IOSXE CPU policer queue frames accepted", | |
labelnames=["target", "policer"], | |
) | |
IOSXE_CPU_policer_queue_accept_bytes = Gauge( | |
"iosxe_cpu_policer_queue_accept_bytes", | |
"IOSXE CPU policer queue bytes accepted", | |
labelnames=["target", "policer"], | |
) | |
IOSXE_CPU_policer_queue_drop_frames = Gauge( | |
"iosxe_cpu_policer_queue_drop_frames", | |
"IOSXE CPU policer queue frames dropped", | |
labelnames=["target", "policer"], | |
) | |
IOSXE_CPU_policer_queue_drop_bytes = Gauge( | |
"iosxe_cpu_policer_queue_drop_bytes", | |
"IOSXE CPU policer queue bytes dropped", | |
labelnames=["target", "policer"], | |
) | |
# NX-OS metrics | |
NXOS_collect = Summary( | |
"nxos_collect", | |
"NX-OS poller details collecting IP pool stats", | |
labelnames=["target"], | |
) | |
NXOS_copp_stats = Gauge( | |
"nxos_copp_stats", | |
"NX-OS CoPP stats", | |
labelnames=["target", "service_policy", "class_map", "module", "match"], | |
) | |
NXOS_copp_counters = Gauge( | |
"nxos_copp_counters", | |
"NX-OS CoPP counters", | |
labelnames=["target", "section", "direction", "match"], | |
) | |
NXOS_copp_throttle = Gauge( | |
"nxos_copp_throttle", | |
"NX-OS CoPP throttle stats", | |
labelnames=["target", "section", "direction", "match", "unit", "state"], | |
) | |
NXOS_napi_stats = Gauge( | |
"nxos_napi_stats", | |
"NX-OS CoPP NAPI stats", | |
labelnames=["target", "section", "direction", "match"], | |
) | |
NXOS_qdisc_stats = Gauge( | |
"nxos_qdisc_stats", | |
"NX-OS CoPP qdisc stats", | |
labelnames=["target", "section", "match"], | |
) | |
class SocketInheritingHTTPServer(_ThreadingSimpleServer): | |
"""A HttpServer subclass that takes over an inherited socket from systemd""" | |
def __init__(self, address_info, handler, fd, bind_and_activate=True): | |
_ThreadingSimpleServer.__init__( | |
self, address_info, handler, bind_and_activate=False | |
) | |
logger.debug("http server init complete - passing socket") | |
self.socket = socket.fromfd(fd, self.address_family, self.socket_type) | |
if bind_and_activate: | |
# NOTE: systemd provides ready-bound sockets, so we only need to activate: | |
logger.debug("http server activating") | |
self.server_activate() | |
else: | |
logger.debug("http server NOT activated") | |
class AKGatherer(Thread): | |
"""Periodically retrieve data from AK in a separate thread, | |
""" | |
def __init__(self): | |
Thread.__init__(self) | |
self.name = "AKGatherer" | |
self.target = None | |
self.login = None | |
self.password = "" | |
self.aktype = None | |
self.net_connect = None | |
# ASA pools | |
self.pools = {} | |
# IOSXE CoPP sections | |
self.sections = {} | |
def settarget(self, target, login, password, aktype): | |
self.name = "AKGatherer@%s" % target | |
logger.debug('Setting AK to "%s"' % target) | |
self.target = target | |
logger.debug('Setting AK login to "%s"' % login) | |
self.login = login | |
self.password = password | |
logger.debug('Setting AK type to "%s"' % aktype) | |
self.aktype = aktype | |
def check_asa(self): | |
if self.net_connect is None: | |
raise ("No SSH connection present!") | |
logger.debug("Fetching ASA pool definitions on %s" % self.target) | |
output = self.net_connect.send_command("show run ip local pool") | |
# connection.send("show run ipv6 \n") | |
for line in output.split("\n"): | |
if line == "": | |
continue | |
logger.debug('Got line: "%s"' % line) | |
line_match = asa_ip_local_pool.match(line) | |
if line_match: | |
pool = line_match.group(1) | |
logger.debug('Got local IP pool: "%s"' % pool) | |
self.pools[pool] = 0 | |
ASA_pool_stats.labels(target=self.target, pool=pool, status="free").set( | |
0 | |
) | |
ASA_pool_stats.labels(target=self.target, pool=pool, status="hold").set( | |
0 | |
) | |
ASA_pool_stats.labels( | |
target=self.target, pool=pool, status="in_use" | |
).set(0) | |
else: | |
logger.debug('Failed to find pool in : "%s"' % line) | |
while True: | |
start_time = time.time() | |
for pool in self.pools: | |
logger.debug('Fetching current usage on local IP pool: "%s"' % pool) | |
output = self.net_connect.send_command( | |
"show ip local pool %s" % pool | |
).split("\n") | |
# Begin End Mask Free Held In use | |
# 192.0.2.1 192.0.2.254 255.255.255.0 254 0 0 | |
# XXX: go straight for second line | |
line_match = asa_ip_local_pool_usage.match(output[1]) | |
if line_match: | |
free = int(line_match.group(1)) | |
hold = int(line_match.group(2)) | |
in_use = int(line_match.group(3)) | |
logger.debug( | |
'Got local IP pool "%s" usage: %d/%d/%d' | |
% (pool, free, hold, in_use) | |
) | |
self.pools[pool] = in_use | |
ASA_pool_stats.labels( | |
target=self.target, pool=pool, status="free" | |
).set(free) | |
ASA_pool_stats.labels( | |
target=self.target, pool=pool, status="hold" | |
).set(hold) | |
ASA_pool_stats.labels( | |
target=self.target, pool=pool, status="in_use" | |
).set(in_use) | |
else: | |
logger.debug('Invalid line "%s"' % output) | |
logger.debug("Finished collecting stats from %s" % self.target) | |
ASA_collect.labels(target=self.target).observe(time.time() - start_time) | |
logger.debug( | |
"Sleeping in ASA thread for %d s before pulling next stats" | |
% args.asa_delay | |
) | |
time.sleep(args.asa_delay) | |
def check_ios(self): | |
if self.net_connect is None: | |
raise ("No SSH connection present!") | |
while True: | |
start_time = time.time() | |
logger.debug("Fetching IOSXE CoPP stats on %s" % self.target) | |
data = self.net_connect.send_command("show policy-map control-plane all") | |
if data: | |
section = "" | |
for l in data.split("\n"): | |
match = class_match.match(l) | |
if match: | |
section = match.group(1) | |
self.sections[section] = {} | |
continue | |
# in "section" | |
m = by_match.match(l) | |
if m: | |
self.sections[section]["packets"] = m.group(1) | |
self.sections[section]["bytes"] = m.group(2) | |
IOSXE_copp_stats.labels( | |
target=self.target, chain=section, status="packets" | |
).set(m.group(1)) | |
IOSXE_copp_stats.labels( | |
target=self.target, chain=section, status="bytes" | |
).set(m.group(2)) | |
continue | |
m = conf_match.match(l) | |
if m: | |
self.sections[section]["conformed"] = m.group(1) | |
IOSXE_copp_stats.labels( | |
target=self.target, chain=section, status="conformed" | |
).set(m.group(1)) | |
continue | |
m = exc_match.match(l) | |
if m: | |
self.sections[section]["exceeded"] = m.group(1) | |
IOSXE_copp_stats.labels( | |
target=self.target, chain=section, status="exceeded" | |
).set(m.group(1)) | |
continue | |
# logger.debug('Ignoring "%s"' % l) | |
else: | |
logger.error('No data received from "%s"' % self.target) | |
# XXX: unfortunately no |format on IOS | |
data = self.net_connect.send_command( | |
"show platform hardware fed switch active fwd-asic resource tcam utilization" | |
) | |
if data: | |
asic = 0 | |
for l in data.split("\n"): | |
# FIXME: in case of multi-asic, match ASIC first: | |
# "CAM Utilization for ASIC [0]" | |
m = tcam_util_single_val.match(l) | |
if m: | |
table = m.group(1) | |
max_val = int(m.group(2)) | |
used_val = int(m.group(3)) | |
logger.debug( | |
"Got IOSXE TCAM utilization for %s as %d/%d" | |
% (table, used_val, max_val) | |
) | |
IOSXE_tcam_utilization.labels( | |
target=self.target, asic=asic, table=table, ref="max" | |
).set(max_val) | |
IOSXE_tcam_utilization.labels( | |
target=self.target, asic=asic, table=table, ref="used_val" | |
).set(used_val) | |
continue | |
m = tcam_util_dual_val.match(l) | |
if m: | |
table = m.group(1) | |
max_val1 = int(m.group(2)) | |
max_val2 = int(m.group(3)) | |
used_val1 = int(m.group(4)) | |
used_val2 = int(m.group(5)) | |
logger.debug( | |
"Got IOSXE TCAM utilization for %s as used:%d/%d, max:%d/%d" | |
% (table, used_val1, used_val2, max_val1, max_val2) | |
) | |
IOSXE_tcam_utilization.labels( | |
target=self.target, asic=asic, table=table, ref="max1" | |
).set(max_val1) | |
IOSXE_tcam_utilization.labels( | |
target=self.target, asic=asic, table=table, ref="max2" | |
).set(max_val2) | |
IOSXE_tcam_utilization.labels( | |
target=self.target, asic=asic, table=table, ref="used_val1" | |
).set(used_val1) | |
IOSXE_tcam_utilization.labels( | |
target=self.target, asic=asic, table=table, ref="used_val2" | |
).set(used_val2) | |
continue | |
logger.debug("Fetching CPU policer stats") | |
data = self.net_connect.send_command( | |
"sh platform hardware fed switch active qos queue stats internal cpu policer" | |
) | |
section = 0 | |
p_queue = {} | |
p_class = {} | |
if data: | |
for l in data.split("\n"): | |
logger.debug('Checking "%s"' % l) | |
match = ios_section_re.match(l) | |
if match: | |
section += 1 | |
logger.debug("Found next section. Now at: %d" % section) | |
if section == 2: | |
logger.debug("Checking CPU Queue Statistics") | |
match = cpu_q_stat_re.match(l) | |
if match: | |
logger.debug("Found match for CPU Queue Statistics line") | |
qid = int(match.group(1)) | |
# plcidx = int(match.group(2)) | |
queue_name = match.group(3) | |
enabled = match.group(4) | |
default_rate = int(match.group(5)) | |
set_rate = int(match.group(6)) | |
drop_bytes = int(match.group(7)) | |
drop_frames = int(match.group(8)) | |
logger.debug( | |
"Got: QId: %d (%s), enabled: %s, rate: %d/%d, drop: %d/%d" | |
% ( | |
qid, | |
queue_name, | |
enabled, | |
default_rate, | |
set_rate, | |
drop_bytes, | |
drop_frames, | |
) | |
) | |
if enabled == "Yes": | |
logger.debug("Queue is enabled. Adding to prometheus.") | |
IOSXE_CPU_queue_stats.labels( | |
target=self.target, | |
queue=queue_name, | |
match="set_rate", | |
).set(set_rate) | |
IOSXE_CPU_queue_stats.labels( | |
target=self.target, | |
queue=queue_name, | |
match="drop_bytes", | |
).set(drop_bytes) | |
IOSXE_CPU_queue_stats.labels( | |
target=self.target, | |
queue=queue_name, | |
match="drop_frames", | |
).set(drop_frames) | |
# else: | |
# logger.debug('Ignoring CPU Queue line!') | |
elif section == 4 or section == 5: | |
logger.debug("Checking CPU Queue Policer Statistics") | |
match = cpu_q_policer_stat_re.match(l) | |
if match: | |
logger.debug("Found match for CPU Queue Policer Statistics") | |
p_index = match.group(1) | |
p_accept_bytes = match.group(2) | |
p_accept_frames = match.group(3) | |
p_drop_bytes = match.group(4) | |
p_drop_frames = match.group(5) | |
p_queue[p_index] = {} | |
p_queue[p_index]["accept_bytes"] = p_accept_bytes | |
p_queue[p_index]["accept_frames"] = p_accept_frames | |
p_queue[p_index]["drop_bytes"] = p_drop_bytes | |
p_queue[p_index]["drop_frames"] = p_drop_frames | |
# else: | |
# logger.debug('Ignoring CPU Queue Policer line!') | |
elif section == 12: | |
logger.debug("Checking CPP Classes to queue map") | |
match = cpp_classes_re.match(l) | |
if match: | |
# logger.debug('Found match for CPP Classes to queue map') | |
p_index = match.group(1) | |
p_class[p_index] = match.group(2) | |
# p_description = match.group(3) | |
# else: | |
# logger.debug('Ignoring CPP Classes to queue map line!') | |
# else: | |
# logger.debug('Ignoring line in block we do not care about!') | |
for c in p_class: | |
logger.debug("Adding class %s to CPU policer Gauge" % c) | |
IOSXE_CPU_policer_queue_accept_bytes.labels( | |
target=self.target, policer=p_class[c] | |
).set(p_queue[c]["accept_bytes"]) | |
IOSXE_CPU_policer_queue_accept_frames.labels( | |
target=self.target, policer=p_class[c] | |
).set(p_queue[c]["accept_frames"]) | |
IOSXE_CPU_policer_queue_drop_bytes.labels( | |
target=self.target, policer=p_class[c] | |
).set(p_queue[c]["drop_bytes"]) | |
IOSXE_CPU_policer_queue_drop_frames.labels( | |
target=self.target, policer=p_class[c] | |
).set(p_queue[c]["drop_frames"]) | |
else: | |
logger.error('No data received from "%s"' % self.target) | |
logger.debug("Finished collecting stats from %s" % self.target) | |
IOSXE_collect.labels(target=self.target).observe(time.time() - start_time) | |
logger.debug( | |
"Sleeping in IOSXE thread for %d s before pulling next stats" | |
% args.iosxe_delay | |
) | |
time.sleep(args.iosxe_delay) | |
def check_nxos(self): | |
if self.net_connect is None: | |
raise ("No SSH connection present!") | |
while True: | |
start_time = time.time() | |
logger.debug("Fetching NX-OS CoPP stats on %s" % self.target) | |
# FIXME: looks funny on 7702 .. currently only works on 7706... | |
data = self.net_connect.send_command( | |
"sh hardware internal cpu-mac inband stats" | |
) | |
if data: | |
section = None | |
for l in data.split("\n"): | |
s = section_re.match(l) | |
if s: | |
section = s.group(1) | |
logger.debug('Found section "%s"' % section) | |
continue | |
if section == "RMON counters": | |
c = pkt_counters_re.match(l) | |
if c: | |
match = c.group(1) | |
rx = int(c.group(2)) | |
tx = int(c.group(3)) | |
logger.debug( | |
"Found %s counters in RMON: Rx:%d/Tx:%d" | |
% (match, rx, tx) | |
) | |
NXOS_copp_counters.labels( | |
target=self.target, | |
section=section, | |
match=match, | |
direction="rx", | |
).set(rx) | |
NXOS_copp_counters.labels( | |
target=self.target, | |
section=section, | |
match=match, | |
direction="tx", | |
).set(tx) | |
continue | |
if section == "Throttle statistics": | |
c = throttle_re.match(l) | |
if c: | |
direction = c.group(1) | |
cur = int(c.group(2)) | |
max_pps = int(c.group(3)) | |
unit = c.group(4) | |
logger.debug( | |
"Found %s counters in throttle stats: %d/%d %s" | |
% (direction, cur, max_pps, unit) | |
) | |
NXOS_copp_throttle.labels( | |
target=self.target, | |
section=section, | |
match=match, | |
direction=direction, | |
unit=unit, | |
state="cur", | |
).set(cur) | |
NXOS_copp_throttle.labels( | |
target=self.target, | |
section=section, | |
match=match, | |
direction=direction, | |
unit=unit, | |
state="max", | |
).set(max_pps) | |
continue | |
if section == "NAPI statistics": | |
p = napi_re.match(l) | |
if p: | |
direction = p.group(1) | |
pkt_match = p.group(2) | |
pkt = int(p.group(3)) | |
logger.debug( | |
"Found %s %s in NAPI stats: %d" | |
% (direction, pkt_match, pkt) | |
) | |
NXOS_napi_stats.labels( | |
target=self.target, | |
section=section, | |
match=pkt_match, | |
direction=direction, | |
).set(pkt) | |
continue | |
if section == "qdisc stats": | |
p = qdisc_re.match(l) | |
if p: | |
pkt_match = p.group(1) | |
pkt = int(p.group(2)) | |
logger.debug( | |
"Found %s in qdisc stats: %d" % (pkt_match, pkt) | |
) | |
NXOS_qdisc_stats.labels( | |
target=self.target, section=section, match=pkt_match | |
).set(pkt) | |
continue | |
# logger.debug('Ignoring "%s"' % l) | |
data = self.net_connect.send_command( | |
"show policy-map interface control-plane | json" | |
) | |
if data: | |
# # if we don't use the "|json" this is the regex style match | |
# service_policy = {} | |
# policy = '' | |
# class_map = '' | |
# module = 1 | |
# for l in data.split('\n'): | |
# match = service_policy_re.match(l) | |
# if match: | |
# policy = match.group(1) | |
# logger.debug('Found service-policy block "%s"'% policy) | |
# service_policy[policy] = {} | |
# continue | |
# | |
# # in service-policy | |
# c = class_map_re.match(l) | |
# if c: | |
# class_map = c.group(1) | |
# logger.debug('Found class-map block "%s" in service-policy "%s"' % (class_map,policy)) | |
# service_policy[policy][class_map] = {} | |
# continue | |
# # in class-map | |
# m = module_re.match(l) | |
# if m: | |
# module = int(m.group(1)) | |
# logger.debug('Found module block "%d" in class-map "%s" in service-policy "%s"' % (module,class_map,policy)) | |
# service_policy[policy][class_map][module] = {} | |
# continue | |
# # under module | |
# val = value_re.match(l) | |
# if val: | |
# val_type = val.group(1) | |
# val_value = int(val.group(2)) | |
# logger.debug('Found %s=%d in module block "%d" in class-map "%s" in service-policy "%s"' % (val_type,val_value,module,class_map,policy)) | |
# NXOS_copp_stats.labels(target=self.target,service_policy=policy,class_map=class_map,module=module,match=val_type).set(val_value) | |
# continue | |
# | |
# #logger.debug('Ignoring "%s"' % l) | |
j_data = json.loads(data) | |
# pp.pprint(j_data) | |
policy = j_data["pmap-name"] | |
for row in j_data["TABLE_cmap"]["ROW_cmap"]: | |
# print('Row:') | |
# pp.pprint(row) | |
# print('----------------------------------------------------------------------------') | |
class_map = row["cmap-name-out"] | |
# print('Got type: %s' % type(row['TABLE_slot']['ROW_slot'])) | |
if type(row["TABLE_slot"]["ROW_slot"]) is dict: | |
# 7702 style... | |
module = row["TABLE_slot"]["ROW_slot"]["slot-no-out"] | |
for ptype in ["conform", "violate"]: | |
for k in row["TABLE_slot"]["ROW_slot"][ | |
"TABLE_%s_inst" % ptype | |
]["ROW_%s_inst" % ptype]: | |
if k.endswith("-ts"): | |
# skip non-integer values | |
continue | |
v = int( | |
row["TABLE_slot"]["ROW_slot"][ | |
"TABLE_%s_inst" % ptype | |
]["ROW_%s_inst" % ptype][k] | |
) | |
logger.debug( | |
"Found %s service_policy, class %s, module %s, match %s = %s" | |
% (policy, class_map, module, k, v) | |
) | |
NXOS_copp_stats.labels( | |
target=self.target, | |
service_policy=policy, | |
class_map=class_map, | |
module=module, | |
match=k, | |
).set(v) | |
else: | |
# 7706 style | |
# array of dict | |
for r in row["TABLE_slot"]["ROW_slot"]: | |
module = r["slot-no-out"] | |
for ptype in ["conform", "violate"]: | |
for k in r["TABLE_%s_inst" % ptype][ | |
"ROW_%s_inst" % ptype | |
]: | |
if k.endswith("-ts"): | |
# skip non-integer values | |
continue | |
v = int( | |
r["TABLE_%s_inst" % ptype][ | |
"ROW_%s_inst" % ptype | |
][k] | |
) | |
logger.debug( | |
"Found %s service_policy, class %s, module %s, match %s = %s" | |
% (policy, class_map, module, k, v) | |
) | |
NXOS_copp_stats.labels( | |
target=self.target, | |
service_policy=policy, | |
class_map=class_map, | |
module=module, | |
match=k, | |
).set(v) | |
# data = self.net_connect.send_command('show hardware capacity forwarding| json') | |
# if data: | |
# j_data = json.loads(data) | |
# module = j_data['TABLE_module']['ROW_module']['module_number'] | |
# for m in j_data['TABLE_module']['ROW_module']['TABLE_resource_util_info']['ROW_resource_util_info']: | |
# res_hdr = m['resource_hdr'] | |
# ents_use = m['ents_use'] | |
# if 'ents_free' in m: | |
# ents_free = m['ents_free'] | |
# ents_pctage = m['ents_pctage'] | |
# logger.debug('%s = %s/%s (%s %%)' % (res_hdr,ents_use,ents_free,ents_pctage)) | |
# else: | |
# logger.debug('%s = %s' % (res_hdr,ents_use)) | |
else: | |
logger.error('No data received from "%s"' % self.target) | |
logger.debug("Finished collecting stats from %s" % self.target) | |
NXOS_collect.labels(target=self.target).observe(time.time() - start_time) | |
logger.debug( | |
"Sleeping in NX-OS thread for %d s before pulling next stats" | |
% args.nxos_delay | |
) | |
time.sleep(args.nxos_delay) | |
def run(self): | |
if self.target is None: | |
logger.error("No AK target set!") | |
return | |
if self.aktype is None: | |
logger.error("No AK type set!") | |
return | |
if self.login is None: | |
logger.error("No AK login set!") | |
return | |
if self.password == "": | |
logger.error("No AK login set!") | |
return | |
logger.debug( | |
"Starting AK data gather (%s) thread for %s as %s" | |
% (self.aktype, self.target, self.login) | |
) | |
ak_login = { | |
"device_type": self.aktype, | |
"host": self.target, | |
"username": self.login, | |
"password": self.password, | |
} | |
while True: | |
try: | |
logger.debug("Open SSH connection to %s" % self.target) | |
if args.ssh_log: | |
session_log = "/tmp/%s.log" % self.target | |
else: | |
session_log = None | |
self.net_connect = ConnectHandler( | |
**ak_login, verbose=args.ssh_log, session_log=session_log | |
) | |
# while we have a working SSH connection, poll CLI output every $delay | |
while True: | |
if self.aktype == "cisco_asa": | |
logger.debug("Running check_asa and produce metrics") | |
self.check_asa() | |
logger.debug("Done: Running check_asa in thread") | |
elif self.aktype == "cisco_ios": | |
logger.debug("Running check_ios and produce metrics") | |
self.check_ios() | |
logger.debug("Done: Running check_ios in thread") | |
elif self.aktype == "cisco_nxos": | |
logger.debug("Running check_nxos and produce metrics") | |
self.check_nxos() | |
logger.debug("Done: Running check_nxos in thread") | |
except Exception: | |
# Ignore failures, we will try again after refresh_interval. | |
# Most of them are termporary ie. connectivity problmes | |
logger.error("Error getting stats", exc_info=True) | |
logger.debug( | |
"Waiting before reconnect in poller thread for %d s" % args.delay | |
) | |
time.sleep(args.delay) | |
if __name__ == "__main__": | |
if "asa" in config: | |
logger.debug("Starting ASA gatherer thread") | |
asa_gatherer = {} | |
for ak in config["asa"]: | |
asa_gatherer[ak] = AKGatherer() | |
logger.debug("Adding Thread for %s (login: %s)" % (ak, config["login"])) | |
asa_gatherer[ak].settarget( | |
ak, config["login"], config["password"], "cisco_asa" | |
) | |
asa_gatherer[ak].start() | |
if "iosxe" in config: | |
logger.debug("Starting IOSXE gatherer thread") | |
iosxe_gatherer = {} | |
for ak in config["iosxe"]: | |
iosxe_gatherer[ak] = AKGatherer() | |
logger.debug("Adding Thread for %s (login: %s)" % (ak, config["login"])) | |
iosxe_gatherer[ak].settarget( | |
ak, config["login"], config["password"], "cisco_ios" | |
) | |
iosxe_gatherer[ak].start() | |
if "nxos" in config: | |
logger.debug("Starting NX-OS gatherer thread") | |
nxos_gatherer = {} | |
for ak in config["nxos"]: | |
nxos_gatherer[ak] = AKGatherer() | |
logger.debug("Adding Thread for %s (login: %s)" % (ak, config["login"])) | |
nxos_gatherer[ak].settarget( | |
ak, config["login"], config["password"], "cisco_nxos" | |
) | |
nxos_gatherer[ak].start() | |
# ...and now serve the registry contents so that we can consume it.. | |
if os.environ.get("LISTEN_PID", None) == str(os.getpid()): | |
# systemd socket activation will need that httpd is waiting for socket | |
# to be passed - while collection still updates in the background | |
# inherit the socket | |
logger.debug("Starting systemd socket activation http server") | |
CustomMetricsHandler = MetricsHandler.factory(REGISTRY) | |
server_args = [("localhost", args.listen_port), CustomMetricsHandler] | |
httpd = SocketInheritingHTTPServer(*server_args, fd=SYSTEMD_FIRST_SOCKET_FD) | |
logging.info( | |
"netstatsexporter started for socket activation on fd %s" | |
% (SYSTEMD_FIRST_SOCKET_FD,) | |
) | |
try: | |
logging.info( | |
"netstatsexporter httpd running on socket fd %s" | |
% (SYSTEMD_FIRST_SOCKET_FD,) | |
) | |
httpd.serve_forever() | |
except KeyboardInterrupt: | |
httpd.socket.close() | |
else: | |
# start the server normally | |
# Start up the server to expose the metrics. | |
logger.debug("Starting http server") | |
start_http_server(args.listen_port) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment