Автоматизация ip-сети с помощью подручных инструментов (Python)

от автора

Эта статья подойдет сетевым специалистам, которые находятся в поисках примеров возможной автоматизации ip сети с помощью подручных инструментов.

Как один из вариантов автоматизации, это взаимодействие программной среды с CLI (Command Line Interface) оборудования, так называемый ‘Screen Scraping’. Собственно, об этом варианте и пойдет речь.

В качестве программной среды, будет использован язык программирования Python версии 3.3. Для сомневающихся в потребности изучения языка программирования, необходимо отметить, что базовые навыки программирования на Python достаточно просты в освоении и для решения описанных ниже задач являются достаточными. В дальнейшем с совершенствованием навыков будет совершенствоваться код и уровень производимых продуктов. Для удаленного взаимодействия с оборудованием в основном будет использоваться протокол SSH, поэтому в качестве работы с SSH, для облегчения задач, выбран дополнительный модуль для Python – Paramiko. Как правило рассмотрение решения конкретных задач, может способствовать лучшему усвоению материала, поэтому не затягивая процесс далее будут рассмотрены выборочные примеры задач по возрастающей степени сложности и их решение с использованием выше описанных инструментов (важно заметить, все ip адреса, логины, пароли, названия и специфические значения параметров с сетевых устройств — вымышленные, любое совпадение случайно).

1. Задача: Анализ показателей сети

Необходимо периодически анализировать таблицу маршрутизации сети, с определением количества префиксов, полученных со стыков Аплинк, пиринг, IX и клиентских включений с разбиением по количеству AS BGP до конечного ресурса. Данный анализ в определенном промежутке времени, может показывать динамику улучшения показателей связности не только исходя из клиентского конуса

Решение: В большинстве сетей разделение маршрутной информации по стыкам можно определить исходя из значение атрибута Local Preference BGP (LP). Соответственно определив какой запрос в CLI маршрутизатора дает возможность вывести значения LP и AS_PATH для активных маршрутов, а затем обработав вывод, можно получить искомую статистику. Допустим на исследуемой сети используются маршрутизаторы Juniper, соответственно одной из таких команд может быть:

 # show route protocol bgp table inet.0 active-path | no-more. 

Результатом запроса подобной команды будет следующий вывод:

 1.1.1.0/24       *[BGP/170] 2w3d 23:44:20, MED 0, localpref 150                       AS path: 3356 6453 4755 45528 I, validation-state: unverified                     > to 2.1.1.1 via ae0.0 1.2.1.0/24       *[BGP/170] 1d 20:20:51, MED 0, localpref 170, from 10.0.0.1                       AS path: 9498 45528 I, validation-state: unverified                     > to 2.1.1.5 via ae10.0 … 

Одной из возможных реализаций, может послужить следующий код (комментарии написаны непосредственно в коде после #):

Код Python

#!/usr/local/bin/python3.3 ## -*- coding: koi8-r -*-  ### Импортируются необходимые библиотеки import paramiko import time import datetime import sys import re import os import socket import base64  ### Задаются исходные параметры user = 'user' secret= pas = base64.b64decode(b'cGFzc3dvcmQ=').decode('ascii') # совсем не надежно зашифрованный пароль, но хоть что то. Для получения зашифрованного пароля # необходиом предварительно выполнить base64.b64encode('password'.encode('ascii')) port = por = 22 host='10.10.10.10'  ### используется модуль paramiko для установления соединения получения результата с оборудования: remote_conn_pre = paramiko.SSHClient() remote_conn_pre.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ### всегда доверяется SHA ключам remote_conn_pre.connect(hostname=host, username=user, password=pas, port=por, timeout=90) ### устанавливается соединение используя заданные параметры remote_conn = remote_conn_pre.invoke_shell() ### сессия постоянно поддерживается до принудительного завершения, либо по истечении времени жизни remote_conn.settimeout(20) ### через 20 sec при отсутствии активности сессия будет разорвана remote_conn.send ('\n') ### 'Enter' для проверки работоспособности time.sleep(1) ### приостановка выполнения скрипта на 1 секунду check=remote_conn.recv(2048) ### чтение данных с консоли не более 2048 байт print(check.decode('ascii')) ### печать вывода консоли. Добавление .decode('ascii') позволяет выполнить вывод в удобочитабельном виде. При повторном использовании, # данную строку целесообразно закомментировать (поставив впереди #) remote_conn.send ('show route protocol bgp table inet.0 active-path | no-more' + '\n') ### Основной запрос, плюс 'Enter' def while_not_end_plus_recive(): ### def - функция выполняемая по запросу, позволяет дождаться окончания выполнения запроса и запись результата         buff = b'' ### перед дельнейшим изменение значения, переменная должна быть изначально задана.         resp1=b''         while not buff.endswith(b'0> '): ### цикл while выполняется до наличия в выводе значения 0> в консоли                 resp = remote_conn.recv(12002048)                 buff += resp ### увеличивается значение buff на значение resp в цикле                 print ('!', end='',  flush=True) ### выводится индикатор выполнения цикла, для понимания факта выполнения, так вывод таблицы на экран занимает продолжительное время         return buff ### возвращается значение функции, с выводом всех значений при повторении цикла While check=str(while_not_end_plus_recive()) ### запускается вышеописанная функция с присвоением значения переменной check, переводим значение переменной в строковый # тип данных str() print('\n') ### выводится для более наглядного отображения результата в конце remote_conn_pre.close() ### SSH сессия с оборудованием больше не требуется.  ### Записывается результат в файл для возможного пост анализа во временной перспективе, обрабатываются данные с оборудования: timestr = time.strftime("%d%m%Y") ### переменная, отображающая текущую дату, для дальнейшего использования в названии файла log_out=open('/usr/SCRIPTS_FOR_PYTHON/route_tables/route_table_'+timestr+'.txt', 'w') ### создается и открывается файл в указанной директории log_out.write(check) ### записывается в файл ранее полученный вывод log_out.close() ### закрывается файл, так как он открыт для записи log_out=open('/usr/SCRIPTS_FOR_PYTHON/route_tables/route_table_'+timestr+'.txt', 'r') ### открывается созданный файл для чтения. Возможно также использовать переменную check. data=log_out.read() ### считываются данные с файла для дальнейшей обработки. log_out.close()  ### вывод с оборудования представляет собой блоки данных для каждого ip префикса. Для работы с каждым блоком в отдельности, необходимо разбить переменную data на список состоящий из # строковых значений. ключом для разбиения служит слово 'BGP' comp=re.compile('BGP') ### при помощи модуля регулярных выражений re, создается шаблон для разбиения с использованием объекта comp. split_out=comp.split(data)### разбиение вывода данных ### задаются начальные значения переменных, необходимых для получения результата: prefixes_1_as_uplink=0 prefixes_2_as_uplink=0 prefixes_3_as_uplink=0 prefixes_4_as_uplink=0 prefixes_more_then_4_as_uplink=0 prefixes_1_as_IX=0 prefixes_2_as_IX=0 prefixes_3_as_IX=0 prefixes_4_as_IX=0 prefixes_more_then_4_as_IX=0 prefixes_1_as_peer=0 prefixes_2_as_peer=0 prefixes_3_as_peer=0 prefixes_4_as_peer=0 prefixes_more_then_4_as_peer=0 prefixes_1_as_client=0 prefixes_2_as_client=0 prefixes_3_as_client=0 prefixes_4_as_client=0 prefixes_more_then_4_as_client=0 own_as_prefixes=0 other_prefixes=0  ### Для обработки результата используется цикл for, выполняемый для каждого значения списка i диапазона значений split_out: for i in range(len(split_out)):         if len(re.findall('localpref 150',str(split_out[i])))==1: ### Цикл if для каждого значения списка i, выполняемого циклом for, определяет         #значения LP относящиеся к префиксу полученному с Uplink                 count_as_from_uplink_pre=re.findall(r'AS path:([^]]*), val', split_out[i]) ### Для префиксов Uplink, определятся список состоящий из транзитных AS BGP                 count_as_from_uplink=re.findall(r'[\d]+', str(count_as_from_uplink_pre)) ### продолжение определения списка из транзитных AS BGP                 if len(set(count_as_from_uplink))==1: ### Для определенного списка транзитных AS BGP, в зависимости от длинны списка, увеличивается счетчик значения переменной                         prefixes_1_as_uplink = prefixes_1_as_uplink + 1                 if len(set(count_as_from_uplink))==2:                         prefixes_2_as_uplink = prefixes_2_as_uplink + 1                 if len(set(count_as_from_uplink))==3:                         prefixes_3_as_uplink = prefixes_3_as_uplink + 1                 if len(set(count_as_from_uplink))==4:                         prefixes_4_as_uplink = prefixes_4_as_uplink + 1                 if len(set(count_as_from_uplink))>4:                         prefixes_more_then_4_as_uplink = prefixes_more_then_4_as_uplink + 1                 else:                         pass         elif len(re.findall('localpref 170|localpref 175',str(split_out[i])))==1: ### Цикл if для каждого значения списка i, выполняемого циклом for, определяет         #значения LP относящиеся к префиксу полученному с IX. Следует отметить, что в данном случае LP может принимать больше 1 значения.                 count_as_from_IX_pre=re.findall(r'AS path:([^]]*), val', split_out[i])                 count_as_from_IX=re.findall(r'[\d]+', str(count_as_from_IX_pre))                 if len(set(count_as_from_IX))==1:                         prefixes_1_as_IX = prefixes_1_as_IX + 1                 if len(set(count_as_from_IX))==2:                         prefixes_2_as_IX = prefixes_2_as_IX + 1                 if len(set(count_as_from_IX))==3:                         prefixes_3_as_IX = prefixes_3_as_IX + 1                 if len(set(count_as_from_IX))==4:                         prefixes_4_as_IX = prefixes_4_as_IX + 1                 if len(set(count_as_from_IX))>4:                         prefixes_more_then_4_as_IX = prefixes_more_then_4_as_IX + 1                 else:                         pass         elif len(re.findall('localpref 180',str(split_out[i])))==1: ### Цикл if для каждого значения списка i, выполняемого циклом for, определяет         #значения LP относящиеся к префиксу полученному с Peer.                 count_as_from_peer_pre=re.findall(r'AS path:([^]]*), val', split_out[i])                 count_as_from_peer=re.findall(r'[\d]+', str(count_as_from_peer_pre))                 if len(set(count_as_from_peer))==1:                         prefixes_1_as_peer = prefixes_1_as_peer + 1                 if len(set(count_as_from_peer))==2:                         prefixes_2_as_peer = prefixes_2_as_peer + 1                 if len(set(count_as_from_peer))==3:                         prefixes_3_as_peer = prefixes_3_as_peer + 1                 if len(set(count_as_from_peer))==4:                         prefixes_4_as_peer = prefixes_4_as_peer + 1                 if len(set(count_as_from_peer))>4:                         prefixes_more_then_4_as_peer = prefixes_more_then_4_as_peer + 1                 else:                         pass         elif len(re.findall('localpref 200|localpref 190',str(split_out[i])))==1: ### Цикл if для каждого значения списка i, выполняемого циклом for, определяет         #значения LP относящиеся к префиксу, полученному с Clients.                 count_as_from_client_pre=re.findall(r'AS path:([^]]*), val', split_out[i])                 count_as_from_client=re.findall(r'[\d]+', str(count_as_from_client_pre))                 if len(set(count_as_from_client))==1:                         prefixes_1_as_client = prefixes_1_as_client + 1                 if len(set(count_as_from_client))==2:                         prefixes_2_as_client = prefixes_2_as_client + 1                 if len(set(count_as_from_client))==3:                         prefixes_3_as_client = prefixes_3_as_client + 1                 if len(set(count_as_from_client))==4:                         prefixes_4_as_client = prefixes_4_as_client + 1                 if len(set(count_as_from_client))>4:                         prefixes_more_then_4_as_client = prefixes_more_then_4_as_client + 1                 if count_as_from_client==[]:                         own_as_prefixes= own_as_prefixes + 1                 else:                         pass         else:                 other_prefixes=other_prefixes + 1  ### Вывод полученных результатов: print('prefixes_1_as_uplink: '+str(prefixes_1_as_uplink)) print('prefixes_2_as_uplink: '+str(prefixes_2_as_uplink)) print('prefixes_3_as_uplink: '+str(prefixes_3_as_uplink)) print('prefixes_4_as_uplink: '+str(prefixes_4_as_uplink)) print('prefixes_more_then_4_as_uplink: '+str(prefixes_more_then_4_as_uplink)) print('all_uplink_prefixes: '+str(prefixes_1_as_uplink+prefixes_2_as_uplink+prefixes_3_as_uplink+prefixes_4_as_uplink+prefixes_more_then_4_as_uplink)+'\n')  print('prefixes_1_as_IX: '+str(prefixes_1_as_IX)) print('prefixes_2_as_IX: '+str(prefixes_2_as_IX)) print('prefixes_3_as_IX: '+str(prefixes_3_as_IX)) print('prefixes_4_as_IX: '+str(prefixes_4_as_IX)) print('prefixes_more_then_4_as_IX: '+str(prefixes_more_then_4_as_IX)) print('all_IX_prefixes: '+str(prefixes_1_as_IX+prefixes_2_as_IX+prefixes_3_as_IX+prefixes_4_as_IX+prefixes_more_then_4_as_IX)+'\n')  print('prefixes_1_as_peer: '+str(prefixes_1_as_peer)) print('prefixes_2_as_peer: '+str(prefixes_2_as_peer)) print('prefixes_3_as_peer: '+str(prefixes_3_as_peer)) print('prefixes_4_as_peer: '+str(prefixes_4_as_peer)) print('prefixes_more_then_4_as_peer: '+str(prefixes_more_then_4_as_peer)) print('all_peer_prefixes: '+str(prefixes_1_as_peer+prefixes_2_as_peer+prefixes_3_as_peer+prefixes_4_as_peer+prefixes_more_then_4_as_peer)+'\n')  print('prefixes_1_as_client: '+str(prefixes_1_as_client)) print('prefixes_2_as_client: '+str(prefixes_2_as_client)) print('prefixes_3_as_client: '+str(prefixes_3_as_client)) print('prefixes_4_as_client: '+str(prefixes_4_as_client)) print('prefixes_more_then_4_as_client: '+str(prefixes_more_then_4_as_client)) print('all_client_prefixes: '+str(prefixes_1_as_client+prefixes_2_as_client+prefixes_3_as_client+prefixes_4_as_client+prefixes_more_then_4_as_client)+'\n')  print('own_as_prefixes: '+str(own_as_prefixes)) print('other_prefixes: '+str(other_prefixes))  

Для запуска скрипта в собственной сети, необходимо на устройстве имеющем прямой доступ к сетевому маршрутизатору Juniper, установить Python3 + Paramiko, скопировать выше приведенный код в файл с расширением .py, подставив собственные значения LP и ip, логин, пароль и порт tcp для ssh. Запустить полученный script (например на FreeBsd командой python3.3 route_scan.py Enter). Вывод программы будет иметь следующий вид:

 prefixes_1_as_uplink: 2684 prefixes_2_as_uplink: 90048 prefixes_3_as_uplink: 132173 prefixes_4_as_uplink: 61119 prefixes_more_then_4_as_uplink: 15472 all_uplink_prefixes: 301496  prefixes_1_as_IX: 21876 prefixes_2_as_IX: 72699 prefixes_3_as_IX: 38738 prefixes_4_as_IX: 13233 prefixes_more_then_4_as_IX: 2960 all_IX_prefixes: 149506  prefixes_1_as_peer: 8990 prefixes_2_as_peer: 18772 prefixes_3_as_peer: 17150 prefixes_4_as_peer: 3236 prefixes_more_then_4_as_peer: 1372 all_peer_prefixes: 49520  prefixes_1_as_client: 14348 prefixes_2_as_client: 13166 prefixes_3_as_client: 981 prefixes_4_as_client: 175 prefixes_more_then_4_as_client: 13 all_client_prefixes: 28683  own_as_prefixes: 103 other_prefixes: 21911 

На основании полученных результатов можно оценить степень связности сети, у сетей со слабо развитыми внешними связями, счётчик будет в большей степени на Аплинк стыках. У сетей с активно проводимой пиринговой политикой, счётчик будет увеличиваться в пользу IX и пирингов и в конечном итоге при умении правильно преподнести связность сети, в сторону клиентских стыков.

2. Задача: Реакция системы, в ответ на происходящие нежелательные события

Необходимо реализовать автоматическое изменение конфигурации сети в ответ на происходящие нежелательные события. Как вариант, имеются внешние стыки с хорошими параметрами качества сети, но недостаточной пропускной способностью и периодической стихийной утилизацией трафика. Также имеются другие внешние стыки с доступностью тех же внешних ресурсов, достаточной емкостью, но менее привлекательными параметрами. Таким образом необходимо в автоматическом режиме перенаправить трафик при достижении определенных порогов утилизации стыка. Работу скрипта желательно отслеживать посредством ведения журнала.

Решение: Допустим в сети используется оборудование Cisco (IOS). Трафик на стыке преобладает исходящий, в результате генерации контента внутри сети. В зависимости от присваиваемых BGP Community на стыке, в другом сегменте собственной сети, происходит присваивание приоритета префиксу и выбор наилучшего маршрута. Соответственно задача программного обеспечения отследить заданный порог утилизации и поменять присваиваемое BGP Community на стыке.

Одной из возможных реализаций, может послужить следующий код (все комментарии написаны непосредственно в коде после #):

Код Python

#!/usr/local/bin/python3.3 ## -*- coding: koi8-r -*- import paramiko import time import datetime import sys import re import os import socket import subprocess import random import cgi import cgitb import base64  host='10.10.10.10' user='user' pas= base64.b64decode(b'cGFzc3dvcmQ=').decode('ascii') por=22  def log( message): ### Для возможности отслеживания работы ПО, создается функция для логирования результата         log_out1=open('/usr/SCRIPTS_FOR_PYTHON/speed_log.txt', 'a') ### файл открывается на запись в конец файла         log_out1.write(str(datetime.datetime.now()) + ':'+ str(message) + '\n') ### Шаблон сообщения с датой и временем         log_out1.close()         pass  def while_not_end_plus_recive(): ### Функция для ожидания выполнения команд на оборудовании записи результата         buff = b''         resp1=b''         try: ### конструкция позволяющая обработать ошибки, в данном случае программа попытается выполнить цикл While                 while not buff.endswith(b'#'):                         resp = remote_conn.recv(12002048)                         buff += resp         except socket.timeout: ### Если выполнение цикла приведет к ошибке, ПО при помощи функции LOG запишет соответствующее сообщение в файл и программа закроется                 log('Device did not respond')                 sys.exit()         return buff  ### Первая часть ПО, находит утилизацию выбранного интерфейса. Утилизацию возможно найти двумя способами: #1.  Наиболее часто встречающийся способ получения значения утилизации происходит посредством получения информации по SNMP протоколу. #2.  Возможно также получить утилизацию, обработав вывод команды маршрутизатора 'sh int ge-1/1/1 | include output' (в зависимости от версии вывод может немного меняться) # Рассмотрим оба варианта по порядку, при этом второй вариант будет закомментирован, так как первый вариант наиболее предпочтителен по мнению автора. #3. Получение информации о загрузке интерфейса с внешней системы, в данной статье рассматриваться не будет.  ### Использование SNMP. Для взаимодействия по SNMP будет использоваться сторонняя утилита NET_SNMP, запускаемая из-под скрипта Python in_rate=open('/usr/SCRIPTS_FOR_PYTHON/test_rate.txt', 'w') ### Создается файл для записи результата ### Далее запускается внешний процесс с необходимым snmp параметрами OID ifHCOutOctets и ifName интерфейса subprocess.call(['snmpwalk -v2c -c TEST 10.222.0.177 1.3.6.1.2.1.31.1.1.1.10.10201'], bufsize=0, shell=True, stdout=(in_rate)) in_rate.close() f=open('/usr/SCRIPTS_FOR_PYTHON/test_rate.txt', 'r') inrate1_pre=f.read() ### записывается первое значение для последующего расчета скорости f.close() time.sleep(60) ### Выполнение программы приостанавливается до следующего замера через минуту in_rate=open('/usr/SCRIPTS_FOR_PYTHON/test_rate.txt', 'w') # создается файл для записи результата subprocess.call(['snmpwalk -v2c -c TEST 10.222.0.177 1.3.6.1.2.1.31.1.1.1.10.10201'], bufsize=0, shell=True, stdout=(in_rate)) in_rate.close() f=open('/usr/SCRIPTS_FOR_PYTHON/test_rate.txt', 'r') inrate2_pre=f.read()### записывается второе значение для последующего расчета скорости f.close() inrate1=re.findall('[\d]+', str(re.findall(': [\d]+', inrate1_pre))) ### выводы обрабатываются для получения цифр inrate2=re.findall('[\d]+', str(re.findall(': [\d]+', inrate2_pre))) rate=(int(inrate2[0])-int(inrate1[0]))*8/60 ### рассчитывается скорость трафика на интерфейсе  ### обработка вывода данных с маршрутизатора (закоментировано). Приводится как пример обработки при успешном подключении к маршрутизатору. # В работе программы не участвует #input_rate_recv=str(remote_conn.send('show interface ge-1/1/1/ | include input\n')) #input_rate_recv_out=while_not_end_plus_recive() #output_rate_recv=str(remote_conn.send('show interface ge-1/1/1/ | include output\n')) #output_rate_recv_out=while_not_end_plus_recive() #input_rate_search_pre=str(re.findall('[\d]+' + bps, input_rate_recv_out)) #input_rate_search=re.findall('[\d]+', input_rate_search_pre) #input_rate=''.join(input_rate_search) #output_rate_search_pre=str(re.findall('[\d]+' + bps, output_rate_recv_out)) #output_rate_search=re.findall('[\d]+', output_rate_search_pre) #output_rate=''.join(output_rate_search)   if rate>9000000000: # получив скорость утилизации порта, проверяется на наличие условия превышения заданного порога         remote_conn_pre = paramiko.SSHClient()### Если порог превышен, происходит подключение к оборудованию по SSH         remote_conn_pre.set_missing_host_key_policy(paramiko.AutoAddPolicy())         remote_conn_pre.connect(hostname=host, username=user, password=pas, port=por, timeout=90)         remote_conn = remote_conn_pre.invoke_shell()         remote_conn.settimeout(90)         remote_conn.send('sh running-config partition route-map | section include FROM-TEST-POLICY'+'\n'+'\n') ### выполняется команда, для проверки политики         check=while_not_end_plus_recive()### записывается результат для обработки         print (check)         check_policy=re.findall('65000:1', str(check)) ### проверка на наличие community         print (str(check_policy))         if check_policy==['65000:1']: ### Если найдено BGP community которое должно быть применено, программа закрывается. Это предохранитель от циклической перезаписи                 remote_conn_pre.close()                 log('Policy already_changed') ### результат логируется                 sys.exit()         else: ### если значение не найдено, происходит дальнейшее выполнение программы                 pass         remote_conn.send('conf t'+'\n') ### вход в конфигурационный режим и выполнение изменений         while_not_end_plus_recive()         remote_conn.send('route-map FROM-TEST-POLICY permit 10'+'\n')         while_not_end_plus_recive()         remote_conn.send('no set community'+'\n')         while_not_end_plus_recive()         remote_conn.send('set community 65000:1 additive'+'\n')         while_not_end_plus_recive()         remote_conn.send('exit'+'\n')         while_not_end_plus_recive()         remote_conn.send('exit'+'\n')         while_not_end_plus_recive()         remote_conn.send('exit'+'\n')         log('Policy has been changed') ### результат логируется         remote_conn_pre.close()         time.sleep(3600) ### выполнение программы приостанавливается на промежуток времени при котором возможно генерация большого количества трафика уменьшится.         # Для того чтобы промежуток динамически изменялся, можно написать код индикатора связанный с генератором контента (в данном примере не рассматривается)         ### По истечении срока, происходит возврат конфигурации         remote_conn_pre.connect(hostname=host, username=user, password=pas, port=por, timeout=90)         remote_conn = remote_conn_pre.invoke_shell()         remote_conn.settimeout(780)         remote_conn.send('sh running-config partition route-map | section include FROM-TEST-POLICY'+'\n'+'\n') ### проверка на возможный откат до завершения скрипта         check=while_not_end_plus_recive()         check_policy=re.findall('65000:2', str(check))         if check_policy==['65000:2']:                 remote_conn_pre.close()                 log('Policy already rewert') ### результат логируется                 sys.exit()         else:                 pass         remote_conn.send('conf t'+'\n'+'\n')         while_not_end_plus_recive()         remote_conn.send('route-map FROM-TEST-POLICY permit 10'+'\n')         while_not_end_plus_recive()         remote_conn.send('no set community'+'\n')         while_not_end_plus_recive()         remote_conn.send('set community 65000:2 additive'+'\n'+'\n')         while_not_end_plus_recive()         remote_conn.send('exit'+'\n')         while_not_end_plus_recive()         remote_conn.send('exit'+'\n')         while_not_end_plus_recive()         remote_conn.send('exit'+'\n')         log('Policy has been rewerted') ### результат логируется         remote_conn_pre.close()         time.sleep(2) else:         pass 

Для периодического запуска скрипта, можно использовать стандартную утилиту cron, которая есть в каждой UNIX системе (цикл не чаще чем раз в 2 минуты). Результаты будут записываться в отдельный файл с указанием даты и времени внесения изменений.

При помощи подобной конструкции, возможно менять и другие параметры, а также влиять и на входящий трафик, используя в политиках экспорта BGP community, выделенные взаимодействующим оператором для управления трафиком.

3. Задача: Плановые изменения на сети

Необходимо внедрить в сеть систему автоматического обновления фильтров BGP на внешних стыках. При этом оператор системы должен выбирать самостоятельно какие фильтры обновляются, а какие нет. Взаимодействие с системой должно осуществляться через Web интерфейс. Для минимизации ошибок, в системе должна быть реализована проверка кандидатной конфигурации перед применением.

Решение: Допустим в сети используется оборудование Juniper (Junos). Фильтры строятся на основании регулярного AS_PATH выражения, с учетом AS Origin. В качестве настроек на маршрутизаторе используется as-path-group в составе policy-statement применяемого в политиках импорта BGP. Соответственно систем должна раз в сутки при наличии изменений в БД Radb, производить обновление фильтров AS_Path на внешних стыках сети. В качестве реализации может быть использована следующая система взаимоувязанных скриптов:

Система строится с использованием нескольких программных файлов:

  1. Главная web страница (html).
  2. Модуль добавления данных AS-SET и маршрутизатора в БД (Python).
  3. Модуль хранения списка данных (Python).
  4. Модуль просмотра БД с формой для удаления позиций (Python).
  5. Модуль удаления данных из списка данных (Python).
  6. Модуль работы с БД RADB.
  7. Модуль работы с оборудованием сети.

Поскольку на наблюдается некий тренд наделения программного обеспечения персонализацией, назовем нашу систему Fibber.

Рассмотрим каждый блок в отдельности:

Главная web страница (html)

Главная web страница служит для занесения as-set и ip адреса маршрутизатора, а также для работы с другими модулями. Ниже представлена одна из простейших реализаций, на базе HTML разметки:

Код HTML

<meta charset="koi8-r"> <form name=f1  method="get"> <div id="parentId">  <div>  <p><b>IP Loopback:</b><br> <!-- Поле для ввода IP маршрутизатора  с проверкой ввода пользователя (работает не во всех браузерах)--> <nobr><input name="name1"  required pattern="\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}" type="text" style="width:300px;" placeholder="Enter ip Loopback" />  <p><b>AS-SET name:</b><br> <!-- Поле для ввода AS-SET, строгое соответствие с записью в RADB --> <input name="url1"  required type="text" style="width:300px;" placeholder="Enter AS-SET name" />   </div> </div> <!-- Кнопка для запуска скрипта добавления данных в БД --> <input type="submit" value="добавить в очередь" onclick="f1.action='/cgi-bin/add_to_db_info.py'" style="background-color:#26c809; color:#fdfafa;"/> </form> <br>  <!-- ссылки для просмотра БД и редактирования, журнал аварий --> <p><b>Список функциональных ссылок:</b><br> <p>Здесь Вы можете посмотреть маршрутизаторы и AS-SETs участвующие в автообновлении по AS-PATH:<br> <a href="/cgi-bin/bd_host_and_aspath_print.py">Конфигурационный файл фильтров по AS-PATH</a> <br> <br> <p>История обновлений находится здесь:<br> <a href="/sdn/error_log.html">Журнал обновлений</a> <br><br><br>  <a> Обновления проводит <b>Fibber</b> - многофункциональный Бот для частичной автоматизации управления сетью </a>  
Модуль добавления данных AS-SET и маршрутизатора в БД (путь /cgi-bin/add_to_db_info.py)

Для упрощения работы системы, не используются специализированные системы управления БД. База данных представляет собой набор текстовых файлов и изменяемый список python.

Итак, после нажатия кнопки «добавить в очередь» на главной странице, система обращается к скрипту Python, посредством которого происходит добавления в список введенных значений. Ниже подробно представлена конструкция Скрипта (пояснения в коде):

Код Python

#!/usr/local/bin/python3.3 # -*- coding: koi8-r -*- import paramiko import time import datetime import sys import re import socket  ### Модули для работы с Веб-серверами import cgi import cgitb cgitb.enable() ### позволяет выводить ошибки выполнения программы в Веб окружение  from bd_host_and_aspath import all_data ### собственный модуль, представляющий собой функцию, содержащую список ip оборудования и AS-SET. Задача данного модуля # добавить данные в список модуля bd_host_and_aspath  form = cgi.FieldStorage() ### конструкция для записи данных из веб формы ip1 = form.getfirst('name1', 'EMPTY') ### присваивается значение переменной, при отсутствии записывается значение 'EMPTY' as_path1= form.getfirst('url1', 'EMPTY')  host_and_aspath=[ip1, as_path1] ### из данных веб формы формируется список data_check=all_data() ### переменной присваивается существующий список значений, внесенных в предыдущих итерациях data_check.append(host_and_aspath) ### формирование одного списка из двух, путем добавления одно к другому. add_data=open('/usr/local/www/apache22/cgi-bin/bd_host_and_aspath.py', 'w') ### Файл с данными открывается для полной перезаписи и записывается полностью новые данные с обновленным списком add_data.write("""#!/usr/local/bin/python3.3 # -*- coding: koi8-r -*-  def all_data():         all_data="""+str(data_check)+"""         return all_data""") add_data.close()  print("Content-type:text/html\r\n\r\n") ### в браузер отправляется уведомление о занесении данных в БД  print ('For router Lo '+str(ip1)+' AS-SET '+str(as_path1)+' added to queue, configuration will be update at night.<br>') print ('Please check <a href="/cgi-bin/bd_host_and_aspath_print.py">Config File</a> if needed, or return to  <a href="/sdn">start filter update page</a>') 
Модуль хранения списка данных (путь /cgi-bin/bd_host_and_aspath.py)

Модуль представляет собой список данных о всех маршрутизаторах и As-set участвующих в автообновлении, заключённый в функцию для возможности использования в виде библиотеки Python. Данный файл полностью перезаписывается при добавлении и удалении данных, это можно увидеть, исходя из примера выше. Конструкция ниже:

Код Python

#!/usr/local/bin/python3.3 # -*- coding: koi8-r -*- def all_data():         all_data=[['10.10.10.1', 'AS-TEST1'], ['10.10.10.10.2', 'AS-TEST2']]         return all_data 

Модуль просмотра БД с возможностью удаления позиций (путь /cgi-bin/bd_host_and_aspath_print.py)

Данный модуль служит для вывода списка ip маршрутизаторов и названия изменяемых as-set. А также содержит HTML форму для удаления элементов списка. Конструкция ниже (пояснения после #):

Код Python

#!/usr/local/bin/python3.3 # -*- coding: koi8-r -*- import cgi import bd_host_and_aspath ### Импортируется модуль со списком данных о маршрутизаторах и as-set data=bd_host_and_aspath.all_data() ### присваивается значение переменной  print ("Content-type:text/html\r\n\r\n") ### форма для вывода данных в Web print ('<a href="/sdn/filters1.html">Back to main page</a><br><br>') ### ссылка для возврата в главное меню print ('Num.-- [ROUTER LOOPBACK, AS-SET]', end='<br><br>') for k in data: ### печать элементов списка в заданной форме.         print(str(data.index(k))+'--') ### печать номера элемента списка         print(k, end='<br>')  print ('<form action="/cgi-bin/remove_data_from_db.py" method="get">') ### форма для удаления элемента списка, при вводе номера элемента и нажатия кнопки remove запускается скрипт. print ('<p><b>------REMOVE DATA--------<br>') print ('<p><b>Enter Number of position for removing data:</b><br><nobr><input name="remove1"  required pattern="^[0-9]+$" type="text" style="width:300px;" placeholder="Enter Number" />') print ('<p><input type="submit" value="Remove" /></form><br>') 
Модуль удаления данных из списка данных (Python /cgi-bin/remove_data_from_db.py)

Модуль работает аналогично модулю добавления данных, с той разницей что производится не добавление, а удаление данных из списка. Конструкция ниже:

Код Python

#!/usr/local/bin/python3.3 # -*- coding: koi8-r -*- import cgi from bd_host_and_aspath import all_data import cgitb cgitb.enable() form = cgi.FieldStorage() num_of_el = int(form.getfirst('remove1', 'EMPTY'))  data_check=all_data() data_check.pop(num_of_el) add_data=open('/usr/local/www/apache22/cgi-bin/bd_host_and_aspath.py', 'w') add_data.write("""#!/usr/local/bin/python3.3 # -*- coding: koi8-r -*- def all_data():         all_data="""+str(data_check)+"""         return all_data""") add_data.close()  print("Content-type:text/html\r\n\r\n")  print ('data has been removed for Prefix filters.<br>') print ('Please check <a href="/cgi-bin/bd_host_and_aspath_print.py">Config File</a> if needed, or return to  <a href="/sdn/filters1.html">start filter update page</a>') 
Модуль работы с БД RADB (Python /usr/SCRIPTS_FOR_PYTHON/radb_v3.py)

Модуль производит получение списка AS BGP из БД RADB для AS-SET, сравнение перечня AS BGP с перечнем, полученным за предыдущий период. Формирование списков AS для изменения конфигурации оборудования. Конструкция ниже (пояснения после #):

Код Python

#!/usr/local/bin/python3.3 ## -*- coding: koi8-r -*- import time import datetime import sys import re import os import socket import check_OS import subprocess import bd_host_and_aspath  ### Функция работы с RADB работает по следующему алгоритму: # 1 Создается временный файл для записей перечня AS в AS-SET # 2 При помощи сторонней утилиты whois находится перечень AS в AS-SET с обращением к БД RADB # 3 Открывается существующий файл с перечнем AS из предыдущих обращений RADB, если такого файла нет (обращение к RADB впервые), создается файл existing и candidate, в candidate записываются только что полученные данные. #  При наличии файла candidate он будет использован модулем работы с оборудованием для обновления конфигурации на маршрутизаторе. # 4 Далее происходит проверка на условие по перечню AS из только что полученного вывода и файла existing. Если файлы не равны или полученный вывод больше 3, то происходит # формирование файла candidate, который будет использован оборудованием для конфигурации. Если условия не выполняются, то происходит попытка удаления файла candidate def asset(host, asset):        radb=open('/usr/SCRIPTS_FOR_PYTHON/data_from_radb/temp/'+asset+'.txt', 'w') # создается файл для записи        subprocess.call(['/usr/bin/whois -h whois.radb.net -p 43 \!i'+asset+',1/n/'], bufsize=0, shell=True, stdout=(radb)) # вызывает внешнюю программу whois        radb.close()         f=open('/usr/SCRIPTS_FOR_PYTHON/data_from_radb/temp/'+asset+'.txt', 'r')         data=f.read()         f.close()         filter_data_pre_temp=str(re.findall('AS[\d]+', data))         try:                 with open('/usr/SCRIPTS_FOR_PYTHON/data_from_radb/existing/'+host+','+asset+'.txt', 'r'): pass         except IOError:                 f=open('/usr/SCRIPTS_FOR_PYTHON/data_from_radb/existing/'+host+','+asset+'.txt', 'w')                 f.close()                 s=open('/usr/SCRIPTS_FOR_PYTHON/data_from_radb/candidate/'+host+','+asset+'.txt', 'w') ## название файла состоит из ip и as-set, данные из названия будут использованы для конфигурации                 s.write(str(filter_data_pre_temp))                 s.close()         f_ext=open('/usr/SCRIPTS_FOR_PYTHON/data_from_radb/existing/'+host+','+asset+'.txt', 'r')         data_ext=f_ext.read()         f_ext.close()         filter_data_pre_ext=str(re.findall('AS[\d]+', data_ext))         if filter_data_pre_ext>filter_data_pre_temp or filter_data_pre_ext<filter_data_pre_temp and len(filter_data_pre_temp)>3:                 update=open('/usr/SCRIPTS_FOR_PYTHON/data_from_radb/existing/'+host+','+asset+'.txt', 'w')                 update.write(filter_data_pre_temp)                 update.close()                 update=open('/usr/SCRIPTS_FOR_PYTHON/data_from_radb/candidate/'+host+','+asset+'.txt', 'w')                 update.write(filter_data_pre_temp)                 update.close()         elif filter_data_pre_ext==filter_data_pre_temp or len(filter_data_pre_temp)<3:                 as_path='not_updated'                 try:                         os.remove('/usr/SCRIPTS_FOR_PYTHON/data_from_radb/candidate/'+host+','+asset+'.txt')                         pass                 except IOError:                         pass         else:                 pass  host_and_asset=bd_host_and_aspath.all_data()  for i in range(len(host_and_asset)): # Происходит выполнение функции asset для всех элементов списка сформированной БД по ip маршрутизатора и AS-SET.         asset(str(host_and_asset[i][0]), str(host_and_asset[i][1])) 
Модуль работы с оборудованием сети (путь /usr/SCRIPTS_FOR_PYTHON/ssh_stend_v5.py)

Модуль работы с оборудованием служит для изменения конфигурации, исходя из сформированных данных для конфигурации в разделе candidate. В процессе выполнения модуль производит не только конфигурацию, но и ряд проверок для максимального уменьшения вероятности ошибок в конфигурации. Конструкция модуля представлена ниже (пояснения в коде после #):

Код Python

#!/usr/local/bin/python3.3 ## -*- coding: koi8-r -*- ### Импортируем необходимы библиотеки import paramiko import time import datetime import sys import re import os import socket import subprocess import random import bd_host_and_aspath import base64  user = 'Fibber' secret = base64.b64decode(b'cGFzc3dvcmQ=').decode('ascii') port = 22  #### Описание вспомогательных функций def chunks_in_filter(data_from_radb, n): # возвращает список с подсписком из n подэлементов, необходима для структурирования as-path-group         return [data_from_radb[i:i + n] for i in range(0, len(data_from_radb), n)]  def asset(host, asset):  # функция возвращает список с вложенными списками из элементов AS_PATH по 15 AS, файла расположенного в candidate,         try:                 f=open('/usr/SCRIPTS_FOR_PYTHON/data_from_radb/candidate/'+host+','+asset+'.txt', 'r')                 pass                 data=f.read()                 f.close()                 filter_data_pre=str(re.findall('AS[\d]+', data))                 filter_data=re.findall('[\d]+', filter_data_pre)                 as_path_pre='|'.join(filter_data)                 as_path=chunks_in_filter(filter_data, 15)         except IOError:                 as_path='ERROR'         return as_path   ### функция для логирования результата, логирование происходит в два файла, общий - куда возможно добавить информацию для разработчика и пользовательский # куда записывается результат работы системы def log(logout, message):         log_out=open('/usr/SCRIPTS_FOR_PYTHON/python_log.txt', 'a')         log_out.write(str(datetime.datetime.now()) +'!!!'+str(message)+'<----------------------------'+'\n'+':'+ str(logout) + '\n')         log_out.close()         log_out1=open('/usr/SCRIPTS_FOR_PYTHON/error_log.html', 'a')         log_out1.write(str(datetime.datetime.now()) + ':'+ str(message) + '\n')         log_out1.close()         pass  #вход в конф режим для разных устройств  def enter_conf(device_type): # переменная device_type проверяется модулем device=check_OS.check_OS(check)         if device_type=='IOS_XR' or device_type=='IOS' or device_type=='JUNOS':                 syntax='configure'         else: #device_type=='HUAWEI':                 syntax='system-view'         return syntax   ### основной модуль конфигурации и проверки на ошибки def config(host, user, pas, por, asset1, number_as_set='500'):         def while_not_end(): ### применяется после ввода команды в конфигурационном режиме, для ожидания  окончания  применения команды  и записи результата                 buff = b''                 try:                         while not buff.endswith(b'# '): # Следует отметить, что для разных устройств конструкция buff.endswith будет разная, необходимо это учитывать при работе с оборудованием разных производителей                                 resp = remote_conn.recv(2048)                                 buff += resp                 except socket.timeout: ### результат логируется, в запись добавляется HTML разметка с цветовой раскраской, для удобства чтения лог файла                         log(' ', '->'+host+'->'+asset1+'-> <font color="red" >ERROR:</font> Device did not respond, please check candidate conf and do rollback if needed<br>')                         buff=b'no data'                 return buff         as_path=asset(host, asset1) ### формируются элементы ас-пас по функции выше (списки в списке по 15 штук)         if as_path=='ERROR': ### проверяется на ошибки ас-пас (на случай отсутствия в папке candidate)                 log(' ', '->'+host+'->'+asset1+'->EROROR: Fibber did not find data in local base module asset in ssh_stend<br>')         else:                 pass         remote_conn_pre = paramiko.SSHClient() ### используется библиотека paramiko для удаленного соединения с маршрутизатором         remote_conn_pre.set_missing_host_key_policy(paramiko.AutoAddPolicy())         try: ### если не соединения нет, результат логируется                 remote_conn_pre.connect(hostname=host, username=user, password=pas, port=por, timeout=90) ### устанавливается соединение                 pass         except:                 log(' ', '->'+host+'->'+asset1+'-> WARNING: Device is unreachable')         remote_conn = remote_conn_pre.invoke_shell() ### постоянное присутствие в течении сессии         remote_conn.settimeout(5*60)         remote_conn.send ('\n')         time.sleep(2) ### пауза для подключения к оборудованию         check=str(remote_conn.recv(2048)) ### присваивание переменной данных с CLI, далее будет использоваться функция while_not_end() для отсутствия необходимости выставлять таймеры вручную (там где возможно)         #device=str(check_OS(check)) ### проверяем тип устройства, строка закоментирована, так как в нашем случае сеть состоит из оборудования Juniper, рассмотрение данной функция производится не будет в рамках данной статьи         device='JUNOS' # Устройство задается принудительно без проверки.         remote_conn.send(enter_conf(device)+'\n'+'\n') ### вход в конфигурационный режим, посредством функции, описанной выше         check_edit=str(while_not_end()) ### присваивание вывода CLI переменной для дальнейшей проверки         if device=='JUNOS':                 if re.findall('Users currently editing the configuration|The configuration has been changed but not committed', check_edit) == []: ### проверка на наличие пользователей изменяющих                 # конфигурацию и наличие кандидатной конфигурации                         remote_conn.send('run set cli screen-length 10000' +'\n') ### Установка количества строк экрана, для возможности вывода большого количества строк, так где конструкции no-more не применима                         while_not_end()                         remote_conn.send('show  policy-options  as-path-group  ?') ### Проверка предварительно сконфигуренного AS-PATH, системa вносит изменения только если первоначальные настройки                         #  предварительно внесены сетевым специалистом                         time.sleep(1)                         remote_conn.send('as-test'+'\n')                         check=while_not_end()                         check=str(check)                         check_find=str(re.findall(asset1, check))                         if check_find=='['"""'"""+asset1+"""'"""']': ### если AS-PATH-GROUP предварительно сконфигурирована, происходит перезапись конфигурации в соответствии с данными из RADB                                 remote_conn.send('delete policy-options as-path-group '+str(asset1)+'\n')                                 while_not_end()                                 asset_out=str(asset1)                                 for z in as_path: ### производится построчный ввод каждого вложенного списка с добавлением необходимого синтаксиса                                         remote_conn.sendall('set  policy-options  as-path-group  '+asset_out+'  as-path  '+asset_out+'-'+str(as_path.index(z))+'  ".*('+'|'.join(z)+')$"'+'\n')                                         while_not_end()                                         ### Для исключения ошибки происходит ряд проверок, для обеспечения изменения нужной конфигурации                                         # Записывается новое значение as-path-gruop для сравнения автономных систем из кандидатоной конфигурацией с значением полученным из RADB                                 remote_conn.send('show policy-options as-path-group '+asset1+' | no-more'+'\n')                                 check_candidate_conf=str(while_not_end())                                 comp=re.compile('no-more') # происходит разделение вывода CLI для выделения нужно части для проверки                                 split_out=comp.split(check_candidate_conf)                                 try:                                         split_one_index_pre=split_out[1]                                         pass                                 except:                                         split_one_index_pre=['no data']                                 split_one_index_pre2=re.findall(r'\((.*?)\)', str(split_one_index_pre)) ### поиск автономных систем в выводе в два этапа                                 split_one_index=re.findall('[\d]+', str(split_one_index_pre2))### второй этап                                 as_path_find_as=re.findall('[\d]+', str(as_path)) ### поиск автономных систем в полученной информации из RADB                                 check_candidate_set=set(split_one_index) ### преобразование в множества, для повышения скорости сравнения значений из CLI и RADB                                 as_path_find_as_set=set(as_path_find_as)                                 remote_conn.send('sh | compare | no-more' + '\n') ### просмотр кандидатной конфигурации на маршрутизаторе, для  проверки факта изменения только текущего as-path-group                                 check_candidate_rollback=str(while_not_end())                                 split_rollback=comp.split(check_candidate_rollback) #происходит разделение вывода CLI для выделения нужно части для проверки                                 try:                                         split_rollback_one_index=split_rollback[1]                                         pass                                 except:                                         split_rollback_one_index=['no data']                                 check_rollback_headers=re.findall(r'\[([^]]*)\]', split_rollback_one_index) ### поиск измененных разделов конфигурации                                 time.sleep(1)                                 ### проверка на ряд условий для логирования результата, проверяются следующие условия:                                 # - проверка на условие изменения только нужного раздела конфигурации                                 # - проверка на условие наличия изменений в as-path-group                                 # - проверка на условие совпадения AS BGP в Кандидатной конфигурации (только что записанной в устройство) и данных из RADB                                 if re.findall('edit (?!policy)|edit policy-options [^a]|edit policy-options as-path[^-]|edit policy-options as-path-group (?!'+asset1+')', str(check_rollback_headers))==[]:                                         pass                                 else:                                         log(check_candidate_rollback, '->'+host+'->'+asset1+'-> <font color="gold" >WARNING:</font> Fibber decided do not commit, reason have another changes candidate conf which Fibber did not make<br>')                                 if re.findall('edit policy-options as-path-group '+asset1, str(check_rollback_headers))!=[]:                                         pass                                 else:                                         log(check_candidate_rollback, '->'+host+'->'+asset1+'-> <font color="gold" >WARNING:</font> Fibber did not see config to commit, reason as-set have no changes on device<br>')                                 if check_candidate_set==as_path_find_as_set:                                         pass                                 else:                                         log(check_candidate_rollback, '->'+host+'->'+asset1+'-> <font color="gold" >WARNING:</font> Fibber decided do not commit, reason candidate as_path and radb as_path is different<br>')                                 ### проверка на ряд тех же условий, для принятия решения о применении конфигурации.                                 if check_candidate_set==as_path_find_as_set and re.findall('edit (?!policy)|edit policy-options [^a]|edit policy-options as-path[^-]|edit policy-options as-path-group (?!'+asset1+')', str(check_rollback_headers))==[] and re.findall('edit policy-options as-path-group '+asset1, str(check_rollback_headers))!=[]:                                         remote_conn.send('commit'+'\n'+'\n') ### если все условия выполняются, кандидатная конфигурация применяется                                         while_not_end()                                         time.sleep(10)                                         remote_conn.send('exit'+'\n'+'\n')                                         time.sleep(1)                                         remote_conn.send('exit'+'\n'+'\n')                                         log(' ', '->'+host+'->'+asset1+'-> <font color="green" >SCCESSFUL:</font>: Filter has been updated via Fibber<br>')                                 else: ### если условия не выполняются, конфигурация откатывается                                         time.sleep(1)                                         remote_conn.send('rollback 0'+'\n')                                         while_not_end()                                         remote_conn.send('exit'+'\n')                                         time.sleep(1)                                         remote_conn.send('exit'+'\n')                         else: ### если не находится предварительно сконфигуренный AS-PATH-GROUP, конфигурация не изменяется                                 remote_conn.send('sh | compare roll 0 | no-more'+'\n')                                 while_not_end()                                 remote_conn.send('exit'+'\n')                                 time.sleep(1)                                 remote_conn.send('exit'+'\n')                                 log(' ', '->'+ host+'->'+asset1+'-> <font color="red" >ERROR:</font> Fibber did not find configured AS-SET on device, please configure AS-SET first via your hand<br>')                 else: # если другой пользователь находится в конфигурационном режиме, либо конфигурация изменена, но не применена, конфигурация не изменяется                         remote_conn.send('sh | compare roll 0 | no-more'+'\n')                         while_not_end()                         remote_conn.send('exit'+'\n')                         time.sleep(1)                         remote_conn.send('exit'+'\n')                         log(' ', '->'+host+'->'+asset1+'-> <font color="red" >ERROR:</font> Fibber detected, someone in edit mode or configuration has been changed but not commited<br>')         else: ### если устройство не Juniper конфигурация не изменяется, в данном разделе возможно поменять else на elif и описать код для другого устройства.                 log(' ', '->'+host+'->'+asset1+'-> <font color="red" >ERROR:</font> Fibber detected - device is not a JUNIPER<br>')                 pass         remote_conn_pre.close()         time.sleep(2)         pass  ### Для запуска выше описанных функций, необходимо сформировать перечь исходных данных, исходные данные берутся из директории candidate сформированную модулем radb_v3.py host_and_asset = os.listdir("/usr/SCRIPTS_FOR_PYTHON/data_from_radb/candidate") # формируется список из  файлов директории, в названии которых есть данные о ip маршрутизатора и AS-SET  for i in range(len(host_and_asset)): # исключаются расширения .txt у списка файлов         host_and_asset[i]=(host_and_asset[i][0:len(host_and_asset[i])-4])  for k in range(len(host_and_asset)): # Элементы списка преобразуются во вложенные списки из двух элементов ip маршрутизатора и названия as-set         host_and_asset[k]=(host_and_asset[k].split(","))  ### Для элементов сформированного списка происходит поочередное выполнение функции host_and_asset for i in range(len(host_and_asset)):         config(str(host_and_asset[i][0]), str(user), str(secret), int(port), str(host_and_asset[i][1])) 

Работу системы можно отслеживать через Веб, посредством файла error_log.html, пример вывода файла ниже:

 LOG FILE 2015-06-15 13:48:57.828503:->10.10.10.1->as-test-> WARNING: Fibber did not see config to commit, reason as-set have no changes on device 2015-06-15 13:51:52.512611:->10.10.10.12->as-test-> ERROR: Fibber detected, someone in edit mode or configuration has been changed but not commited 2015-06-15 14:54:06.267404:->10.10.10.13->as-test-> WARNING: Fibber did not see config to commit, reason as-set have no changes on device 2015-06-15 14:55:26.954442:->10.10.10.17->as-test1-> SCCESSFUL: Filter has been updated via Fibber 2015-06-21 11:47:45.127530:->10.10.10.17->as-test-> SCCESSFUL: Filter has been updated via Fibber 2015-06-21 11:48:41.204475:->10.10.10.1->as-test-> SCCESSFUL: Filter has been updated via Fibber 2015-06-21 11:49:09.487539:->10.10.10.1->as-test3-> SCCESSFUL: Filter has been updated via Fibber 2015-06-21 11:50:14.816424:->10.10.10.1->as-test-> SCCESSFUL: Filter has been updated via Fibber 2015-06-21 11:50:41.835588:->10.10.10.5->as-test5-> SCCESSFUL: Filter has been updated via Fibber 2015-06-21 11:51:09.127567:->10.10.10.1->as-test-> SCCESSFUL: Filter has been updated via Fibber 2015-06-21 11:52:10.606458:->10.10.10.1->as-test-> SCCESSFUL: Filter has been updated via Fibber 2015-06-22 00:00:04.385457:->10.10.10.1->as-test-> ERROR: Fibber detected, someone in edit mode or configuration has been changed but not commited 2015-06-22 00:00:13.438379:->10.10.10.1->as-test1-> ERROR: Device did not respond, please check candidate conf and do rollback if needed 2015-07-26 19:43:06.316584:->10.10.10.13->as-test7-> SCCESSFUL: Filter has been updated via Fibber 2015-07-26 19:44:36.849450:->10.10.10.1->as-test-> WARNING: Fibber did not see config to commit, reason as-set have no changes on device 

Важным аспектом работы системы, является расписание выполнения ее частей, предлагается следующее расписание:
— Модуль добавления данных AS-SET и маршрутизатора в БД (Python) – изменения желательно производить в промежутке с 8.00 до 18.00
— Модуль удаления данных из списка данных (Python) – изменения желательно производить в промежутке с 8.00 до 18.00
— Модуль работы с БД RADB – 18.00
— Модуль работы с оборудованием сети – 00.00

Следует также отметить, что для больших сетей, следует увеличивать количество единовременно выполняемых обновлений, размножив модуль работы с оборудованием. При этом каждый модуль должен обслуживать набор маршрутизаторов, разделённых определенными признаками (например, производитель, географическое расположение, временные зоны и т.д.). Также систему можно расширить, дополнив различными другим функциями для работы через Веб, например, двумя предыдущими примерами, обновлением по префиксам и т.д.

В качестве инструмента периодического выполнения, как было написано выше, возможно использовать встроенную утилиту в unix подобные системы Crontab, пример настроек:

 [lost@servertest SCRIPTS_FOR_PYTHON ]# crontab -l 0     0       *       *       *               /usr/local/bin/python3.3 /usr/SCRIPTS_FOR_PYTHON/ssh_stend_v5.py >> /usr/SCRIPTS_FOR_PYTHON/cron_python.log 0     17       *       *       *                 /usr/local/bin/python3.3 /usr/SCRIPTS_FOR_PYTHON/radb_v3.py >> /usr/SCRIPTS_FOR_PYTHON/cron_python.log  

Итого, в данной статье были рассмотрены три примера возможного использования Python в автоматизации выполнения сетевых сценариев.

В качестве дальнейших шагов написания более сложного ПО для автоматизации и управления сетью, необходимо в первую очередь структурировать типовые обращения к сетевым устройствам, для этого очень желательно проработать отдельные библиотеки и далее использовать при реализации конкретных задач. Это в свою очередь приведет к определённой иерархии, упростит понимание кода на своем уровне и в конечном итоге оптимизирует затраченное время. Также с позволения разработчиков, можно использовать уже готовые библиотеки, например, одну из самых популярных Netmiko.

Решение поддерживает несколько производителей сетевого оборудования и в своей структуре также использует библиотеку Paramiko. Возможно также стоит посмотреть в сторону протокола сетевого управления устройствами NetConf, который в свою очередь, как минимум должен помочь избавиться от необходимости подстраиваться под изменяющийся синтаксис командной строки сетевого оборудования.

Лит-ра:

blog.dzinko.org/2011/03/python.html — Подробное описание регулярных выражений с примерами.
stackoverflow.com — Сайт вопрос ответ для программистов. Скорее всего ответ на не разрешаемую проблему там есть.
habrahabr.ru/post/115436 — подробное описание регулярных выражений.
pythonworld.ru — много доступной понятной информации по python с примерами.
www.radb.net
github.com/paramiko/paramiko — библиотека Paramiko.
github.com/ktbyers/netmiko — готовая библиотека Python для работы с сетевыми устройствами.

ссылка на оригинал статьи http://habrahabr.ru/post/263859/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *