Встала необходимость поставить на мониторинг QNAP, гугл ответил что есть прекрасный prometheus snmp exporter https://github.com/prometheus/snmp_exporter, зашел, почитал, в целом все понятно. Что нужно? Нужно создать конфигурацию на основе MIB с помощью так называемого SNMP Exporter Config Generator, его надо собрать на Go или воспользоваться docker контейнером, все это описано в репозитории. Я скачал необходимый мне MIB, собрал генератор и…… И пришло разочарование, потому как половина значений которые сгенерировались при запуске экспортера возвращались как метки, а не как значения. В целом, можно выбирать метки как значения через Grafana, но, что по мне, это лишняя работа. К тому же, после генерации, я не обнаружил необходимых мне метрик в файле. Хорошо, пошли искать в интернетах какие OID-ы вообще есть в QNAP, вот что нашлось https://bestmonitoringtools.com/mibdb/mibdb_search.php?mib=NAS-MIB. Хорошо, значит корневой oid будет 1.3.6.1.4.1.24681, а значит можно посмотреть и значения. Для этого нам потребуется утилита snmpwalk, она входит в пакет snmp практически всех linux дистрибутивов. Как её установить, думаю вы найдете в интернете, чтобы не нагромождать статью. И так смотрим что нам выведет дерево:
# snmpwalk -v2c -c public ip_qnap 1.3.6.1.4.1.24681
SNMPv2-SMI::enterprises.24681.1.2.1.0 = STRING: "4.5 %"
SNMPv2-SMI::enterprises.24681.1.2.9.1.1.1 = INTEGER: 1
SNMPv2-SMI::enterprises.24681.1.2.9.1.1.2 = INTEGER: 2
SNMPv2-SMI::enterprises.24681.1.2.9.1.1.3 = INTEGER: 3
SNMPv2-SMI::enterprises.24681.1.2.9.1.1.4 = INTEGER: 4
SNMPv2-SMI::enterprises.24681.1.2.9.1.1.5 = INTEGER: 5
SNMPv2-SMI::enterprises.24681.1.2.9.1.2.1 = STRING: "eth0"
SNMPv2-SMI::enterprises.24681.1.2.9.1.2.2 = STRING: "eth1"
SNMPv2-SMI::enterprises.24681.1.2.9.1.2.3 = STRING: "eth2"
SNMPv2-SMI::enterprises.24681.1.2.9.1.2.4 = STRING: "eth3"
SNMPv2-SMI::enterprises.24681.1.2.9.1.2.5 = STRING: "eth4"
SNMPv2-SMI::enterprises.24681.1.2.9.1.3.1 = Counter32: 0
SNMPv2-SMI::enterprises.24681.1.2.9.1.3.2 = Counter32: 0
SNMPv2-SMI::enterprises.24681.1.2.9.1.3.3 = Counter32: 0
SNMPv2-SMI::enterprises.24681.1.2.9.1.3.4 = Counter32: 0
SNMPv2-SMI::enterprises.24681.1.2.9.1.3.5 = Counter32: 3068842230
SNMPv2-SMI::enterprises.24681.1.2.9.1.4.1 = Counter32: 0
SNMPv2-SMI::enterprises.24681.1.2.9.1.4.2 = Counter32: 0
SNMPv2-SMI::enterprises.24681.1.2.9.1.4.3 = Counter32: 0
SNMPv2-SMI::enterprises.24681.1.2.9.1.4.4 = Counter32: 0
SNMPv2-SMI::enterprises.24681.1.2.9.1.4.5 = Counter32: 2780806888
SNMPv2-SMI::enterprises.24681.1.2.9.1.5.1 = Counter32: 0
SNMPv2-SMI::enterprises.24681.1.2.9.1.5.2 = Counter32: 0
SNMPv2-SMI::enterprises.24681.1.2.9.1.5.3 = Counter32: 0
SNMPv2-SMI::enterprises.24681.1.2.9.1.5.4 = Counter32: 0
SNMPv2-SMI::enterprises.24681.1.2.9.1.5.5 = Counter32: 0
SNMPv2-SMI::enterprises.24681.1.2.11.1.1.1 = INTEGER: 1
SNMPv2-SMI::enterprises.24681.1.2.11.1.1.2 = INTEGER: 2
SNMPv2-SMI::enterprises.24681.1.2.11.1.1.3 = INTEGER: 3
SNMPv2-SMI::enterprises.24681.1.2.11.1.1.4 = INTEGER: 4
SNMPv2-SMI::enterprises.24681.1.2.11.1.2.1 = STRING: "HDD1"
SNMPv2-SMI::enterprises.24681.1.2.11.1.2.2 = STRING: "HDD2"
SNMPv2-SMI::enterprises.24681.1.2.11.1.2.3 = STRING: "HDD3"
SNMPv2-SMI::enterprises.24681.1.2.11.1.2.4 = STRING: "HDD4"
SNMPv2-SMI::enterprises.24681.1.2.11.1.3.1 = STRING: "32 C/89 F"
SNMPv2-SMI::enterprises.24681.1.2.11.1.3.2 = STRING: "32 C/89 F"
SNMPv2-SMI::enterprises.24681.1.2.11.1.3.3 = STRING: "32 C/89 F"
SNMPv2-SMI::enterprises.24681.1.2.11.1.3.4 = STRING: "31 C/87 F"
[skip]
Список огромный, хорошо. Теперь у нас есть данные, но, данные у нас динамические, если вдруг я уберу диск из корзины их станет 3 или 2, или например подключу ещё один сетевой порт, и так далее. Задача ясна, но сначала надо написать сам экспортер который будет брать эти данные и наполнять ими прометей. Так как я пишу больше на Python, то, идем в документацию: https://github.com/prometheus/client_python, вдумчиво читаем и понимаем что все проще простого. Мы не будем углубляться в этой статье о типах данных в прометее, скажу лишь что я взял изменяемый тип, как написано в документации: Gauges can go up and down. Это то, что нужно! Что же дальше, а дальше нам надо взять экземпляр этого класса на его основе создать свой и суть кроется в метках (lablesname), мы создаем один экземпляр на каждую метрику, но в каждой метрике может быть сколь угодно конечных точек. Например, мы создаем метрику для HDD, но у нас их 4, а может быть 6 и так далее, по этому, мы создаем только одну метрику, а то количество дисков мы будем помещать в метки, думаю по коду это будет понятно. Хорошо. Сначала научим Python забирать данные по snmp. Библиотека pysnmp, вполне подойдет, pip install pysnmp
.
from pysnmp.hlapi import *
import sys
snmp_host = 'ip_qnap'
# Я взял основные UID которые мне показались самыми важными на момент написания,
# ничто не мешает добавить по вкусу то, что считаете необходимым.
oid_list = [
'1.3.6.1.4.1.24681.1.3.1', # CPU usage
'1.3.6.1.4.1.24681.1.3.9.1', # Ethernet params
'1.3.6.1.4.1.24681.1.3.11.1', # HDD params
'1.3.6.1.4.1.24681.1.3.15.1', # Fan params
'1.3.6.1.4.1.24681.1.3.2', # Total memory
'1.3.6.1.4.1.24681.1.3.3', # Free memory
'1.3.6.1.4.1.24681.1.3.5', # CPU temperature
'1.3.6.1.4.1.24681.1.3.6', # System temperature
'1.3.6.1.4.1.24681.1.2.13', # System name
'1.3.6.1.4.1.24681.1.2.12' # Model name
]
def walk(host, oid):
snmp_response = []
for (errorIndication,
errorStatus,
errorIndex,
varBinds) in nextCmd(SnmpEngine(),
CommunityData('public'),
UdpTransportTarget((host, 161)),
ContextData(),
ObjectType(ObjectIdentity(oid)),
lookupMib=False,
lexicographicMode=False):
if errorIndication:
print(errorIndication, file=sys.stderr)
break
elif errorStatus:
print('%s at %s' % (errorStatus.prettyPrint(),
errorIndex and varBinds[int(errorIndex) - 1][0] or '?'), file=sys.stderr)
break
else:
for varBind in varBinds:
snmp_response.append((str(varBind[0]), str(varBind[1])))
return_value = {}
for i in snmp_response:
return_value.setdefault(i[0].split('.')[-1], []).append(i[1])
return_value.update({'oid': [oid]})
return return_value
На выходе получаем словарь со списками данных:
PyDev console: starting.
Python 3.9.6 (default, Sep 26 2022, 11:37:49)
[Clang 14.0.0 (clang-1400.0.29.202)] on darwin
>>> from snmp_exporter import walk
>>> print(walk('qnap_ip', '1.3.6.1.4.1.24681.1.3.15.1'))
{'1': ['1', 'System FAN 1', '6849'], '2': ['2', 'System FAN 2', '6849'], 'oid': ['1.3.6.1.4.1.24681.1.3.15.1']}
Отлично, то что надо, остается только отдать это прометею, для этого импортируем библиотеку прометея pip install prometheus-client
после чего можно создавать наши метрики:
from prometheus_client import start_http_server, Gauge
prometheus_metric_cpu = Gauge('cpu', 'CPU utilization', labelnames=['instance', 'cpu'])
prometheus_metric_network_in = Gauge('network_incomming', 'Network Incoming stack', labelnames=['instance', 'id', 'if_name'])
prometheus_metric_network_out = Gauge('network_output', 'Network Output stack', labelnames=['instance', 'id', 'if_name'])
prometheus_metric_hdd_temperature = Gauge('hdd_temperature', 'HDD stack', labelnames=['instance', 'id', 'name', 'model', 'state'])
prometheus_metric_hdd_capacity = Gauge('hdd_capacity', 'HDD stack', labelnames=['instance', 'id', 'name', 'model', 'state'])
prometheus_metric_fan = Gauge('system_fan_speed', 'FAN stack', labelnames=['instance', 'id', 'name'])
prometheus_metric_memory_total = Gauge('memory_total', 'Memory stack', labelnames=['instance'])
prometheus_metric_memory_fee = Gauge('memory_free', 'Memory stack', labelnames=['instance'])
prometheus_metric_cpu_temperature = Gauge('cpu_temperature', 'CPU temperature', labelnames=['instance'])
prometheus_metric_system_temperature = Gauge('system_temperature', 'System Temperature', labelnames=['instance'])
prometheus_metric_system_name = Gauge('system_name', 'System name', labelnames=['instance', 'systemname'])
prometheus_metric_model_name = Gauge('model_name', 'Model name', labelnames=['instance', 'modelname'])
В каждый отдельный экземпляр помещаю то, что мне будет необходимо, обратите внимание что в метрики где данные меняются динамически пишем больше меток чтобы туда помещать значения. Теперь нам необходима логика, в зависимости от того какой oid у нас в списке подставлять ему нужное значение, это я уже оборачиваю непосредственно в метрику которую буду отдавать:
if __name__ == '__main__':
start_http_server(8001)
while True:
for oid in oid_list:
get_all_oid = walk(snmp_host, oid)
if get_all_oid.get('oid')[0] == '1.3.6.1.4.1.24681.1.3.1':
get_all_oid.pop('oid')
for get_value in get_all_oid.values():
prometheus_metric_cpu.labels(snmp_host, 'cpu').set(get_value[0])
elif get_all_oid.get('oid')[0] == '1.3.6.1.4.1.24681.1.3.9.1':
get_all_oid.pop('oid')
for get_value in get_all_oid.values():
prometheus_metric_network_in.labels(snmp_host, get_value[0], get_value[1]).set(get_value[2])
prometheus_metric_network_out.labels(snmp_host, get_value[0], get_value[1]).set(get_value[3])
elif get_all_oid.get('oid')[0] == '1.3.6.1.4.1.24681.1.3.11.1':
get_all_oid.pop('oid')
for get_value in get_all_oid.values():
prometheus_metric_hdd_temperature.labels(snmp_host, get_value[0], get_value[1], get_value[4], get_value[6]).set(get_value[2])
prometheus_metric_hdd_capacity.labels(snmp_host, get_value[0], get_value[1], get_value[4], get_value[6]).set(get_value[5])
elif get_all_oid.get('oid')[0] == '1.3.6.1.4.1.24681.1.3.15.1':
get_all_oid.pop('oid')
for get_value in get_all_oid.values():
prometheus_metric_fan.labels(snmp_host, get_value[0], get_value[1]).set(get_value[2])
elif get_all_oid.get('oid')[0] == '1.3.6.1.4.1.24681.1.3.2':
get_all_oid.pop('oid')
for get_value in get_all_oid.values():
prometheus_metric_memory_total.labels(snmp_host).set(get_value[0])
elif get_all_oid.get('oid')[0] == '1.3.6.1.4.1.24681.1.3.3':
get_all_oid.pop('oid')
for get_value in get_all_oid.values():
prometheus_metric_memory_fee.labels(snmp_host).set(get_value[0])
elif get_all_oid.get('oid')[0] == '1.3.6.1.4.1.24681.1.3.5':
get_all_oid.pop('oid')
for get_value in get_all_oid.values():
prometheus_metric_cpu_temperature.labels(snmp_host).set(get_value[0])
elif get_all_oid.get('oid')[0] == '1.3.6.1.4.1.24681.1.3.6':
get_all_oid.pop('oid')
for get_value in get_all_oid.values():
prometheus_metric_system_temperature.labels(snmp_host).set(get_value[0])
elif get_all_oid.get('oid')[0] == '1.3.6.1.4.1.24681.1.2.13':
get_all_oid.pop('oid')
for get_value in get_all_oid.values():
prometheus_metric_system_name.labels(snmp_host, get_value[0]).set(1)
elif get_all_oid.get('oid')[0] == '1.3.6.1.4.1.24681.1.2.12':
get_all_oid.pop('oid')
for get_value in get_all_oid.values():
prometheus_metric_model_name.labels(snmp_host, get_value[0]).set(1)
time.sleep(15)
Вот собственно и все, порт 8001 доступен и можно зайти и посмотреть данные:
# curl http://localhost:8001/
Out:
# HELP python_gc_objects_collected_total Objects collected during gc
# TYPE python_gc_objects_collected_total counter
python_gc_objects_collected_total{generation="0"} 3.507817e+06
python_gc_objects_collected_total{generation="1"} 677080.0
python_gc_objects_collected_total{generation="2"} 4.8128355e+07
# HELP python_gc_objects_uncollectable_total Uncollectable object found during GC
# TYPE python_gc_objects_uncollectable_total counter
python_gc_objects_uncollectable_total{generation="0"} 0.0
python_gc_objects_uncollectable_total{generation="1"} 0.0
python_gc_objects_uncollectable_total{generation="2"} 0.0
# HELP python_gc_collections_total Number of times this generation was collected
# TYPE python_gc_collections_total counter
python_gc_collections_total{generation="0"} 208464.0
python_gc_collections_total{generation="1"} 18951.0
python_gc_collections_total{generation="2"} 1722.0
# HELP python_info Python platform information
# TYPE python_info gauge
python_info{implementation="CPython",major="3",minor="9",patchlevel="6",version="3.9.6"} 1.0
# HELP cpu CPU utilization
# TYPE cpu gauge
cpu{cpu="cpu",instance="ip_qnap"} 2.0
# HELP network_incomming Network Incoming stack
# TYPE network_incomming gauge
network_incomming{id="1",if_name="eth0",instance="ip_qnap"} 0.0
network_incomming{id="2",if_name="eth1",instance="ip_qnap"} 0.0
network_incomming{id="3",if_name="eth2",instance="ip_qnap"} 0.0
network_incomming{id="4",if_name="eth3",instance="ip_qnap"} 0.0
network_incomming{id="5",if_name="eth4",instance="ip_qnap"} 3.120115368e+09
# HELP network_output Network Output stack
# TYPE network_output gauge
network_output{id="1",if_name="eth0",instance="ip_qnap"} 0.0
network_output{id="2",if_name="eth1",instance="ip_qnap"} 0.0
network_output{id="3",if_name="eth2",instance="ip_qnap"} 0.0
network_output{id="4",if_name="eth3",instance="ip_qnap"} 0.0
network_output{id="5",if_name="eth4",instance="ip_qnap"} 1.455572796e+09
# HELP hdd_temperature HDD stack
# TYPE hdd_temperature gauge
hdd_temperature{id="1",instance="ip_qnap",model="WD4003FFBX-68MU3N0",name="HDD1",state="GOOD"} 33.0
hdd_temperature{id="2",instance="ip_qnap",model="WD4003FFBX-68MU3N0",name="HDD2",state="GOOD"} 34.0
hdd_temperature{id="3",instance="ip_qnap",model="WD4003FFBX-68MU3N0",name="HDD3",state="GOOD"} 32.0
hdd_temperature{id="4",instance="ip_qnap",model="WD4003FFBX-68MU3N0",name="HDD4",state="GOOD"} 32.0
# HELP hdd_capacity HDD stack
# TYPE hdd_capacity gauge
hdd_capacity{id="1",instance="ip_qnap",model="WD4003FFBX-68MU3N0",name="HDD1",state="GOOD"} 4.000787030016e+012
hdd_capacity{id="2",instance="ip_qnap",model="WD4003FFBX-68MU3N0",name="HDD2",state="GOOD"} 4.000787030016e+012
hdd_capacity{id="3",instance="ip_qnap",model="WD4003FFBX-68MU3N0",name="HDD3",state="GOOD"} 4.000787030016e+012
hdd_capacity{id="4",instance="ip_qnap",model="WD4003FFBX-68MU3N0",name="HDD4",state="GOOD"} 4.000787030016e+012
# HELP system_fan_speed FAN stack
# TYPE system_fan_speed gauge
system_fan_speed{id="1",instance="ip_qnap",name="System FAN 1"} 6224.0
system_fan_speed{id="2",instance="ip_qnap",name="System FAN 2"} 6122.0
# HELP memory_total Memory stack
# TYPE memory_total gauge
memory_total{instance="ip_qnap"} 4.126560256e+09
# HELP memory_free Memory stack
# TYPE memory_free gauge
memory_free{instance="ip_qnap"} 3.284451328e+09
# HELP cpu_temperature CPU temperature
# TYPE cpu_temperature gauge
cpu_temperature{instance="ip_qnap"} 56.0
# HELP system_temperature System Temperature
# TYPE system_temperature gauge
system_temperature{instance="ip_qnap"} 47.0
# HELP system_name System name
# TYPE system_name gauge
system_name{instance="ip_qnap",systemname="QNAP-DS2\n"} 1.0
# HELP model_name Model name
# TYPE model_name gauge
model_name{instance="ip_qnap",modelname="TS-463U-RP"} 1.0
Отлично, все что остается это настроить Dashboard в Grafana, дело это конечно не быстрое и кропотливое, но результат того стоит.
![](https://blog.yakunin.dev/wp-content/uploads/2022/11/image.png)
В чем плюсы? В том что я могу вносить изменения в экпортер те, которые мне необходимы а не получать то что мне дали из коробки в бинарном файле, понимание работы прометея, написания экспортеров и многое другое. Это был интересный опыт, плоды которого теперь могут применяться как шаблон для дальнейшей разработки под другие виды железа и не только. Спасибо что дочитали.
Git: https://git.yakunin.dev/yakunin/python/-/tree/main/qnap_snmp_exporter