Этот пост представляет собой введение и сравнение инструментов автоматизации сети Paramiko, Netmiko, NAPALM, Ansible и Nornir. Прежде чем мы разберем инструменты, я считаю необходимым обсудить, как работают различные инструменты автоматизации и в чем их различия.
Я ни в коей мере не являюсь экспертом ни в одном из следующих инструментов, но я постараюсь описать их в непрофессиональных терминах, основываясь на своем опыте.
Paramiko: Просто удобная библиотека Python SSH, используемая для ssh-ing (очевидно) к устройствам.
Netmiko: Еще одна библиотека Python SSH, основанная на Paramiko, но больше ориентированная на сетевые устройства. В отличие от Paramiko, она поддерживает Telnet. В сочетании со сценариями Python, этого инструмента достаточно, чтобы начать автоматизацию сети. Очень легко приступить к работе и увидеть результаты печати ваших команд, а также отсканировать нужные вам вещи с помощью regex или даже лучше, если вы используете шаблоны TextFSM и NTC для разбора результатов. Одна небольшая оговорка – если вы собираетесь использовать Netmiko для крупномасштабной сети, вы должны сами управлять многопоточностью.
NAPALM: Библиотека/фреймворк для Python, поддерживающая множество поставщиков с помощью API. Это абстракция, и сетевые драйверы, лежащие в основе NAPALM, позволяют ему возвращать одинаковый результат на ваш запрос (например: get_interfaces, get_facts), независимо от того, с каким типом устройства вы работаете. Если ваши устройства поддерживаются не полностью, у вас есть возможность написать свою собственную библиотеку Python (что я пробовал и не рекомендую; я подробно расскажу об этом позже в этом посте). Фреймворк NAPALM можно использовать вместе с Ansible, Salt и Nornir.
Ansible: Один из самых известных инструментов автоматизации. Особенность Ansible в том, что вы должны писать игровые книги Ansible на YAML, что является обоюдоострым мечом; это не требует от вас знания кода на Python, но работа только с YAML лишает гибкости ваши задачи автоматизации. Ansible, на мой взгляд, лучше подходит для общей среды, а не для сетевых устройств.
Nornir: Python-фреймворк, созданный специально для сетевых устройств, разработанный и поддерживаемый теми же людьми, которые сделали NAPALM и Netmiko. Это 100% Python, фреймворк написан на Python, и вы пишете код на Python для того, чтобы использовать Nornir. Библиотеки NAPALM и Netmiko могут выступать в качестве геттеров и драйверов соединений для Nornir. Кроме того, Nornir является многопоточным и работает абсурдно быстро по сравнению с Ansible.
Есть и другие инструменты автоматизации, которые я не упомянул, например, Scrapli, Puppet, Chef, Salt, pyATS и так далее.
Инструменты автоматизации тестирования
В этом разделе я собираюсь показать базовое использование некоторых инструментов: Netmiko, NAPALM и Nornir. Для подключения по SSH сначала включите SSH на каждом устройстве:
Juniper Junos: 20.4R3.8:
set system login user juniper class super-user authentication plain-text-password
set system services ssh root-login allow
set system services ssh protocol-version v2
set system services ssh connection-limit 10
Huawei VRP: 8.180:
aaa
local-user huawei password irreversible-cipher $1c$jF...
local-user huawei service-type telnet ssh
local-user huawei state block fail-times 3 interval 5
user-interface vty 0 4
authentication-mode aaa
user privilege level 3
idle-timeout 0 0
stelnet server enable
ssh user huawei
ssh user huawei authentication-type all
ssh user huawei service-type all
ssh authorization-type default password
Cisco IOS-XR: 6.1.3:
crypto key generate rsa
ssh server v2
line console transport input all
Приведенные выше команды обеспечат полное подключение по SSH извне виртуальной машины (ВМ), которую я использую. И моя виртуальная машина EVE-NG, и моя виртуальная машина Linux (Kali Linux) настроены как NAT, и я буду запускать свою автоматизацию на Kali Linux. Поскольку мой NAT использует подсеть 192.168.13.0/24, я изменил адреса интерфейсов между маршрутизаторами на 10.0.XY.X, вместо пула 192.168.XY.X. Все три устройства подключены к узлу Management(Cloud0), что обеспечивает их связь с моей Kali Linux (Рисунок 1).
Netmiko
Здесь приведен очень простой пример использования Netmiko ConnectHandler для получения вывода команд show с маршрутизаторов. В этом примере метод send_command() вернет описание интерфейса и IP-адреса каждого устройства. Если вы хотите преобразовать результат в структурированные данные, вам может понадобиться использование шаблонов TextFSM & NTC.
#netmiko_test.py
from netmiko import ConnectHandler
juniper_vMX = {
'device_type': 'juniper',
'ip': '192.168.13.11',
'username': 'juniper',
'password': 'Juniper'
}
net_connect = ConnectHandler(**juniper_vMX)
output = net_connect.send_command("show interface terse")
print("Juniper IP:\n\n"+output+"\n---------------------------------------\n")
huawei_vrp = {
'device_type': 'huawei',
'ip': '192.168.13.22',
'username': 'huawei',
'password': 'Admin@1231'
}
net_connect = ConnectHandler(**huawei_vrp)
output = net_connect.send_command("display ip int br")
print("Huawei IP:\n\n"+output+"\n---------------------------------------\n")
ios_xr = {
'device_type': 'cisco_xr',
'ip': '192.168.13.33',
'username': 'cisco',
'password': 'cisco'
}
net_connect = ConnectHandler(**ios_xr)
output = net_connect.send_command("show ip int br")
print("Cisco IP:\n\n"+output+"\n---------------------------------------\n")
NAPALM
Мой план состоял в том, чтобы получить информацию о соседях по протоколу Border Gateway Protocol (BGP) с помощью NAPALM. NAPALM – очень простой инструмент, если поддерживаются все типы устройств. Однако в нашем сценарии это не так.
Маршрутизаторы Huawei официально не поддерживаются NAPALM; существует библиотека сообщества NAPALM (NAPALM-Huawei-VRP), но в нее встроен лишь минимум функций. Единственным выходом для меня было написание собственных функций на основе версии сообщества. Пока я занимался своими исследованиями, я обнаружил, что не только я пытался это сделать. Майкл Альварес проделал большую работу, но в его коде все еще не хватало некоторых частей. Используйте следующий фрагмент кода для реализации BGP-соседа платформы Huawei VRP. В моем репозитории GitHub есть полный код, так что вам не придется изобретать велосипед.
@staticmethod
def bgp_time_conversion(bgp_uptime):
"""
Convert string time to seconds.
Examples
00:14:23
00:13:40
00:00:21
00:00:13
00:00:49
1d11h
1d17h
1w0d
8w5d
1y28w
never
"""
bgp_uptime = bgp_uptime.strip()
uptime_letters = set(['w', 'h', 'd', 'm'])
if 'never' in bgp_uptime:
return -1
elif ':' in bgp_uptime:
times = bgp_uptime.split(":")
times = [int(x) for x in times]
hours, minutes, seconds = times
return (hours * 3600) + (minutes * 60) + seconds
# Check if any letters 'w', 'h', 'd' are in the time string
elif uptime_letters & set(bgp_uptime):
form0 = r'(\d+)h(\d+)m' # 03h21m
form1 = r'(\d+)d(\d+)h' # 1d17h
form2 = r'(\d+)w(\d+)d' # 8w5d
form3 = r'(\d+)y(\d+)w' # 1y28w
match = re.search(form0, bgp_uptime)
if match:
hours = int(match.group(1))
minutes = int(match.group(2))
return (hours * 3600) + (minutes * 60)
match = re.search(form1, bgp_uptime)
if match:
days = int(match.group(1))
hours = int(match.group(2))
return (days * DAY_SECONDS) + (hours * 3600)
match = re.search(form2, bgp_uptime)
if match:
weeks = int(match.group(1))
days = int(match.group(2))
return (weeks * WEEK_SECONDS) + (days * DAY_SECONDS)
match = re.search(form3, bgp_uptime)
if match:
years = int(match.group(1))
weeks = int(match.group(2))
return (years * YEAR_SECONDS) + (weeks * WEEK_SECONDS)
raise ValueError("Unexpected value for BGP uptime string: {}".format(bgp_uptime))
## custom bgp config for VRP, reference:https://codingnetworks.blog/napalm-network-automation-python-collect-data-from-multiple-vendors/
def get_bgp_neighbors(self):
"""
Returns a dictionary of dictionaries. The keys for the first dictionary will be the vrf
(global if no vrf). The inner dictionary will contain the following data for each vrf:
* router_id
* peers - another dictionary of dictionaries. Outer keys are the IPs of the neighbors. \
The inner keys are:
* local_as (int)
* remote_as (int)
* remote_id - peer router id
* is_up (True/False)
* is_enabled (True/False)
* description (string)
* uptime (int in seconds)
* address_family (dictionary) - A dictionary of address families available for the \
neighbor. So far it can be 'ipv4' or 'ipv6'
* received_prefixes (int)
* accepted_prefixes (int)
* sent_prefixes (int)
Note, if is_up is False and uptime has a positive value then this indicates the
uptime of the last active BGP session.
Example::
{
"global": {
"router_id": "10.0.1.1",
"peers": {
"10.0.0.2": {
"local_as": 65000,
"remote_as": 65000,
"remote_id": "10.0.1.2",
"is_up": True,
"is_enabled": True,
"description": "internal-2",
"uptime": 4838400,
"address_family": {
"ipv4": {
"sent_prefixes": 637213,
"accepted_prefixes": 3142,
"received_prefixes": 3142
},
"ipv6": {
"sent_prefixes": 36714,
"accepted_prefixes": 148,
"received_prefixes": 148
}
}
}
}
}
}
"""
afi_supported = {
"Ipv6 Unicast" : "ipv6 unicast",
"Ipv4 Unicast" : "ipv4 unicast",
"Vpnv4 All" : "vpnv4 unicast",
"Vpnv6 All" : "vpnv6 unicast"
}
bgp_neighbors = {}
command_bgp = "display bgp all summary"
output = self.device.send_command(command_bgp)
if output == "":
return bgp_neighbors
ASN_REGEX = r"[\d\.]+"
IP_ADDR_REGEX = r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"
IPV4_ADDR_REGEX = IP_ADDR_REGEX
IPV6_ADDR_REGEX_1 = r"::"
IPV6_ADDR_REGEX_2 = r"[0-9a-fA-F:]{1,39}::[0-9a-fA-F:]{1,39}"
IPV6_ADDR_REGEX_3 = r"[0-9a-fA-F]{1,3}:[0-9a-fA-F]{1,3}:[0-9a-fA-F]{1,3}:[0-9a-fA-F]{1,3}:" \
r"[0-9a-fA-F]{1,3}:[0-9a-fA-F]{1,3}:[0-9a-fA-F]{1,3}:[0-9a-fA-F]{1,3}"
# Should validate IPv6 address using an IP address library after matching with this regex
IPV6_ADDR_REGEX = r"(?:{}|{}|{})".format(IPV6_ADDR_REGEX_1, IPV6_ADDR_REGEX_2, IPV6_ADDR_REGEX_3)
IPV4_OR_IPV6_REGEX = r"(?:{}|{})".format(IPV4_ADDR_REGEX, IPV6_ADDR_REGEX)
#Regular Expressions
re_separator = r"\n\s*(?=Address Family:\s*\w+)"
re_vpn_instance_separator = r"\n\s*(?=VPN-Instance\s+[-_a-zA-Z0-9]+)"
re_address_family = r"Address Family:(?P<address_family>\w+\s\w+)"
re_global_router_id = r"BGP local router ID :\s+(?P<glob_router_id>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})"
re_global_local_as = r"Local AS number :\s+(?P<local_as>{})".format(ASN_REGEX)
re_vrf_router_id = r"VPN-Instance\s+(?P<vrf>[-_a-zA-Z0-9]+), [rR]outer ID\s+" \
r"(?P<vrf_router_id>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})"
re_peers = r"(?P<peer_ip>({})|({}))\s+" \
r"(?P<as>{})\s+\d+\s+\d+\s+\d+\s+(?P<updown_time>[a-zA-Z0-9:]+)\s+" \
r"(?P<state>[a-zA-Z0-9\(\)]+)\s+(?P<received_prefixes>\d+)\s+(?P<adv_prefixes>\d+)".format(
IPV4_ADDR_REGEX, IPV6_ADDR_REGEX, ASN_REGEX)
re_remote_rid = r"Remote router ID\s+(?P<remote_rid>{})".format(IPV4_ADDR_REGEX)
re_peer_description = r"Peer's description:\s+\"(?P<peer_description>.*)\""
re_accepted_routes = r"Received active routes total:\s+(?P<accepted_routes>.*)"
#Separation of AFIs
afi_list = re.split(re_separator, output, flags=re.M)
#return afi_list
bgp_global_router_id = ""
bgp_global_local_as = ""
for afi in afi_list:
match_afi = re.search(re_global_router_id,afi, flags=re.M)
match_local_as = re.search(re_global_local_as,afi, flags=re.M)
if match_afi is not None:
bgp_global_router_id = match_afi.group('glob_router_id')
bgp_global_local_as = match_local_as.group('local_as')
match_afi = re.search(re_address_family, afi, flags=re.M)
if match_afi is not None and any((s in match_afi.group('address_family') for s in ['Ipv4 Unicast','Ipv6 Unicast'])):
bgp_neighbors.update({"global": {"router_id": bgp_global_router_id, "peers" : {}}})
for peer in afi.splitlines():
match_peer = re.search(re_peers, peer, flags=re.M)
if match_peer:
peer_bgp_command = ""
if "Ipv6" in match_afi.group('address_family'):
peer_bgp_command = "display bgp ipv6 peer {} verbose".format(match_peer.group('peer_ip'))
else:
peer_bgp_command = "display bgp peer {} verbose".format(match_peer.group('peer_ip'))
peer_detail = self.device.send_command(peer_bgp_command)
match_remote_rid = re.search(re_remote_rid, peer_detail, flags=re.M)
match_peer_description = re.search(re_peer_description, peer_detail, flags=re.M)
match_accepted_routes = re.search(re_accepted_routes,peer_detail, flags=re.M )
bgp_neighbors["global"]["peers"].update( {
match_peer.group('peer_ip'): {
"local_as": int(bgp_global_local_as),
"remote_as": int(match_peer.group('as')),
"remote_id": "" if match_remote_rid is None else match_remote_rid.group('remote_rid'),
"is_up": True if "Established" in match_peer.group('state') else False,
"is_enabled": False if "Admin" in match_peer.group('state') else True,
"description": "" if match_peer_description is None else match_peer_description.group('peer_description'),
"uptime": int(self.bgp_time_conversion(match_peer.group('updown_time'))),
"address_family": {
afi_supported[match_afi.group('address_family')]: {
"received_prefixes": int(match_peer.group('received_prefixes')),
#"accepted_prefixes": "Unknown",
"accepted_prefixes": int(match_accepted_routes.group('accepted_routes')),
"sent_prefixes": int(match_peer.group('adv_prefixes'))
}
}
}
})
elif match_afi is not None and any((s in match_afi.group('address_family') for s in ['Vpnv4 All','Vpnv6 All'])):
if bgp_neighbors['global'] is False:
bgp_neighbors.update({"global": {"router_id": bgp_global_router_id, "peers" : {}}})
#Separation of VPNs
vpn_instance_list = re.split(re_vpn_instance_separator, afi, flags=re.M)
for vpn_peers in vpn_instance_list:
if "VPN-Instance " not in vpn_peers:
for peer in vpn_peers.splitlines():
match_peer = re.search(re_peers, peer, flags=re.M)
if match_peer:
if bgp_neighbors["global"]["peers"][match_peer.group('peer_ip')]:
bgp_neighbors["global"]["peers"][match_peer.group('peer_ip')]["address_family"].update(
{
afi_supported[match_afi.group('address_family')]: {
"received_prefixes": int(match_peer.group('received_prefixes')),
"accepted_prefixes": "Unknown",
"sent_prefixes": int(match_peer.group('adv_prefixes'))
}
}
)
else:
peer_bgp_command = ""
if "Ipv6" in match_afi.group('address_family'):
peer_bgp_command = "display bgp ipv6 peer {} verbose".format(match_peer.group('peer_ip'))
else:
peer_bgp_command = "display bgp peer {} verbose".format(match_peer.group('peer_ip'))
peer_detail = self.device.send_command(peer_bgp_command)
match_remote_rid = re.search(re_remote_rid, peer_detail, flags=re.M)
match_peer_description = re.search(re_peer_description, peer_detail, flags=re.M)
bgp_neighbors["global"]["peers"].update( {
match_peer.group('peer_ip'): {
"local_as": int(bgp_global_local_as),
"remote_as": int(match_peer.group('as')),
"remote_id": "" if match_remote_rid is None else match_remote_rid.group('remote_rid'),
"is_up": True if "Established" in match_peer.group('state') else False,
"is_enabled": False if "Admin" in match_peer.group('state') else True,
"description": "" if match_peer_description is None else match_peer_description.group('peer_description'),
"uptime": int(self.bgp_time_conversion(match_peer.group('updown_time'))),
"address_family": {
afi_supported[match_afi.group('address_family')]: {
"received_prefixes": int(match_peer.group('received_prefixes')),
"accepted_prefixes": "Unknown",
"sent_prefixes": int(match_peer.group('adv_prefixes'))
}
}
}
})
else:
match_vrf_router_id = re.search(re_vrf_router_id, vpn_peers, flags=re.M)
if match_vrf_router_id is None:
msg = "No Match Found"
raise ValueError(msg)
peer_vpn_instance = match_vrf_router_id.group('vrf')
peer_router_id = match_vrf_router_id.group('vrf_router_id')
bgp_neighbors.update({peer_vpn_instance: {
"router_id": peer_router_id, "peers" : {}}})
for peer in vpn_peers.splitlines():
match_peer = re.search(re_peers, peer, flags=re.M)
if match_peer:
peer_bgp_command = ""
afi_vrf = ""
if "Ipv6" in match_afi.group('address_family'):
peer_bgp_command = "display bgp ipv6 peer {} verbose".format(match_peer.group('peer_ip'))
afi_vrf = "ipv6 unicast"
else:
peer_bgp_command = "display bgp peer {} verbose".format(match_peer.group('peer_ip'))
afi_vrf = "ipv4 unicast"
peer_detail = self.device.send_command(peer_bgp_command)
match_remote_rid = re.search(re_remote_rid, peer_detail, flags=re.M)
match_peer_description = re.search(re_peer_description, peer_detail, flags=re.M)
bgp_neighbors[peer_vpn_instance]["peers"].update( {
match_peer.group('peer_ip'): {
"local_as": int(bgp_global_local_as),
"remote_as": int(match_peer.group('as')),
"remote_id": "" if match_remote_rid is None else match_remote_rid.group('remote_rid'),
"is_up": True if "Established" in match_peer.group('state') else False,
"is_enabled": False if "Admin" in match_peer.group('state') else True,
"description": "" if match_peer_description is None else match_peer_description.group('peer_description'),
"uptime": int(self.bgp_time_conversion(match_peer.group('updown_time'))),
"address_family": {
afi_vrf: {
"received_prefixes": int(match_peer.group('received_prefixes')),
"accepted_prefixes": "Unknown",
"sent_prefixes": int(match_peer.group('adv_prefixes'))
}
}
}
})
return bgp_neighbors
Теперь библиотека NAPALM наконец-то готова к работе, давайте попробуем!
#napalm_test.py
import napalm
def main():
driver_juniper = napalm.get_network_driver("junos")
driver_vrp = napalm.get_network_driver("huawei_vrp")
driver_ios = napalm.get_network_driver("iosxr")
juniper_router = driver_juniper(
hostname = "192.168.13.11",
username = "juniper",
password = "Juniper"
)
vrp_router = driver_vrp(
hostname = "192.168.13.22",
username = "huawei",
password = "Admin@1231"
)
ios_router = driver_ios(
hostname = "192.168.13.33",
username = "cisco",
password = "cisco"
)
print("Connecting to Juniper Router...")
juniper_router.open()
print("Checking Juniper Router BGP Neighbors:")
print(juniper_router.get_bgp_neighbors())
juniper_router.close()
print("Test Completed\n")
print("Connecting to Huawei Router...")
vrp_router.open()
print("Checking Huawei Router BGP Neighbors:")
print(vrp_router.get_bgp_neighbors())
vrp_router.close()
print("Test Completed\n")
print("Connecting to IOS Router...")
ios_router.open()
print("Checking IOS Router BGP Neighbors:")
print(ios_router.get_bgp_neighbors())
ios_router.close()
print("Test Completed\n")
if __name__ == "__main__":
main()
В отличие от Netmiko, NAPALM генерирует структурированные результаты для каждого маршрутизатора. Результат BGP-соседа возвращается в виде словаря словарей, как показано ниже.
Juniper:
{
"global": {
"router_id": "1.1.1.1",
"peers": {
"10.0.12.2": {
"local_as": 123,
"remote_as": 123,
"remote_id": "2.2.2.2",
"is_up": True,
"is_enabled": True,
"description": "",
"uptime": 211073,
"address_family": {
"ipv4": {
"received_prefixes": 1,
"accepted_prefixes": 1,
"sent_prefixes": 1,
},
"ipv6": {
"received_prefixes": -1,
"accepted_prefixes": -1,
"sent_prefixes": -1,
},
},
},
"10.0.13.3": {
"local_as": 123,
"remote_as": 123,
"remote_id": "3.3.3.3",
"is_up": True,
"is_enabled": True,
"description": "",
"uptime": 211079,
"address_family": {
"ipv4": {
"received_prefixes": 1,
"accepted_prefixes": 1,
"sent_prefixes": 1,
},
"ipv6": {
"received_prefixes": -1,
"accepted_prefixes": -1,
"sent_prefixes": -1,
},
},
},
},
}
}
Huawei:
{
"global": {
"router_id": "2.2.2.2",
"peers": {
"10.0.12.1": {
"local_as": 123,
"remote_as": 123,
"remote_id": "1.1.1.1",
"is_up": True,
"is_enabled": True,
"description": "",
"uptime": 211080,
"address_family": {
"ipv4 unicast": {
"received_prefixes": 1,
"accepted_prefixes": 1,
"sent_prefixes": 1,
}
},
},
"10.0.23.3": {
"local_as": 123,
"remote_as": 123,
"remote_id": "3.3.3.3",
"is_up": True,
"is_enabled": True,
"description": "",
"uptime": 211080,
"address_family": {
"ipv4 unicast": {
"received_prefixes": 1,
"accepted_prefixes": 1,
"sent_prefixes": 1,
}
},
},
},
}
}
Cisco:
{
"global": {
"peers": {
"10.0.13.1": {
"local_as": 123,
"remote_as": 123,
"remote_id": "1.1.1.1",
"description": "",
"is_enabled": False,
"is_up": True,
"uptime": 211044,
"address_family": {
"ipv4": {
"received_prefixes": 1,
"accepted_prefixes": 1,
"sent_prefixes": 1,
}
},
},
"10.0.23.2": {
"local_as": 123,
"remote_as": 123,
"remote_id": "2.2.2.2",
"description": "",
"is_enabled": False,
"is_up": True,
"uptime": 211062,
"address_family": {
"ipv4": {
"received_prefixes": 1,
"accepted_prefixes": 1,
"sent_prefixes": 1,
}
},
},
},
"router_id": "3.3.3.3",
}
}
С помощью вложенных словарей вы можете легко получить доступ к элементам, используя синтаксис [ ]. Эта строка кода возвращает время работы BGP для peer 10.0.12.2 в секундах:
print (juniper_router.get_bgp_neighbors()['global']['peers']['10.0.12.2']['uptime'])
> 211073
Nornir
Для инициализации Nornir мы будем использовать плагин SimpleInventory, который хранит все необходимые данные в трех файлах (hosts.yaml, groups.yaml и defaults.yaml).
hosts.yaml:
---
router1:
hostname: 192.168.13.11
username: juniper
password: Juniper
groups:
- juniper
router2:
hostname: 192.168.13.22
username: huawei
password: Admin@1231
groups:
- huawei
router2`:
hostname: 192.168.13.22
username: huawei
password: Admin@1231
groups:
- huawei_vrpv8
router3:
hostname: 192.168.13.33
username: cisco
password: cisco
groups:
- cisco
groups.yaml:
---
cisco:
platform: ios-xr
huawei:
platform: huawei_vrp
huawei_vrpv8:
platform: huawei_vrpv8
juniper:
platform: junos
defaults.yaml:
---
username: juniper
password: Juniper
Нам нужен файл config.yaml, чтобы сообщить Nornir, что у нас есть готовые файлы инвентаризации для Nornir. В этом файле также можно изменить параметр многопоточности.
#config.yaml
---
inventory:
plugin: SimpleInventory
options:
host_file: "hosts.yaml"
group_file: "groups.yaml"
defaults_file: "defaults.yaml"
runner:
plugin: threaded
options:
num_workers: 100
Теперь мы можем создать объект Nornir:
from nornir import InitNornir
nr = InitNornir(config_file="config.yaml")
Как уже упоминалось, Nornir поддерживает сторонние плагины, такие как Netmiko, Scrapli, NAPALM, Ansible, Jinja2, Netbox и так далее. Давайте посмотрим, как мы можем использовать Netmiko и NAPALM в Nornir.
Nornir с плагином Netmiko
Прежде всего, мы попробуем использовать плагин Netmiko. Давайте сделаем его более интересным с использованием функции фильтра Nornir для выбора определенной группы маршрутизаторов в нашем инвентаре и отображения информации о BGP-соседях на этом маршрутизаторе с помощью Netmiko. Для получения командных результатов требуется импортировать плагин и выполнить всего одну строку кода:
#nornir_netmiko_test.py
from nornir import InitNornir
from nornir_netmiko import netmiko_send_command
from nornir_utils.plugins.functions import print_result
from nornir.core.filter import F
nr = InitNornir(config_file="config.yaml")
group1 = nr.filter(F(groups__contains="huawei_vrpv8"))
results = group1.run(netmiko_send_command, command_string='dis bgp peer')
print_result(results)
Вот результат, полученный с помощью приведенного выше кода:
Nornir с плагином NAPALM
Теперь давайте попробуем использовать плагин NAPALM для Nornir. На этот раз я использую функцию ~F для фильтрации маршрутизаторов, которые не являются ‘huawei_vrpv8’. Затем запустите геттеры NAPALM для получения информации о соседях BGP:
#nornir_napalm_test.py
from nornir import InitNornir
from nornir_napalm.plugins.tasks import napalm_get
from nornir_utils.plugins.functions import print_result
from nornir.core.filter import F
nr = InitNornir(config_file="config.yaml", dry_run=True)
group2 = nr.filter(~F(groups__contains="huawei_vrpv8"))
results = group2.run(task=napalm_get, getters=["bgp_neighbors"])
print_result(results)
Опять же, плагин NAPALM способен возвращать структурированные данные для более легкого манипулирования данными.
Баг? Вы заметили, что у меня были дубликаты в файлах hosts.yaml и groups.yaml? Это потому, что Netmiko и NAPALM используют разные названия платформ для маршрутизаторов Huawei VRP, и потребовалось небольшое обходное решение для устранения несовместимости.
Эта заметка лишь поверхностно описывает возможности Nornir и других инструментов автоматизации, поэтому…
Какой инструмент автоматизации следует использовать?
Никто не может сказать вам, какой инструмент автоматизации лучше; все зависит от сценария. Отличным способом начать изучение автоматизации является Python в сочетании с Netmiko. Создание лаборатории на GNS3 или EVE-NG и написание нескольких сценариев со временем повысит вашу уверенность.
Хотя Netmiko и NAPALM отлично подходят для лабораторной среды, в реальных условиях вам может понадобиться учесть масштабируемость и производительность. Я считаю, что Ansible и Nornir более эффективны в реальных производственных сетях. И, конечно, вы можете создать свой собственный сценарий многопроцессорной/многопоточной обработки и с помощью инструментов автоматизации более низкого уровня, но это усложняет код больше, чем нужно.
Лично я в настоящее время выбираю инструмент автоматизации Nornir, поскольку он дает мне возможность писать на Python. Он очень мощный в сочетании с различными сторонними инструментами, и он быстрый, в соответствии с этой задачей по скорости.
Кевин Джин – старший сетевой инженер и менеджер по решениям в China Mobile International. Его специализация – IP-сети и проектирование глобальных магистралей. Он также является сторонником автоматизации сетей и NetDevOps.
Это сообщение было адаптировано и переведено из оригинала в блоге Кевина Джина.