diff options
Diffstat (limited to 'overlays/zte-prometheus-exporter')
-rw-r--r-- | overlays/zte-prometheus-exporter/default.nix | 28 | ||||
-rw-r--r-- | overlays/zte-prometheus-exporter/python-packages.nix | 21 | ||||
-rw-r--r-- | overlays/zte-prometheus-exporter/zte-prometheus-exporter.py | 216 |
3 files changed, 265 insertions, 0 deletions
diff --git a/overlays/zte-prometheus-exporter/default.nix b/overlays/zte-prometheus-exporter/default.nix new file mode 100644 index 00000000..1609724b --- /dev/null +++ b/overlays/zte-prometheus-exporter/default.nix | |||
@@ -0,0 +1,28 @@ | |||
1 | final: prev: | ||
2 | let | ||
3 | packageOverrides = final.callPackage ./python-packages.nix {}; | ||
4 | inpPython = final.python310.override { inherit packageOverrides; }; | ||
5 | in { | ||
6 | zte-prometheus-exporter = prev.stdenv.mkDerivation rec { | ||
7 | name = "zte-prometheus-exporter"; | ||
8 | src = ./zte-prometheus-exporter.py; | ||
9 | |||
10 | phases = [ "buildPhase" "checkPhase" "installPhase" ]; | ||
11 | |||
12 | python = inpPython.withPackages (ps: with ps; [pytimeparse requests]); | ||
13 | |||
14 | buildPhase = '' | ||
15 | substituteAll $src zte-prometheus-exporter | ||
16 | ''; | ||
17 | |||
18 | doCheck = true; | ||
19 | checkPhase = '' | ||
20 | ${python}/bin/python -m py_compile zte-prometheus-exporter | ||
21 | ''; | ||
22 | |||
23 | installPhase = '' | ||
24 | install -m 0755 -D -t $out/bin \ | ||
25 | zte-prometheus-exporter | ||
26 | ''; | ||
27 | }; | ||
28 | } | ||
diff --git a/overlays/zte-prometheus-exporter/python-packages.nix b/overlays/zte-prometheus-exporter/python-packages.nix new file mode 100644 index 00000000..89fa2f99 --- /dev/null +++ b/overlays/zte-prometheus-exporter/python-packages.nix | |||
@@ -0,0 +1,21 @@ | |||
1 | # Generated by pip2nix 0.8.0.dev1 | ||
2 | # See https://github.com/nix-community/pip2nix | ||
3 | |||
4 | { pkgs, fetchurl, fetchgit, fetchhg }: | ||
5 | |||
6 | self: super: { | ||
7 | "pytimeparse" = super.buildPythonPackage rec { | ||
8 | pname = "pytimeparse"; | ||
9 | version = "1.1.8"; | ||
10 | src = fetchurl { | ||
11 | url = "https://files.pythonhosted.org/packages/1b/b4/afd75551a3b910abd1d922dbd45e49e5deeb4d47dc50209ce489ba9844dd/pytimeparse-1.1.8-py2.py3-none-any.whl"; | ||
12 | sha256 = "1g9nc03jya5scx1xlsbypkk4xhrsdj948m1jlr3md7xxr1nbxdq4"; | ||
13 | }; | ||
14 | format = "wheel"; | ||
15 | doCheck = false; | ||
16 | buildInputs = []; | ||
17 | checkInputs = []; | ||
18 | nativeBuildInputs = []; | ||
19 | propagatedBuildInputs = []; | ||
20 | }; | ||
21 | } | ||
diff --git a/overlays/zte-prometheus-exporter/zte-prometheus-exporter.py b/overlays/zte-prometheus-exporter/zte-prometheus-exporter.py new file mode 100644 index 00000000..13ff5a85 --- /dev/null +++ b/overlays/zte-prometheus-exporter/zte-prometheus-exporter.py | |||
@@ -0,0 +1,216 @@ | |||
1 | #!@python@/bin/python | ||
2 | |||
3 | import requests | ||
4 | import xml.etree.ElementTree as ET | ||
5 | from getpass import getpass | ||
6 | from hashlib import sha256 | ||
7 | |||
8 | from time import sleep | ||
9 | |||
10 | import re | ||
11 | |||
12 | import json | ||
13 | |||
14 | from os import environ | ||
15 | import sys | ||
16 | |||
17 | from http.server import BaseHTTPRequestHandler, HTTPServer | ||
18 | |||
19 | from urllib.parse import urlparse | ||
20 | |||
21 | from pytimeparse.timeparse import timeparse | ||
22 | |||
23 | from textwrap import dedent | ||
24 | |||
25 | |||
26 | def _format_prom_attrs(**attrs): | ||
27 | if not attrs: | ||
28 | return '' | ||
29 | |||
30 | return '{' + ','.join(map(lambda k: f'{k}="{attrs[k]}"', attrs)) + '}' | ||
31 | |||
32 | def _format_prom_metrics(metricName, metricType, metrics, metricHelp=''): | ||
33 | metricStr = dedent(f''' | ||
34 | # HELP {metricName} {metricHelp} | ||
35 | # TYPE {metricName} {metricType} | ||
36 | ''').lstrip() | ||
37 | for (attrs, val) in metrics: | ||
38 | attrs_str = _format_prom_attrs(**attrs) | ||
39 | metricStr += dedent(f''' | ||
40 | {metricName}{attrs_str} {val} | ||
41 | ''').lstrip() | ||
42 | return metricStr | ||
43 | |||
44 | |||
45 | class ZTEMetrics: | ||
46 | _instance = None | ||
47 | |||
48 | @classmethod | ||
49 | def instance(cls): | ||
50 | if cls._instance is None: | ||
51 | cls._instance = cls.__new__(cls) | ||
52 | cls._instance.base_url = environ.get('ZTE_BASEURL') | ||
53 | cls._instance.username = environ.get('ZTE_USERNAME') | ||
54 | cls._instance.password = environ.get('ZTE_PASSWORD') | ||
55 | cls._instance.attrs = None | ||
56 | return cls._instance | ||
57 | |||
58 | |||
59 | def __init__(self): | ||
60 | raise RuntimeError('Call instance() instead') | ||
61 | |||
62 | _error_pattern = re.compile('^IF_ERROR(PARAM|TYPE|STR|ID)$') | ||
63 | _obj_pattern = re.compile('^(?:OBJ_(.+)_ID)|(?:ID_(WAN_COMFIG))$') | ||
64 | def update(self): | ||
65 | attrs = dict() | ||
66 | |||
67 | with requests.Session() as session: | ||
68 | session.get(self.base_url) | ||
69 | |||
70 | tok_req = session.get(f'{self.base_url}/function_module/login_module/login_page/logintoken_lua.lua') | ||
71 | tok_tree = ET.fromstring(tok_req.text) | ||
72 | login_token = tok_tree.text | ||
73 | |||
74 | password_hash = sha256((self.password + login_token).encode('utf-8')).hexdigest() | ||
75 | |||
76 | session.post(self.base_url, data = { 'Username': self.username, 'Password': password_hash, 'action': 'login' }) | ||
77 | |||
78 | dev_req = session.get(f'{self.base_url}/common_page/ManagReg_lua.lua') | ||
79 | sntp_req = session.get(f'{self.base_url}/getpage.lua?pid=1005&nextpage=Internet_sntp_lua.lua') | ||
80 | session.get(f'{self.base_url}/getpage.lua?pid=123&nextpage=Internet_AdminInternetStatus_DSL_t.lp') | ||
81 | dsl_req = session.get(f'{self.base_url}/common_page/internet_dsl_interface_lua.lua') | ||
82 | ppp_req = session.get(f'{self.base_url}/common_page/Internet_Internet_lua.lua?TypeUplink=1&pageType=1') | ||
83 | session.get(f'{self.base_url}/getpage.lua?pid=123&nextpage=Localnet_LocalnetStatusAd_t.lp') | ||
84 | lan_req = session.get(f'{self.base_url}/common_page/lanStatus_lua.lua') | ||
85 | dhcp_req = session.get(f'{self.base_url}/common_page/Localnet_LanMgrIpv4_DHCPHostInfo_lua.lua') | ||
86 | |||
87 | for req in [dev_req, sntp_req, dsl_req, ppp_req, lan_req, dhcp_req]: | ||
88 | xml = ET.fromstring(req.text) | ||
89 | for child in xml: | ||
90 | if self._error_pattern.match(child.tag): | ||
91 | continue | ||
92 | obj_tag = self._obj_pattern.match(child.tag) | ||
93 | if not obj_tag: | ||
94 | continue | ||
95 | obj_type = obj_tag.group(1) or obj_tag.group(2) | ||
96 | |||
97 | for instance in child.findall('Instance'): | ||
98 | instance_dict = dict() | ||
99 | name = None | ||
100 | value = None | ||
101 | for child in instance: | ||
102 | match child.tag: | ||
103 | case 'ParaName': | ||
104 | name = child.text | ||
105 | case 'ParaValue': | ||
106 | value = child.text | ||
107 | case _: | ||
108 | pass | ||
109 | if not name is None and not value is None: | ||
110 | instance_dict[name] = value | ||
111 | name = None | ||
112 | value = None | ||
113 | |||
114 | if obj_type not in attrs: | ||
115 | attrs[obj_type] = dict() | ||
116 | attrs[obj_type][instance_dict['_InstID']] = instance_dict | ||
117 | |||
118 | self.attrs = attrs | ||
119 | |||
120 | def json_text(self): | ||
121 | return json.dumps(self.attrs) | ||
122 | |||
123 | _link_pattern = re.compile('^IGD\.WD1\.LINE([0-9]+)$') | ||
124 | _eth_pattern = re.compile('^IGD\.LD1\.ETH([0-9]+)$') | ||
125 | def prometheus(self): | ||
126 | metrics = '' | ||
127 | |||
128 | uptime_seconds = timeparse(self.attrs['SYSTEMYIME']['IGD']['systemTime']) | ||
129 | metrics += _format_prom_metrics('uptime_seconds', 'gauge', [({}, uptime_seconds)], 'Seconds device has been running') | ||
130 | |||
131 | link_metrics = dict() | ||
132 | for link in self.attrs['DSLINTERFACE']: | ||
133 | link_match = self._link_pattern.match(link) | ||
134 | link_number = link_match.group(1) | ||
135 | |||
136 | if 'crc_errors_count' not in link_metrics: | ||
137 | link_metrics['crc_errors_count'] = {'type': 'counter', 'metrics': []} | ||
138 | link_metrics['crc_errors_count']['metrics'] += [({"direction": "up", "link": link_number}, int(self.attrs['DSLINTERFACE'][link]['UpCrc_errors']))] | ||
139 | link_metrics['crc_errors_count']['metrics'] += [({"direction": "down", "link": link_number}, int(self.attrs['DSLINTERFACE'][link]['DownCrc_errors']))] | ||
140 | |||
141 | if 'noise_margin_db' not in link_metrics: | ||
142 | link_metrics['noise_margin_db'] = {'type': 'gauge', 'metrics': []} | ||
143 | link_metrics['noise_margin_db']['metrics'] += [({"direction": "up", "link": link_number}, int(self.attrs['DSLINTERFACE'][link]['Upstream_noise_margin']))] | ||
144 | link_metrics['noise_margin_db']['metrics'] += [({"direction": "down", "link": link_number}, int(self.attrs['DSLINTERFACE'][link]['Downstream_noise_margin']))] | ||
145 | |||
146 | if 'attenuation_db' not in link_metrics: | ||
147 | link_metrics['attenuation_db'] = {'type': 'gauge', 'metrics': []} | ||
148 | link_metrics['attenuation_db']['metrics'] += [({"direction": "up", "link": link_number}, int(self.attrs['DSLINTERFACE'][link]['Upstream_attenuation']))] | ||
149 | link_metrics['attenuation_db']['metrics'] += [({"direction": "down", "link": link_number}, int(self.attrs['DSLINTERFACE'][link]['Downstream_attenuation']))] | ||
150 | |||
151 | if 'max_rate_kbps' not in link_metrics: | ||
152 | link_metrics['max_rate_kbps'] = {'type': 'gauge', 'metrics': []} | ||
153 | link_metrics['max_rate_kbps']['metrics'] += [({"direction": "up", "link": link_number}, int(self.attrs['DSLINTERFACE'][link]['Upstream_max_rate']))] | ||
154 | link_metrics['max_rate_kbps']['metrics'] += [({"direction": "down", "link": link_number}, int(self.attrs['DSLINTERFACE'][link]['Downstream_max_rate']))] | ||
155 | |||
156 | if 'current_rate_kbps' not in link_metrics: | ||
157 | link_metrics['current_rate_kbps'] = {'type': 'gauge', 'metrics': []} | ||
158 | link_metrics['current_rate_kbps']['metrics'] += [({"direction": "up", "link": link_number}, int(self.attrs['DSLINTERFACE'][link]['Upstream_current_rate']))] | ||
159 | link_metrics['current_rate_kbps']['metrics'] += [({"direction": "down", "link": link_number}, int(self.attrs['DSLINTERFACE'][link]['Downstream_current_rate']))] | ||
160 | |||
161 | if 'dsl_uptime_seconds' not in link_metrics: | ||
162 | link_metrics['dsl_uptime_seconds'] = {'type': 'gauge', 'metrics': []} | ||
163 | link_metrics['dsl_uptime_seconds']['metrics'] += [({"link": link_number}, int(self.attrs['DSLINTERFACE'][link]['Showtime_start']))] | ||
164 | if link_metrics: | ||
165 | for metric_name in link_metrics: | ||
166 | metrics += _format_prom_metrics('dsl_{metric_name}', link_metrics[metric_name]['type'], link_metrics[metric_name]['metrics']) | ||
167 | |||
168 | eth_metrics = dict() | ||
169 | for link in self.attrs['ETH']: | ||
170 | link_match = self._eth_pattern.match(link) | ||
171 | link_number = link_match.group(1) | ||
172 | |||
173 | if 'received_bytes' not in eth_metrics: | ||
174 | eth_metrics['received_bytes'] = {'type': 'counter', 'metrics': []} | ||
175 | eth_metrics['received_bytes']['metrics'] += [({"link": link_number}, int(self.attrs['ETH'][link]['BytesReceived']))] | ||
176 | if 'sent_bytes' not in eth_metrics: | ||
177 | eth_metrics['sent_bytes'] = {'type': 'counter', 'metrics': []} | ||
178 | eth_metrics['sent_bytes']['metrics'] += [({"link": link_number}, int(self.attrs['ETH'][link]['BytesSent']))] | ||
179 | if eth_metrics: | ||
180 | for metric_name in eth_metrics: | ||
181 | metrics += _format_prom_metrics('eth_{metric_name}', eth_metrics[metric_name]['type'], eth_metrics[metric_name]['metrics']) | ||
182 | |||
183 | return metrics.encode('utf-8') | ||
184 | |||
185 | class ZTEMetricsServer(BaseHTTPRequestHandler): | ||
186 | def do_GET(self): | ||
187 | zte_metrics = ZTEMetrics.instance() | ||
188 | zte_metrics.update() | ||
189 | |||
190 | url = urlparse(self.path) | ||
191 | |||
192 | match url.path: | ||
193 | case '/metrics.json': | ||
194 | self.send_response(200) | ||
195 | self.send_header("Content-type", "application/json") | ||
196 | self.end_headers() | ||
197 | |||
198 | self.wfile.write(zte_metrics.json_text().encode('utf-8')) | ||
199 | case '/metrics': | ||
200 | self.send_response(200) | ||
201 | self.send_header("Content-type", "text/plain") | ||
202 | self.end_headers() | ||
203 | |||
204 | self.wfile.write(zte_metrics.prometheus()) | ||
205 | case _: | ||
206 | self.send_response(404) | ||
207 | self.end_headers() | ||
208 | |||
209 | |||
210 | def main(): | ||
211 | webServer = HTTPServer((str(environ.get('ZTE_HOSTNAME')), int(environ.get('ZTE_PORT'))), ZTEMetricsServer) | ||
212 | |||
213 | webServer.serve_forever() | ||
214 | |||
215 | if __name__ == "__main__": | ||
216 | sys.exit(main()) | ||