From a806adad2017413071d20d519d9a5d9b6b937474 Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Sat, 1 Jan 2022 16:51:10 +0100 Subject: vidhar: prometheus: nftables --- hosts/vidhar/prometheus/default.nix | 44 ++++++ overlays/nftables-prometheus-exporter/default.nix | 27 ++++ .../nftables-prometheus-exporter.py | 151 +++++++++++++++++++++ 3 files changed, 222 insertions(+) create mode 100644 overlays/nftables-prometheus-exporter/default.nix create mode 100644 overlays/nftables-prometheus-exporter/nftables-prometheus-exporter.py diff --git a/hosts/vidhar/prometheus/default.nix b/hosts/vidhar/prometheus/default.nix index f915fc68..87035d5d 100644 --- a/hosts/vidhar/prometheus/default.nix +++ b/hosts/vidhar/prometheus/default.nix @@ -142,6 +142,13 @@ in { relabel_configs = relabelHosts; scrape_interval = "1s"; } + { job_name = "nftables"; + static_configs = [ + { targets = ["localhost:9901"]; } + ]; + relabel_configs = relabelHosts; + scrape_interval = "1s"; + } ]; }; users.users.${config.services.prometheus.exporters.unbound.user} = { @@ -193,5 +200,42 @@ in { format = "binary"; sopsFile = ./zte_10.141.1.3; }; + + systemd.services."prometheus-nftables-exporter" = { + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + serviceConfig = { + Restart = "always"; + PrivateTmp = true; + WorkingDirectory = "/tmp"; + DynamicUser = true; + CapabilityBoundingSet = [""]; + DeviceAllow = [""]; + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateDevices = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectSystem = "strict"; + RemoveIPC = true; + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + UMask = "0077"; + AmbientCapabilities = [ "CAP_NET_ADMIN" ]; + + Type = "simple"; + ExecStart = "${pkgs.nftables-prometheus-exporter}/bin/nftables-prometheus-exporter"; + Environment = "ZTE_HOSTNAME=localhost ZTE_PORT=9901"; + }; + }; }; } diff --git a/overlays/nftables-prometheus-exporter/default.nix b/overlays/nftables-prometheus-exporter/default.nix new file mode 100644 index 00000000..452f160f --- /dev/null +++ b/overlays/nftables-prometheus-exporter/default.nix @@ -0,0 +1,27 @@ +final: prev: +let + inpPython = final.python310; +in { + nftables-prometheus-exporter = prev.stdenv.mkDerivation rec { + name = "nftables-prometheus-exporter"; + src = ./nftables-prometheus-exporter.py; + + phases = [ "buildPhase" "checkPhase" "installPhase" ]; + + python = inpPython.withPackages (ps: with ps; []); + + buildPhase = '' + substituteAll $src nftables-prometheus-exporter + ''; + + doCheck = true; + checkPhase = '' + ${python}/bin/python -m py_compile nftables-prometheus-exporter + ''; + + installPhase = '' + install -m 0755 -D -t $out/bin \ + nftables-prometheus-exporter + ''; + }; +} diff --git a/overlays/nftables-prometheus-exporter/nftables-prometheus-exporter.py b/overlays/nftables-prometheus-exporter/nftables-prometheus-exporter.py new file mode 100644 index 00000000..c5c5139d --- /dev/null +++ b/overlays/nftables-prometheus-exporter/nftables-prometheus-exporter.py @@ -0,0 +1,151 @@ +#!@python@/bin/python + +import json + +from os import environ +import sys + +from http.server import BaseHTTPRequestHandler, HTTPServer + +from urllib.parse import urlparse + +from textwrap import dedent + +import subprocess + + +def _format_prom_attrs(**attrs): + if not attrs: + return '' + + return '{' + ','.join(map(lambda k: f'{k}="{attrs[k]}"', attrs)) + '}' + +def _format_prom_metrics(metricName, metricType, metrics, metricHelp=''): + metricStr = dedent(f''' + # HELP {metricName} {metricHelp} + # TYPE {metricName} {metricType} + ''').lstrip() + for (attrs, val) in metrics: + attrs_str = _format_prom_attrs(**attrs) + metricStr += dedent(f''' + {metricName}{attrs_str} {val} + ''').lstrip() + return metricStr + + +class NFTMetrics: + _instance = None + + @classmethod + def instance(cls): + if cls._instance is None: + cls._instance = cls.__new__(cls) + cls._instance.attrs = None + return cls._instance + + + def __init__(self): + raise RuntimeError('Call instance() instead') + + def update(self): + attrs = dict() + queries = dict() + + for query_name in ['ruleset', 'counters', 'maps', 'meters', 'sets']: + process = subprocess.run( + ('nft', '--json', 'list', query_name), + capture_output = True, check = True, text = True + ) + data = json.loads(process.stdout) + version = data['nftables'][0]['metainfo']['json_schema_version'] + if version != 1: + raise RuntimeError(f'nftables json schema v{version} is not supported') + queries[query_name] = data['nftables'][1:] + + + def extract_query(query_name, type_name): + return [ + item[type_name] + for item in queries[query_name] + if type_name in item + ] + + attrs['rules_count'] = len(extract_query('ruleset', 'rule')) + attrs['chain_count'] = len(extract_query('ruleset', 'chain')) + attrs['counters'] = extract_query('counters', 'counter') + attrs['maps'] = extract_query('maps', 'map') + attrs['meters'] = extract_query('meters', 'meter') + attrs['sets'] = extract_query('sets', 'set') + + self.attrs = attrs + + def json_text(self): + return json.dumps(self.attrs) + + def prometheus(self): + metrics = '' + + metrics += _format_prom_metrics('nftables_rules_count', 'gauge', [({}, self.attrs['rules_count'])], 'Number of nftables rules') + metrics += _format_prom_metrics('nftables_chains_count', 'gauge', [({}, self.attrs['chain_count'])], 'Number of nftables chains') + + counter_bytes = [] + counter_packets = [] + for counter in self.attrs['counters']: + labels = { k: v for k, v in counter.items() if k not in set(['bytes', 'packets']) } + counter_bytes += [(labels, counter['bytes'])] + counter_packets += [(labels, counter['packets'])] + metrics += _format_prom_metrics('nftables_counter_bytes', 'counter', counter_bytes) + metrics += _format_prom_metrics('nftables_counter_packets_count', 'counter', counter_packets) + + map_counts = [] + for meter in self.attrs['maps']: + labels = { k: v for k, v in counter.items() if k not in set(['elem']) } + map_counts += [(labels, len(meter['elem']))] + metrics += _format_prom_metrics('nftables_map_elem_count', 'gauge', map_counts) + + meter_counts = [] + for meter in self.attrs['meters']: + labels = { k: v for k, v in counter.items() if k not in set(['elem']) } + meter_counts += [(labels, len(meter['elem']))] + metrics += _format_prom_metrics('nftables_meter_elem_count', 'gauge', meter_counts) + + set_counts = [] + for meter in self.attrs['sets']: + labels = { k: v for k, v in counter.items() if k not in set(['elem']) } + set_counts += [(labels, len(meter['elem']))] + metrics += _format_prom_metrics('nftables_set_elem_count', 'gauge', set_counts) + + return metrics.encode('utf-8') + +class NFTMetricsServer(BaseHTTPRequestHandler): + def do_GET(self): + zte_metrics = NFTMetrics.instance() + zte_metrics.update() + + url = urlparse(self.path) + + match url.path: + case '/metrics.json': + self.send_response(200) + self.send_header("Content-type", "application/json") + self.end_headers() + + self.wfile.write(zte_metrics.json_text().encode('utf-8')) + case '/metrics': + self.send_response(200) + self.send_header("Content-type", "text/plain") + self.end_headers() + + self.wfile.write(zte_metrics.prometheus()) + case _: + self.send_response(404) + self.end_headers() + + +def main(): + webServer = HTTPServer((str(environ.get('NFT_HOSTNAME')), int(environ.get('NFT_PORT'))), NFTMetricsServer) + + webServer.serve_forever() + +if __name__ == "__main__": + sys.exit(main()) -- cgit v1.2.3