Всем привет! На днях захотелось сделать графики по всем нашим точкам доступа, у нас их много, часть на базе 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 делаем чтобы входящий трафик отобразился вниз, да, я знаю, что есть трансформации, но у нас это отработало как-то криво, график пропал совсем, так и не поняли в чём дело.
И на выходе получаем возможность просматривать график по любой из точек доступа:

