Эта статья подойдет сетевым специалистам, которые находятся в поисках примеров возможной автоматизации 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 для работы с сетевыми устройствами.

Комментарии (9)


  1. ls1
    30.07.2015 17:50
    +1

    github.com/ktbyers/netmiko — готовая библиотека Python для работы с сетевыми устройствами.

    О, спасибо. Надо будет ею попробовать в алкатель потыкать


  1. Dehu4ka
    30.07.2015 18:53

    Вливайтесь nocproject.org


  1. domix32
    30.07.2015 19:02
    +5

    ## -*- coding: koi8-r -*-

    Но почему?


    1. lampslave
      03.08.2015 12:23

      Закомментировано же :)


      1. domix32
        03.08.2015 13:15

        А вот тут?

        add_data.write("""#!/usr/local/bin/python3.3
        # -*- coding: koi8-r -*-
        def all_data():
                all_data="""+str(data_check)+"""
                return all_data""")
        


  1. evg_krsk
    30.07.2015 20:08

    Спасибо за статью, но опять костыли :-). OSS/BSS для такого нужны, нет? Как платформа, на которой можно строить бизнес-логику. Тот же NOC Project.


  1. bosha
    01.08.2015 12:32

    Зачем SSH, когда можно по SNMP?


    1. Lost63 Автор
      03.08.2015 20:08

      Ответ Juniper, Can I provision or configure a device using SNMP on Junos OS? No, provisioning or configuring a device using SNMP is not allowed on Junos OS. www.juniper.net/documentation/en_US/junos15.1/topics/reference/general/snmp-junos-faq.html.
      Тем не менее во втором примере, для получения значений утилизации, используется SNMP


      1. bosha
        03.08.2015 22:20

        Тогда понятно. :)