summaryrefslogtreecommitdiff
path: root/overlays/zte-prometheus-exporter
diff options
context:
space:
mode:
Diffstat (limited to 'overlays/zte-prometheus-exporter')
-rw-r--r--overlays/zte-prometheus-exporter/default.nix28
-rw-r--r--overlays/zte-prometheus-exporter/python-packages.nix21
-rw-r--r--overlays/zte-prometheus-exporter/zte-prometheus-exporter.py216
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 @@
1final: prev:
2let
3 packageOverrides = final.callPackage ./python-packages.nix {};
4 inpPython = final.python310.override { inherit packageOverrides; };
5in {
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
6self: 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
3import requests
4import xml.etree.ElementTree as ET
5from getpass import getpass
6from hashlib import sha256
7
8from time import sleep
9
10import re
11
12import json
13
14from os import environ
15import sys
16
17from http.server import BaseHTTPRequestHandler, HTTPServer
18
19from urllib.parse import urlparse
20
21from pytimeparse.timeparse import timeparse
22
23from textwrap import dedent
24
25
26def _format_prom_attrs(**attrs):
27 if not attrs:
28 return ''
29
30 return '{' + ','.join(map(lambda k: f'{k}="{attrs[k]}"', attrs)) + '}'
31
32def _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
45class 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
185class 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
210def main():
211 webServer = HTTPServer((str(environ.get('ZTE_HOSTNAME')), int(environ.get('ZTE_PORT'))), ZTEMetricsServer)
212
213 webServer.serve_forever()
214
215if __name__ == "__main__":
216 sys.exit(main())