[Из песочницы] Автоматизация 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

IP Loopback:

AS-SET name:


Список функциональных ссылок:

Здесь Вы можете посмотреть маршрутизаторы и AS-SETs участвующие в автообновлении по AS-PATH:
Конфигурационный файл фильтров по AS-PATH

История обновлений находится здесь:
Журнал обновлений


Обновления проводит Fibber - многофункциональный Бот для частичной автоматизации управления сетью

Модуль добавления данных 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.
') print ('Please check Config File if needed, or return to start filter update page')

Модуль хранения списка данных (путь /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 ('Back to main page

') ### ссылка для возврата в главное меню print ('Num.-- [ROUTER LOOPBACK, AS-SET]', end='

') for k in data: ### печать элементов списка в заданной форме. print(str(data.index(k))+'--') ### печать номера элемента списка print(k, end='
') print ('
') ### форма для удаления элемента списка, при вводе номера элемента и нажатия кнопки remove запускается скрипт. print ('

------REMOVE DATA--------
') print ('

Enter Number of position for removing data:
') print ('


')

Модуль удаления данных из списка данных (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.
') print ('Please check Config File if needed, or return to start filter update page')

Модуль работы с БД 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_ext3:
                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+'-> ERROR: Device did not respond, please check candidate conf and do rollback if needed
') 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
') 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) remo

© Habrahabr.ru