Linksys SPA8000 monitoring through Zabbix

How it all started
Some of our customers have Linksys (Cisco) SPA8000 VoIP gateways installed. Not bad in its own way, despite some oddities, for example, NAT nailed on slave ports and strange behavior in some situations. I wanted to put my sweaty little hands on them and I couldn’t monitor them by all means. But the trouble is: their monitoring is extremely poor - only a web-interface for a visual assessment of the chaos created there and an external Syslog-server, which is not very much poured: the data on the registration status does. In general, such an impregnable piece of iron, as severe as the Mongol-Tatar yoke. As a result, it was decided to use a roundabout maneuver and grab the opponent by ... web-interface. What came of it - read on.
What do we take?
Since I don’t need someone else’s, but we won’t give our own, we won’t rake all the tons of information that is in the web interface. Enough only line registration statuses and a few general indicators - you need to draw beautiful graphics from something. By analyzing the info page, the following metrics were selected:
# Hostname шлюза. Его же надо как-то называть.
spa8000.name = SPA8000
# Серийный номер. Это больше для инвентаризации.
spa8000.serial = CFH01N704858
# Версия ПО.
spa8000.software = 6.1.12(XU)
# Версия платы, насколько я понимаю.
spa8000.hardware = 1.0.0
# MAC-адрес сетевого интерфейса. Без него никуда.
spa8000.mac = D0C789784BE4
# Есть ли сертификат.
spa8000.cert = Installed
# Открыт ли шлюз для изменения конфигурации.
spa8000.customization = Open
# Время и аптайм. Не забывайте про NTP, товарищи.
spa8000.time = 1/12/2003 14:15:42
spa8000.system.uptime = 11 days and 02:15:42
# Средства для рисования красивых графиков, разнообразные счетчики
# сообщений, пакетов и байт.
spa8000.rtp.packets.sent = 3463097
spa8000.rtp.bytes.sent = 646834968
spa8000.rtp.packets.recv = 3435569
spa8000.rtp.bytes.recv = 641524220
spa8000.sip.msg.sent = 76279
spa8000.sip.bytes.sent = 36830167
spa8000.sip.msg.recv = 73969
spa8000.sip.bytes.recv = 30078139
# Autodiscovery линии.
# Статус линии.
spa8000.line{#LINE}.hook.state = On
# Статус регистрации линии.
spa8000.line{#LINE}.reg.state = Registered
# Дата и время последней регистрации.
spa8000.line{#LINE}.reg.time = 1/12/2003 14:08:01
# Количество секунд до перерегистрации.
spa8000.line{#LINE}.reg.next = 1321 s
# Состояние уведомления о голосовой почте.
spa8000.line{#LINE}.mvi = No
# Состояние уведомления о callback.
spa8000.line{#LINE}.callback = No
# Последний номер, на который звонили с этой линии.
spa8000.line{#LINE}.last.called = 8965ХХХХХХХ
# Последний номер с которого звонили на линию.
spa8000.line{#LINE}.last.caller = +7911ХХХХХХХ
Since trunk lines were not configured on the SPA, data sampling on them was not done either.
How do we take it?
All of the above data was supposed to be retrieved from the web interface by directly requesting it with a script and subsequent parsing. Since there is no way to control how often Zabbix will poll the web interface (suddenly the genius of colleagues decides that it is very fun to poll the interface every five seconds), and as you know, SPA is very leisurely, it was decided to enter The script is a simple caching system that requests an interface no more than once every 30 seconds. The cache itself is a regular CSV file, which lies in the / tmp folder and contains data in the following format:
"info","SPA8000","CFH01N704858","6.1.12(XU)","1.0.0","D0C789784BE4","Installed","Open","1/12/2003 14:15:42","11 days and 02:15:42",3463097,646834968,3435569,641524220,76279,36830167,73969,30078139
"line1","On","Registered","1/12/2003 14:08:01",1321,"No","No","8965ХХХХХХХ","+7911ХХХХХХХ"
"line2",
. . .
The first field on each line is the cache line identifier. The first line contains general information, the second and subsequent lines contain information on all lines in order.
Since this is the first version of the script, it does not support authorization, but adding it if necessary will not be difficult. When you run the script without parameters, you get autodiscovery for the lines. If a specific parameter value is required, then the script is called in the format zabbix_spa8000.py $ 1 $ 2, where $ 1 is the group of data that is requested, the first value in the cache line, $ 2 is the specific value to be obtained.
#!/usr/bin/python
# Get Cisco SPA status for Zabbix.
# codecs импортируется, если происходит отладка скрипта с открытием локального Файла.
#import codecs
import csv
import datetime
import os.path
import re
import sys
import tempfile
import time
import urllib
# Ссылка на интерфейс.
url = 'http:///voice/'
# Путь до файла кэша.
temp = tempfile.gettempdir()
base = temp + '/spa.csv'
# Генератор кэша.
# Если файл кэша не существует или старее 30 секунд, то получаем данные из web-интерфейса.
# Кэш проверяется и создается всегда самым первым, потому что все данные возвращаются
# на основе выборок из файла кэша, то есть напрямую со страницы данные читаются только
# в кэш.
if(not(os.path.exists(base)) or ((int(time.time()) - os.path.getmtime(base)) > 30)):
# Читаем HTML страницу.
html = urllib.urlopen(url).read()
# Отладочная секция - читает файл с диска.
#f = codecs.open("text.html", 'r', "utf-8")
#html = f.read()
#f.close()
# Нормализуем и парсим HTML.
# Приводим текст к одной строке.
html = re.sub(r"\n+", " ", html)
# Убираем пробелы между тегами.
html = re.sub(r">\s+<", "><", html)
# Страница очень длинная - там все вкладки интерфейса. Чтобы уменьшить вероятность
# выборки не того отрезаем нужный нам кусок страницы.
html = re.search(r".+?Trunk", html).group(0)
# Получаем количество линий.
count = len(re.findall(r"]+>]+>Line\s(\d+)\sStatus",html))
# Извлекаем все данные вперемешку. Так как в HTML формат всех полей с данными
# одинаковый, то просто ищем все совпавшие кусочки данных и считаем их по
# умолчанию значениями. дальше разберутся.
data = re.findall(r"([^<:>]+):.+?]+>([^<]*)",html)
# Создаем CSV файл.
number = 0
rows = []
# Создаем заголовки строк.
rows.append(['info'])
count += 1
for i in range (1, count):
rows.append(['line' + str(i)])
count -= 1
# Заполняем строки. Выбираем из собранных данных все по одному и внимательно
# присматриваемся. Так как данные лежат в массиве в том порядке, в котором
# они отображаются на странице, то можем считать, что все значения до определенного
# относятся к общей информации, а все что дальше, относится к линиям. Так все и устроено
# первое значение каждой линии - это Hook State. Все что находится до первого такого значения
# относится к общей информации, все что после - к линиям.
# Цикл по извлеченным данным.
for entry in data:
# Значение, по которому разделяются данные общей информации и линий.
if(entry[0] == 'Hook State'):
number += 1
# Обрабатываем все нужные данные, кроме всяких Call 1 Forward и иже с ними.
if(not(re.match(r'Call\s\d+\s', entry[0]))):
value = entry[1]
# Преобразуем время в соответствующих полях. В Zabbix время уходит в виде unixtime.
if(((entry[0] == 'Current Time') or (entry[0] == 'Last Registration At')) and (value)): # 1/13/2003 14:03:52
if(value != '0/0/0 00:00:00'):
value = int(time.mktime(datetime.datetime.strptime(entry[1], "%m/%d/%Y %H:%M:%S").timetuple()))
else:
value = 0
# Извлекаем количество секунд.
if((entry[0] == 'Next Registration In') and (value)): # 1263 s
value = int(entry[1].split()[0])
# Сохраняем значение в соответствующей строке массива.
rows[number].extend([value])
# Сохраняем кэш.
with open(base, 'w') as csvfile:
writer = csv.writer(csvfile)
writer.writerows(rows)
csvfile.close()
# Ну вот, кэш сохранен, можно теперь его использовать для вывода данных. Проверяем
# наличие параметров. Если они переданы, значит мы хотим чего-то конкретного.
if(len(sys.argv) > 1):
# Открываем файл кэша и ищем строку, которую мы запрашиваем первым параметром.
with open(base, 'rb') as csvfile:
reader = csv.reader(csvfile)
for row in reader:
# TODO: Прерывание цикла, если строка нашлась.
if(row[0] == sys.argv[1]):
line = row
csvfile.close()
# Проверяем, что запрошенная строка вообще есть в кэше.
if('line' in locals()):
# Определяем, что у нас спрашивают вторым параметром, и что вообще возвращать.
# Блок общей информации.
if(line[0] == 'info'):
try:
print({
'name': line[1],
'serial': line[2],
'software': line[3],
'hardware': line[4],
'mac': line[5],
'cert': line[6],
'customization': line[7],
'time': line[8],
'uptime': line[9],
'rtp.packets.sent': line[10],
'rtp.bytes.sent': line[11],
'rtp.packets.recv': line[12],
'rtp.bytes.recv': line[13],
'sip.msg.sent': line[14],
'sip.bytes.sent': line[15],
'sip.msg.recv': line[16],
'sip.bytes.recv': line[17]
}.get(sys.argv[2]))
# Если запрошенного поля не нашлось, так и говорим.
except IndexError as e:
print('ZBX_UNSUPPORTED')
# Блок линий.
else:
try:
print({
'hook.state': line[1],
'reg.state': line[2],
'reg.time': line[3],
'reg.next': line[4],
'mvi': line[5],
'callback': line[6],
'last.called': line[7],
'last.caller': line[8],
}.get(sys.argv[2]))
except IndexError as e:
print('ZBX_UNSUPPORTED')
# Если запрашиваемая строка не нашлась в кэше, то тоже возвращаем ошибку.
# Это позволяет не смущать Zabbix пустым результатом.
else:
print('ZBX_UNSUPPORTED')
else:
# Получаем autodiscovery для линий.
# Выходная строка.
s = ""
# Открываем файл кэша и читаем его, формируя выходную строку.
with open(base, 'rb') as csvfile:
reader = csv.reader(csvfile)
for row in reader:
# Проставляем разделители.
if(len(s) > 0):
s = s + ","
# Формируем JSON строку вручную.
if(row[0] != 'info'):
s = s + '{"{#LINE}":"' + str(row[0]) + '"}'
csvfile.close()
# Выводим сформированную строку.
print('{"data":[' + s + ']}')
# Завершаем работу скрипта.
quit()
The script must be put in / share / zabbix / externalscripts. If there is no such folder, you need to create it and give Zabbix user access to it.
We make the script executable.
sudo chmod +x /share/zabbix/externalscripts/zabbix_spa8000.py
We test the correct operation of Autodiscovery.
/share/zabbix/externalscripts/zabbix_spa8000.py
If everything is correct at the output, we get approximately this:
{"data":[{"{#LINE}":"line1"},{"{#LINE}":"line2"},{"{#LINE}":"line3"},{"{#LINE}":"line4"},{"{#LINE}":"line5"},{"{#LINE}":"line6"},{"{#LINE}":"line7"},{"{#LINE}":"line8"}]}
We test the correctness of the transmission of parameter values.
/share/zabbix/externalscripts/zabbix_spa8000.py info serial
If everything is correct at the output, we get approximately this:
CFH01N704858
Add the UserParameter to Zabbix to receive data by the agent.
/etc/zabbix_agentd.conf.d/spa8000.conf
UserParameter=spa[*],/share/zabbix/externalscripts/zabbix_spa.py $1 $2
UserParameter=spa.lines,/share/zabbix/externalscripts/zabbix_spa.py
Reboot the Zabbix agent.
sudo service zabbix-agent restart
Check that the Zabbix agent can receive script data.
zabbix_agentd -t spa.lines
spa.lines [t|{"data":[{"{#LINE}":"line1"},{"{#LINE}":"line2"},{"{#LINE}":"line3"},{"{#LINE}":"line4"},{"{#LINE}":"line5"},{"{#LINE}":"line6"},{"{#LINE}":"line7"},{"{#LINE}":"line8"}]}]
zabbix_agentd -t spa[info,serial]
spa[info,serial] [t|CFH01N70
4858]
Where do we put it?
Template for Zabbix server. Joins the node from where access to the SPA web-interface is available.
3.0 2016-09-09T11:04:05Z Шаблоны общие Template_Cisco_Linksys_SPATemplate Cisco(Linksys) SPA Шаблон мониторинга Cisco(Linksys) SPA. Шаблоны общие RTP SIP Информация Линии Сертификат 0 0 spa[info,cert] 3600 10 0 0 4 0 0 0 0 1 0 0 Наличие сертификата SPA. 0 Информация Блокировка 0 0 spa[info,customization] 3600 10 0 0 4 0 0 0 0 1 0 0 Блокировка конфигурации SPA от изменения. 0 Информация Аппаратное обеспечение 0 0 spa[info,hardware] 3600 10 0 0 4 0 0 0 0 1 0 0 Версия аппаратного обеспечения SPA. 0 Информация MAC-адрес 0 0 spa[info,mac] 3600 10 0 0 4 0 0 0 0 1 0 0 МАС-адрес SPA. 0 Информация Имя 0 0 spa[info,name] 3600 10 0 0 4 0 0 0 0 1 0 0 Hostname SPA. 0 Информация Получено байт RTP 0 0 spa[info,rtp.bytes.recv] 30 30 60 0 3 B 0 0 0 0 1 0 0 Количество полученных байт RTP. 0 RTP Отправлено байт RTP 0 0 spa[info,rtp.bytes.sent] 30 30 60 0 3 B 0 0 0 0 1 0 0 Количество отправленных байт RTP. 0 RTP Получено пакетов RTP 0 0 spa[info,rtp.packets.recv] 30 30 60 0 3 0 0 0 0 1 0 0 Количество полученных пакетов RTP. 0 RTP Отправлено пакетов RTP 0 0 spa[info,rtp.packets.sent] 30 30 60 0 3 0 0 0 0 1 0 0 Количество отправленных пакетов RTP. 0 RTP Серийный номер 0 0 spa[info,serial] 3600 10 0 0 4 0 0 0 0 1 0 0 Серийный номер SPA. 0 Информация Получено байт SIP 0 0 spa[info,sip.bytes.recv] 30 30 60 0 3 B 0 0 0 0 1 0 0 Количество полученных байт SIP. 0 SIP Отправлено байт SIP 0 0 spa[info,sip.bytes.sent] 30 30 60 0 3 B 0 0 0 0 1 0 0 Количество отправленных байт SIP. 0 SIP Получено сообщений SIP 0 0 spa[info,sip.msg.recv] 30 30 60 0 3 0 0 0 0 1 0 0 Количество полученных сообщений SIP. 0 SIP Отправлено сообщений SIP 0 0 spa[info,sip.msg.sent] 30 30 60 0 3 0 0 0 0 1 0 0 Количество отправленных сообщений SIP. 0 SIP Программное обеспечение 0 0 spa[info,software] 3600 10 0 0 4 0 0 0 0 1 0 0 Версия программного обеспечения SPA. 0 Информация Локальное время 0 0 spa[info,time] 60 30 60 0 3 unixtime 0 0 0 0 1 0 0 Локальное время SPA. 0 Информация Аптайм 0 0 spa[info,uptime] 60 30 60 0 3 uptime 0 0 0 0 1 0 0 Время непрерывной работы SPA. 0 Информация Линии 0 spa.lines 3600 0 0 0 0 0 0 30 Линии SPA. Статус callback линии {#LINE} 0 0 spa[{#LINE},callback] 30 10 0 0 4 0 0 0 0 1 0 0 Статус callback линии {#LINE}. 0 Линии Статус Hook линии {#LINE} 0 0 spa[{#LINE},hook.state] 30 10 0 0 4 0 0 0 0 1 0 0 Статус Hook линии {#LINE}. 0 Линии Последний исходящий с линии {#LINE} 0 0 spa[{#LINE},last.called] 30 5 0 0 4 0 0 0 0 1 0 0 Последний исходящий с линии {#LINE}. 0 Линии Последний входящий на линию {#LINE} 0 0 spa[{#LINE},last.caller] 30 5 0 0 4 0 0 0 0 1 0 0 Последний входящий на линию {#LINE}. 0 Линии Статус MVI линии {#LINE} 0 0 spa[{#LINE},mvi] 30 10 0 0 4 0 0 0 0 1 0 0 Статус MVI линии {#LINE}. 0 Линии Интервал регистрации линии {#LINE} 0 0 spa[{#LINE},reg.next] 30 30 60 0 3 uptime 0 0 0 0 1 0 0 Время до следующей регистрации линии {#LINE}. 0 Линии Статус регистрации линии {#LINE} 0 0 spa[{#LINE},reg.state] 30 10 0 0 4 0 0 0 0 1 0 0 Статус регистрации линии {#LINE}. 0 Линии Локальное время регистрации линии {#LINE} 0 0 spa[{#LINE},reg.time] 30 30 60 0 3 unixtime 0 0 0 0 1 0 0 Локальное время регистрации линии {#LINE}. 0 Линии {Template_Cisco_Linksys_SPA:spa[{#LINE},reg.state].count(#2,Registered,eq,0)}<2 Ошибка регистрации линии {#LINE} 0 4 Линия {#LINE} не может зарегистрироваться в течение 1 минуты. 0 {Template_Cisco_Linksys_SPA:spa[info,uptime].last()}<180 SPA перезагружен 0 2 Время работы SPA менее 3 минут. 0 What is the result?
Zabbix once again confirms that with direct hands you can monitor anything, and there is an approach to any piece of hardware. Just look at it from a non-standard angle. And knowledge of programming at least at a basic level significantly expands not only the monitoring capabilities, but also the affordable tools of the system administrator.
PS I hope someone comes in handy. I will be glad to your comments.