Всем привет! На днях захотелось сделать графики по всем нашим точкам доступа, у нас их много, часть на базе Mikrotik и с ними нет проблем, он легко опрашивается по SNMP и отдаёт статистику сразу по всем точкам, а вот с Unifi всё сложней, нужно опрашивать каждую точку доступа отдельно, а они у нас иногда меняют, соответственно, нужно какое-то решение, которое будет отслеживать эти изменения автоматически. На просторах Интернет не нашлось ничего готового, поэтому решили написать очередной "велосипед", но хотелось сделать максимально совместимо, чтобы этим могли пользоваться другие.
Наш подход
Мы решили, что напишем приложение/демон, которое может авторизоваться в контроллере 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.1docker-compose
docker-compose.yaml
version: "2"
services:
nexus:
image: prom/snmp-exporter
ports:
- "9116:9116"kubernetes
snmp-exporter.yaml
---
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-exporterunifi-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 логин от exporter-контроллера [$CONTROLLER_LOGIN]
--controller-password value пароль от exporter-контроллера [$CONTROLLER_PASSWORD]
--controller-address value адрес exporter-контроллера (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 делаем чтобы входящий трафик отобразился вниз, да, я знаю, что есть трансформации, но у нас это отработало как-то криво, график пропал совсем, так и не поняли в чём дело
и на выходе получаем возможность просматривать график по любой из точек доступа:

