From f1b36bd171bb6c9a17019025fcb06ac87d116d30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20B=C3=BChler?= Date: Mon, 11 Apr 2022 18:17:00 +0200 Subject: [PATCH] prometheus stats cli tool --- setup.cfg | 1 + src/capport/stats.py | 61 ++++++++++++++++++++++++++++++++ src/capport/utils/ipneigh.py | 21 +++++++++++ stats-to-prometheus-collector.sh | 24 +++++++++++++ stats.sh | 8 +++++ 5 files changed, 115 insertions(+) create mode 100644 src/capport/stats.py create mode 100755 stats-to-prometheus-collector.sh create mode 100755 stats.sh diff --git a/setup.cfg b/setup.cfg index 81d1b13..e67429a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,3 +34,4 @@ where = src [options.entry_points] console_scripts = capport-control = capport.control.run:main + capport-stats = capport.stats:main diff --git a/src/capport/stats.py b/src/capport/stats.py new file mode 100644 index 0000000..2831890 --- /dev/null +++ b/src/capport/stats.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +import ipaddress +import sys +import typing +import time + +import trio + +import capport.utils.ipneigh +import capport.utils.nft_set +from . import cptypes + + +def print_metric(name: str, mtype: str, value, *, now: typing.Optional[int]=None, help: typing.Optional[str]=None): + # no labels in our names for now, always print help and type + if help: + print(f'# HELP {name} {help}') + print(f'# TYPE {name} {mtype}') + if now: + print(f'{name} {value} {now}') + else: + print(f'{name} {value}') + + +async def amain(client_ifname: str): + ns = capport.utils.nft_set.NftSet() + captive_allowed_entries: typing.Set[cptypes.MacAddress] = { + entry['mac'] + for entry in ns.list() + } + seen_allowed_entries: typing.Set[cptypes.MacAddress] = set() + total_ipv4 = 0 + total_ipv6 = 0 + unique_clients = set() + unique_ipv4 = set() + unique_ipv6 = set() + async with capport.utils.ipneigh.connect() as ipn: + ipn.ip.strict_check = True + async for (mac, addr) in ipn.dump_neighbors(client_ifname): + if mac in captive_allowed_entries: + seen_allowed_entries.add(mac) + unique_clients.add(mac) + if isinstance(addr, ipaddress.IPv4Address): + total_ipv4 += 1 + unique_ipv4.add(mac) + else: + total_ipv6 += 1 + unique_ipv6.add(mac) + print_metric('capport_allowed_macs', 'gauge', len(captive_allowed_entries), help='Number of allowed client mac addresses') + print_metric('capport_allowed_neigh_macs', 'gauge', len(seen_allowed_entries), help='Number of allowed client mac addresses seen in neighbor cache') + print_metric('capport_unique', 'gauge', len(unique_clients), help='Number of clients (mac addresses) in client network seen in neighbor cache') + print_metric('capport_unique_ipv4', 'gauge', len(unique_ipv4), help='Number of IPv4 clients (unique per mac) in client network seen in neighbor cache') + print_metric('capport_unique_ipv6', 'gauge', len(unique_ipv6), help='Number of IPv4 clients (unique per mac) in client network seen in neighbor cache') + print_metric('capport_total_ipv4', 'gauge', total_ipv4, help='Number of IPv4 addresses seen in neighbor cache') + print_metric('capport_total_ipv6', 'gauge', total_ipv6, help='Number of IPv6 addresses seen in neighbor cache') + + +def main(): + assert len(sys.argv) == 2, "Need name of client interface as argument" + trio.run(amain, sys.argv[1]) diff --git a/src/capport/utils/ipneigh.py b/src/capport/utils/ipneigh.py index 8cc6dcf..4c9c60a 100644 --- a/src/capport/utils/ipneigh.py +++ b/src/capport/utils/ipneigh.py @@ -2,10 +2,14 @@ from __future__ import annotations import contextlib import errno +import ipaddress +import socket import typing import pr2modules.iproute.linux import pr2modules.netlink.exceptions +import pr2modules.netlink.rtnl +import pr2modules.netlink.rtnl.ndmsg from capport import cptypes @@ -18,6 +22,7 @@ async def connect(): class NeighborController: def __init__(self): self.ip = pr2modules.iproute.linux.IPRoute() + self.ip.bind() async def get_neighbor( self, @@ -61,3 +66,19 @@ class NeighborController: if e.code == errno.ENOENT: return None raise + + async def dump_neighbors(self, interface: str) -> typing.Generator[typing.Tuple[cptypes.MacAddress, cptypes.IPAddress]]: + ifindex = socket.if_nametoindex(interface) + unicast_num = pr2modules.netlink.rtnl.rt_type['unicast'] + # ip.neigh doesn't support AF_UNSPEC (as it is 0 and evaluates to `False` and gets forced to AF_INET) + for family in (socket.AF_INET, socket.AF_INET6): + for neigh in self.ip.neigh('dump', ifindex=ifindex, family=family): + if neigh['ndm_type'] != unicast_num: + continue + mac = neigh.get_attr(neigh.name2nla('lladdr')) + if not mac: + continue + dst = ipaddress.ip_address(neigh.get_attr(neigh.name2nla('dst'))) + if dst.is_link_local: + continue + yield (cptypes.MacAddress.parse(mac), dst) diff --git a/stats-to-prometheus-collector.sh b/stats-to-prometheus-collector.sh new file mode 100755 index 0000000..6b226a4 --- /dev/null +++ b/stats-to-prometheus-collector.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +set -e + +base=$(dirname "$(readlink -f "$0")") +cd "${base}" + +instance=$1 +ifname=$2 + +if [ -z "${instance}" -o -z "${ifname}" ]; then + echo >&2 "Syntax: $0 instancename clientifname" + exit 1 +fi + +targetname="/var/lib/prometheus/node-exporter/capport-${instance}.prom" +tmpname="${targetname}.$$" + +if ./stats.sh "${ifname}" > "${tmpname}"; then + mv "${tmpname}" "${targetname}" +else + rm "${tmpname}" + exit 1 +fi diff --git a/stats.sh b/stats.sh new file mode 100755 index 0000000..28d8a14 --- /dev/null +++ b/stats.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -e + +base=$(dirname "$(readlink -f "$0")") +cd "${base}" + +exec ./venv/bin/capport-stats "$@"