Back to Home

Linksys SPA8000 Monitoring Through Zabbix / Business Infinity Group Blog

zabbix · monitoring · cisco spa8000 · linksys

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.

    zabbix_spa8000.py
    #!/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.

    Zabbix server template
    3.02016-09-09T11:04:05ZШаблоны общие{Template_Cisco_Linksys_SPA:spa[info,uptime].last()}<180SPA перезагружен02Время работы 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.

    Read Next