Всем привет! На днях захотелось сделать графики по всем нашим точкам доступа, у нас их много, часть на базе Mikrotik и с ними нет проблем, он легко опрашивается по SNMP и отдаёт статистику сразу по всем точкам, а вот с Unifi всё сложней, нужно опрашивать каждую точку доступа отдельно, а они у нас иногда меняются, соответственно, нужно какое-то решение, которое будет отслеживать эти изменения автоматически. В момент поиска готового решения наткнулся на unpoller, но у нас это не заработало, решение не смогло авторизоваться в нашем контроллере с кодом 400, поэтому написали свое простое решение, решил поделиться, вдруг кому-то пригодится.
Наш подход
Мы решили, что напишем приложение/демон, которое может авторизоваться в контроллере Unifi, получить список точек доступа, а дальше по запросу в ручку /metrics будет обращаться ко всем точкам доступа по snmp и отдавать результат наружу в формате prometheus
Реализация
Подробно останавливаться на деталях реализации приложения не буду, всё залил на github и подробно задокументировал. Есть также docker-образ.
Опишу некоторые подходы, которые были использованы:
язык разработки Golang;
для обработки cli-аргументов и переменных окружения использован фреймворк urfave/cli/v2;
в качестве роутера http-запросов использовали gorilla/mux;
для ограничения одновременного опроса точек доступа использован примитив синхронизации "семафор";
использовали mutex для синхронизации списка точек доступа между горутинами;
для опроса точек доступа приложение обращается к сторонней реализации snmp-exporter.
Настройка окружения
Нам нужно настроить две приложеньки:
snmp-exporter - демон, в которого мы будем обращаться из нашей приложеньки;
unifi-prometheus-exporter - наша приложенька.
Я сразу скажу, что я честно закопался в код snmp-exporter, в надежде, что можно будет импортировать код и использовать его функции нативно, но там всё не очень хорошо, код не модульный, его нельзя импортировать, поэтому придётся использовать две приложеньки.
Snmp-exporter
Для начала надо запустить snmp-exporter, к которому мы будем обращаться для опроса наших точек доступа, покажу как это можно запустить в docker-compose и в kubernetes. Установку docker-compose и kubernetes тут рассматривать не буду, это не является целью данного поста.
Экспортер слушает на порту 9116, чтобы опросить удалённый узел по snmp, достаточно послать в экспортер http-запрос в ручку /snmp c параметрами module (по умолчанию if_mib) и target (что опрашиваем), например:
curl http://127.0.0.1:9116/snmp?target=10.0.0.1
docker-compose
version: "2"
services:
nexus:
image: prom/snmp-exporter
ports:
- "9116:9116"
kubernetes
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: snmp-exporter
labels:
app: snmp-exporter
spec:
replicas: 1
selector:
matchLabels:
app: snmp-exporter
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
type: RollingUpdate
template:
metadata:
labels:
app: snmp-exporter
spec:
containers:
- image: prom/snmp-exporter
imagePullPolicy: IfNotPresent
name: exporter
ports:
- containerPort: 9116
---
apiVersion: v1
kind: Service
metadata:
name: snmp-exporter
spec:
ports:
- port: 9116
protocol: TCP
targetPort: 9116
name: snmp-exporter
selector:
app: snmp-exporter
unifi-prometheus-exporter
Теперь запускаем нашу приложеньку, у неё есть ряд cli-аргументов, продублированных переменными окружения:
NAME:
exporter - экспортер snmp-метрик от точек доступа unifi
USAGE:
exporter [global options] command [command options] [arguments...]
COMMANDS:
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--controller-login value логин от unifi-контроллера [$CONTROLLER_LOGIN]
--controller-password value пароль от unifi-контроллера [$CONTROLLER_PASSWORD]
--controller-address value адрес unifi-контроллера (default: "https://127.0.0.1:8443") [$CONTROLLER_ADDRESS]
--snmp-exporter-address value адрес snmp-экспортера (default: "http://snmp-exporter:9116") [$SNMP_EXPORTER_ADDRESS]
--access-points-update-interval value интервал обновления списка точек (default: 1h0m0s) [$ACCESS_POINTS_UPDATE_INTERVAL]
--listen-port value порт прослушки http-сервера (default: 8080) [$LISTEN_PORT]
--parallel value количество потоков для опроса точек-доступа (default: 10) [$PARALLEL]
--poll-timeout value таймаут для опроса точек доступа (default: 15s) [$POLL_TIMEOUT]
--help, -h show help (default: false)
В принципе, тут, на мой взгляд, всё понятно. Запускаем в docker-compose/kubernetes.
docker-compose
version: "2"
services:
nexus:
image: maetx777/unifi-prometheus-exporter
environment:
- CONTROLLLER_LOGIN=admin
- CONTROLLER_PASSWORD=123456
ports:
- "9116:9116"
kubernetes
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: unifi-snmp-exporter
labels:
app: unifi-snmp-exporter
spec:
replicas: 1
selector:
matchLabels:
app: unifi-snmp-exporter
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
type: RollingUpdate
template:
metadata:
labels:
app: unifi-snmp-exporter
annotations:
prometheus.io/scrape: 'true'
prometheus.io/port: '8080'
prometheus.io/path: '/metrics'
spec:
containers:
- image: maetx777/unifi-prometheus-exporter
name: exporter
ports:
- containerPort: 8080
env:
- name: CONTROLLER_LOGIN
value: admin
- name: CONTROLLER_PASSWORD
value: 123456
restartPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
name: unifi-prometheus-exporter
spec:
ports:
- port: 8080
protocol: TCP
targetPort: 8080
name: unifi-prometheus-exporter
selector:
app: unifi-prometheus-exporter
Остановлюсь на нюансах:
CONTROLLER_LOGIN - при необходимости меняем;
CONTROLLER_PASSWORD - при необходимости меняем :)
CONTROLLER_ADDRESS - меняем на адрес unifi-контроллера с указанием протокола и порта, например https://1.2.3.4:8443;
SNMP_EXPORTER_ADDRESS - меняем на адрес snmp-экспортера, что запустили ранее, можно использовать dns-имя;
передавать секреты в kubernetes указанным способом плохо, для этого есть специальный ресурс Secret, но здесь это не рассматривается, каждый сам под себя допилит.
После запуска
После запуска мы увидим в логах unifi-prometheus-controller нечто подобное:
INFO[0000] Daemon start
INFO[0000] Start http server
INFO[0000] Start fatals catcher
INFO[0000] Start signals catcher
INFO[0000] Start access points updater
INFO[0001] Http client authorized
INFO[0001] Update access points list
INFO[0001] Access point name Room1, ip 10.0.0.10
INFO[0001] Access point name Room2, ip 10.0.0.20
Теперь можно обратиться к нашему контроллеру чтобы получить метрики найденных точек:
# curl -s http://127.0.0.1:8080/metrics|grep ifOutOctets
ifOutOctets{ap_name="Room1",ap_ip="10.0.0.10",ifAlias="",ifDescr="ath0",ifIndex="6",ifName="ath0"} 0
ifOutOctets{ap_name="Room1",ap_ip="10.0.0.10",ifAlias="",ifDescr="ath1",ifIndex="7",ifName="ath1"} 3.319545249e+09
ifOutOctets{ap_name="Room1",ap_ip="10.0.0.10",ifAlias="",ifDescr="br0",ifIndex="9",ifName="br0"} 2.7572029e+07
ifOutOctets{ap_name="Room1",ap_ip="10.0.0.10",ifAlias="",ifDescr="eth0",ifIndex="2",ifName="eth0"} 4.93001573e+08
ifOutOctets{ap_name="Room1",ap_ip="10.0.0.10",ifAlias="",ifDescr="eth1",ifIndex="3",ifName="eth1"} 0
ifOutOctets{ap_name="Room1",ap_ip="10.0.0.10",ifAlias="",ifDescr="lo",ifIndex="1",ifName="lo"} 3572
ifOutOctets{ap_name="Room1",ap_ip="10.0.0.10",ifAlias="",ifDescr="teql0",ifIndex="5",ifName="teql0"} 0
ifOutOctets{ap_name="Room1",ap_ip="10.0.0.10",ifAlias="",ifDescr="vwire2",ifIndex="8",ifName="vwire2"} 0
ifOutOctets{ap_name="Room2",ap_ip="10.0.0.20",ifAlias="",ifDescr="ath0",ifIndex="6",ifName="ath0"} 0
ifOutOctets{ap_name="Room2",ap_ip="10.0.0.20",ifAlias="",ifDescr="ath1",ifIndex="7",ifName="ath1"} 6.28150693e+08
ifOutOctets{ap_name="Room2",ap_ip="10.0.0.20",ifAlias="",ifDescr="br0",ifIndex="9",ifName="br0"} 2.7178302e+07
ifOutOctets{ap_name="Room2",ap_ip="10.0.0.20",ifAlias="",ifDescr="eth0",ifIndex="2",ifName="eth0"} 4.95262026e+08
ifOutOctets{ap_name="Room2",ap_ip="10.0.0.20",ifAlias="",ifDescr="eth1",ifIndex="3",ifName="eth1"} 0
ifOutOctets{ap_name="Room2",ap_ip="10.0.0.20",ifAlias="",ifDescr="lo",ifIndex="1",ifName="lo"} 8180
ifOutOctets{ap_name="Room2",ap_ip="10.0.0.20",ifAlias="",ifDescr="teql0",ifIndex="5",ifName="teql0"} 0
ifOutOctets{ap_name="Room2",ap_ip="10.0.0.20",ifAlias="",ifDescr="vwire2",ifIndex="8",ifName="vwire2"} 0
ifOutOctets{ap_name="Room2",ap_ip="10.0.0.20",ifAlias="",ifDescr="wifi0",ifIndex="4",ifName="wifi0"} 0
Запрос занимает какое-то время, чем больше точек - тем дольше будет работать запрос, но опрос происходит асинхронно в многопоточном режиме, так что обычно он укладывается в приемлемое время (у нас 10 точек опрашивается за 10 секунд).
Также метрики "обогащаются" тегами с именем (ap_name) и ip-адресом (ap_ip) опрошенных точек.
Prometheus
Как устанавливать prometheus я тут писать не буду, это не является целью данной статьи, у нас это дело работает на базе kubernetes_sd_config, в представленном конфиге kubernetes задаётся аннотация, которая сообщает системе prometheus порт и ручку для опроса.
Grafana
Наконец, покажу простенький дашборд grafana для просмотра графиков.
После создания дашборда сразу идём в Dashboard settings, создаём переменную:
Query: ifOutOctets{ap_name=~".+"}
Regex: /ap_name="([^"]+)"/
Смысл этой переменной в том, что мы выбираем все уникальные значения ap_name, чтобы дальше это можно было выбирать из списка:
Теперь создаём график и пишем там формулы:
A: irate(ifOutOctets{ap_name=~"[[ap_name]]"}[5m])*8
A.Legend: {{ap_name}} {{ap_ip}} {{ifDescr}} out
B: irate(ifInOctets{ap_name=~"[[ap_name]]"}[5m])*-8
B.Legend: {{ap_name}} {{ap_ip}} {{ifDescr}} in
Умножение на 8 необходимо по той причине, что ifOutOctets - значение в байтах, а нам нужен график в мегабит/сек.
Умножение на -8 делаем чтобы входящий трафик отобразился вниз, да, я знаю, что есть трансформации, но у нас это отработало как-то криво, график пропал совсем, так и не поняли в чём дело.
И на выходе получаем возможность просматривать график по любой из точек доступа: