...

Инструменты автоматизации: Paramiko, Netmiko, NAPALM, Ansible, Nornir

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

topo
Рисунок 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 netmiko
Рисунок 2 – Результаты работы команды.

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 способен возвращать структурированные данные для более легкого манипулирования данными.

nornir napalm
Рисунок 3 – Результат: Плагин 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.

Это сообщение было адаптировано и переведено из оригинала в блоге Кевина Джина.

Ответить