Visualizing Network Topologies: Zero to Hero in Two Days / Habrhttps://habr.com/en/post/534716/

翻译

原作者: Igor Korotchenkov 又名@Debug_all

嘿大家!这是我在 2020 年 5 月参加的本地 Cisco Russia DevNet Marathon 在线活动的后续文章。这是一系列关于网络自动化的教育网络研讨会,随后是基于讨论主题的日常挑战。
在最后一天,参与者面临的挑战是自动化任意网段的拓扑分析和可视化,并可选择跟踪和可视化更改。

这项任务绝对不是微不足道的,也没有在公共博客文章中广泛涉及。在本文中,我想分解我自己最终占据首位的解决方案,并描述所选的工具集和注意事项。

让我们开始吧。


免责声明

这是我在 2020 年 5 月发布的俄语原文文章的翻译。如果您愿意帮助改进翻译,请 DM 我。
本文并不旨在涵盖所有可能的场景,但确实描述了基于特定案例的一般方法。
如无特别说明,以上及以下均为作者个人主观意见。
所有列出的代码都是在MIT许可下发布的,不提供任何形式的保证。

该解决方案是用 Python 和 JavaScript 编写的。阅读需要了解编程和网络基础知识。

如果您发现拼写错误,请使用 Ctrl+Enter 或 ⌘+Enter 将其发送给作者。

任务

最终的任务描述如下:

有一个由运行 IOS/IOS-XE 的各种 L2/L3 网络设备组成的网络。您有所有设备的管理 IP 地址列表。所有设备均可通过其 IP 地址访问。您有权执行任何“show”命令。您可以自由使用任何数据收集方法。但是相信我们,您不太可能需要 SNMP。不过,我们不应该限制您的幻想。

主要任务是根据 LLDP 数据识别物理拓扑和设备互连,并以人性化的格式将其可视化(是的,我们都发现可视化图表更具可读性)。然后你应该将结果保存为适合计算机进一步分析的格式(是的,机器不擅长阅读可视化图表)。

拓扑视图应包括:

  • 每种设备类型的图标不同(路由器和交换机可能有相同的图标)。
  • 设备主机名。
  • 接口名称(您可以使用缩短的格式,例如 Gi0/0 表示 GigabitEthernet0/0/0)。

允许实施过滤器限制或隐藏某些信息。
一个额外的任务是识别拓扑变化(通过比较当前和以前的版本)并以人性化的格式将它们可视化。

总结:IP 地址和凭据作为输入,可视化拓扑作为输出(以及用于实验和介于两者之间的选项的大空间)。

我还指出了在选择解决方案工具集时要遵循的一些其他个人注意事项:

  • 功能丰富与简单的平衡。
    该解决方案应在可用功能及其使用和实施的易用性方面进行平衡。可能会使用一些现成的开源免费工具。
  • 熟悉所选工具集。
    我们有三天的时间来完成任务。第一天我不得不花在一些紧急情况上。因此,为了能够在如此有限的时间范围内提供可行的解决方案,必须使用一些已知工具。
  • 解决方案可重用性。
    该任务可能适用于许多生产网络。人们应该牢记这一点。
  • 支持多种网络平台。
    现实世界的网络由许多平台组成。这也是需要注意的。
  • 所选工具集的文档必须可用。
    我相信这是任何可重用解决方案的强制性要求。

现有解决方案

在自己重新发明轮子之前,先检查一下轮子是否已经被发明出来,这总是一个好主意。我对现有的网络可视化解决方案进行了一些研究。毫不奇怪,没有任何解决方案可以开箱即用地满足所有要求。大多数此类解决方案都内置在更大(通常远非免费)的企业级网络监控和分析系统中,这将显着降低可重用性和定制潜力。

任务分解和工具集选择

我将一个抽象的网络可视化任务分为以下层次和步骤:

让我们关注每一个,同时牢记我们的要求:

  1. 网络设备
    初始任务要求我们支持 IOS 和 IOS-XE。
    现实世界的网络往往更加异构。让我们试着考虑一下。

  2. 拓扑数据源
    任务说明建议我们使用LLDP(链路层发现协议)协议。这是 IEEE 802.1AB 标准中描述的 L2(OSI 链路层)协议。它受到现代网络平台(包括IOS和IOS-XE)和操作系统(包括Linux和Window)的广泛支持,因此很适合我们的情况。
    拓扑数据还可以通过网络设备的各种输出来丰富,例如路由和交换表、路由协议数据等。让我们标记它以备将来改进。

  3. 数据访问协议和标准
    最现代的平台通常支持闪亮和铬NETCONF、REST API、带有YANG模型和数据结构的RESTCONF。传统设备和平台的存在通常会迫使我们恢复到 SSH、Telnet 和良好的 CLI。

  4. 特定于协议和供应商的驱动程序或插件
    该解决方案的核心逻辑将用 Python 编写,主要有两个原因: Python 有一套非常全面的网络自动化模块和库,这是我最有经验的一种编程语言在
    Python中,API 驱动的网络自动化可以使用请求模块或一些专门的模块来完成。
    从 Python 对网络设备的裸 SSH/Telnet 访问通常依赖于netmikoparamikoscrapli模块。它们让您模拟标准 CLI:向会话发送一些文本命令,并期望返回或多或少可预测的可读性级别和格式的文本输出。
    还有几个高级 Python 框架允许在我上面提到的工具之上提供附加功能。在我们的案例中,其中最有用的两个是NAPALM和Nornir。NAPALM 提供供应商中立的GETTER用于从网络设备获取结构化数据。Nornir 实现了许多有用的抽象和开箱即用的多线程。
    至于 SNMP,让我们将其留作网络监控之用。

  5. 非结构化数据 -> 数据规范化工具集 -> 结构化数据
    使用 API 收集数据通常可以让您立即获得结构化输出。您从网络设备 CLI 获得的文本输出本身不适用于进一步的机器处理。从 Python 的 CLI 输出中提取数据的传统方法是重新模块和正则表达式。现代的方法是TextFSM由谷歌和全新开发的框架TTP(模板文本解析器)所开发dmulyalin。与正则表达式相比,这两种工具都使用更可用的模板执行数据解析。
    上面提到的 NAPALM 模块在内部为支持的 GETTER 执行非结构化数据规范化并返回结构化输出。在我们的情况下,这可能会使事情变得更容易。

  6. 数据处理和分析 -> Python
    数据结构中的拓扑表示一旦我们从所有设备中获得结构化拓扑数据片段,我们所需要做的就是将其带入通用表示、分析和组装最终难题。

  7. 可视化引擎格式中的拓扑表示
    根据可视化引擎的选择,您可能需要根据工具支持的输入转换最终的拓扑数据格式。

  8. 可视化引擎
    这一点对我来说是最不明显的,我之前没有这种开发经验。谷歌搜索和 DevNet Marathon 电报频道与同事的讨论向我介绍了几个 Python(pygraphviz、matplotlib、networkx)和 JavaScript(JS D3.js、vis.js.)框架。然后我发现了 JavaScript+HTML5 NeXt UI Toolkit,我之前在 Cisco DevNet 实验室中挖掘时曾将其添加为书签。这是 Cisco 开发的专用网络可视化工具包。它具有许多功能和体面的文档。

  9. 可视化拓扑
    我们的最终目标。视图可以从简单的静态图像或 HTML 文档到更高级和交互式的内容。

以下是我们拥有的最常用工具的摘要:

根据上述要求,我为我的目标解决方案选择了以下工具:

  • LLDP 是拓扑数据源。
  • 用于与网络设备交互的 SSH 和 CLI。
  • Nornir用于多线程、更有用的数据收集结果处理和处理,并将有关我们设备的信息保存在结构化清单中。
  • NAPALM从手动 CLI 报废中抽象出来。
  • Python3 用于编写核心逻辑。
  • NeXt UI (JS+HTML5) 基于我们从 Python 代码和平中获得的结果进行拓扑可视化。

我之前已经成功地使用 NAPALM 和 Nornir 进行网络审计和从数百个网络设备收集数据。默认 NAPALM GETTER 在 Cisco IOS/IOS-XE、IOS-XR、NX-OS、Juniper JunOS 和 Arista EOS 上支持 LLDP。
此外,上面讨论的逻辑分离将允许我们在不影响整个代码库的情况下添加更多数据源和网络连接器。
Next UI 是一个需要熟悉并弄清楚它在运行时如何工作的东西。然而,这些例子看起来很有希望。

准备

测试实验室

我使用Cisco Modeling Labs作为测试实验室。这是 VIRL 网络模拟器的新版本。Cisco DevNet Sandbox 允许在有限的时间内免费使用它。您只需要注册并继续进行预订,只需点击几下鼠标(在您通过电子邮件收到预订详细信息后,点击几下鼠标即可使用 AnyConnect VPN 连接到实验室)。在过去,我们必须使用生产网络、裸机家庭实验室或使用 GNS3 获得乐趣。

CML Web 界面上的实验室拓扑如下(我们应该得到类似的结果):

它由任何类型的 Cisco 设备组成: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 框架。它在 Python 3.6.2 及更高版本的 PyPI 上可用。Nornir 有十几个依赖项,包括 NAPALM 和 netmiko。建议使用 Python 虚拟环境(venv来隔离依赖项。我的本地开发环境在 MacOS 10.15 上使用了 Nornir 2.4.0 和 Python 3.7。这应该也适用于 Linux 和 Windows。Nornir 安装很简单:

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

重要提示: Nornir 在 3.X 版本中发生了一些巨大的变化。其中一些不向后兼容 2.X 版本。Nornir 相关配置和代码与 2.X 版本相关。

Nornir 支持各种库存插件。它们都提供了一种以编程方式构建和操作网络设备信息的便捷方式。对于这个解决方案,标准的 SimpleInventory 插件就足够了。
常规 Nornir 设置列在一组 YAML 文件中。配置文件名可以是任意的,但您应该在 Python 初始化期间将 Nornir 指向它们的确切名称。

nornir_config.yaml:

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

您可以在上面看到的示例 Nornir 主配置文件包含对另外两个 YAML 文件的引用:主机文件和组文件。这些文件定义 SimpleInventory 插件配置。Hosts 文件包含我们的网络设备(主机)及其属性的列表。组文件包含组及其属性的列表。单个主机可以包含在一个或多个组中。主机继承它所属的所有组的属性。主机和组的文件名和位置也可以是任意的。

库存/hosts_devnet_sb_cml.yml具有以下结构:

---internet-rtr01:hostname: 10.10.20.181platform: iosgroups:- devnet-cml-labdist-sw01:hostname: 10.10.20.177platform: nxos_sshtransport: sshgroups:- devnet-cml-lab

为简洁起见,只显示了两个主机。两台主机都有 IP 地址、平台属性。dist-sw01 具有专门分配的传输类型。对于 internet-rtr01,Nornir 将根据平台类型(IOS 默认为 SSH)选择传输类型。两台主机都属于“devnet-cml-lab”组。

groups.yml将为它们定义所有组设置:

---devnet-cml-lab:username: ciscopassword: ciscoconnection_options:napalm:extras:optional_args:secret: cisco

上面的组属性包含访问凭据并为 Cisco 设备启用机密。这些属性将被所有组成员继承。
重要提示:永远不要在您的生产环境中以这样的明文配置存储凭据(和任何敏感数据)。这个简单的配置仅用于演示和实验室目的。
这些都是一般的 Nornir 配置步骤。我们现在需要做的就是从 Python 代码初始化它。

下载 NeXt UI

对于本地使用和测试,从GitHub下载 NeXt UI 源代码就足够了。让我们将源代码放入项目根目录中的 ./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的 Python 脚本中。

初始化女巫

一旦我们的 Nornir 配置准备好,它就可以在 Python 中简单地初始化:

from nornir import InitNornir
from nornir.plugins.tasks.networking import napalm_getNORNIR_CONFIG_FILE = "nornir_config.yml"nr = InitNornir(config_file=NORNIR_CONFIG_FILE)

就是这样。Nornir 已准备好工作。
上面导入的NAPALM_get允许我们直接从 Nornir 使用 NAPALM。

LLDP 概览

启用 LLDP 的设备与它们的直接邻居交换由TLV字段组成的定期 LLDP 消息。LLDP 消息通常不会被中继。
必需的 TLV 字段:机箱 ID、端口 ID、生存时间。
可选的 TLV 字段:系统名称和描述;端口名称和描述;VLAN 名称;IP管理地址;系统功能(交换、路由等)等。
由于检查的拓扑段在我们的控制之下,让我们考虑系统名称和端口名称 TLV 字段所需的和可在内部发布的字段。
它不会造成重大的安全风险,但允许我们唯一地识别具有共享控制平面(例如堆叠交换机)和设备互连的多机箱设备。

在这种情况下,整个拓扑分析任务可以简化为分析每个设备上接收到的邻居数据。这使我们能够识别独特的设备及其互连(即拓扑图的顶点和边)。
顺便说一下,OSPF LSA 交换和分析的工作方式非常相似。可视化路由协议数据也可能是一个很好的用例(我建议查看@ Vadims06于 2020 年 10 月发布的Topolograph服务)。但是现在让我们专注于 LLDP。

在我们的实验室环境中,所有边缘、核心和分布层设备都应该看到它们的直接 LLDP 邻居。internet-rtr01 与网络的其余部分隔离,因此它不应有任何 LLDP 邻居。

这是来自 dist-rtr01的手动“显示 lldp 邻居”输出:

dist-rtr01#show lldp neighbors
Capability codes:(R) Router, (B) Bridge, (T) Telephone, (C) DOCSIS Cable Device(W) WLAN Access Point, (P) Repeater, (S) Station, (O) OtherDevice 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/2Total entries displayed: 5

五个邻居。看起来挺好的。
core-rtr02 的相同输出:

RP/0/0/CPU0:core-rtr02#show lldp neighbors
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) OtherDevice 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               Gi3Total entries displayed: 4

四个邻居。这也是正确的。
请注意,在这两种情况下,输出的设备 ID 列中都包含不完整的主机名。
CLI 自动化总是伴随着这样的问题。
在我们给定的情况下,解决方法是使用详细的输出格式。
举个例子:

显示来自基于 IOSXE 的 dist-rtr01 的 lldp 邻居详细信息
显示来自基于 NXOS 的 dist-sw01 的 lldp 邻居详细信息

从设备收集数据

我们将从运行 IOS (edge-sw01)、IOSXE (internet-rtr01, distr-rtr01, distr-rtr02) 和 NXOS (dist-sw01, dist-sw02) 的设备收集 LLDP 数据。
基于 IOS-XR 的核心路由器(core-rtr01、core-rtr02)将被有意限制为管理访问。
因此将涵盖以下场景:

  1. 所有分布层设备的全网状邻居处理。
    应正确发现所有唯一节点和链接。
  2. 处理 core-rtr01 和 core-rtr02 的设备访问或连接问题。
    这不应影响与其余设备一起工作的能力。
  3. 基于来自不连续网段的部分数据构建拓扑。
    边缘交换机和分布路由器都从不同的方面“看到”core-rtr01 和core-rtr02。
    这应该足以构建全貌。
完整清单/hosts_devnet_sb_cml.yml 主机文件内容

使用 NAPALM GETTER:

  • GET_LLDP_NEIGHBORS_DETAILS。
    选择详细的输出版本是因为它提供了更一致的数据。
  • GET_FACTS。
    它收集一些扩展设备信息,如 FQDN、型号、序列号等。

让我们将数据收集任务包装成一个 Nornir Task 函数。
这是对单个主机上的操作进行分组的有用方法之一。

def get_host_data(task):"""Nornir Task for data collection on target hosts."""task.run(task=napalm_get,getters=['facts', 'lldp_neighbors_detail'])

现在我们可以运行任务并将结果保存到变量中以供进一步处理。
默认执行范围是所有设备。

get_host_data_result = nr.run(get_host_data)

您还可以使用简单和复杂的清单过滤器将执行范围限制为单个主机或组。

处理从设备接收的数据

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"]}

每个主机结果对象都有返回布尔值的失败方法。False 表示在特定主机上执行任务期间未发生错误。
全局任务结果可作为字典对象迭代:

>>> 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 的结果对象内容
dist-sw01 的结果对象内容

结果对象是一个字典,其键与选定的 GETTER 名称匹配:'facts' 和 'lldp_neighbors_detail'。
字典值包含由 NAPALM 处理和返回的结构化数据。
让我们比较一个邻居集:

dist-rtr01 的 LLDP 邻居
dist-sw01 的 LLDP 邻居

dist-rtr01 的五个邻居和 dist-sw01 的四个邻居,这正是我们之前在 CLI 输出中看到的。
其余数据也是有效的。

为了便于处理,让我们将 LLDP 和事实数据拆分为单独的实体。
任何设备也可能出现在多个输出中。为了区分它们,需要使用一些唯一的节点标识符。让我们按以下降序选择它:

  • 设备 FQDN 如果可用(为了简单起见,可以进一步称为主机名)。
  • 设备主机名(如果可用)。
  • Nornir Inventory 中的设备主机对象名称。

LLDP 也依赖于前两个步骤。

def normalize_result(nornir_job_result):""" get_host_data result parser. Returns LLDP and FACTS data dicts with hostname keys. """global_lldp_data = {}global_facts = {}for device, output in nornir_job_result.items():if output[0].failed:# Write default data to dicts if the task is failed.# Use the host inventory object name as a key.global_lldp_data[device] = {}global_facts[device] = {'nr_ip': nr.inventory.hosts[device].get('hostname', 'n/a'),}continue# Use FQDN as unique ID for devices withing the script.device_fqdn = output[1].result['facts']['fqdn']if not device_fqdn:# If FQDN is not set use hostname.# LLDP TLV follows the same logic.device_fqdn = output[1].result['facts']['hostname']if not device_fqdn:# Use host inventory object name as a key if# neither FQDN nor hostname are setdevice_fqdn = deviceglobal_facts[device_fqdn] = output[1].result['facts']# Populate device facts with its IP address or hostname as per Inventory dataglobal_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

然后我们应该提取设备已知的所有邻居的列表并基于此进行构建:

  • 唯一主机列表。
  • 它们之间的唯一链接列表。

为确保链接的明确标识,我们将其存储为以下格式:
((source_device_id, source_port_name), (destination_device_id, destination_port_name))

还需要记住的是:

  • 如果我们从连接的两个设备收集数据,则该链接可能从两侧可见。
    我们必须检查 A 面和 B 面的排列以过滤掉重复项。
  • 在 LLDP 公告和本地输出中,端口名称的格式可能不同。例如,本地GigabitEthernet4和LLDP 端口名称中的Gi4

为了保证数据的一致性,我们将端口名称翻译成完整格式以供分析阶段使用。同时,让我们实现一个名称缩短功能,以在可视化过程中提供更好的视觉体验。
可以根据 LLDP 中通告的设备功能实现自动图标选择。让我们将它们提取到一个单独的 {"hostname": "primary_capability"} 字典中。
相同的代码明智:

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 ifnameif ifname.startswith(k):return ifname.replace(k, v)return ifnamedef if_shortname(ifname):for k, v in interface_full_name_map.items():if ifname.startswith(v):return ifname.replace(v, k)return ifnamedef extract_lldp_details(lldp_data_dict):""" LLDP data dict parser. Returns set of all the discovered hosts, LLDP capabilities dict with all LLDP-discovered host, and all discovered interconnections between hosts. """discovered_hosts = set()lldp_capabilities_dict = {}global_interconnections = []for host, lldp_data in lldp_data_dict.items():if not host:continuediscovered_hosts.add(host)if not lldp_data:continuefor interface, neighbors in lldp_data.items():for neighbor in neighbors:if not neighbor['remote_system_name']:continuediscovered_hosts.add(neighbor['remote_system_name'])if neighbor['remote_system_enable_capab']:# In case of multiple enable capabilities pick first in the listlldp_capabilities_dict[neighbor['remote_system_name']] = (neighbor['remote_system_enable_capab'][0])else:lldp_capabilities_dict[neighbor['remote_system_name']] = ''# Store interconnections in a following format:# ((source_hostname, source_port), (dest_hostname, dest_port))local_end = (host, interface)remote_end = (neighbor['remote_system_name'],if_fullname(neighbor['remote_port']))# Check if the link is not a permutation of already added one# (local_end, remote_end) equals (remote_end, local_end)link_is_already_there = ((local_end, remote_end) in global_interconnectionsor (remote_end, local_end) in global_interconnections)if link_is_already_there:continueglobal_interconnections.append(((host, interface),(neighbor['remote_system_name'], if_fullname(neighbor['remote_port']))))return [discovered_hosts, global_interconnections, lldp_capabilities_dict]

初始化 NeXt UI 应用程序

拓扑可视化逻辑将基于 Next UI在next_app.js脚本中实现。
让我们从基础开始:

(function (nx) {/** * NeXt UI based application */// Initialize topologyvar topo = new nx.graphic.Topology({// View dimensionswidth: 1200,height: 700,// Dataprocessor is responsible for spreading // the Nodes across the view.// 'force' data processor spreads the Nodes so// they would be as distant from each other// as possible. Follow social distancing and stay healthy.// 'quick' dataprocessor picks random positions// for the Nodes.dataProcessor: 'force',// Node and Link identity key attribute nameidentityKey: 'id',// Node settingsnodeConfig: {label: 'model.name',iconType:'model.icon',},// Link settingslinkConfig: {// Display Links as curves in case of // multiple links between Node Pairs.// Set to 'parallel' to use parallel links.linkType: 'curve',},// Display Node icon. Displays a dot if set to 'false'.showIcon: true,});var Shell = nx.define(nx.ui.Application, {methods: {start: function () {// Read topology data from variabletopo.data(topologyData);// Attach it to the documenttopo.attach(this);}}});// Create an application instancevar shell = new Shell();// Run the applicationshell.start();
})(nx);

拓扑数据结构将被存储在一个topologyData变量。让我们把它移到一个单独的topology.js文件中。格式细节将在下面讨论。

最终的可视化结果将显示在带有附加 JS 组件的本地 HTML 表单中:

<!DOCTYPE html><html><head><meta charset="utf-8"><link rel="stylesheet" href="next_sources/css/next.css"><link rel="stylesheet" href="styles_main_page.css"><script src="next_sources/js/next.js"></script><script src="topology.js"></script><script src="next_app.js"></script></head><body></body>
</html>

在 Python 中生成 NeXt UI 拓扑

我们已经编写了所需的数据收集结果处理程序,并使用 Python 数据结构对其进行了规范化。
让我们应用这个:

GLOBAL_LLDP_DATA, GLOBAL_FACTS = normalize_result(get_host_data_result)
TOPOLOGY_DETAILS = extract_lldp_details(GLOBAL_LLDP_DATA)

一般的 NeXt UI 拓扑表示如下所示:

// Two nodes connected with two links
var topologyData = {"links": [{"id": 0,"source": 0,"target": 1,}, {"id": 1,"source": 0,"target": 1,}],"nodes": [{"icon": "router","id": 0,},{"icon": "router","id": 1,}]

如您所见,这是一个 JSON 对象,它可以映射到以下格式的 Python 数据结构:{'nodes': [], 'links': []}。
我们将把我们所有的数据放在一起。
此外,让我们在选择节点图标时考虑设备模型,以处理缺少 LLDP 功能的情况。
节点对象还将填充一些从 GET_FACTS(模型、S/N 等)派生的扩展属性,以丰富拓扑视图。

icon_capability_map = {'router': 'router','switch': 'switch','bridge': 'switch','station': 'host'
}icon_model_map = {'CSR1000V': 'router','Nexus': 'switch','IOSXRv': 'router','IOSv': 'switch','2901': 'router','2911': 'router','2921': 'router','2951': 'router','4321': 'router','4331': 'router','4351': 'router','4421': 'router','4431': 'router','4451': 'router','2960': 'switch','3750': 'switch','3850': 'switch',
}def get_icon_type(device_cap_name, device_model=''):""" Device icon selection function. Selection order: - LLDP capabilities mapping. - Device model mapping. - Default 'unknown'. """if device_cap_name:icon_type = icon_capability_map.get(device_cap_name)if icon_type:return icon_typeif device_model:# Check substring presence in icon_model_map keys# string until the first matchfor model_shortname, icon_type in icon_model_map.items():if model_shortname in device_model:return icon_typereturn 'unknown'def generate_topology_json(*args):""" JSON topology object generator. Takes as an input: - discovered hosts set, - LLDP capabilities dict with hostname keys, - interconnections list, - facts dict with hostname keys. """discovered_hosts, interconnections, lldp_capabilities_dict, facts = argshost_id = 0host_id_map = {}topology_dict = {'nodes': [], 'links': []}for host in discovered_hosts:device_model = 'n/a'device_serial = 'n/a'device_ip = 'n/a'if facts.get(host):device_model = facts[host].get('model', 'n/a')device_serial = facts[host].get('serial_number', 'n/a')device_ip = facts[host].get('nr_ip', 'n/a')host_id_map[host] = host_idtopology_dict['nodes'].append({'id': host_id,'name': host,'primaryIP': device_ip,'model': device_model,'serial_number': device_serial,'icon': get_icon_type(lldp_capabilities_dict.get(host, ''),device_model)})host_id += 1link_id = 0for link in interconnections:topology_dict['links'].append({'id': link_id,'source': host_id_map[link[0][0]],'target': host_id_map[link[1][0]],'srcIfName': if_shortname(link[0][1]),'srcDevice': link[0][0],'tgtIfName': if_shortname(link[1][1]),'tgtDevice': link[1][0],})link_id += 1return topology_dict

然后我们应该将这个 Python 拓扑字典写入到topology.js文件中。一个标准的json模块将为此完美地提供可读和格式化的输出:

import jsonOUTPUT_TOPOLOGY_FILENAME = 'topology.js'
TOPOLOGY_FILE_HEAD = "\n\nvar topologyData = "def write_topology_file(topology_json, header=TOPOLOGY_FILE_HEAD, dst=OUTPUT_TOPOLOGY_FILENAME):with open(dst, 'w') as topology_file:topology_file.write(header)topology_file.write(json.dumps(topology_json, indent=4, sort_keys=True))topology_file.write(';')TOPOLOGY_DICT = generate_topology_json(*TOPOLOGY_DETAILS)
write_topology_file(TOPOLOGY_DICT)
生成的topology.js 文件内容

现在让我们最终运行main.html并查看我们的可视化 Hello World:

看起来正确。显示所有已知节点和它们之间的链接。
节点可以在任何方向拖动。鼠标单击节点和链接时,会出现 Next UI 工具提示菜单。它包含我们之前在 Python 中传递给节点和链接拓扑对象的所有属性:

还不错。还有改进的余地。稍后我们将回到这一点。现在让我们为任务的第二部分实现一个解决方案。

检测和可视化拓扑变化

一个额外的任务是检测和可视化拓扑变化。
为了完成它,需要一些补充:

  • 拓扑缓存文件cached_topology.js用于存储先前的拓扑状态。
    generate_topology.py脚本将在每次运行时读取此缓存文件,并在必要时使用更新的状态重写。
  • diff_topology.js拓扑文件,用于存储差异拓扑。
  • diff_page.html页面以显示可视化的拓扑差异。

Diff HTML 表单将如下所示:

<!DOCTYPE html><html><head><meta charset="utf-8"><link rel="stylesheet" href="next_sources/css/next.css"><link rel="stylesheet" href="styles_main_page.css"><script src="next_sources/js/next.js"></script><script src="diff_topology.js"></script><script src="next_app.js"></script></head><body><a href="main.html"><button>Display current topology</button></a></p></body>
</html>

我们只需要读写拓扑缓存文件:

CACHED_TOPOLOGY_FILENAME = 'cached_topology.json'def write_topology_cache(topology_json, dst=CACHED_TOPOLOGY_FILENAME):with open(dst, 'w') as cached_file:cached_file.write(json.dumps(topology_json, indent=4, sort_keys=True))def read_cached_topology(filename=CACHED_TOPOLOGY_FILENAME):if not os.path.exists(filename):return {}if not os.path.isfile(filename):return {}cached_topology = {}with open(filename, 'r') as file:try:cached_topology = json.loads(file.read())except:return {}return cached_topology

拓扑差异分析步骤:

  1. 从当前和缓存的拓扑字典中提取节点和链接属性以进行比较。

    节点格式:(
    具有所有属性的节点对象,(主机名,))
    链接格式:(
    具有所有属性的链接对象,(源主机名,源端口),(目标主机名,目标端口))
    节点和链接格式都允许进一步扩展。

  2. 比较提取的节点和链接对象。应考虑链接格式排列。

    节点和链接的差异结果将按以下格式写入两个字典:

    • diff_nodes = {'添加':[],'删除':[]}
    • diff_links = {'添加':[],'删除':[]}
  3. 将当前和缓存的拓扑与差异数据合并。
    结果拓扑将写入 diff_merged_topology 字典。
    删除的节点和链接对象将使用is_dead属性进行扩展。为了获得更好的视觉体验,将自定义删除的节点图标(下面将讨论对此的下一步 UI 更改)。
    新节点和链接对象将使用is_new属性进行扩展。

让我们编码:

def get_topology_diff(cached, current):""" Topology diff analyzer and generator. Accepts two valid topology dicts as an input. Returns: - dict with added and deleted nodes, - dict with added and deleted links, - dict with merged input topologies with extended attributes for topology changes visualization """diff_nodes = {'added': [], 'deleted': []}diff_links = {'added': [], 'deleted': []}diff_merged_topology = {'nodes': [], 'links': []}# Parse links from topology dicts into the following format:# (topology_link_obj, (source_hostnme, source_port), (dest_hostname, dest_port))cached_links = [(x, ((x['srcDevice'], x['srcIfName']), (x['tgtDevice'], x['tgtIfName']))) for x in cached['links']]links = [(x, ((x['srcDevice'], x['srcIfName']), (x['tgtDevice'], x['tgtIfName']))) for x in current['links']]# Parse nodes from topology dicts into the following format:# (topology_node_obj, (hostname,))# Some additional values might be added for comparison later on to the tuple above.cached_nodes = [(x, (x['name'],)) for x in cached['nodes']]nodes = [(x, (x['name'],)) for x in current['nodes']]# Search for deleted and added hostnames.node_id = 0host_id_map = {}for raw_data, node in nodes:if node in [x[1] for x in cached_nodes]:raw_data['id'] = node_idhost_id_map[raw_data['name']] = node_idraw_data['is_new'] = 'no'raw_data['is_dead'] = 'no'diff_merged_topology['nodes'].append(raw_data)node_id += 1continuediff_nodes['added'].append(node)raw_data['id'] = node_idhost_id_map[raw_data['name']] = node_idraw_data['is_new'] = 'yes'raw_data['is_dead'] = 'no'diff_merged_topology['nodes'].append(raw_data)node_id += 1for raw_data, cached_node in cached_nodes:if cached_node in [x[1] for x in nodes]:continuediff_nodes['deleted'].append(cached_node)raw_data['id'] = node_idhost_id_map[raw_data['name']] = node_idraw_data['is_new'] = 'no'raw_data['is_dead'] = 'yes'raw_data['icon'] = 'dead_node'diff_merged_topology['nodes'].append(raw_data)node_id += 1# Search for deleted and added interconnections.# Interface change on some side is considered as# one interconnection deletion and one interconnection insertion.# Check for permutations as well:# ((h1, Gi1), (h2, Gi2)) and ((h2, Gi2), (h1, Gi1)) are equal.link_id = 0for raw_data, link in links:src, dst = linkif not (src, dst) in [x[1] for x in cached_links] and not (dst, src) in [x[1] for x in cached_links]:diff_links['added'].append((src, dst))raw_data['id'] = link_idlink_id += 1raw_data['source'] = host_id_map[src[0]]raw_data['target'] = host_id_map[dst[0]]raw_data['is_new'] = 'yes'raw_data['is_dead'] = 'no'diff_merged_topology['links'].append(raw_data)continueraw_data['id'] = link_idlink_id += 1raw_data['source'] = host_id_map[src[0]]raw_data['target'] = host_id_map[dst[0]]raw_data['is_new'] = 'no'raw_data['is_dead'] = 'no'diff_merged_topology['links'].append(raw_data)for raw_data, link in cached_links:src, dst = linkif not (src, dst) in [x[1] for x in links] and not (dst, src) in [x[1] for x in links]:diff_links['deleted'].append((src, dst))raw_data['id'] = link_idlink_id += 1raw_data['source'] = host_id_map[src[0]]raw_data['target'] = host_id_map[dst[0]]raw_data['is_new'] = 'no'raw_data['is_dead'] = 'yes'diff_merged_topology['links'].append(raw_data)return diff_nodes, diff_links, diff_merged_topology

get_topology_diff 实现了两个任意有效格式的拓扑字典的比较
这允许我们在未来实现拓扑缓存版本控制。
让我们也实现一个控制台差异打印功能:

def print_diff(diff_result):""" Formatted get_topology_diff result console print function. """diff_nodes, diff_links, *ignore = diff_resultif not (diff_nodes['added'] or diff_nodes['deleted'] or diff_links['added'] or diff_links['deleted']):print('No topology changes since last run.')returnprint('Topology changes have been discovered:')if diff_nodes['added']:print('')print('^^^^^^^^^^^^^^^^^^^^')print('New Network Devices:')print('vvvvvvvvvvvvvvvvvvvv')for node in diff_nodes['added']:print(f'Hostname: {node[0]}')if diff_nodes['deleted']:print('')print('^^^^^^^^^^^^^^^^^^^^^^^^')print('Deleted Network Devices:')print('vvvvvvvvvvvvvvvvvvvvvvvv')for node in diff_nodes['deleted']:print(f'Hostname: {node[0]}')if diff_links['added']:print('')print('^^^^^^^^^^^^^^^^^^^^^^')print('New Interconnections:')print('vvvvvvvvvvvvvvvvvvvvvv')for src, dst in diff_links['added']:print(f'From {src[0]}({src[1]}) To {dst[0]}({dst[1]})')if diff_links['deleted']:print('')print('^^^^^^^^^^^^^^^^^^^^^^^^^')print('Deleted Interconnections:')print('vvvvvvvvvvvvvvvvvvvvvvvvv')for src, dst in diff_links['deleted']:print(f'From {src[0]}({src[1]}) To {dst[0]}({dst[1]})')print('')

最后,让我们将上面编写的代码片段总结成一个专用的 main() 函数。
这是一个相当自我记录的代码和我个人对“为什么不 Ansible”这个问题的回答:

def good_luck_have_fun():"""Main script logic"""get_host_data_result = nr.run(get_host_data)GLOBAL_LLDP_DATA, GLOBAL_FACTS = normalize_result(get_host_data_result)TOPOLOGY_DETAILS = extract_lldp_details(GLOBAL_LLDP_DATA)TOPOLOGY_DETAILS.append(GLOBAL_FACTS)TOPOLOGY_DICT = generate_topology_json(*TOPOLOGY_DETAILS)CACHED_TOPOLOGY = read_cached_topology()write_topology_file(TOPOLOGY_DICT)write_topology_cache(TOPOLOGY_DICT)print('Open main.html in a project root with your browser to view the topology')if CACHED_TOPOLOGY:DIFF_DATA = get_topology_diff(CACHED_TOPOLOGY, TOPOLOGY_DICT)print_diff(DIFF_DATA)write_topology_file(DIFF_DATA[2], dst='diff_topology.js')if topology_is_changed:print('Open diff_page.html in a project root to view the changes.')print("Optionally, open main.html and click 'Display diff' button")else:# write current topology to diff file if the cache is missingwrite_topology_file(TOPOLOGY_DICT, dst='diff_topology.js')if __name__ == '__main__':good_luck_have_fun()

测试

首先,让我们限制对 dist-rtr01 的访问并运行脚本。得到的拓扑:

然后让我们恢复对 dist-rtr02 的访问,限制对 edge-sw01 的访问,然后再次执行脚本。
以前的版本被缓存。当前拓扑如下所示:

基于它们的比较的 Diff 拓扑文件 diff_topology.js。

上次运行的控制台输出:

$ python3.7 generate_topology.py
Open main.html in a project root with your browser to view the topologyTopology changes have been discovered:^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
New network devices:
vvvvvvvvvvvvvvvvvvvvvvvvvvvvv
Hostname: dist-rtr01.devnet.lab^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Deleted devices:
vvvvvvvvvvvvvvvvvvvvvvvvvvvvv
Hostname: edge-sw01.devnet.lab^^^^^^^^^^^^^^^^^^^^^
New interconnections:
vvvvvvvvvvvvvvvvvvvvv
From dist-rtr01.devnet.lab(Gi3) To core-rtr02.devnet.lab(Gi0/0/0/2)
From dist-rtr01.devnet.lab(Gi4) To dist-sw01.devnet.lab(Eth1/3)
From dist-rtr01.devnet.lab(Gi6) To dist-rtr02.devnet.lab(Gi6)
From dist-rtr01.devnet.lab(Gi5) To dist-sw02.devnet.lab(Eth1/3)
From dist-rtr01.devnet.lab(Gi2) To core-rtr01.devnet.lab(Gi0/0/0/2)^^^^^^^^^^^^^^^^^^^^^^^^^
Deleted interconnections:
vvvvvvvvvvvvvvvvvvvvvvvvv
From edge-sw01.devnet.lab(Gi0/2) To core-rtr01.devnet.lab(Gi0/0/0/1)
From edge-sw01.devnet.lab(Gi0/3) To core-rtr02.devnet.lab(Gi0/0/0/1)Open diff_page.html to view the changes.
Optionally, open main.html and click the 'Display diff' button

一切看起来都正确。输出与更改匹配。
为了正确地可视化差异拓扑,我们将在下面的 next_app.js 中进行一些调整。

增强 NeXt UI 应用程序

下面的大部分改进都是基于 Next UI 文档和教程中的示例进行的。

添加接口标签

为了添加接口标签,让我们扩展标准的 nx.graphic.Topology.Link 类:

    nx.define('CustomLinkClass', nx.graphic.Topology.Link, {properties: {sourcelabel: null,targetlabel: null},view: function(view) {view.content.push({name: 'source',type: 'nx.graphic.Text',props: {'class': 'sourcelabel','alignment-baseline': 'text-after-edge','text-anchor': 'start'}}, {name: 'target',type: 'nx.graphic.Text',props: {'class': 'targetlabel','alignment-baseline': 'text-after-edge','text-anchor': 'end'}});return view;},methods: {update: function() {this.inherited();var el, point;var line = this.line();var angle = line.angle();var stageScale = this.stageScale();line = line.pad(18 * stageScale, 18 * stageScale);if (this.sourcelabel()) {el = this.view('source');point = line.start;el.set('x', point.x);el.set('y', point.y);el.set('text', this.sourcelabel());el.set('transform', 'rotate(' + angle + ' ' + point.x + ',' + point.y + ')');el.setStyle('font-size', 12 * stageScale);}if (this.targetlabel()) {el = this.view('target');point = line.end;el.set('x', point.x);el.set('y', point.y);el.set('text', this.targetlabel());el.set('transform', 'rotate(' + angle + ' ' + point.x + ',' + point.y + ')');el.setStyle('font-size', 12 * stageScale);}}}});

现在可以在拓扑拓扑对象属性中列出自定义链接类。
让我们还突出显示 diff 拓扑上的链接。新链接为绿色,删除的链接为红色和虚线。

linkConfig: {linkType: 'curve',sourcelabel: 'model.srcIfName',targetlabel: 'model.tgtIfName',style: function(model) {if (model._data.is_dead === 'yes') {// Deleted links contain 'is_dead' attribute set to 'yes'.// Make them dashed.return { 'stroke-dasharray': '5' }}},color: function(model) {if (model._data.is_dead === 'yes') {// Deleted links contain 'is_dead' attribute set to 'yes'// Make them red.return '#E40039'}if (model._data.is_new === 'yes') {// New links contain 'is_new' attribute set to 'yes'// Make them green.return '#148D09'}},
},
// Use extended link class version with interface labels enabled
linkInstanceClass: 'CustomLinkClass' 

添加自定义节点图标

Next UI 已经包含一组广泛的网络设备默认图标。
但是,您可以根据自己的要求自由添加自定义图标。对于删除的节点,我们需要一些特别的东西。
要添加新图标,您应该将图像放入 Next UI 可访问的目录中,并在拓扑对象中对其进行初始化。

// the image is saved to ./img/dead_node.png
topo.registerIcon("dead_node", "img/dead_node.png", 49, 49);

完成差异可视化

我们实际上已经做了我们需要的一切。让我们打开diff_page.html看看我们之前所做的更改是怎样的:

拓扑视图不言自明,不是吗?

更新节点工具提示

默认情况下,节点工具提示包含过多的信息,如内部节点 ID 和坐标。
在 NeXt UI 中,可以对其进行自定义以获得更好的可读性和可用性。
现在让我们包括以下信息:

  • 设备主机名。
    让我们也让主机名成为指向任意资源的可定制链接。它可能是 Netbox 中的设备页面或监控系统。
    链接模板将存储在 dcimDeviceLink 变量中。
    它可以在拓扑生成过程中添加。在缺少值的情况下,主机名将只是一个简单的文本。
  • 设备 IP 地址、序列号和型号。

为了实现这一点,让我们扩展一个标准的 nx.ui.Component 类并在其中构建一个简单的 HTML 表单:

    nx.define('CustomNodeTooltip', nx.ui.Component, {properties: {node: {},topology: {}},view: {content: [{tag: 'div',content: [{tag: 'h5',content: [{tag: 'a',content: '{#node.model.name}',props: {"href": "{#node.model.dcimDeviceLink}"}}],props: {"style": "border-bottom: dotted 1px; font-size:90%; word-wrap:normal; color:#003688"}}, {tag: 'p',content: [{tag: 'label',content: 'IP: ',}, {tag: 'label',content: '{#node.model.primaryIP}',}],props: {"style": "font-size:80%;"}},{tag: 'p',content: [{tag: 'label',content: 'Model: ',}, {tag: 'label',content: '{#node.model.model}',}],props: {"style": "font-size:80%;"}}, {tag: 'p',content: [{tag: 'label',content: 'S/N: ',}, {tag: 'label',content: '{#node.model.serial_number}',}],props: {"style": "font-size:80%; padding:0"}},],props: {"style": "width: 150px;"}}]}});nx.define('Tooltip.Node', nx.ui.Component, {view: function(view){view.content.push({});return view;},methods: {attach: function(args) {this.inherited(args);this.model();}}});

现在自定义类版本可以在拓扑拓扑对象属性中列出:

tooltipManagerConfig: {nodeTooltipContentClass: 'CustomNodeTooltip'
},

这是我们在此之后在节点上单击鼠标时看到的:

自定义节点布局

如前所述,默认的 Next UI 数据处理器是“force”。它依赖于传播节点的尽力而为算法,因此它们会尽可能远离彼此。

即使对于复杂的分层网络拓扑,此逻辑也能生成适当的布局,但拓扑层可能不会按需要定向。当然,您可以在之后手动拖动它们。然而,这不是我们的方式。

幸运的是,有一些内置工具可以在 Next UI 中处理图层。
让我们在 Next UI 应用程序的节点内使用一个新的数字layerSortPreference属性。
在这种情况下,定义该值的逻辑可以在拓扑对象生成阶段的可视化应用程序之外实现。下一个 UI 只会按照我们告诉它的方式对图层进行排序。这是一种更具可扩展性的方法。

让我们添加一些功能,以便能够在布局之间切换:

    var currentLayout = 'auto'horizontal = function() {if (currentLayout === 'horizontal') {return;};currentLayout = 'horizontal';var layout = topo.getLayout('hierarchicalLayout');layout.direction('horizontal');layout.levelBy(function(node, model) {return model.get('layerSortPreference');});topo.activateLayout('hierarchicalLayout');};vertical = function() {if (currentLayout === 'vertical') {return;};currentLayout = 'vertical';var layout = topo.getLayout('hierarchicalLayout');layout.direction('vertical');layout.levelBy(function(node, model) {return model.get('layerSortPreference');});topo.activateLayout('hierarchicalLayout');};

将这些函数映射到 main.html 和 diff_page.html 上的按钮元素:

<button onclick='horizontal()'>Horizontal layout</button>
<button onclick="vertical()">Vertical layout</button>

让我们改进generate_topology.py脚本并向 Nornir hosts 文件添加一些附加属性以实现自动节点层次结构计算。
该脚本将定义一个人性化的图层名称的有序列表,并将其转换为数值:

# Topology layers would be sorted
# in the same descending order
# as in the tuple below
NX_LAYER_SORT_ORDER = ('undefined','outside','edge-switch','edge-router','core-router','core-switch','distribution-router','distribution-switch','leaf','spine','access-switch'
)def get_node_layer_sort_preference(device_role):"""Layer priority selection function Layer sort preference is designed as a numeric value. This function identifies it by NX_LAYER_SORT_ORDER object position by default. With numeric values, the logic may be improved without changes on the NeXt app side. 0(null) causes an undefined layer position in the NeXt UI. Valid indexes start with 1. """for i, role in enumerate(NX_LAYER_SORT_ORDER, start=1):if device_role == role:return ireturn 1

图层的数字图层排序顺序将由其在 NX_LAYER_SORT_ORDER 中的相对位置定义。
重要提示:NeXt UI 将 0(null) 解释为未定义。有效的图层索引从 1 开始。

设备层将基于其在 Nornir 主机清单文件中的角色属性。
数据字段允许我们指定具有任意名称的属性列表:

dist-rtr01:hostname: 10.10.20.175platform: iosgroups:- devnet-cml-labdata:role: distribution-router

然后可以在 Python 中调用主机数据中的任何属性作为字典键,如下所示:

nr.inventory.hosts[device].get('some_attribute_name')

为了反映这些变化,让我们更新我们的 Python 代码。一个新的nr_role节点属性将与其他属性一起附加到normalize_result函数内的global_facts 中

# Full function is omitted here for brevity
global_facts[device_fqdn]['nr_role'] = nr.inventory.hosts[device].get('role', 'undefined')

然后我们应该在generate_topology_json函数中的节点对象生成期间读取这个属性:

# Full function is omitted here for brevity
device_role = facts[host].get('nr_role', 'undefined')
topology_dict['nodes'].append({'id': host_id,'name': host,'primaryIP': device_ip,'model': device_model,'serial_number': device_serial,'layerSortPreference': get_node_layer_sort_preference(device_role),'icon': get_icon_type(lldp_capabilities_dict.get(host, ''),device_model)
})

现在我们可以控制混乱在按钮点击时水平和垂直对齐图层。这是它的样子:

结果项目结构

最终的项目结构如下所示:

$ tree . -L 2
.
├── LICENSE
├── README.md
├── diff_page.html
├── diff_topology.js
├── generate_topology.py
├── img
│   └── dead_node.png
├── inventory
│   ├── groups.yml
│   └── hosts_devnet_sb_cml.yml
├── main.html
├── next_app.js
├── next_sources
│   ├── css
│   ├── doc
│   ├── fonts
│   └── js
├── nornir_config.yml
├── requirements.txt
├── samples
│   ├── sample_diff.png
│   ├── sample_layout_horizontal.png
│   ├── sample_link_details.png
│   ├── sample_node_details.png
│   └── sample_topology.png
├── styles_main_page.css
└── topology.js

结论

首先,感谢您的阅读。我希望你喜欢它。

在本文中,我尝试重现并记录解决方案创建阶段及其背后的考虑因素。

我的GitHub页面上提供了完整的源代码。

根据与会者和组织者的投票,该解决方案在马拉松比赛中名列第一。同样重要的是,它具有重复使用和可扩展性的电势(扰流板:我公司开发的NETBOX插件重用从这个项目的核心代码)。

你觉得这个解决方案怎么样?有什么可以改进的?你会如何解决这个任务?
请随时分享您自己的经验和想法。

可视化网络拓扑:两天之内从零到英雄相关推荐

  1. R语言ggplot2可视化将两个dataframe可视化的结果组合在一起实战:combining two plots from different data.frames

    R语言ggplot2可视化将两个dataframe可视化的结果组合在一起实战:combining two plots from different data.frames 目录 R语

  2. Chrome用户请尽快更新:谷歌发现两个严重的零日漏洞

    强烈建议:Chrome用户请尽快升级浏览器!在谷歌今天发布的紧急补丁程序中修复了两个严重的零日漏洞,而其中一个已经被黑客利用.Chrome安全小组表示,这两个漏洞均为use-after-free形式, ...

  3. boilerplate_完整的React Boilerplate教程-从零到英雄

    boilerplate by Leonardo Maldonado 莱昂纳多·马尔多纳多(Leonardo Maldonado) 完整的React Boilerplate教程-从零到英雄 (A Com ...

  4. javascript测试_了解有关JavaScript承诺的更多信息:25次测试中从零到英雄

    javascript测试 by Andrea Koutifaris 由Andrea Koutifaris 了解有关JavaScript承诺的更多信息:25次测试中从零到英雄 (Learn more a ...

  5. 从零开发英雄联盟、王者荣耀电竞比分预测系统

    从零开发英雄联盟.王者荣耀电竞比分预测系统 快速开发一款电竞比分预测系统 想要快速熟悉电竞比分预测的逻辑,你总是需要付出时间和精力的,对于程序员的成长,最好的方法就是从一个项目入手,下面让我来教你手把 ...

  6. 「合规」震惊!地图可视化竟能如此玩,零门槛,全免费,效果远胜主流作图工具!...

    在数据可视化领域,早晚会遇上地图可视化的需求,一个高大上的地图可视化,瞬间拔高整个报告的层次. Excel催化剂有幸接触并将地图可视化完全落地于Excel中完成.相对主流Python.R.PowerB ...

  7. Pandas可视化综合指南:手把手从零教你绘制数据图表

    晓查 编译整理 量子位 出品 | 公众号 QbitAI 数据可视化本来是一个非常复杂的过程,但随着Pandas数据帧plot()函数的出现,使得创建可视化图形变得很容易. 在数据帧上进行操作的plot ...

  8. labview曲线上两点画延长线_零失手的‘万能眼线公式’,关键鼻翼延长线、画出适合自己的眼线...

    眼线其实和画眉一样,不能只是跟着流行画,如果不适合自己很容易变灾难的!这回有请专家教学怎么画出最适合自己的眼线,搭配上几个化妆小技巧,你也能零失手画出放电眼神唷!一起看看吧-右滑图片-看怎么画出IU这 ...

  9. caffe学习日记--lesson7:caffemodel可视化的两种方法

    在Caffe中,目前有两种可视化prototxt格式网络结构的方法: 使用Netscope在线可视化 使用Caffe提供的draw_net.py 本文将就这两种方法加以介绍 Netscope:支持Ca ...

最新文章

  1. 用C#实现的条形码和二维码编码解码器
  2. 列举一些分析次级代谢物基因簇相关的数据库
  3. 三菱fx5u编程手册_实用分享 | 三菱FX 5U特点是什么?
  4. Dalvik/ART(ANDROID)中的多线程机制(1)
  5. 理解 C# 项目 csproj 文件格式的本质和编译流程
  6. 用python画树_Python+Turtle动态绘制一棵树实例分享
  7. Java for LeetCode 042 Trapping Rain Water
  8. vs2008 USB转COM口发送字符中途出错的问题.
  9. JavaEE基础(01):Servlet实现方式,生命周期执行过程
  10. WINDOWS常用端口列表
  11. webstorm(10.0.2)设置测试服务器 -- 局域网内其他设备访问
  12. PowerDesigner数据模型(CDM—PDM)
  13. 静态链接库和动态链接库
  14. revit 转换ifc_将IFC转换成GLTF格式
  15. Golang 逃逸分析
  16. 别再用PS啦!用Excel轻松实现更换证件照背景颜色!
  17. LinkedHashMap倒叙反转
  18. 未收到服务器返回信息吗,inode 未收到服务器回应
  19. c语言一些算法解题技巧,c语言常见小算法的解题思路.doc
  20. linux用shell脚本写游戏,shell脚本实现猜数游戏

热门文章

  1. Python 中的取余与取整操作
  2. unshift() :将一个或多个元素添加到数组的开头
  3. 怎样远程控制别人的电脑
  4. mywife.cc 神一样的存在!
  5. windows添加右键菜单
  6. 微擎支付返回商户单号_微信小程序支付流程
  7. 什么是TCP/IP协议?
  8. 使用一条sql语句在postgres中查询总数和分页数据
  9. c#在output窗口输出调试信息
  10. 4.计算机网络与信息安全