Визуализация сетевых топологий, или зачем еще сетевому инженеру Python #2

Привет, Хабр! Эта статья написана по мотивам решения задания на недавно прошедшем онлайн-марафоне DevNet от Cisco. Участникам предлагалось автоматизировать анализ и визуализацию произвольной сетевой топологии и, опционально, происходящих в ней изменений.

Задача является не самой тривиальной, и в блогосфере встречается довольно мало статей на эту тему. Ниже представляю разбор собственной реализации, а также описание используемых инструментов и подходов.

Всем заинтересовавшимся добро пожаловать по кат!

5hzip10y5vl8l71jpwwfmqp-xes.png



*И немного Javascript.


Дисклеймер


Данная статья не претендует на всеобщий охват вариантов и проблематики визуализации сетей, но при описании частного случая руководствуется и общими соображениями.
Все нижесказанное и вышесказанное является личным оценочным суждением автора, если не указано обратное.
Приведенный код распространяется под лицензией MIT и не дает гарантий никакого рода.

Обсуждение и конструктивная критика всячески приветствуются.

Если вы заметили опечатку, пожалуйста, воспользуйтесь комбинацией Ctrl+Enter или ⌘+Enter для ее отправки автору.

В исходном виде задание выглядело следующим образом:


Имеется сеть, состоящая из различных L2/L3 сетевых устройств под управлением IOS/IOSXE. Известен список IP-адресов управления для всех устройств, все устройства доступны по IP, для каждого устройства есть доступ для выполнения show-команд. Для вас доступны любые способы сбора информации, но поверьте, вам вряд ли нужен SNMP. Хотя мы не в праве ограничивать вашу фантазию.

Основная задача: определить физическую топологию соединений устройств по LLDP и визуализировать ее в удобном для восприятия человеком виде (да, нам всем удобны графические схемы). Выбрать формат хранения данных о топологии в удобном для машинного анализа виде (да, машинам неудобны графические схемы).

На рисунке c топологией должны быть отображены:
• Пиктограмма каждого устройства (коммутаторы и маршрутизаторы могут быть отмечены одинаковым типом пиктограммы).
• Hostname устройства.
• Название каждого интерфейса (можно в сокращённом формате, например, вместо GigabitEthernet0/0 — G0/0).

Допускается реализация фильтров, ограничивающих (скрывающих) информацию.
Дополнительная задача: определить изменения в топологии (сравнив текущую и предыдущую версии) и визуализировать их в удобном для восприятия человеком виде.

На входе — IP-адреса и учетные данные для доступа на оборудование, на выходе — готовая топология. Огромное пространство для экспериментов и вариантов где-то посередине.

Для себя дополнительно ввел следующие директивы для выбора инструментов реализации:


  • Функциональность vs простота.
    Должен быть соблюден баланс между функциональностью решения и его простотой. Для прототипа можно задействовать часть готовых свободно распространяемых решений.
  • Наличие опыта работы с наибольшей частью инструментов.
    На реализацию было трое суток, одни из которых у меня выпали по неотложным делам. Чтобы уложиться в сроки и сделать полнофункциональное решение, желательно было выбрать какую-то часть уже знакомых фреймворков.
  • Возможность переиспользования решения.
    Задача вполне применима ко многим продакшнам, стоит это учесть.
  • Мультиплатформенность.
    Как стоит учесть и наличие разных платформ в реальности.
  • Наличие документации к выбранным инструментам в свободном доступе.
    Необходимое требование к любому неодноразовому решению.

Перед изобретением велосипеда всегда стоит поинтересоваться, не решена ли задача до нас. Вот в этой статье на Хабре довольно хорошо расписаны существующие продукты для построения сетевых карт. Ожидаемо, ничего напрямую подходящего, а модули визуализации зачастую идут как составная часть большой системы мониторинга, что значительно уменьшает возможные сценарии переиспользования и кастомизации.

Абстрактную задачу на автоматизированную визуализацию сетевых топологий можно разделить примерно на следующие уровни и этапы:
high_level

Пройдемся по ним снизу вверх с оглядкой на имеющиеся требования:


  1. Сетевое оборудование.
    По условиям нужно реализовать поддержку IOS и IOS-XE.
    Но в реальности может быть зоопарк намного более гетерогенная сеть. Постараемся это учесть.
  2. Источники данных о топологии.
    В задании предлагается использовать протокол LLDP (Link Layer Discovery Protocol), работающий, как следует из названия, на канальном уровне (L2) в модели OSI. Это стандартизированный протокол, описанный в IEEE 802.1AB. Поддерживается большинством производителей сетевого оборудования и системами на Linux и Windows, что нам подходит.
    Потенциально информация о топологии может также быть обогащена информацией из специфических для устройств выводов таблиц маршрутизации, коммутации, протоколов маршрутизации и т.д. Оставим это на будущее.
  3. Протоколы и интерфейсы доступа.
    Наиболее новые устройства и платформы поддерживают красивые и модные NETCONF, REST API, да RESTCONF с YANG моделями и структурами данных. Но наличие легаси диктует необходимость использования SSH, Telnet и стандартного CLI.
  4. Протокол- и вендор-специфичные драйверы/плагины.
    Как и обещает заголовок статьи, основная часть логики будет написана на Python, т.к. он обладает развитой экосистемой фреймворков для работы с сетевым оборудованием, и с ним у меня имеется наибольший опыт.
    Для работы с API устройств может использоваться стандартный модуль requests либо специализированные сторонние модули.
    Для доступа к оборудованию через SSH/Telnet могут быть использованы фреймворки netmiko, scrapli, paramiko. Они позволяют эмулировать CLI из Python — т.е. отправлять на оборувание команды и получать в ответ на них, как правило, текстовый вывод той или иной степени форматированности и предсказуемости.
    Также существует некоторое количество более высокоуровневых сетевых фреймворков, реализующих дополнительные возможности над уже упомянутым инструментарием. К их числу можно отнести NAPALM и Nornir. NAPALM предоставляет вендор-нейтральные GETTER’ы для получения определенных типов данных с оборудования, включая LLDP. Nornir же реализует дополнительные инструменты для удобства и многопоточности из коробки.
    SNMP оставим более традиционные для него задачи мониторинга.
  5. Неструктурированные данные → Инструменты нормализации данных → Структурированные данные.
    Доступ через API обычно позволяет получить уже структурированный вывод, но вот текстовый вывод, получаемый через CLI от сетевых устройств является непригодным для прямой обработки. Для извлечения полезных данных традиционно используется стандартный модуль re и регулярные выражения. Более новым подходом является фреймворк TextFSM от Google с более удобными для использования шаблонами.
    Уже упомянутый выше NAPALM для поддерживаемых GETTER’ов реализует всю эту обработу внутри себя и на выходе отдает уже форматированный вывод, что позволяет облегчить задачу.
  6. Обработка данных Представление топологии в структурах данных.
    Имея на данные о топологии со всех устройств, остается привести их к общему виду, проанализировать и собрать итоговый пазл.
  7. Представление топологии в формате инструмента визуализации.
    В зависимости от выбора инструмента для визуализации может потребоваться дополнительное преобразование итоговой топологии в формат данных, принимаемый им на вход.
  8. Движок визуализации
    Самый неочевидный для меня пункт, раньше подобного опыта собственной разработки не было. Изучение гугла и советы коллег наметили потенциальный список фреймворков под Python (pygraphviz, matplotlib, networkx) и фреймворки под JS D3.js, vis.js. А в собственных заметках на полях нашелся JS+HTML5 Toolkit NeXt UI, виденный ранее на просторах лаб Cisco DevNet и разработанный ими же. Он неплохо документирован, заточен на визуализацию сетей и умеет многое из коробки.
  9. Визуализированная топология
    Наша конечная цель. Может представлять из себя как статическое изображение или HTML-документ, так и что-то более продвинутое и интерактивное.

Суммируя, далеко не полный список вариантов:
detailed

В итоге выбор пал на следующий стек:


  • LLDP как источник информации о топологии.
  • SSH для доступа на оборудование.
  • Nornir для многопоточности, удобства обработки результатов и организации данных об оборудовании в inventory.
  • NAPALM для абстрагирования от задач ручного парсинга CLI.
  • Python3 для написания основной логики.
  • NeXt UI (JS+HTML5) для визуализации полученного через Python результата.

NAPALM и Nornir до этого уже доводилось вполне успешно использовать для задач сетевого аудита со сбором различных данных с сотен стройств. NAPALM из коробки умеет в LLDP на Cisco IOS/IOSXE, IOS-XR, NX-OS, Juniper JunOS и Arista EOS.
К тому же, с учетом задуманного разделения логики выше, дополнительные источники данных и коннекторы к ним могут быть добавлены параллельно и учтены при дальнейшем сведении и обработке данных.
С Next UI же предстояло разобраться на ходу, но уж больно интересно выглядели примеры.


Тестовый стенд с оборудованием

В качестве тестового стенда использовался эмулятор Cisco Modeling Labs. Это новая версия эмулятора VIRL. В Cisco DevNet Sandbox можно получить бесплатный доступ к лабе с ним, предварительно зарезервировав время (в пару кликов) и настроив VPN-доступ (через AnyConnect). А когда-то единственными вариантами были железо дома или продакшн приключения с GNS3. :)

Вид тестовой топологии в интерфейсе CML, на выходе должно получиться что-то похожее:

x5ibhuvg2oem-vjb-x3q0aq_66w.png

Имеются устройства на IOS (edge-sw01), IOSXE (internet-rtr01, distr-rtr01, distr-rtr02), NXOS (dist-sw01, dist-sw02), IOSXR (core-rtr01, core-rtr02) и ASA (edge-firewall01). На всех коммутаторах и маршрутизаторах включен LLDP. Доступ по SSH включен на IOS, IOSXE и NXOS нодах.


Установка и инициализация Nornir

Nornir является сторонним Python-фреймворком. Распространяется через PyPI, требует Python версии 3.6.2 и выше. За собой тянет вереницу зависимостей, включая NAPALM и netmiko. При установке не на чистую систему рекомендуется использовать виртуальное окружение Python (venv) для изоляции зависимостей. Тестирование и разработка велись на MacOS, но Linux-дистрибутивы и Windows тоже должны поддерживаться.

$ mkdir ~/testenv
$ python3.7 -m venv ~/testenv/
$ source ~/testenv/bin/activate
(testenv)$ pip install nornir

Nornir поддерживает различные варианты реализации inventory для систематизации информации об устройствах и параметрах доступа на них.
В этом примере остановимся на его стандартном модуле SimpleInventory.
Общие настройки Nornir хранятся в yaml файле, имя может быть произвольным, но нужно будет указать его при дальнейшей инициализации в Python-скрипте.
nornir_config.yaml:

---
core:
    num_workers: 20
inventory:
    plugin: nornir.plugins.inventory.simple.SimpleInventory
    options:
        host_file: "inventory/hosts_devnet_sb_cml.yml"
        group_file: "inventory/groups.yml"

Как видно в примере выше, в опциях определены еще два yaml-файла: файл хостов и групп. В первом хранится информация об индивидуальных хостах и их свойствах. Во втором — список групп и их свойств. Хост может быть отнесен к одной или более групп и наследует все их свойства, что уменьшает размер конфигурации. Имена файлов могут быть произвольными, но тоже должны совпадать с указанными в освновном конфигурационном файле.
Параметр num_workers указывает Nornir количество потоков, в которое дожно происходить взаимодействие с сетевым оборудованием. По умолчанию 20.

inventory/hosts_devnet_sb_cml.yml имеет общий вид:

---

internet-rtr01:
    hostname: 10.10.20.181
    platform: ios
    groups:
        - devnet-cml-lab

dist-sw01:
    hostname: 10.10.20.177
    platform: nxos_ssh
    transport: ssh
    groups:
        - devnet-cml-lab

Для примера указаны два хоста. В них заданы IP-адреса и тип платформы, используемый в сумме с транспортом (для IOS по умолчанию SSH) для правильного выбора Норниром и его плагинами коннектора к оборудованию. Оба хоста включены в группу 'devnet-cml-lab'.

В groups.yml определим групповые настройки для них:

---

devnet-cml-lab:
    username: cisco
    password: cisco
    connection_options:
        napalm:
            extras:
                optional_args:
                    secret: cisco

Выше заданы используемые логин, пароль и пароль на enable режим для оборудования Cisco. Они будут унаследованы всеми членами группы.
Важно! Никогда не делайте так в продакшне и не храните пароли и логины в открытом виде, настройки приведены для демонстрации.
Это базовые настройки, далее необходимо инициализировать Nornir в Python-скрипте и начать работу с ним.


Скачивание NeXt UI

Для локального использования и тестирования достаточно скачать исходники с GitHub, что мы и сделаем. Его компоненты будут лежать в ./next_sources.


И предварительно имеем:

$ tree . -L 2
.
├── inventory
│   ├── groups.yml
│   └── hosts_devnet_sb_cml.yml
├── next_sources
│   ├── css
│   ├── doc
│   ├── fonts
│   └── js
├── nornir_config.yml

Основную логику будет реализовывать скрипт generate_topology.py.


Финальный штрих для инициализации Nornir

Инициализируем Nornir в Python:

from nornir import InitNornir
from nornir.plugins.tasks.networking import napalm_get

NORNIR_CONFIG_FILE = "nornir_config.yml"

nr = InitNornir(config_file=NORNIR_CONFIG_FILE)

Теперь он полностью готов к работе.
Импортированный napalm_get дает доступ к NAPALM через Nornir.


Минутка LLDP

По LLDP устройства обмениваются с прямыми соседями фреймами, содержащими набор TLV полей. LLDP-сообщения не ретранслируются.
Обязательные TLV: Chassis ID, Port ID и Time-to-Live
Опциональные: System name and description; Port name and description; VLAN name; IP management address; System capabilities (switching, routing, etc.) и прочие.
Т.к. сеть находится под нашим управлением, включим System name и Port name в набор минимально необходимых TLV.
Это не несет значительных рисков безопасности, но поможет однозначно идентифицировать мульти-шасси устройства с единым control plane (например, стеки) и связи между устройствами.

Задача построения топологии в этом случае сводится к сбору индивидуальных данных с устройств об их соседствах и определении на их основе уникальных устройств и связей между ними (т.е. вершин и ребер совокупного графа).
Схожим образом работает, например, OSPF при сборе и анализе индивидуальных LSA. И визуализация связности для протоколов маршрутизации — тоже вполне себе кейс. Но вернемся пока к LLPD.

В тестовой топологии все edge, core и distribution должны видеть своих прямых соседей. internet-rtr01 изолирован от всех и не должен иметь LLDP-соседств.
К примеру, суммарный вывод соседств с dist-rtr01:

dist-rtr01#sh lldp nei
Capability codes:
    (R) Router, (B) Bridge, (T) Telephone, (C) DOCSIS Cable Device
    (W) WLAN Access Point, (P) Repeater, (S) Station, (O) Other

Device ID           Local Intf     Hold-time  Capability      Port ID
dist-rtr02.devnet.laGi6            120        R               Gi6
dist-sw01.devnet.labGi4            120        B,R             Ethernet1/3
dist-sw02.devnet.labGi5            120        B,R             Ethernet1/3
core-rtr02.devnet.laGi3            120        R               Gi0/0/0/2
core-rtr01.devnet.laGi2            120        R               Gi0/0/0/2

Total entries displayed: 5

Пять соседей, все верно.
И с core-rtr02:

RP/0/0/CPU0:core-rtr02#sh lldp nei
Sun May 10 22:07:05.776 UTC
Capability codes:
        (R) Router, (B) Bridge, (T) Telephone, (C) DOCSIS Cable Device
        (W) WLAN Access Point, (P) Repeater, (S) Station, (O) Other

Device ID       Local Intf          Hold-time  Capability     Port ID
core-rtr01.devnet.la Gi0/0/0/0           120        R               Gi0/0/0/0
edge-sw01.devnet.lab Gi0/0/0/1           120        R               Gi0/3
dist-rtr01.devnet.la Gi0/0/0/2           120        R               Gi3
dist-rtr02.devnet.la Gi0/0/0/3           120        R               Gi3

Total entries displayed: 4

4 соседства, тоже корректно.
Обратите внимание, в обоих случаях в таблице присутствуют обрезанные хостнеймы в столбце Device ID.
Такие проблемы — извечные спутники CLI-автоматизации.
В качестве обходного пути будем ориентироваться на детальный вывод с каждого из устройств.
Для примера:


'show lldp neighbors detail' с dist-rtr01 на IOSXE
dist-rtr01#sh lldp nei det
------------------------------------------------
Local Intf: Gi6
Chassis id: 001e.e57c.cf00
Port id: Gi6
Port Description: L3 Link to dist-rtr01
System Name: dist-rtr02.devnet.lab

System Description: 
Cisco IOS Software [Gibraltar], Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.11.1b, RELEASE SOFTWARE (fc2)
Technical Support: http://www.cisco.com/techsupport
Copyright (c) 1986-2019 by Cisco Systems, Inc.
Compiled Tue 28-May-19 12:45

Time remaining: 91 seconds
System Capabilities: B,R
Enabled Capabilities: R
Management Addresses:
    IP: 172.16.252.18
Auto Negotiation - not supported
Physical media capabilities - not advertised
Media Attachment Unit type - not advertised
Vlan ID: - not advertised

------------------------------------------------
Local Intf: Gi4
Chassis id: 5254.0007.5d59
Port id: Ethernet1/3
Port Description: L3 link to dist-rtr01
System Name: dist-sw01.devnet.lab

System Description: 
Cisco Nexus Operating System (NX-OS) Software 9.2(3)
TAC support: http://www.cisco.com/tac
Copyright (c) 2002-2019, Cisco Systems, Inc. All rights reserved.

Time remaining: 108 seconds
System Capabilities: B,R
Enabled Capabilities: B,R
Management Addresses:
    IP: 10.10.20.177
    Other: 52 54 00 07 5D 59 00
Auto Negotiation - not supported
Physical media capabilities - not advertised
Media Attachment Unit type - not advertised
Vlan ID: - not advertised

------------------------------------------------
Local Intf: Gi5
Chassis id: 5254.0007.b7e6
Port id: Ethernet1/3
Port Description: L3 link to dist-rtr01
System Name: dist-sw02.devnet.lab

System Description: 
Cisco Nexus Operating System (NX-OS) Software 9.2(3)
TAC support: http://www.cisco.com/tac
Copyright (c) 2002-2019, Cisco Systems, Inc. All rights reserved.

Time remaining: 97 seconds
System Capabilities: B,R
Enabled Capabilities: B,R
Management Addresses:
    IP: 10.10.20.178
    Other: 52 54 00 07 FF FF 00
Auto Negotiation - not supported
Physical media capabilities - not advertised
Media Attachment Unit type - not advertised
Vlan ID: - not advertised

------------------------------------------------
Local Intf: Gi3
Chassis id: 02c7.9dc0.0c06
Port id: Gi0/0/0/2
Port Description: L3 Link to dist-rtr01
System Name: core-rtr02.devnet.lab

System Description: 
Cisco IOS XR Software, Version 6.3.1[Default]
Copyright (c) 2017 by Cisco Systems, Inc., IOS XRv Series

Time remaining: 94 seconds
System Capabilities: R
Enabled Capabilities: R
Management Addresses:
    IP: 172.16.252.26
Auto Negotiation - not supported
Physical media capabilities - not advertised
Media Attachment Unit type - not advertised
Vlan ID: - not advertised

------------------------------------------------
Local Intf: Gi2
Chassis id: 0288.15c0.0c06
Port id: Gi0/0/0/2
Port Description: L3 Link to dist-rtr01
System Name: core-rtr01.devnet.lab

System Description: 
Cisco IOS XR Software, Version 6.3.1[Default]
Copyright (c) 2017 by Cisco Systems, Inc., IOS XRv Series

Time remaining: 110 seconds
System Capabilities: R
Enabled Capabilities: R
Management Addresses:
    IP: 172.16.252.22
Auto Negotiation - not supported
Physical media capabilities - not advertised
Media Attachment Unit type - not advertised
Vlan ID: - not advertised

Total entries displayed: 5


show lldp neighbors detail c dist-sw01 на NXOS
dist-sw01# sh lldp nei det
Capability codes:
  (R) Router, (B) Bridge, (T) Telephone, (C) DOCSIS Cable Device
  (W) WLAN Access Point, (P) Repeater, (S) Station, (O) Other
Device ID            Local Intf      Hold-time  Capability  Port ID  

Chassis id: 5254.0007.b7e4
Port id: Ethernet1/1
Local Port id: Eth1/1
Port Description: VPC Peer Link
System Name: dist-sw02.devnet.lab
System Description: Cisco Nexus Operating System (NX-OS) Software 9.2(3)
TAC support: http://www.cisco.com/tac
Copyright (c) 2002-2019, Cisco Systems, Inc. All rights reserved.
Time remaining: 112 seconds
System Capabilities: B, R
Enabled Capabilities: B, R
Management Address: 10.10.20.178
Management Address IPV6: not advertised
Vlan ID: 1

Chassis id: 5254.0007.b7e5
Port id: Ethernet1/2
Local Port id: Eth1/2
Port Description: VPC Peer Link
System Name: dist-sw02.devnet.lab
System Description: Cisco Nexus Operating System (NX-OS) Software 9.2(3)
TAC support: http://www.cisco.com/tac
Copyright (c) 2002-2019, Cisco Systems, Inc. All rights reserved.
Time remaining: 112 seconds
System Capabilities: B, R
Enabled Capabilities: B, R
Management Address: 10.10.20.178
Management Address IPV6: not advertised
Vlan ID: 1

Chassis id: 001e.7a2a.3900
Port id: Gi4
Local Port id: Eth1/3
Port Description: L3 Link to dist-sw01
System Name: dist-rtr01.devnet.lab
System Description: Cisco IOS Software [Gibraltar], Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.11.1b, RELEASE SOFTWARE (fc2)
Technical Support: http://www.cisco.com/techsupport
Copyright (c) 1986-2019 by Cisco Systems, Inc.
Compiled Tue 28-May-19 12:45
Time remaining: 109 seconds
System Capabilities: B, R
Enabled Capabilities: R
Management Address: 172.16.252.2
Management Address IPV6: not advertised
Vlan ID: not advertised

Chassis id: 001e.e57c.cf00
Port id: Gi4
Local Port id: Eth1/4
Port Description: L3 Link to dist-sw01
System Name: dist-rtr02.devnet.lab
System Description: Cisco IOS Software [Gibraltar], Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.11.1b, RELEASE SOFTWARE (fc2)
Technical Support: http://www.cisco.com/techsupport
Copyright (c) 1986-2019 by Cisco Systems, Inc.
Compiled Tue 28-May-19 12:45
Time remaining: 108 seconds
System Capabilities: B, R
Enabled Capabilities: R
Management Address: 172.16.252.6
Management Address IPV6: not advertised
Vlan ID: not advertised

Total entries displayed: 4


Получение данных с оборудования

Данные будем собирать с устройств на IOS (edge-sw01), IOSXE (internet-rtr01, distr-rtr01, distr-rtr02) и NXOS (dist-sw01, dist-sw02).
На устройствах на IOSXR (core-rtr01, core-rtr02) доступ будет закрыт.
Таким образом будут покрыты сценарии:


  1. Анализа полной связности для distribution устройств.
    Должны верно определяться уникальные ноды и линки.
  2. Обработки ошибок при отсутвии доступа для core-rtr01 и core-rtr02.
    Это не должно влиять на возможность работы с оставшимися устройтсвами.
  3. Восстановления части топологии без доступа на промежуточные устройства.
    И edge-sw01, и distr-rtr01 с distr-sw02 видят core-rtr01 и core-rtr02 с разных сторон по LLDP.
    В этом случае топология должна собраться в единое целое.


Файл хостов inventory/hosts_devnet_sb_cml.yml
---

internet-rtr01:
    hostname: 10.10.20.181
    platform: ios
    site: devnet_sandbox
    groups:
        - devnet-cml-lab

edge-sw01:
    hostname: 10.10.20.172
    platform: ios
    site: devnet_sandbox
    groups:
        - devnet-cml-lab

core-rtr01:
    # доступ на устройстве заблокирован для теста
    hostname: 10.10.20.173
    platform: iosxr
    groups:
        - devnet-cml-lab

core-rtr02:
    # доступ на устройстве заблокирован для теста
    hostname: 10.10.20.174
    platform: iosxr
    groups:
        - devnet-cml-lab

dist-rtr01:
    hostname: 10.10.20.175
    platform: ios
    groups:
        - devnet-cml-lab

dist-rtr02:
    hostname: 10.10.20.176
    platform: ios
    groups:
        - devnet-cml-lab

dist-sw01:
    hostname: 10.10.20.177
    platform: nxos_ssh
    transport: ssh
    groups:
        - devnet-cml-lab

dist-sw02:
    hostname: 10.10.20.178
    platform: nxos_ssh
    transport: ssh
    groups:
        - devnet-cml-lab

Задействуем два геттера NAPALM:


  • GET_LLDP_NEIGHBORS_DETAILS (сбор LLDP-соседств).
    Выбран детализированный вывод, т.к. в CLI-выводах суммарного могут обрезаться длинные хостнеймы.
  • GET_FACTS (общие данные об устройстве).
    Этот геттер включает данные о хостнейме и FQDN, они понадобятся.
    Помимо них, вывод может включать информацию о модели и серийном номере. Может пригодиться при визуализации.

Сбор данных обернем в функцию-Task для Nornir.
Это один из его механизмов для группировки действий на индивидуальных хостах.
Таски при массвовом запуске на устройствах обрабатываются в num_workers потоков.

def get_host_data(task):
    """Nornir Task для сбора данных с целевых устройств."""
    task.run(
        task=napalm_get,
        getters=['facts', 'lldp_neighbors_detail']
    )

# Запустим таск на всех устройствах в инвентори.
# Результат сохраним в переменную для дальнейшего разбора.
get_host_data_result = nr.run(get_host_data)

Если нужно запустить таск на определенных хостах или группах, Nornir поддерживает механизм простых и комплексных фильтров над инвентори.


Разбор полученных от устройств данных

В переменной get_host_data_result хранится результат выполнения таска get_host_data на каждом из устройств.

>>> get_host_data_result
AggregatedResult (get_host_data): {'internet-rtr01': MultiResult: [Result: "get_host_data", Result: "napalm_get"], 'edge-sw01': MultiResult: [Result: "get_host_data", Result: "napalm_get"], 'core-rtr01': MultiResult: [Result: "get_host_data", Result: "napalm_get"], 'core-rtr02': MultiResult: [Result: "get_host_data", Result: "napalm_get"], 'dist-rtr01': MultiResult: [Result: "get_host_data", Result: "napalm_get"], 'dist-rtr02': MultiResult: [Result: "get_host_data", Result: "napalm_get"], 'dist-sw01': MultiResult: [Result: "get_host_data", Result: "napalm_get"], 'dist-sw02': MultiResult: [Result: "get_host_data", Result: "napalm_get"]}

Объекты результата содержат метод failed, возвращающий булевое значение, по нему можно определить, удачно ли завершился таск.
Результат можно итерировать как словарь:

>>> for device, result in get_host_data_result.items():
...     print(f'{device} failed: {result.failed}')
... 
internet-rtr01 failed: False
edge-sw01 failed: False
core-rtr01 failed: True
core-rtr02 failed: True
dist-rtr01 failed: False
dist-rtr02 failed: False
dist-sw01 failed: False
dist-sw02 failed: False

Выглядит ожидаемо.

Полная структура результата для примера:


Содержимое объекта результата с dist-rtr01
>>> get_host_data_result['dist-rtr01'][1].result
{'facts': {'uptime': 6120, 'vendor': 'Cisco', 'os_version': 'Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.11.1b, RELEASE SOFTWARE (fc2)', 'serial_number': '9JDCOVUDSWN', 'model': 'CSR1000V', 'hostname': 'dist-rtr01', 'fqdn': 'dist-rtr01.devnet.lab', 'interface_list': ['GigabitEthernet1', 'GigabitEthernet2', 'GigabitEthernet3', 'GigabitEthernet4', 'GigabitEthernet5', 'GigabitEthernet6', 'Loopback0']}, 'lldp_neighbors_detail': {'GigabitEthernet6': [{'remote_chassis_id': '001e.e57c.cf00', 'remote_port': 'Gi6', 'remote_port_description': 'L3 Link to dist-rtr01', 'remote_system_name': 'dist-rtr02.devnet.lab', 'remote_system_description': 'Cisco IOS Software [Gibraltar], Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.11.1b, RELEASE SOFTWARE (fc2)', 'remote_system_capab': ['bridge', 'router'], 'remote_system_enable_capab': ['router'], 'parent_interface': ''}], 'GigabitEthernet4': [{'remote_chassis_id': '5254.0007.5d59', 'remote_port': 'Ethernet1/3', 'remote_port_description': 'L3 link to dist-rtr01', 'remote_system_name': 'dist-sw01.devnet.lab', 'remote_system_description': 'Cisco Nexus Operating System (NX-OS) Software 9.2(3)', 'remote_system_capab': ['bridge', 'router'], 'remote_system_enable_capab': ['bridge', 'router'], 'parent_interface': ''}], 'GigabitEthernet5': [{'remote_chassis_id': '5254.0007.b7e6', 'remote_port': 'Ethernet1/3', 'remote_port_description': 'L3 link to dist-rtr01', 'remote_system_name': 'dist-sw02.devnet.lab', 'remote_system_description': 'Cisco Nexus Operating System (NX-OS) Software 9.2(3)', 'remote_system_capab': ['bridge', 'router'], 'remote_system_enable_capab': ['bridge', 'router'], 'parent_interface': ''}], 'GigabitEthernet3': [{'remote_chassis_id': '02c7.9dc0.0c06', 'remote_port': 'Gi0/0/0/2', 'remote_port_description': 'L3 Link to dist-rtr01', 'remote_system_name': 'core-rtr02.devnet.lab', 'remote_system_description': 'Cisco IOS XR Software, Version 6.3.1[Default]', 'remote_system_capab': ['router'], 'remote_system_enable_capab': ['router'], 'parent_interface': ''}], 'GigabitEthernet2': [{'remote_chassis_id': '0288.15c0.0c06', 'remote_port': 'Gi0/0/0/2', 'remote_port_description': 'L3 Link to dist-rtr01', 'remote_system_name': 'core-rtr01.devnet.lab', 'remote_system_description': 'Cisco IOS XR Software, Version 6.3.1[Default]', 'remote_system_capab': ['router'], 'remote_system_enable_capab': ['router'], 'parent_interface': ''}]}}


Содержимое объекта результата с dist-sw01
>>> get_host_data_result['dist-sw01'][1].result
{'facts': {'uptime': 6090, 'vendor': 'Cisco', 'os_version': '9.2(3)', 'serial_number': '9P5OMCCMSQ4', 'model': 'Nexus9000 9000v Chassis', 'hostname': 'dist-sw01', 'fqdn': 'dist-sw01.devnet.lab', 'interface_list': ['mgmt0', 'Ethernet1/1', 'Ethernet1/2', 'Ethernet1/3', 'Ethernet1/4', 'Ethernet1/5', 'Ethernet1/6', 'Ethernet1/7', 'Ethernet1/8', 'Ethernet1/9', 'Ethernet1/10', 'Ethernet1/11', 'Ethernet1/12', 'Ethernet1/13', 'Ethernet1/14', 'Ethernet1/15', 'Ethernet1/16', 'Ethernet1/17', 'Ethernet1/18', 'Ethernet1/19', 'Ethernet1/20', 'Ethernet1/21', 'Ethernet1/22', 'Ethernet1/23', 'Ethernet1/24', 'Ethernet1/25', 'Ethernet1/26', 'Ethernet1/27', 'Ethernet1/28', 'Ethernet1/29', 'Ethernet1/30', 'Ethernet1/31', 'Ethernet1/32', 'Ethernet1/33', 'Ethernet1/34', 'Ethernet1/35', 'Ethernet1/36', 'Ethernet1/37', 'Ethernet1/38', 'Ethernet1/39', 'Ethernet1/40', 'Ethernet1/41', 'Ethernet1/42', 'Ethernet1/43', 'Ethernet1/44', 'Ethernet1/45', 'Ethernet1/46', 'Ethernet1/47', 'Ethernet1/48', 'Ethernet1/49', 'Ethernet1/50', 'Ethernet1/51', 'Ethernet1/52', 'Ethernet1/53', 'Ethernet1/54', 'Ethernet1/55', 'Ethernet1/56', 'Ethernet1/57', 'Ethernet1/58', 'Ethernet1/59', 'Ethernet1/60', 'Ethernet1/61', 'Ethernet1/62', 'Ethernet1/63', 'Ethernet1/64', 'Ethernet1/65', 'Ethernet1/66', 'Ethernet1/67', 'Ethernet1/68', 'Ethernet1/69', 'Ethernet1/70', 'Ethernet1/71', 'Ethernet1/72', 'Ethernet1/73', 'Ethernet1/74', 'Ethernet1/75', 'Ethernet1/76', 'Ethernet1/77', 'Ethernet1/78', 'Ethernet1/79', 'Ethernet1/80', 'Ethernet1/81', 'Ethernet1/82', 'Ethernet1/83', 'Ethernet1/84', 'Ethernet1/85', 'Ethernet1/86', 'Ethernet1/87', 'Ethernet1/88', 'Ethernet1/89', 'Ethernet1/90', 'Ethernet1/91', 'Ethernet1/92', 'Ethernet1/93', 'Ethernet1/94', 'Ethernet1/95', 'Ethernet1/96', 'Ethernet1/97', 'Ethernet1/98', 'Ethernet1/99', 'Ethernet1/100', 'Ethernet1/101', 'Ethernet1/102', 'Ethernet1/103', 'Ethernet1/104', 'Ethernet1/105', 'Ethernet1/106', 'Ethernet1/107', 'Ethernet1/108', 'Ethernet1/109', 'Ethernet1/110', 'Ethernet1/111', 'Ethernet1/112', 'Ethernet1/113', 'Ethernet1/114', 'Ethernet1/115', 'Ethernet1/116', 'Ethernet1/117', 'Ethernet1/118', 'Ethernet1/119', 'Ethernet1/120', 'Ethernet1/121', 'Ethernet1/122', 'Ethernet1/123', 'Ethernet1/124', 'Ethernet1/125', 'Ethernet1/126', 'Ethernet1/127', 'Ethernet1/128', 'Port-channel1', 'Loopback0', 'Vlan1', 'Vlan101', 'Vlan102', 'Vlan103', 'Vlan104', 'Vlan105']}, 'lldp_neighbors_detail': {'Ethernet1/1': [{'remote_chassis_id': '5254.0007.b7e4', 'remote_port': 'Ethernet1/1', 'remote_port_description': 'VPC Peer Link', 'remote_system_name': 'dist-sw02.devnet.lab', 'remote_system_description': 'Cisco Nexus Operating System (NX-OS) Software 9.2(3)', 'remote_system_capab': ['bridge', 'router'], 'remote_system_enable_capab': ['bridge', 'router'], 'parent_interface': ''}], 'Ethernet1/2': [{'remote_chassis_id': '5254.0007.b7e5', 'remote_port': 'Ethernet1/2', 'remote_port_description': 'VPC Peer Link', 'remote_system_name': 'dist-sw02.devnet.lab', 'remote_system_description': 'Cisco Nexus Operating System (NX-OS) Software 9.2(3)', 'remote_system_capab': ['bridge', 'router'], 'remote_system_enable_capab': ['bridge', 'router'], 'parent_interface': ''}], 'Ethernet1/3': [{'remote_chassis_id': '001e.7a2a.3900', 'remote_port': 'Gi4', 'remote_port_description': 'L3 Link to dist-sw01', 'remote_system_name': 'dist-rtr01.devnet.lab', 'remote_system_description': 'Cisco IOS Software [Gibraltar], Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.11.1b, RELEASE SOFTWARE (fc2)', 'remote_system_capab': ['bridge', 'router'], 'remote_system_enable_capab': ['router'], 'parent_interface': ''}], 'Ethernet1/4': [{'remote_chassis_id': '001e.e57c.cf00', 'remote_port': 'Gi4', 'remote_port_description': 'L3 Link to dist-sw01', 'remote_system_name': 'dist-rtr02.devnet.lab', 'remote_system_description': 'Cisco IOS Software [Gibraltar], Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.11.1b, RELEASE SOFTWARE (fc2)', 'remote_system_capab': ['bridge', 'router'], 'remote_system_enable_capab': ['router'], 'parent_interface': ''}]}}

Результат представляет из себя словарь с ключами 'facts' 'lldp_neighbors_detail' по названиям использованных геттеров.
Внутри все уже разложено NAPALM’ом по структурам данных.
Сверим соседства:


Соседи dist-rtr01
>>> for neighbor in get_host_data_result['dist-rtr01'][1].result['lldp_neighbors_detail'].items():
...     print(neighbor)
...     print('\n')
... 
('GigabitEthernet6', [{'remote_chassis_id': '001e.e57c.cf00', 'remote_port': 'Gi6', 'remote_port_description': 'L3 Link to dist-rtr01', 'remote_system_name': 'dist-rtr02.devnet.lab', 'remote_system_description': 'Cisco IOS Software [Gibraltar], Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.11.1b, RELEASE SOFTWARE (fc2)', 'remote_system_capab': ['bridge', 'router'], 'remote_system_enable_capab': ['router'], 'parent_interface': ''}])

('GigabitEthernet4', [{'remote_chassis_id': '5254.0007.5d59', 'remote_port': 'Ethernet1/3', 'remote_port_description': 'L3 link to dist-rtr01', 'remote_system_name': 'dist-sw01.devnet.lab', 'remote_system_description': 'Cisco Nexus Operating System (NX-OS) Software 9.2(3)', 'remote_system_capab': ['bridge', 'router'], 'remote_system_enable_capab': ['bridge', 'router'], 'parent_interface': ''}])

('GigabitEthernet5', [{'remote_chassis_id': '5254.0007.b7e6', 'remote_port': 'Ethernet1/3', 'remote_port_description': 'L3 link to dist-rtr01', 'remote_system_name': 'dist-sw02.devnet.lab', 'remote_system_description': 'Cisco Nexus Operating System (NX-OS) Software 9.2(3)', 'remote_system_capab': ['bridge', 'router'], 'remote_system_enable_capab': ['bridge', 'router'], 'parent_interface': ''}])

('GigabitEthernet3', [{'remote_chassis_id': '02c7.9dc0.0c06', 'remote_port': 'Gi0/0/0/2', 'remote_port_description': 'L3 Link to dist-rtr01', 'remote_system_name': 'core-rtr02.devnet.lab', 'remote_system_description': 'Cisco IOS XR Software, Version 6.3.1[Default]', 'remote_system_capab': ['router'], 'remote_system_enable_capab': ['router'], 'parent_interface': ''}])

('GigabitEthernet2', [{'remote_chassis_id': '0288.15c0.0c06', 'remote_port': 'Gi0/0/0/2', 'remote_port_description': 'L3 Link to dist-rtr01', 'remote_system_name': 'core-rtr01.devnet.lab', 'remote_system_description': 'Cisco IOS XR Software, Version 6.3.1[Default]', 'remote_system_capab': ['router'], 'remote_system_enable_capab': ['router'], 'parent_interface': ''}])


Соседи dist-sw01
>>> for neighbor in get_host_data_result['dist-sw01'][1].result['lldp_neighbors_detail'].items():
...     print(neighbor)
...     print('\n')
... 
('Ethernet1/1', [{'remote_chassis_id': '5254.0007.b7e4', 'remote_port': 'Ethernet1/1', 'remote_port_description': 'VPC Peer Link', 'remote_system_name': 'dist-sw02.devnet.lab', 'remote_system_description': 'Cisco Nexus Operating System (NX-OS) Software 9.2(3)', 'remote_system_capab': ['bridge', 'router'], 'remote_system_enable_capab': ['bridge', 'router'], 'parent_interface': ''}])

('Ethernet1/2', [{'remote_chassis_id': '5254.0007.b7e5', 'remote_port': 'Ethernet1/2', 'remote_port_description': 'VPC Peer Link', 'remote_system_name': 'dist-sw02.devnet.lab', 'remote_system_description': 'Cisco Nexus Operating System (NX-OS) Software 9.2(3)', 'remote_system_capab': ['bridge', 'router'], 'remote_system_enable_capab': ['bridge', 'router'], 'parent_interface': ''}])

('Ethernet1/3', [{'remote_chassis_id': '001e.7a2a.3900', 'remote_port': 'Gi4', 'remote_port_description': 'L3 Link to dist-sw01', 'remote_system_name': 'dist-rtr01.devnet.lab', 'remote_system_description': 'Cisco IOS Software [Gibraltar], Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.11.1b, RELEASE SOFTWARE (fc2)', 'remote_system_capab': ['bridge', 'router'], 'remote_system_enable_capab': ['router'], 'parent_interface': ''}])

('Ethernet1/4', [{'remote_chassis_id': '001e.e57c.cf00', 'remote_port': 'Gi4', 'remote_port_description': 'L3 Link to dist-sw01', 'remote_system_name': 'dist-rtr02.devnet.lab', 'remote_system_description': 'Cisco IOS Software [Gibraltar], Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.11.1b, RELEASE SOFTWARE (fc2)', 'remote_system_capab': ['bridge', 'router'], 'remote_system_enable_capab': ['router'], 'parent_interface': ''}])

5 соседей у dist-rtr01, совпадает с выводом из CLI выше.
4 соседа у dist-sw01, тоже все сходится.
Так же и на других хостах.

Для удобства дальнейшей обработки достанем из результата данные отдельно по LLDP и фактам.
Для сведения всех данных за уникальный идентификатор устройства примем в порядке убывания приоритета:


  • Его FQDN, если доступен (далее по тексту может называться хостнеймом для упрощения).
  • Его hostname, если доступен.
  • Его имя в хост-объекте inventory Nornir.
    Первыми двумя пунктами руководствуется и LLDP.
    def normalize_result(nornir_job_result):
    """
    Парсер для результата работы get_host_data.
    Возвращает словари с данными LLDP и FACTS с разбиением
    по устройствам с ключами в виде хостнеймов.
    """
    global_lldp_data = {}
    global_facts = {}
    for device, output in nornir_job_result.items():
        if output[0].failed:
            # Если таск для специфического хоста завершился ошибкой,
            # в результат для него записываются пустые списки.
            # Ключом будет являться имя его host-объекта в инвентори.
            global_lldp_data[device] = {}
            global_facts[device] = {
                'nr_ip': nr.inventory.hosts[device].get('hostname', 'n/a'),
            }
            continue
        # Для различения устройств в топологии при ее анализе
        # за идентификатор принимается FQDN устройства, как и в LLDP TLV.
        device_fqdn = output[1].result['facts']['fqdn']
        if not device_fqdn:
            # Если FQDN не задан, используется хостнейм.
            device_fqdn = output[1].result['facts']['hostname']
        if not device_fqdn:
            # Если и хостнейм не задан,
            # используется имя host-объекта в инвентори.
            device_fqdn = device
        global_facts[device_fqdn] = output[1].result['facts']
        # Допишем в facts IP-адрес оборудования
        global_facts[device_fqdn]['nr_ip'] = nr.inventory.hosts[device].get('hostname', 'n/a')
        global_lldp_data[device_fqdn] = output[1].result['lldp_neighbors_detail']
    return global_lldp_data, global_facts

Из данных по LLDP теперь необходимо извлечь список всех соседств со всех устройств и сформировать на его основе:


  • Список уникальных хостов.
  • Список уникальных линков между ними.

Для однозначной идентификации линков будем хранить их в формате:
((source_device_id, source_port_name), (destination_device_id, destination_port_name))

Стоит также учесть, что:


  • Один и тот же линк может быть виден с разных сторон с двух устройств, если на оба есть доступ.
    Нужно проверять перестановки источника и назначения при добавлении новых линков.
  • Локальное имя порта и анонсируемое по LLDP может иметь разный формат. Например, GigabitEthernet4 локально, но Gi4 в анонсе.

Для однозначной идентификации будем транслировать их в полный формат. И добавим функцию для трансляции в сокращенный вид для дальнейшего удобства визуализации.
Для автоматического выбора правильной пиктограммы устройства при визуализации будем учитывать его capabilities, анонсируемые по LLDP. Сведем их в отдельный словарь по хостнеймам.
Код:

interface_full_name_map = {
    'Eth': 'Ethernet',
    'Fa': 'FastEthernet',
    'Gi': 'GigabitEthernet',
    'Te': 'TenGigabitEthernet',
}

def if_fullname(ifname):
    for k, v in interface_full_name_map.items():
        if ifname.startswith(v):
            return ifname
        if ifname.startswith(k):
            return ifname.replace(k, v)
    return ifname

def if_shortname(ifname):
    for k, v in interface_full_name_map.items():
        if ifname.startswith(v):
            return ifname.replace(v, k)
    return ifname

def extract_lldp_details(lldp_data_dict):
    """
    Парсер данных из словаря LLDP-данных.
    Возвращает сет из всех обнаруженных в топологии хостов,
    словарь обнаруженных LLDP capabilities с ключами в виде
    хостнеймов и список уникальных связностей между хостами.
    """
    discovered_hosts = set()
    lldp_capabilities_dict = {}
    global_interconnections = []
    for host, lldp_data in lldp_data_dict.items():
        if not host:
            continue
        discovered_hosts.add(host)
        if not lldp_data:
            continue
        for interface, neighbors in lldp_data.items():
            for neighbor in neighbors:
                if not neighbor['remote_system_name']:
                    continue
                discovered_hosts.add(neighbor['remote_system_name'])
                if neighbor['remote_system_enable_capab']:
                    # В случае наличия нескольких enable capabilities
                    # в расчет берется первая по списку
                    lldp_capabilities_dict[neighbor['remote_system_name']] = (
                        neighbor['remote_system_enable_capab'][0]
                    )
                else:
                    lldp_capabilities_dict[neighbor['remote_system_name']] = ''
                # Связи между хостами первоначально сохраняются в формате:
                # ((хостнейм_источника, порт источника), (хостнейм назначения, порт_назначения))
                # и добавляются в общий список.
                local_end = (host, interface)
                remote_end = (
                    neighbor['remote_system_name'],
                    if_fullname(neighbor['remote_port'])
                )
                # При добавлении проверяется, не является ли линк перестановкой
                # источника и назначения или дублем.
                link_is_already_there = (
                    (local_end, remote_end) in global_interconnections
                    or (remote_end, local_end) in global_interconnections
                )
                if link_is_already_there:
                    continue
                global_interconnections.append((
                    (host, interface),
                    (neighbor['remote_system_name'], if_fullname(neighbor['remote_port']))
                ))
    return [discovered_hosts, global_interconnections, lldp_capabilities_dict]


Инициализация приложения NeXt UI

За всю логику отрисовки топологии будет отвечать скрипт next_app.js на основе NeXt UI.
Начнем с базовых вещей:

(function (nx) {
    /**
     * Приложение на NeXt UI
     */
    // Инициализация топологии
    var topo = new nx.graphic.Topology({
        // Ширина и высота view приложения
        width: 1200,
        height: 700,
        // Процессор данных, отвечает за расстановку нод.
        // 'force' стремится расставить ноды на равном 
        // удалении друг от друга. 'quick' расставляет их
        // в произвольных местах
        dataProcessor: 'force',
        // уникальный идентификатор нод и линков
        identityKey: 'id',
        // Конфигурация нод
        nodeConfig: {
            label: 'model.name',
            iconType:'model.icon',
        },
        // Конфигурация линков
        linkConfig: {
            // Отображение множественных линков дугами,
            // можно поменять на 'parallel'
            linkType: 'curve',
        },
        // Отображать пиктограммы нод, при false отрисует точку
        showIcon: true,
    });

    var Shell = nx.define(nx.ui.Application, {
        methods: {
            start: function () {
                // записать данные топологии из переменной
                topo.data(topologyData);
                // и прикрепить их к документу
                topo.attach(this);
            }
        }
    });

    // создать инстанс приложения
    var shell = new Shell();
    // запустить приложение
    shell.start();
})(nx);

Тополология собирается из переменной topologyData, вынесем ее в отдельный файл topology.js. Ее внутренний формат рассмотрим ниже.

Для просмотра визуализации будет использоваться локальная HTML форма, куда подключим все составные части:




    
        
        
        
        
        
        
    
    
    












Формирование топологии для NeXT UI в Python

Ранее мы уже

© Habrahabr.ru