diff options
-rw-r--r-- | hosts/vidhar/prometheus/default.nix | 44 | ||||
-rw-r--r-- | overlays/nftables-prometheus-exporter/default.nix | 27 | ||||
-rw-r--r-- | overlays/nftables-prometheus-exporter/nftables-prometheus-exporter.py | 151 |
3 files changed, 222 insertions, 0 deletions
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 { | |||
142 | relabel_configs = relabelHosts; | 142 | relabel_configs = relabelHosts; |
143 | scrape_interval = "1s"; | 143 | scrape_interval = "1s"; |
144 | } | 144 | } |
145 | { job_name = "nftables"; | ||
146 | static_configs = [ | ||
147 | { targets = ["localhost:9901"]; } | ||
148 | ]; | ||
149 | relabel_configs = relabelHosts; | ||
150 | scrape_interval = "1s"; | ||
151 | } | ||
145 | ]; | 152 | ]; |
146 | }; | 153 | }; |
147 | users.users.${config.services.prometheus.exporters.unbound.user} = { | 154 | users.users.${config.services.prometheus.exporters.unbound.user} = { |
@@ -193,5 +200,42 @@ in { | |||
193 | format = "binary"; | 200 | format = "binary"; |
194 | sopsFile = ./zte_10.141.1.3; | 201 | sopsFile = ./zte_10.141.1.3; |
195 | }; | 202 | }; |
203 | |||
204 | systemd.services."prometheus-nftables-exporter" = { | ||
205 | wantedBy = [ "multi-user.target" ]; | ||
206 | after = [ "network.target" ]; | ||
207 | serviceConfig = { | ||
208 | Restart = "always"; | ||
209 | PrivateTmp = true; | ||
210 | WorkingDirectory = "/tmp"; | ||
211 | DynamicUser = true; | ||
212 | CapabilityBoundingSet = [""]; | ||
213 | DeviceAllow = [""]; | ||
214 | LockPersonality = true; | ||
215 | MemoryDenyWriteExecute = true; | ||
216 | NoNewPrivileges = true; | ||
217 | PrivateDevices = true; | ||
218 | ProtectClock = true; | ||
219 | ProtectControlGroups = true; | ||
220 | ProtectHome = true; | ||
221 | ProtectHostname = true; | ||
222 | ProtectKernelLogs = true; | ||
223 | ProtectKernelModules = true; | ||
224 | ProtectKernelTunables = true; | ||
225 | ProtectSystem = "strict"; | ||
226 | RemoveIPC = true; | ||
227 | RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; | ||
228 | RestrictNamespaces = true; | ||
229 | RestrictRealtime = true; | ||
230 | RestrictSUIDSGID = true; | ||
231 | SystemCallArchitectures = "native"; | ||
232 | UMask = "0077"; | ||
233 | AmbientCapabilities = [ "CAP_NET_ADMIN" ]; | ||
234 | |||
235 | Type = "simple"; | ||
236 | ExecStart = "${pkgs.nftables-prometheus-exporter}/bin/nftables-prometheus-exporter"; | ||
237 | Environment = "ZTE_HOSTNAME=localhost ZTE_PORT=9901"; | ||
238 | }; | ||
239 | }; | ||
196 | }; | 240 | }; |
197 | } | 241 | } |
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 @@ | |||
1 | final: prev: | ||
2 | let | ||
3 | inpPython = final.python310; | ||
4 | in { | ||
5 | nftables-prometheus-exporter = prev.stdenv.mkDerivation rec { | ||
6 | name = "nftables-prometheus-exporter"; | ||
7 | src = ./nftables-prometheus-exporter.py; | ||
8 | |||
9 | phases = [ "buildPhase" "checkPhase" "installPhase" ]; | ||
10 | |||
11 | python = inpPython.withPackages (ps: with ps; []); | ||
12 | |||
13 | buildPhase = '' | ||
14 | substituteAll $src nftables-prometheus-exporter | ||
15 | ''; | ||
16 | |||
17 | doCheck = true; | ||
18 | checkPhase = '' | ||
19 | ${python}/bin/python -m py_compile nftables-prometheus-exporter | ||
20 | ''; | ||
21 | |||
22 | installPhase = '' | ||
23 | install -m 0755 -D -t $out/bin \ | ||
24 | nftables-prometheus-exporter | ||
25 | ''; | ||
26 | }; | ||
27 | } | ||
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 @@ | |||
1 | #!@python@/bin/python | ||
2 | |||
3 | import json | ||
4 | |||
5 | from os import environ | ||
6 | import sys | ||
7 | |||
8 | from http.server import BaseHTTPRequestHandler, HTTPServer | ||
9 | |||
10 | from urllib.parse import urlparse | ||
11 | |||
12 | from textwrap import dedent | ||
13 | |||
14 | import subprocess | ||
15 | |||
16 | |||
17 | def _format_prom_attrs(**attrs): | ||
18 | if not attrs: | ||
19 | return '' | ||
20 | |||
21 | return '{' + ','.join(map(lambda k: f'{k}="{attrs[k]}"', attrs)) + '}' | ||
22 | |||
23 | def _format_prom_metrics(metricName, metricType, metrics, metricHelp=''): | ||
24 | metricStr = dedent(f''' | ||
25 | # HELP {metricName} {metricHelp} | ||
26 | # TYPE {metricName} {metricType} | ||
27 | ''').lstrip() | ||
28 | for (attrs, val) in metrics: | ||
29 | attrs_str = _format_prom_attrs(**attrs) | ||
30 | metricStr += dedent(f''' | ||
31 | {metricName}{attrs_str} {val} | ||
32 | ''').lstrip() | ||
33 | return metricStr | ||
34 | |||
35 | |||
36 | class NFTMetrics: | ||
37 | _instance = None | ||
38 | |||
39 | @classmethod | ||
40 | def instance(cls): | ||
41 | if cls._instance is None: | ||
42 | cls._instance = cls.__new__(cls) | ||
43 | cls._instance.attrs = None | ||
44 | return cls._instance | ||
45 | |||
46 | |||
47 | def __init__(self): | ||
48 | raise RuntimeError('Call instance() instead') | ||
49 | |||
50 | def update(self): | ||
51 | attrs = dict() | ||
52 | queries = dict() | ||
53 | |||
54 | for query_name in ['ruleset', 'counters', 'maps', 'meters', 'sets']: | ||
55 | process = subprocess.run( | ||
56 | ('nft', '--json', 'list', query_name), | ||
57 | capture_output = True, check = True, text = True | ||
58 | ) | ||
59 | data = json.loads(process.stdout) | ||
60 | version = data['nftables'][0]['metainfo']['json_schema_version'] | ||
61 | if version != 1: | ||
62 | raise RuntimeError(f'nftables json schema v{version} is not supported') | ||
63 | queries[query_name] = data['nftables'][1:] | ||
64 | |||
65 | |||
66 | def extract_query(query_name, type_name): | ||
67 | return [ | ||
68 | item[type_name] | ||
69 | for item in queries[query_name] | ||
70 | if type_name in item | ||
71 | ] | ||
72 | |||
73 | attrs['rules_count'] = len(extract_query('ruleset', 'rule')) | ||
74 | attrs['chain_count'] = len(extract_query('ruleset', 'chain')) | ||
75 | attrs['counters'] = extract_query('counters', 'counter') | ||
76 | attrs['maps'] = extract_query('maps', 'map') | ||
77 | attrs['meters'] = extract_query('meters', 'meter') | ||
78 | attrs['sets'] = extract_query('sets', 'set') | ||
79 | |||
80 | self.attrs = attrs | ||
81 | |||
82 | def json_text(self): | ||
83 | return json.dumps(self.attrs) | ||
84 | |||
85 | def prometheus(self): | ||
86 | metrics = '' | ||
87 | |||
88 | metrics += _format_prom_metrics('nftables_rules_count', 'gauge', [({}, self.attrs['rules_count'])], 'Number of nftables rules') | ||
89 | metrics += _format_prom_metrics('nftables_chains_count', 'gauge', [({}, self.attrs['chain_count'])], 'Number of nftables chains') | ||
90 | |||
91 | counter_bytes = [] | ||
92 | counter_packets = [] | ||
93 | for counter in self.attrs['counters']: | ||
94 | labels = { k: v for k, v in counter.items() if k not in set(['bytes', 'packets']) } | ||
95 | counter_bytes += [(labels, counter['bytes'])] | ||
96 | counter_packets += [(labels, counter['packets'])] | ||
97 | metrics += _format_prom_metrics('nftables_counter_bytes', 'counter', counter_bytes) | ||
98 | metrics += _format_prom_metrics('nftables_counter_packets_count', 'counter', counter_packets) | ||
99 | |||
100 | map_counts = [] | ||
101 | for meter in self.attrs['maps']: | ||
102 | labels = { k: v for k, v in counter.items() if k not in set(['elem']) } | ||
103 | map_counts += [(labels, len(meter['elem']))] | ||
104 | metrics += _format_prom_metrics('nftables_map_elem_count', 'gauge', map_counts) | ||
105 | |||
106 | meter_counts = [] | ||
107 | for meter in self.attrs['meters']: | ||
108 | labels = { k: v for k, v in counter.items() if k not in set(['elem']) } | ||
109 | meter_counts += [(labels, len(meter['elem']))] | ||
110 | metrics += _format_prom_metrics('nftables_meter_elem_count', 'gauge', meter_counts) | ||
111 | |||
112 | set_counts = [] | ||
113 | for meter in self.attrs['sets']: | ||
114 | labels = { k: v for k, v in counter.items() if k not in set(['elem']) } | ||
115 | set_counts += [(labels, len(meter['elem']))] | ||
116 | metrics += _format_prom_metrics('nftables_set_elem_count', 'gauge', set_counts) | ||
117 | |||
118 | return metrics.encode('utf-8') | ||
119 | |||
120 | class NFTMetricsServer(BaseHTTPRequestHandler): | ||
121 | def do_GET(self): | ||
122 | zte_metrics = NFTMetrics.instance() | ||
123 | zte_metrics.update() | ||
124 | |||
125 | url = urlparse(self.path) | ||
126 | |||
127 | match url.path: | ||
128 | case '/metrics.json': | ||
129 | self.send_response(200) | ||
130 | self.send_header("Content-type", "application/json") | ||
131 | self.end_headers() | ||
132 | |||
133 | self.wfile.write(zte_metrics.json_text().encode('utf-8')) | ||
134 | case '/metrics': | ||
135 | self.send_response(200) | ||
136 | self.send_header("Content-type", "text/plain") | ||
137 | self.end_headers() | ||
138 | |||
139 | self.wfile.write(zte_metrics.prometheus()) | ||
140 | case _: | ||
141 | self.send_response(404) | ||
142 | self.end_headers() | ||
143 | |||
144 | |||
145 | def main(): | ||
146 | webServer = HTTPServer((str(environ.get('NFT_HOSTNAME')), int(environ.get('NFT_PORT'))), NFTMetricsServer) | ||
147 | |||
148 | webServer.serve_forever() | ||
149 | |||
150 | if __name__ == "__main__": | ||
151 | sys.exit(main()) | ||