[Из песочницы] Автоматизация 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 …
Одной из возможных реализаций, может послужить следующий код (комментарии написаны непосредственно в коде после #):
#!/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 на стыке.
Одной из возможных реализаций, может послужить следующий код (все комментарии написаны непосредственно в коде после #):
#!/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 на внешних стыках сети. В качестве реализации может быть использована следующая система взаимоувязанных скриптов:
Система строится с использованием нескольких программных файлов:
- Главная web страница (html).
- Модуль добавления данных AS-SET и маршрутизатора в БД (Python).
- Модуль хранения списка данных (Python).
- Модуль просмотра БД с формой для удаления позиций (Python).
- Модуль удаления данных из списка данных (Python).
- Модуль работы с БД RADB.
- Модуль работы с оборудованием сети.
Поскольку на наблюдается некий тренд наделения программного обеспечения персонализацией, назовем нашу систему Fibber.
Рассмотрим каждый блок в отдельности:
Главная web страница (html)
Главная web страница служит для занесения as-set и ip адреса маршрутизатора, а также для работы с другими модулями. Ниже представлена одна из простейших реализаций, на базе HTML разметки:
Список функциональных ссылок:
Здесь Вы можете посмотреть маршрутизаторы и AS-SETs участвующие в автообновлении по AS-PATH:
Конфигурационный файл фильтров по AS-PATH
История обновлений находится здесь:
Журнал обновлений
Обновления проводит Fibber - многофункциональный Бот для частичной автоматизации управления сетью
Модуль добавления данных AS-SET и маршрутизатора в БД (путь /cgi-bin/add_to_db_info.py)
Для упрощения работы системы, не используются специализированные системы управления БД. База данных представляет собой набор текстовых файлов и изменяемый список 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. Данный файл полностью перезаписывается при добавлении и удалении данных, это можно увидеть, исходя из примера выше. Конструкция ниже:
#!/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 форму для удаления элементов списка. Конструкция ниже (пояснения после #):
#!/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 ('
')
Модуль удаления данных из списка данных (Python /cgi-bin/remove_data_from_db.py)
Модуль работает аналогично модулю добавления данных, с той разницей что производится не добавление, а удаление данных из списка. Конструкция ниже:
#!/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 для изменения конфигурации оборудования. Конструкция ниже (пояснения после #):
#!/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. В процессе выполнения модуль производит не только конфигурацию, но и ряд проверок для максимального уменьшения вероятности ошибок в конфигурации. Конструкция модуля представлена ниже (пояснения в коде после #):
#!/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