Ansible с костыльком может автоматизировать сеть и non-CloudEngine коммутаторов Huawei, как недавно было доказано на нашем Enterprise форуме. Однако в сети, в которой работают разные модели коммутаторов, Ansible не представляется эффективным инструментом на данный момент. И несмотря на бесспорное улучшение качество кода Python для Telnet, данный скрипт также не подходил по ряду причин.
Я хотел найти простое решение, которое могло бы работать в гибридной сети с разными моделями коммутаторов Huawei: устанавливать соединение через SSH и решать задачи по конфигурированию из реального мира. При этом кодом мог бы оперировать такой же, как и я не разбирающийся в программировании человек: менять и адаптировать скрипт под свои задачи, используя открытые источники. На помощь пришел Netmiko.
В качестве тестового стенда я собрал топологию в eNSP из четырех закольцованных коммутаторов Huawei, имитирующих уровень агрегации с STP. Задачу поставил - пробросить через это кольцо несколько дополнительных VLANs, добавив их в транк на портах, при этом вывести команду верификации до начала наката конфигурации и после нее, чтобы убедиться, что ничего не сломалось. Топология получилась следующая:
В качестве платформы для автоматизации использовал Ubuntu 20.04.2 для Windows, Python 3.8.5 и главного игрока - Netmiko 3.4.0.
Netmiko - это мультивендорная библиотека, которая базируется на библиотеке Paramiko SSH и упрощает соединение с сетевыми устройствами. Paramiko тоже позволяет устанавливать защищенные соединения с устройствами, но ее использование принято считать более сложным, я же пытался выполнить задачу самым коротким путем.
Netmiko - это открытая библиотека, все детали о ней (в т. ч. руководство и примеры скриптов) доступны на GitHub.
Так как Netmiko мультивендорная библиотека, ей нужно знать для подключения к какому именно устройству она будет использоваться и выбрать для него соответствующий класс. Функция, которая сделает это, называется ConnectHandler. Сначала импортировал ее:
from netmiko import ConnectHandler
Функция ConnectHandler просматривает значение переменной ‘device_type’. Поддерживаемые Netmiko типы устройств можно посмотреть в ssh_dispatcher.py в разделе CLASS_MAPPER_BASE.
Интересовавший меня тип устройства так и назывался: huawei.
Все остальные переменные были известны, а именно ip сетевого устройства, имя пользователя, пароль. Поэтому можно было создать словарь Python, назвав его согласно сетевого имени CE_1_BORDER - главного коммутатора кольца, как указано в топологии, и определить в нем значения всех переменных:
from netmiko import ConnectHandler
CE_1_BORDER = {
'device_type': 'huawei',
'ip': '7.7.7.1',
'username': 'vasyo1',
'password': '@ghjcnjnF358986'
}
На этом этапе скрипт должен был способен установить SSH-соединение с устройством. Осталось вызвать функцию ConnectHandler и передать ей в распоряжение словарь CE_1_BORDER с параметрами устройства, к которому она будет подключаться:
ssh_connect = ConnectHandler(**CE_1_BORDER)
Или (альтернативный путь) обойтись без словаря:
ssh_connect = ConnectHandler(device_type='huawei', ip='7.7.7.1', username='vasyo1', password='@ghjcnjnF358986')
Для меня со словарем скрипт выглядит более структурированным и понятным.
Теперь можно было отправить команду по SSH-соединению и получить обратно выходные данные. Здесь я использовал метод .send_command() для отправки одной команды 'display stp brief' и функцию print() для вывода на экран полученных данных. Для отправки нескольких команд нужно использовать другой метод.
output = ssh_connect.send_command('display stp brief')
print(output)
Самый простой способ добавить дополнительные устройства - это определить их в виде словарей, а после перечислить в виде списка Python:
from netmiko import ConnectHandler
CE_1_BORDER = {
'device_type': 'huawei',
'ip': '7.7.7.1',
'username': 'vasyo1',
'password': '@ghjcnjnF358986'
}
CE_2 = {
'device_type': 'huawei',
'ip': '7.7.7.2',
'username': 'vasyo1',
'password': '@ghjcnjnF358986'
}
CE_3 = {
'device_type': 'huawei',
'ip': '7.7.7.3',
'username': 'vasyo1',
'password': '@ghjcnjnF358986'
}
CE_4 = {
'device_type': 'huawei',
'ip': '7.7.7.4',
'username': 'vasyo1',
'password': '@ghjcnjnF358986'
}
all_devices = [CE_1_BORDER, CE_2, CE_3, CE_4]
После я применил цикл for, который будет повторять для всех этих устройств одну и ту же операцию: SSH-подключение к устройству, выполнение команды 'display stp brief', и отображение вывода.
for device in all_devices:
ssh_connect = ConnectHandler(**device)
output = ssh_connect.send_command('display stp brief')
Если запустить скрипт в таком виде, то вывод будет отображен в сплошном виде и сложно будет понять, где конец вывода первого коммутатора и начало второго. Лучшим решением было бы в начале каждого вывода указать имя коммутатора, но я не сообразил как это сделать пока, и вместо этого указал ip коммутатора, используя функцию print(f).
for device in all_devices:
ssh_connect = ConnectHandler(**device)
output = ssh_connect.send_command('display stp brief')
print(f"\n\n-------------- Device {device['ip']} --------------")
print(output)
print("-------------------- End -------------------")
В фигурных скобках указал переменную, значение которой хотел подставить. В данном случае, переменная ‘device’ равна переменной ‘all_devices’, которая, в свою очередь, содержит список словарей, и так как функция print(f) находится в цикле for (смещение на 4 пробела), то для каждого вывода будет подставляться по порядку значение переменной ‘ip’.
\n\n - создаст две пустые строчки между Device и End. Если сделать одну \n, то будет одна пустая строчка.
Запуск команды ‘display stp brief’ нужен только для того, чтобы зафиксировать состояние сети до внесения изменений. Вместо нее, понятно, можно использовать любую другую команду.
После этого я определил список операций, которые мне нужно было выполнить. Это:
Создать VLANs 300 и 301 и присвоить им имена (description).
Прописать созданные VLANs на порты (добавить в транк).
Применить конфигурацию (commit).
Прежде чем добавлять команды в скрипт, я убедился, что они работают “в ручном режиме”:
#
vlan 300
description NETMIKO_VLAN 300
#
vlan 301
description NETMIKO_VLAN 301
#
int range GE 1/0/9 GE 1/0/10
port trunk allow-pass vlan 300 301
#
commit
#
Для создания нескольких VLANs я использовал цикл for, а для применения списка команд (а не одной команды) метод .send_config_set():
for device in all_devices:
ssh_connect = ConnectHandler(**device)
output = ssh_connect.send_command('display stp brief')
print(f"\n\n-------------- Device {device['ip']} --------------")
print(output)
for n in range (300,302):
print ("Creating VLAN " + str(n))
config_commands = [
'vlan ' + str(n),
'desc NETMIKO_VLAN ' + str(n),
'Commit'
]
output = ssh_connect.send_config_set(config_commands)
output = ssh_connect.send_config_set(
[
'interface range GE 1/0/9 GE 1/0/10',
'port trunk allow-pass vlan 300 301',
'commit'
]
)
print(output)
print("-------------------- End -------------------")
В таком виде скрипт должен был сначала показать вывод команды 'display stp brief', потом применить конфигурационные команды и завершить процесс. Однако, я бы хотел после конфигурирования снова увидеть вывод команды 'display stp brief', чтобы убедиться, что сеть не поломалась. Красивее всего было бы прописать такое короткое правило, которое в конце запускало тот же участок скрипта снова, но я пока не разобрался как это сделать, поэтому примитивно вставил кусок кода с 'display stp brief' в конец скрипта:
for device in all_devices:
ssh_connect = ConnectHandler(**device)
output = ssh_connect.send_command('display stp brief')
print(f"\n\n-------------- Device {device['ip']} --------------")
print(output)
print("-------------------- End -------------------")
Итоговый код получился таким:
from netmiko import ConnectHandler
CE_1_BORDER = {
'device_type': 'huawei',
'ip': '7.7.7.1',
'username': 'vasyo1',
'password': '@ghjcnjnF358986'
}
CE_2 = {
'device_type': 'huawei',
'ip': '7.7.7.2',
'username': 'vasyo1',
'password': '@ghjcnjnF358986'
}
CE_3 = {
'device_type': 'huawei',
'ip': '7.7.7.3',
'username': 'vasyo1',
'password': '@ghjcnjnF358986'
}
CE_4 = {
'device_type': 'huawei',
'ip': '7.7.7.4',
'username': 'vasyo1',
'password': '@ghjcnjnF358986'
}
all_devices = [CE_1_BORDER, CE_2, CE_3, CE_4]
for device in all_devices:
ssh_connect = ConnectHandler(**device)
output = ssh_connect.send_command('display stp brief')
print(f"\n\n-------------- Device {device['ip']} --------------")
print(output)
for n in range (300,302):
print ("Creating VLAN " + str(n))
config_commands = [
'vlan ' + str(n),
'desc NETMIKO_VLAN ' + str(n),
'Commit'
]
output = ssh_connect.send_config_set(config_commands)
output = ssh_connect.send_config_set(
[
'interface range GE 1/0/9 GE 1/0/10',
'port trunk allow-pass vlan 300 301',
'commit'
]
)
print(output)
print("-------------------- End -------------------")
for device in all_devices:
ssh_connect = ConnectHandler(**device)
output = ssh_connect.send_command('display stp brief')
print(f"\n\n-------------- Device {device['ip']} --------------")
print(output)
print("-------------------- End -------------------")
Для запуска скрипта потребовалось установить модуль netmiko.
Я применил команду:
pip3 install -U netmiko
Создал файл с именем netmiko10.py с помощью редактора nano:
nano netmiko10.py
И запустил скрипт:
python3 netmiko10.py
Последовал вывод (привожу пример вывода только CE_4, иначе слишком длинно будет):
Как видно, скрипт повел себя именно так, как было задано: вначале показал разделительную линию с указанием ip-адреса коммутатора, затем вывод команды 'display stp brief', которая дала возможность убедиться, что порт GE1/0/10 находится в заблокированном (discarding) состоянии, затем идет создание двух VLANs и добавление их в транк на указанные порты и, наконец, применение конфигурации командой ‘commit’. Команда ‘return’ применяется автоматически библиотекой netmiko в соответствии с заданным типом устройства (huawei): она возвращает пользовательское представление из режима конфигурирования. Разделительная линия со словом End указывает, что цикл для заданного устройства завершился.
Однако нельзя было не обратить внимание, что создалось только два VLANs: 300 и 301, хотя в функции range (300,302) указано с 300 по 302. Дело в том, что функция range () состоит из двух настраиваемых параметров:
range(stop), где stop - это количество целых чисел для генерации, начиная с нуля, например, range(3) == [0, 1, 2].
range([start], stop[, step]), где start - первое число в последовательности, stop - число до которого генерируется значение, не включая его, и step - это разница между каждым числом в последовательности.
Стало быть функция range (300,302) значила начать генерацию последовательности чисел с 300 и закончить 302, не включая его.
Когда цикл для всех устройств завершился, то последовал запуск только команды 'display stp brief' для всех устройств и вывод в таком виде:
По выводу Device 7.7.7.4 было видно, что порт GE1/0/10 остался в том же состоянии discarding, как и до наката конфигурации, а значит, все прошло успешно.
Какие улучшения хотелось бы сделать:
Применить inventory файл, в котором перечислить ip-адреса всех сетевых устройств, а не создавать словарь для каждого из них.
Короткая команда для повторного запуска кода ‘display stp brief’, вместо прописывания всего кода снова.
Применить одни команды для одних сетевых устройств, а другие команды для других сетевых устройств. В реальном мире номера портов на коммутаторах, скорей всего, будут отличаться, поэтому применить один и тот же конфиг на все устройства не получится.
Но эти улучшения уже для следующего поста!
Ресурсы:
https://pynet.twb-tech.com/blog/automation/netmiko.html
https://pyneng.readthedocs.io/en/latest/book/18_ssh_telnet/netmiko.html
https://github.com/ktbyers/netmiko
https://github.com/ktbyers/netmiko/blob/master/netmiko/ssh_dispatcher.py
Udemy.com - Python Network Programming for Network Engineers (Python 3) (David Bombal)
https://www.pythoncentral.io/pythons-range-function-explained/