Initial commit
This commit is contained in:
commit
e030f5c9a0
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