Initial commit
This commit is contained in:
		
							
								
								
									
										6
									
								
								.flake8
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.flake8
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| [flake8] | ||||
| inline-quotes = " | ||||
| multiline-quotes = """ | ||||
| indent-size = 4 | ||||
| max-line-length = 160 | ||||
| ignore = Q003 E128 E704 E731 | ||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| .env | ||||
| *.swp | ||||
							
								
								
									
										6
									
								
								.pre-commit-config.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.pre-commit-config.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| repos: | ||||
| -   repo: https://github.com/PyCQA/flake8 | ||||
|     rev: 3.9.2 | ||||
|     hooks: | ||||
|     -   id: flake8 | ||||
|         additional_dependencies: [flake8-typing-imports==1.10.0,flake8-quotes==3.2.0] | ||||
							
								
								
									
										5
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| FROM python:3 | ||||
| RUN pip install aio_api_ros aiostream sanic | ||||
| ADD mikrotik.py /mikrotik.py | ||||
| ENTRYPOINT /mikrotik.py | ||||
| EXPOSE 3001 | ||||
							
								
								
									
										20
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| # Background | ||||
|  | ||||
| This is Prometheus exporter for Mikrotik routers and switches. | ||||
|  | ||||
|  | ||||
| # Usage | ||||
|  | ||||
| Supply `MIKROTIK_USER`, `MIKROTIK_PASSWORD` and comma separated `TARGETS` | ||||
| as environment variables via `.env` file. | ||||
| Optionally configure `PROMETHEUS_BEARER_TOKEN` for securing the | ||||
| metrics endpoint. | ||||
|  | ||||
|  | ||||
| # Why not SNMP | ||||
|  | ||||
| SNMP for whatever reason is horribly slow on Mikrotik, | ||||
| see [here](https://forum.mikrotik.com/viewtopic.php?t=132304) for discussion. | ||||
| It takes 2+ minutes to scrape over SNMP vs few seconds over management API. | ||||
| Also PoE status codes are not fully documented for SNMP, | ||||
| see forum post [here](https://forum.mikrotik.com/viewtopic.php?t=162423). | ||||
							
								
								
									
										8
									
								
								docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| version: '3.7' | ||||
|  | ||||
| services: | ||||
|   app: | ||||
|     build: | ||||
|       context: . | ||||
|     env_file: .env | ||||
|     network_mode: host | ||||
							
								
								
									
										186
									
								
								mikrotik.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										186
									
								
								mikrotik.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,186 @@ | ||||
| #!/usr/bin/env python | ||||
| import os | ||||
| from aio_api_ros import create_rosapi_connection | ||||
| from aio_api_ros.unpacker import SentenceUnpacker | ||||
| from aio_api_ros.parser import parse_sentence | ||||
| from aiostream import stream | ||||
| from sanic import Sanic, response, exceptions | ||||
|  | ||||
| app = Sanic("exporter") | ||||
|  | ||||
| PREFIX = os.getenv("PROMETHEUS_PREFIX", "mikrotik_") | ||||
| PROMETHEUS_BEARER_TOKEN = os.getenv("PROMETHEUS_BEARER_TOKEN") | ||||
| MIKROTIK_USER = os.getenv("MIKROTIK_USER") | ||||
| MIKROTIK_PASSWORD = os.getenv("MIKROTIK_PASSWORD") | ||||
| TARGETS = os.getenv("TARGETS") | ||||
|  | ||||
| if not MIKROTIK_USER: | ||||
|     raise ValueError("MIKROTIK_USER not specified") | ||||
| if not MIKROTIK_PASSWORD: | ||||
|     raise ValueError("MIKROTIK_PASSWORD not specified") | ||||
| if not TARGETS: | ||||
|     raise ValueError("TARGETS not specified") | ||||
|  | ||||
| RATE_MAPPING = { | ||||
|     "40Gbps": 40 * 10 ** 9, | ||||
|     "10Gbps": 10 * 10 ** 9, | ||||
|     "1Gbps": 10 ** 9, | ||||
|     "100Mbps": 100 * 10 ** 6, | ||||
|     "10Mbps": 10 * 10 ** 6, | ||||
| } | ||||
|  | ||||
|  | ||||
| async def wrap(i): | ||||
|     metrics_seen = set() | ||||
|     async for name, tp, value, labels in i: | ||||
|         if name not in metrics_seen: | ||||
|             yield "# TYPE %s %s" % (PREFIX + name, tp) | ||||
|             metrics_seen.add(name) | ||||
|         yield "%s%s %s" % ( | ||||
|             PREFIX + name, | ||||
|             ("{%s}" % ",".join(["%s=\"%s\"" % j for j in labels.items()]) if labels else ""), | ||||
|             value) | ||||
|  | ||||
|  | ||||
| async def mikrotik_run(conn, cmd): | ||||
|     conn.talk_sentence(cmd) | ||||
|     data = await conn.read(full_answer=True, parse=False) | ||||
|     unpacker = SentenceUnpacker() | ||||
|     unpacker.feed(data) | ||||
|     return [parse_sentence(sentence) for sentence in unpacker] | ||||
|  | ||||
|  | ||||
| async def scrape_mikrotik(target): | ||||
|     mk = await create_rosapi_connection( | ||||
|         mk_ip=target, | ||||
|         mk_port=8728, | ||||
|         mk_user=MIKROTIK_USER, | ||||
|         mk_psw=MIKROTIK_PASSWORD, | ||||
|  | ||||
|     ) | ||||
|  | ||||
|     ports = ",".join([str(j) for j in range(0, 24)]) | ||||
|  | ||||
|     res = await mikrotik_run(mk, ["/interface/print"]) | ||||
|     for resp, _, obj in res: | ||||
|         if resp in ("!trap", "!done"): | ||||
|             break | ||||
|         labels = {"host": target, "port": obj["name"], "type": obj["type"]} | ||||
|  | ||||
|         yield "interface-rx-bytes", "counter", obj["rx-byte"], labels | ||||
|         yield "interface-tx-bytes", "counter", obj["tx-byte"], labels | ||||
|         yield "interface-rx-packets", "counter", obj["rx-packet"], labels | ||||
|         yield "interface-tx-packets", "counter", obj["tx-packet"], labels | ||||
|         try: | ||||
|             yield "interface-rx-errors", "counter", obj["rx-error"], labels | ||||
|             yield "interface-tx-errors", "counter", obj["tx-error"], labels | ||||
|         except KeyError: | ||||
|             pass | ||||
|         try: | ||||
|             yield "interface-rx-drops", "counter", obj["rx-drop"], labels | ||||
|             yield "interface-tx-drops", "counter", obj["tx-drop"], labels | ||||
|         except KeyError: | ||||
|             pass | ||||
|         yield "interface-running", "gauge", int(obj["tx-byte"]), labels | ||||
|         yield "interface-actual-mtu", "gauge", obj["actual-mtu"], labels | ||||
|  | ||||
|     res = await mikrotik_run(mk, ["/interface/ethernet/monitor", "=once=", "=numbers=%s" % ports]) | ||||
|     for resp, _, obj in res: | ||||
|         if resp in ("!trap", "!done"): | ||||
|             break | ||||
|         labels = {"host": target, "port": obj["name"]} | ||||
|  | ||||
|         try: | ||||
|             rate = obj["rate"] | ||||
|         except KeyError: | ||||
|             pass | ||||
|         else: | ||||
|             yield "interface-rate", "gauge", RATE_MAPPING[rate], labels | ||||
|  | ||||
|         try: | ||||
|             labels["sfp-vendor-name"] = obj["sfp-vendor-name"] | ||||
|         except KeyError: | ||||
|             pass | ||||
|         try: | ||||
|             labels["sfp-vendor-part-number"] = obj["sfp-vendor-part-number"] | ||||
|         except KeyError: | ||||
|             pass | ||||
|  | ||||
|         try: | ||||
|             yield "interface-sfp-temperature", "gauge", obj["sfp-temperature"], labels | ||||
|             yield "interface-sfp-tx-power", "gauge", obj["sfp-tx-power"], labels | ||||
|             yield "interface-sfp-rx-power", "gauge", obj["sfp-tx-power"], labels | ||||
|         except KeyError: | ||||
|             pass | ||||
|  | ||||
|         labels["status"] = obj["status"] | ||||
|         try: | ||||
|             labels["sfp-module-present"] = int(obj["sfp-module-present"]) | ||||
|         except KeyError: | ||||
|             pass | ||||
|         yield "interface-status", "gauge", 1, labels | ||||
|  | ||||
|     res = await mikrotik_run(mk, ["/interface/ethernet/poe/monitor", "=once=", "=numbers=%s" % ports]) | ||||
|     for resp, _, obj in res: | ||||
|         if resp in ("!trap", "!done"): | ||||
|             break | ||||
|  | ||||
|         labels = {"host": target, "port": obj["name"]} | ||||
|         try: | ||||
|             yield "poe-out-voltage", "gauge", float(obj["poe-out-voltage"]), labels | ||||
|             yield "poe-out-current", "gauge", int(obj["poe-out-current"]) / 1000.0, labels | ||||
|         except KeyError: | ||||
|             pass | ||||
|  | ||||
|         labels["status"] = obj["poe-out-status"] | ||||
|         yield "poe-out-status", "gauge", 1, labels | ||||
|  | ||||
|     res = await mikrotik_run(mk, ["/system/resource/print"]) | ||||
|     for resp, _, obj in res: | ||||
|         if resp in ("!trap", "!done"): | ||||
|             break | ||||
|  | ||||
|         labels = {"host": target} | ||||
|         yield "system-write-sect-total", "counter", obj["write-sect-total"], labels | ||||
|         yield "system-free-memory", "gauge", obj["free-memory"], labels | ||||
|         try: | ||||
|             yield "system-bad-blocks", "counter", obj["bad-blocks"], labels | ||||
|         except KeyError: | ||||
|             pass | ||||
|  | ||||
|         for key in ("version", "cpu", "cpu-count", "board-name", "architecture-name"): | ||||
|             labels[key] = obj[key] | ||||
|         yield "system-version", "gauge", 1, labels | ||||
|  | ||||
|     res = await mikrotik_run(mk, ["/system/health/print"]) | ||||
|     for resp, _, obj in res: | ||||
|         if resp in ("!trap", "!done"): | ||||
|             break | ||||
|         for key, value in obj.items(): | ||||
|             labels = {"host": target} | ||||
|             try: | ||||
|                 value = float(value) | ||||
|             except ValueError: | ||||
|                 labels["state"] = value | ||||
|                 yield "system-health-%s" % key, "gauge", 1, labels | ||||
|             else: | ||||
|                 yield "system-health-%s" % key, "gauge", value, labels | ||||
|     mk.close() | ||||
|  | ||||
|  | ||||
| @app.route("/metrics") | ||||
| async def view_export(request): | ||||
|     if PROMETHEUS_BEARER_TOKEN and request.token != PROMETHEUS_BEARER_TOKEN: | ||||
|         raise exceptions.Forbidden("Invalid bearer token") | ||||
|  | ||||
|     async def streaming_fn(response): | ||||
|         args = [scrape_mikrotik(target) for target in TARGETS.split(",")] | ||||
|         combine = stream.merge(*args) | ||||
|         async with combine.stream() as streamer: | ||||
|             async for line in wrap(streamer): | ||||
|                 await response.write(line + "\n") | ||||
|  | ||||
|     return response.stream(streaming_fn, content_type="text/plain") | ||||
|  | ||||
|  | ||||
| app.run(host="0.0.0.0", port=3001) | ||||
		Reference in New Issue
	
	Block a user