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

Visualizing Network Topologies: Zero to Hero in Two Days / Habricon-default.png?t=LA46https://habr.com/en/post/534716/

翻译

嘿大家!这是我在 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 框架允许在我上面提到的工具之上提供附加功能。在我们的案例中,其中最有用的两个是NAPALMNornir。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.SimpleInventory
    options:
        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.181
    platform: ios
    groups:
        - devnet-cml-lab

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

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

 

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

 
---

devnet-cml-lab:
    username: cisco
    password: cisco
    connection_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_get

NORNIR_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) 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#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) 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
 

四个邻居。这也是正确的。
请注意,在这两种情况下,输出的设备 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 set
            device_fqdn = device
        global_facts[device_fqdn] = output[1].result['facts']
        # Populate device facts with its IP address or hostname as per Inventory data
        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
 

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

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

为确保链接的明确标识,我们将其存储为以下格式:
((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 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 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:
            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']:
                    # In case of multiple enable capabilities pick first in the list
                    lldp_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_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 UI在next_app.js脚本中实现。
让我们从基础开始:

 
(function (nx) {
    /** * NeXt UI based application */
    // Initialize topology
    var topo = new nx.graphic.Topology({
        // View dimensions
        width: 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 name
        identityKey: 'id',
        // Node settings
        nodeConfig: {
            label: 'model.name',
            iconType:'model.icon',
        },
        // Link settings
        linkConfig: {
            // 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 variable
                topo.data(topologyData);
                // Attach it to the document
                topo.attach(this);
            }
        }
    });

    // Create an application instance
    var shell = new Shell();
    // Run the application
    shell.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_type
    if device_model:
        # Check substring presence in icon_model_map keys
        # string until the first match
        for model_shortname, icon_type in icon_model_map.items():
            if model_shortname in device_model:
                return icon_type
    return '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 = args
    host_id = 0
    host_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_id
        topology_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 += 1
    link_id = 0
    for 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 += 1
    return topology_dict
 

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

 
import json

OUTPUT_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 = 0
    host_id_map = {}
    for raw_data, node in nodes:
        if node in [x[1] for x in cached_nodes]:
            raw_data['id'] = node_id
            host_id_map[raw_data['name']] = node_id
            raw_data['is_new'] = 'no'
            raw_data['is_dead'] = 'no'
            diff_merged_topology['nodes'].append(raw_data)
            node_id += 1
            continue
        diff_nodes['added'].append(node)
        raw_data['id'] = node_id
        host_id_map[raw_data['name']] = node_id
        raw_data['is_new'] = 'yes'
        raw_data['is_dead'] = 'no'
        diff_merged_topology['nodes'].append(raw_data)
        node_id += 1
    for raw_data, cached_node in cached_nodes:
        if cached_node in [x[1] for x in nodes]:
            continue
        diff_nodes['deleted'].append(cached_node)
        raw_data['id'] = node_id
        host_id_map[raw_data['name']] = node_id
        raw_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 = 0
    for raw_data, link in links:
        src, dst = link
        if 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_id
            link_id += 1
            raw_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)
            continue
        raw_data['id'] = link_id
        link_id += 1
        raw_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 = link
        if 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_id
            link_id += 1
            raw_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_result
    if not (diff_nodes['added'] or diff_nodes['deleted'] or diff_links['added'] or diff_links['deleted']):
        print('No topology changes since last run.')
        return
    print('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 missing
        write_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 topology

Topology 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 i
    return 1
 

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

 

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

 
dist-rtr01:
    hostname: 10.10.20.175
    platform: ios
    groups:
        - devnet-cml-lab
    data:
        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插件重用从这个项目的核心代码)。

 

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

 

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Python数据可视化是一种强大的工具,可以帮助我们更好地理解和分析数据。本指南将介绍如何使用Python绘制专业图表,包括线图、散点图、柱状图、饼图、热力图等。我们将使用Python的常用数据可视化库,如Matplotlib、Seaborn和Plotly等。通过本指南,您将学习如何选择合适的图表类型、调整图表样式、添加标签和注释等技巧,以及如何将图表嵌入到网页或报告中。无论您是数据分析师、科学家、工程师还是学生,本指南都将为您提供有用的技能和知识,帮助您更好地展示和传达数据。 ### 回答2: Python是一个功能强大且灵活的编程语言,可以用于数据处理和可视化。由于其易用性和广泛的社区支持,Python成为了数据科学家、研究人员和工程师最喜欢的编程语言之一。Python提供了一些非常强大的库,如Matplotlib,Seaborn等,可以绘制几乎所有类型的图表。 Python数据可视化之美:专业图表绘制指南是一本教授如何使用Python绘制各种图表的书籍。它旨在帮助初学者和专业人士创建专业的图形,以便更好地理解和传达数据。以下是本书的一些要点。 首先,本书介绍了数据可视化的原则和最佳实践。在这里,作者讨论了数据可视化的目的和优点,以及如何选择最佳的可视化工具。 其次,本书提供了一些基本的绘图技巧,如创建柱形图、线形图、散点图和饼图等。此外,还介绍了如何添加标签、颜色和其他元素,以使图表更具吸引力和专业性。 然后,本书介绍了一些高级绘图技巧,如处理时间序列数据、创建多个子图和使用Seaborn绘图库。这些技术有助于创建更复杂和专业的图表。 最后,本书提供了许多实用的例子来概述如何应用这些技术来解决真实的数据科学问题。这些例子包括如何分析数据并识别趋势、如何可视化预测结果,以及如何使用地理信息系统(GIS)数据。 总的来说,Python数据可视化之美:专业图表绘制指南是一本非常实用、丰富和易于理解的书籍。无论您是学生、研究人员、工程师还是任何想要使用Python进行数据可视化的人,都可以从本书中受益。它提供了广泛的教程和实例,使您可以轻松创建各种类型的图表,同时增强您对数据的理解。 ### 回答3: 数据可视化是数据分析和处理工作流程中非常重要的一部分。由于很多数据分析工程师使用Python进行分析,因此掌握Python数据可视化技术非常有必要。Python数据可视化之美:专业图表绘制指南提供了广泛的数据可视化技术,帮助用户将数据转换为易于读取的图形。 Python数据可视化之美:专业图表绘制指南提供了许多常见图表的绘制方法,包括散点图、折线图、柱状图、饼图、分布图等等。这些方法被精细地整合在几个包中,例如Matplotlib、Seaborn等等。例如,Seaborn可以轻松制作双变量的比较图,使得预测模型的评估更容易,而Matplotlib则可以定制更多的视觉效果,创造更精美的图表。 Python数据可视化之美:专业图表绘制指南不仅提供了单一的图表绘制方法,而且介绍了如何绘制多个图表并将其组合在一起。例如,可以将散点图和折线图组合在一起来展示两个变量之间的关系,并可以在同一张图表上显示多个系列。这些技术使得制作复杂的图表变得非常容易,并且可以将多个图表组合在一起分享。 Python数据可视化之美:专业图表绘制指南的优点不仅在于提供了普通数据分析工程师经常使用的图表绘制方法,还在于包括一些复杂的方法,例如制作3D图、热图、动态图和交互式图表。这些复杂的图表非常适合高级数据分析操作和数据科学项目,可以帮助用户更好地理解数据。 总之, Python数据可视化之美:专业图表绘制指南是给人们在数据分析工程师领域内提供好用和强大工具的一本书。它提供了许多有用的技术和工具,使得数据可视化过程变得更加简单、容易。这个书的内容可以为那些使用Python进行大规模数据库处理的数据分析工程师提供非常有价值的帮助,而它也适合那些处于数据科学学习阶段的初学者。无论是工程师还是初学者,Python数据可视化之美:专业图表绘制指南都是非常有益的一本书。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值