10
0

Initial commit

This commit is contained in:
Lauri Võsandi 2021-06-30 23:03:27 +03:00
commit e030f5c9a0
7 changed files with 233 additions and 0 deletions

6
.flake8 Normal file
View 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
View File

@ -0,0 +1,2 @@
.env
*.swp

6
.pre-commit-config.yaml Normal file
View 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
View 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
View 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
View File

@ -0,0 +1,8 @@
version: '3.7'
services:
app:
build:
context: .
env_file: .env
network_mode: host

186
mikrotik.py Executable file
View 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)