好的,这是这里两篇文章的延续,首先是我最近的 NETCONF 教程,其次是我非常古老的项目(当时在 Java 中)使用名为“HelloRoute”的 SNMP 信息可视化网络拓扑。所以这是一个非常古老的想法的复活,只是使用更新的方法和工具。但首先是关于可视化的前言。
内容[隐藏]
前言——作者经验在网络基础设施中的可视化使用
好吧,就我而言,自动化网络可视化或文档从未真正作为文档的主要来源,无论我在哪里看到我们仍然维护手动创建的带有版本控制的地图,试图使它们在更改过程中保持最新状态,并且等等……,之所以会这样,是因为人类作者可以给出地图的上下文,例如按目的或建筑物的部分或合法组织映射的办公网络。看看下图,这是大多数通用网络建模工具中人工地图和自动地图之间的区别。
人类与计算机生成的网络图
现在为了不完全扼杀你完成本教程的重点,我认为问题在于市场上的可视化工具大多是通用产品,我的意思是他们有通用算法,只遵循供应商可以提前“期望”的东西如何在不向供应商支付 $$$ 的情况下向它们添加额外的代码/逻辑以适应您的可视化上下文的方法并不多。因此,从我的角度来看,对于任何网络基础设施维护组织(例如我自己在工作或自由顾问),解决方案是使用可用组件(库、算法)在内部构建可视化工具,而不是提出通用供应商软件如果它们不能被大量定制(我还没有找到这样的供应商)。
那么我们将在本教程中构建什么?
简单,我们将构建这样的东西,这是两个极端之间的中间地带,或者至少对于我的小型 Spine-Leaf 网络(现场演示链接在这里):
这不是世界上最好的可视化(我并没有声称),这里的重点是试图向您展示,自己构建一些东西并不像使用一点点脚本看起来那么难。
第 I 部分 LAB 拓扑结构和先决条件
我们将继续我们上次在 NETCONF 教程中离开的地方,并重新迭代相同的实验室和要求。
A. 实验室拓扑
和上次一样简单,我的开发笔记本电脑可以通过 SSH/IP 访问两个 comware7 交换机,这些交换机在上行链路和服务器下行链路上都有一些 LLDP 邻居。
LAB Topology 是两个具有 IP 管理访问权限的主动式 comware7 交换机
注意:是的,服务器使用 LLDP,从我的角度来看,运行它是一个很好的做法,在 Windows 和 Linux 上,只需单击几下/命令即可启用,并且 DC 中的管理程序也可以使用它,例如在 ESX 上/vmWare 的分布式交换机设置,这是一个已启用的复选框。
B. 已安装的python + 库
我将使用 python 2.7 解释器,因此为您的系统下载它(首选 linux,以便安装简单)并使用以下几个命令下载/安装使用 pip 和 HP Networkings PyHPEcw7 库的 ncclient 库:
# Install ncclient
pip install ncclient
# Install HPN's pyhpecw7 library
git clone https://github.com/HPENetworking/pyhpecw7.git
cd pyhpecw7
sudo python setup.py install
C.(可选)Web 开发工具插件
当我们开始开发可视化时,我们将使用简单的 HTML/CSS + Javascript,你只需要一个记事本来处理这个问题,但为了排除故障,我真的建议你在浏览器上搜索“Web 开发工具”插件或在至少要了解您的特定浏览器在何处具有“控制台”视图,以便您可以看到 Javascript 在出现问题时作为调试消息打印到控制台的内容。
第二部分。将 LLDP 邻居拉到 JSON 映射的示例 Python 脚本
使用你在我之前的 NETCONF 教程中获得的知识,你应该能够理解这里发生了什么,所以我将整个代码放在这里,然后提供一些解释。但我不会逐行介绍,因为这不是 Python 教程(其他页面)。代码要点:
输入:需要附近存在“DEVICES.txt”文件,该文件具有要访问的IP/主机名列表,每行一个主机,例如我的DEVICES.txt中有:
AR21-U12-ICB1
AR21-U12-ICB2
OUTPUT:然后脚本将生成三个 JSON 文件,它们是:
- graph.json – 主要拓扑文件,它将以 JSON 格式保存所有图形节点(设备)和链接(接口),我们将使用它作为可视化的输入
- no_neighbor_interfaces.json – 这是一个额外的 JSON 文件,我决定创建它以提供处于“UP”操作状态的接口的快速列表,但它们背后没有 LLDP 邻居,这是一个“未知因素”风险我想在我的可视化练习中意识到
- neighborships.json – 这描述了每个设备的接口列表和每个接口后面的 LLDP 邻居。
脚本来源:
#!/bin/python
from pyhpecw7.comware import HPCOM7
from pyhpecw7.features.vlan import Vlan
from pyhpecw7.features.interface import Interface
from pyhpecw7.features.neighbor import Neighbors
from pyhpecw7.utils.xml.lib import *
import yaml
import pprint
import json
import re
##########################
# CONFIGURATION OF ACCESS
##########################
USER="admin"
PASS="admin"
PORT=830
#########################################################
# REGULAR EXPLRESSIONS FOR MATCHING PORT NAMES TO SPEEDS
# NOTE: This is used in visuzation later to color lines
#########################################################
LINK_SPEEDS = [ ("^TwentyGigE*","20"),
("^FortyGig*","40") ,
("^Ten-GigabitEthernet*","10") ,
("^GigabitEthernet*","1") ]
#########################################################
# REGULAR EXPLRESSIONS FOR MATCHING DEVICES HIERARHY
# E.g. Access layer switches have "AC" in their name
# or aggregation layer devices have "AG" in their names
#########################################################
NODE_HIERARCHY = [ ('.+ICB.*',"2"),
('^[a-zA-Z]{5}AG.*',"3"),
('^[a-zA-Z]{5}AC.*',"2"),
('^[a-zA-Z]{5}L2.*',"4") ]
####################
# Connection method
####################
def connect(host,username,password,port):
args = dict(host=host, username=username, password=password, port=port)
print("Connecting " + host)
# CREATE CONNECTION
device = HPCOM7(**args)
device.open()
return device
################################
# Returns RAW python Dictionary
# with Neighbor NETCONF details
#################################
def getNeighbors(device):
print 'getNeighbors'
neighbors = Neighbors(device)
neigh_type=dict(default='lldp', choices=['cdp', 'lldp'])
response = getattr(neighbors, "lldp")
results = dict(neighbors=response)
clean_results = list()
for neighbor in results['neighbors']:
if str(neighbor['neighbor']) == "None" or str(neighbor['neighbor']) == "":
print("Removing probably bad neighbor \""+ str(neighbor['neighbor']) + "\"");
else:
clean_results.append(neighbor)
return clean_results
###############################################
# Takes RAW Dictionary of Neighbors and returns
# simplified Dictionary of only Neighbor nodes
# for visuzation as a node (point)
#
# NOTE: Additionally this using RegEx puts layer
# hierarchy into the result dictionary
###############################################
def getNodesFromNeighborships(neighborships):
print "getNodesFromNeighborships:"
nodes = {'nodes':[]}
for key,value in neighborships.iteritems():
print("Key:" + str(key) + ":")
'''
PATTERNS COMPILATIOn
'''
print ("Hostname matched[key]: " + key)
group = "1" # for key (source hostname)
for node_pattern in NODE_HIERARCHY:
print ("Pattern: " + node_pattern[0]);
pattern = re.compile(node_pattern[0]);
if pattern.match(key):
print("match")
group = node_pattern[1]
break
print("Final GROUP for key: " + key + " is " +group)
candidate = {"id":key,"group":group}
if candidate not in nodes['nodes']:
print("adding")
nodes['nodes'].append(candidate)
for neighbor in value:
print("neighbor: " + str(neighbor['neighbor']) + ":")
'''
PATTERNS COMPILATIOn
'''
print ("Hostname matched: " + neighbor['neighbor'])
group = "1"
for node_pattern in NODE_HIERARCHY:
print ("Pattern: " + node_pattern[0]);
pattern = re.compile(node_pattern[0]);
if pattern.match(neighbor['neighbor']):
print("match")
group = node_pattern[1]
break
print("Final GROUP for neighbor: " + key + " is " +group)
candidate2 = {"id":neighbor['neighbor'],"group":group}
if candidate2 not in nodes['nodes']:
print("adding")
nodes['nodes'].append(candidate2)
return nodes
###############################################
# Takes RAW Dictionary of Neighbors and returns
# simplified Dictionary of only links between
# nodes for visuzation later (links)
#
# NOTE: Additionally this using RegEx puts speed
# into the result dictionary
###############################################
def getLinksFromNeighborships(neighborships):
print "getLinksFromNeighborships:"
links = {'links':[]}
for key,value in neighborships.iteritems():
print(str(key))
for neighbor in value:
'''
PATTERNS COMPILATIOn
'''
print ("Interface matched: " + neighbor['local_intf'])
speed = "1" # DEFAULT
for speed_pattern in LINK_SPEEDS:
print("Pattern: " + speed_pattern[0])
pattern = re.compile(speed_pattern[0])
if pattern.match(neighbor['local_intf']):
speed = speed_pattern[1]
print("Final SPEED:" + speed)
links['links'].append({"source":key,"target":neighbor['neighbor'],"value":speed})
return links
##############################################
# Filters out links from simplified Dictionary
# that are not physical
# (e.g Loopback or VLAN interfaces)
#
# NOTE: Uses the same RegEx definitions as
# speed assignment
##############################################
def filterNonPhysicalLinks(interfacesDict):
onlyPhysicalInterfacesDict = dict()
print "filterNonPhysicalLinks"
for key,value in interfacesDict.iteritems():
print("Key:" + str(key) + ":")
onlyPhysicalInterfacesDict[key] = [];
for interface in value:
bIsPhysical = False;
for name_pattern in LINK_SPEEDS:
pattern = re.compile(name_pattern[0])
if pattern.match(interface['local_intf']):
bIsPhysical = True;
onlyPhysicalInterfacesDict[key].append({"local_intf":interface['local_intf'],
"oper_status":interface['oper_status'],
"admin_status":interface['admin_status'],
"actual_bandwith":interface['actual_bandwith'],
"description":interface['description']})
break;
print(str(bIsPhysical) + " - local_intf:" + interface['local_intf'] + " is physical.")
return onlyPhysicalInterfacesDict;
##############################################
# Filters out links from simplified Dictionary
# that are not in Operational mode "UP"
##############################################
def filterNonActiveLinks(interfacesDict):
onlyUpInterfacesDict = dict()
print "filterNonActiveLinks"
for key,value in interfacesDict.iteritems():
print("Key:" + str(key) + ":")
onlyUpInterfacesDict[key] = [];
for interface in value:
if interface['oper_status'] == 'UP':
onlyUpInterfacesDict[key].append({"local_intf":interface['local_intf'],
"oper_status":interface['oper_status'],
"admin_status":interface['admin_status'],
"actual_bandwith":interface['actual_bandwith'],
"description":interface['description']})
print("local_intf:" + interface['local_intf'] + " is OPRATIONAL.")
return onlyUpInterfacesDict;
################################################
# Takes RAW neighbors dictionary and simplified
# links dictionary and cross-references them to
# find links that are there, but have no neighbor
################################################
def filterLinksWithoutNeighbor(interfacesDict,neighborsDict):
neighborlessIntlist = dict()
print "filterLinksWithoutNeighbor"
for devicename,neiInterfaceDict in neighborships.iteritems():
print("Key(device name):" + str(devicename) + ":")
neighborlessIntlist[devicename] = []
for interface in interfacesDict[devicename]:
bHasNoNeighbor = True
for neighbor_interface in neiInterfaceDict:
print("local_intf: " + interface['local_intf']
+ " neighbor_interface['local_intf']:" + neighbor_interface['local_intf'])
if interface['local_intf'] == neighbor_interface['local_intf']:
# Tries to remove this interface from list of interfaces
#interfacesDict[devicename].remove(interface)
bHasNoNeighbor = False
print("BREAK")
break;
if bHasNoNeighbor:
neighborlessIntlist[devicename].append(interface)
print("Neighborless Interface on device: " + devicename + " int:" + interface['local_intf'])
return neighborlessIntlist;
###########################
# Collects all Interfaces
# using NETCONF interface
# from a Device
#
# NOTE: INcludes OperStatus
# and ActualBandwidth and
# few other parameters
###########################
def getInterfaces(device):
print 'getInterfaces'
E = data_element_maker()
top = E.top(
E.Ifmgr(
E.Interfaces(
E.Interface(
)
)
)
)
nc_get_reply = device.get(('subtree', top))
intName = findall_in_data('Name', nc_get_reply.data_ele)
## 2 == DOWN ; 1 == UP
intOperStatus = findall_in_data('OperStatus', nc_get_reply.data_ele)
## 2 == DOWN ; 1 == UP
intAdminStatus = findall_in_data('AdminStatus', nc_get_reply.data_ele)
IntActualBandwidth = findall_in_data('ActualBandwidth', nc_get_reply.data_ele)
IntDescription = findall_in_data('Description', nc_get_reply.data_ele)
deviceActiveInterfacesDict = []
for index in range(len(intName)):
# Oper STATUS
OperStatus = 'UNKNOWN'
if intOperStatus[index].text == '2':
OperStatus = 'DOWN'
elif intOperStatus[index].text == '1':
OperStatus = 'UP'
# Admin STATUS
AdminStatus = 'UNKNOWN'
if intAdminStatus[index].text == '2':
AdminStatus = 'DOWN'
elif intAdminStatus[index].text == '1':
AdminStatus = 'UP'
deviceActiveInterfacesDict.append({"local_intf":intName[index].text,
"oper_status":OperStatus,
"admin_status":AdminStatus,
"actual_bandwith":IntActualBandwidth[index].text,
"description":IntDescription[index].text})
return deviceActiveInterfacesDict
###########################
# MAIN ENTRY POINT TO THE
# SCRIPT IS HERE
###########################
if __name__ == "__main__":
print("Opening DEVICES.txt in local directory to read target device IP/hostnames")
with open ("DEVICES.txt", "r") as myfile:
data=myfile.readlines()
'''
TRY LOADING THE HOSTNAMES
'''
print("DEBUG: DEVICES LOADED:")
for line in data:
line = line.replace('\n','')
print(line)
#This will be the primary result neighborships dictionary
neighborships = dict()
#This will be the primary result interfaces dictionary
interfaces = dict()
'''
LETS GO AND CONNECT TO EACH ONE DEVICE AND COLLECT DATA
'''
print("Starting LLDP info collection...")
for line in data:
#print(line + USER + PASS + str(PORT))
devicehostname = line.replace('\n','')
device = connect(devicehostname,USER,PASS,PORT)
if device.connected: print("success")
else:
print("failed to connect to " + line + " .. skipping");
continue;
###
# Here we are connected, let collect Interfaces
###
interfaces[devicehostname] = getInterfaces(device)
###
# Here we are connected, let collect neighbors
###
new_neighbors = getNeighbors(device)
neighborships[devicehostname] = new_neighbors
'''
NOW LETS PRINT OUR ALL NEIGHBORSHIPS FOR DEBUG
'''
pprint.pprint(neighborships)
with open('neighborships.json', 'w') as outfile:
json.dump(neighborships, outfile, sort_keys=True, indent=4)
print("JSON printed into neighborships.json")
'''
NOW LETS PRINT OUR ALL NEIGHBORSHIPS FOR DEBUG
'''
interfaces = filterNonActiveLinks(filterNonPhysicalLinks(interfaces))
pprint.pprint(interfaces)
with open('interfaces.json', 'w') as outfile:
json.dump(interfaces, outfile, sort_keys=True, indent=4)
print("JSON printed into interfaces.json")
'''
GET INTERFACES WITHOUT NEIGHRBOR
'''
print "====================================="
print "no_neighbor_interfaces.json DICTIONARY "
print "======================================"
interfacesWithoutNeighbor = filterLinksWithoutNeighbor(interfaces,neighborships)
with open('no_neighbor_interfaces.json', 'w') as outfile:
json.dump(interfacesWithoutNeighbor, outfile, sort_keys=True, indent=4)
print("JSON printed into no_neighbor_interfaces.json")
'''
NOW LETS FORMAT THE DICTIONARY TO NEEDED D3 LIbary JSON
'''
print "================"
print "NODES DICTIONARY"
print "================"
nodes_dict = getNodesFromNeighborships(neighborships)
pprint.pprint(nodes_dict)
print "================"
print "LINKS DICTIONARY"
print "================"
links_dict = getLinksFromNeighborships(neighborships)
pprint.pprint(links_dict)
print "=========================================="
print "VISUALIZATION graph.json DICTIONARY MERGE"
print "=========================================="
visualization_dict = {'nodes':nodes_dict['nodes'],'links':links_dict['links']}
with open('graph.json', 'w') as outfile:
json.dump(visualization_dict, outfile, sort_keys=True, indent=4)
print("")
print("JSON printed into graph.json")
# Bugfree exit at the end
quit(0)
是的,我意识到这是一个超长的脚本,但是我没有使用本文描述所有脚本内容,而是非常努力地使用代码中的注释来描述其操作,您唯一需要为实验室进行调整的真正工作是更改第 15、16、17 行的配置参数,根据您的实验室命名约定,您可能希望更改第 33-36 行的正则表达式以匹配您的逻辑。
第三部分。检查输出 JSON
因此,在我的实验室拓扑中运行第二部分中的 python 脚本后,生成了两个 JSON 文件,第一个是使用简单的节点(又名设备)和链接(又名接口)列表对拓扑结构的简单描述,这里是graph.json:
{
"nodes": [
{
"group": "2",
"id": "AR21-U12-ICB1"
},
{
"group": "3",
"id": "usplnAGVPCLAB1003"
},
{
"group": "1",
"id": "ng1-esx12"
},
{
"group": "1",
"id": "ng1-esx11"
},
{
"group": "2",
"id": "AR21-U12-ICB2"
},
{
"group": "3",
"id": "usplnAGVPCLAB1004"
}
],
"links": [
{
"source": "AR21-U12-ICB1",
"target": "usplnAGVPCLAB1003",
"value": "40"
},
{
"source": "AR21-U12-ICB1",
"target": "ng1-esx12",
"value": "10"
},
{
"source": "AR21-U12-ICB1",
"target": "ng1-esx11",
"value": "10"
},
{
"source": "AR21-U12-ICB2",
"target": "usplnAGVPCLAB1004",
"value": "40"
},
{
"source": "AR21-U12-ICB2",
"target": "ng1-esx12",
"value": "10"
},
{
"source": "AR21-U12-ICB2",
"target": "ng1-esx11",
"value": "10"
},
{
"source": "AR21-U12-ICB2",
"target": "AR21-U12-ICB1",
"value": "10"
},
{
"source": "AR21-U12-ICB2",
"target": "AR21-U12-ICB1",
"value": "10"
}
]
}
好的,然后还有两个重要的 JSON,称为 neighborship.json 和 no_neighbor_interfaces.json,它们以非常相似的方式保存有关邻居的信息,但为了节省空间,如果您想查看它们的结构,请看这里。
第三部分。使用 D3 JavaScript 库可视化 graph.json
我的第一个灵感来自在这里查看力定向图的D3 库演示。它显示了一个简单但功能强大的代码,用于可视化 Victor Hugo 的Les Misérables 中的角色共同出现 , 但也使用输入作为节点和链接的 JSON 结构。(说实话,在我知道这个 D3 演示后,我在第二部分中的 python 生成的正是这种类型的 JSON,所以你看到的 python 已经被重新编写过,以创建这种类型的 JSON 文件结构)。
我对该演示所做的第二个也是最大的变化是我需要在我的图中使用某种形式的层次结构来分离核心/聚合/访问层和端点,因此我劫持了 D3 的 Y 轴重力设置来创建多个人工重力线,有看看第 178-189 行,看看我是如何做到的。
再次就像在第二部分的前一个 Python 脚本中一样,这里是一个完整的 JavaScript,我试图在代码中加入一些好的注释,因为这里没有足够的空间来逐行解释这一点。但是这段代码很简单。
<!DOCTYPE html>
<meta charset="utf-8">
<link rel='stylesheet' href='style.css' type='text/css' media='all' />
<body>
<table><tr><td>
<svg width="100%" height="100%"></svg>
</td><td>
<div class="infobox" id="infobox" >
CLICK ON DEVICE FOR NEIGHBOR/INTERFACE INFORMATION
<br>
<a href="http://networkgeekstuff.com"><img src="img/logo.png" align="right" valign="top" width="150px" height="150px"></a>
</div>
<div class="infobox2" id="infobox2" >
</div>
</td></tr></table>
</body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script>
<script>
// =============================
// PRINTING DEVICE DETAILS TABLE
// =============================
// ====================
// READING OF JSON FILE
// ====================
function readTextFile(file, callback) {
var rawFile = new XMLHttpRequest();
rawFile.overrideMimeType("application/json");
rawFile.open("GET", file, true);
rawFile.onreadystatechange = function() {
if (rawFile.readyState === 4 && rawFile.status == "200") {
callback(rawFile.responseText);
}
}
rawFile.send(null);
}
function OnClickDetails(deviceid){
//alert("devicedetails: " + deviceid);
//usage:
// #############################
// # READING NEIGHBORS #
// #############################
readTextFile("python/neighborships.json", function(text){
var data = JSON.parse(text);
console.log(data);
console.log(deviceid);
bFoundMatch = 0;
for (var key in data) {
console.log("Key: " + key + " vs " + deviceid);
if ((deviceid.localeCompare(key)) == 0){
console.log("match!");
bFoundMatch = 1;
text = tableFromNeighbor(key,data);
printToDivWithID("infobox","<h2><u>" + key + "</u></h2>" + text);
}
}
if (!(bFoundMatch)){
warning_text = "<h4>The selected device id: ";
warning_text+= deviceid;
warning_text+= " is not in database!</h4>";
warning_text+= "This is most probably as you clicked on edge node ";
warning_text+= "that is not NETCONF data gathered, try clicking on its neighbors.";
printToDivWithID("infobox",warning_text);
}
});
// ####################################
// # READING NEIGHBOR-LESS INTERFACES #
// ####################################
readTextFile("python/no_neighbor_interfaces.json", function(text){
var data = JSON.parse(text);
console.log(data);
console.log(deviceid);
bFoundMatch = 0;
for (var key in data) {
console.log("Key: " + key + " vs " + deviceid);
if ((deviceid.localeCompare(key)) == 0){
console.log("match!");
bFoundMatch = 1;
text = tableFromUnusedInterfaces(key,data);
printToDivWithID("infobox2","<font color=\"red\">Enabled Interfaces without LLDP Neighbor:</font><br>" + text);
}
}
if (!(bFoundMatch)){
printToDivWithID("infobox2","");
}
});
}
// ####################################
// # using input parameters returns
// # HTML table with these inputs
// ####################################
function tableFromUnusedInterfaces(key,data){
text = "<table class=\"infobox2\">";
text+= "<thead><th><u><h4>LOCAL INT.</h4></u></th><th><u><h4>DESCRIPTION</h4></u></th><th><u><h4>Bandwith</h4></u></th>";
text+= "</thead>";
for (var neighbor in data[key]) {
text+= "<tr>";
console.log("local_intf:" + data[key][neighbor]['local_intf']);
text+= "<td>" + data[key][neighbor]['local_intf'] + "</td>";
console.log("description:" + data[key][neighbor]['description']);
text+= "<td>" + data[key][neighbor]['description'] + "</td>";
console.log("actual_bandwith:" + data[key][neighbor]['actual_bandwith']);
text+= "<td>" + data[key][neighbor]['actual_bandwith'] + "</td>";
text+= "</tr>";
}
text+= "</table>";
return text;
}
// ####################################
// # using input parameters returns
// # HTML table with these inputs
// ####################################
function tableFromNeighbor(key,data){
text = "<table class=\"infobox\">";
text+= "<thead><th><u><h4>LOCAL INT.</h4></u></th><th><u><h4>NEIGHBOR</h4></u></th><th><u><h4>NEIGHBOR'S INT</h4></u></th>";
text+= "</thead>";
for (var neighbor in data[key]) {
text+= "<tr>";
console.log("local_intf:" + data[key][neighbor]['local_intf']);
text+= "<td>" + data[key][neighbor]['local_intf'] + "</td>";
console.log("neighbor_intf:" + data[key][neighbor]['neighbor_intf']);
text+= "<td>" + data[key][neighbor]['neighbor'] + "</td>";
console.log("neighbor:" + data[key][neighbor]['neighbor']);
text+= "<td>" + data[key][neighbor]['neighbor_intf'] + "</td>";
text+= "</tr>";
}
text+= "</table>";
return text;
}
// ####################################
// # replaces content of specified DIV
// ####################################
function printToDivWithID(id,text){
div = document.getElementById(id);
div.innerHTML = text;
}
// ########
// # MAIN #
// ########
var svg = d3.select("svg"),
//width = +svg.attr("width"),
//height = +svg.attr("height");
width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth,
height = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
d3.select("svg").attr("height",height)
d3.select("svg").attr("width",width*0.7)
var color = d3.scaleOrdinal(d3.schemeCategory20);
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }).distance(100).strength(0.001))
.force("charge", d3.forceManyBody().strength(-200).distanceMax(500).distanceMin(50))
.force("x", d3.forceX(function(d){
if(d.group === "1"){
return 3*(width*0.7)/4
} else if (d.group === "2"){
return 2*(width*0.7)/4
} else if (d.group === "3"){
return 1*(width*0.7)/4
} else {
return 0*(width*0.7)/4
}
}).strength(1))
.force("y", d3.forceY(height/2))
.force("center", d3.forceCenter((width*0.7) / 2, height / 2))
.force("collision", d3.forceCollide().radius(35));
// ######################################
// # Read graph.json and draw SVG graph #
// ######################################
d3.json("python/graph.json", function(error, graph) {
if (error) throw error;
var link = svg.append("g")
.selectAll("line")
.data(graph.links)
.enter().append("line")
.attr("stroke", function(d) { return color(parseInt(d.value)); })
.attr("stroke-width", function(d) { return Math.sqrt(parseInt(d.value)); });
var node = svg.append("g")
.attr("class", "nodes")
.selectAll("a")
.data(graph.nodes)
.enter().append("a")
.attr("target", '_blank')
.attr("xlink:href", function(d) { return (window.location.href + '?device=' + d.id) });
node.on("click", function(d,i){
d3.event.preventDefault();
d3.event.stopPropagation();
OnClickDetails(d.id);
}
);
node.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
node.append("image")
.attr("xlink:href", function(d) { return ("img/group" + d.group + ".png"); })
.attr("width", 32)
.attr("height", 32)
.attr("x", - 16)
.attr("y", - 16)
.attr("fill", function(d) { return color(d.group); });
node.append("text")
.attr("font-size", "0.8em")
.attr("dx", 12)
.attr("dy", ".35em")
.attr("x", +8)
.text(function(d) { return d.id });
node.append("title")
.text(function(d) { return d.id; });
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links);
function ticked() {
link
.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"});
}
});
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
</script>
第四部分。我的实验室的最终可视化
当您将第二部分中的 python 脚本生成的 JSON 文件和第三部分中的 D3 可视化组合在一起时,您在像我的 LAB 这样的超小型拓扑上的结果将是这样的(现在使用屏幕截图):
无聊,对吧?单击此处查看实时 JavaScript 动画演示。
是的,但这不仅限于我的小实验室,我实际上尝试在实验室中使用更大的设备数量运行它,这就是结果,再次单击此处查看更大的现场演示。
在较大的 LAB 上使用显示的 python/JavaScript 可视化的示例
概括
好吧,我在这里与您分享了我的快速实验/示例,说明如何使用 NETCONF+python+PyHPEcw7 的第一次快速映射对网络拓扑进行快速可视化,然后使用 Javascript+D3 库对其进行可视化以创建交互式自组织映射单击它后,每个设备的拓扑结构和一些小信息弹出窗口。您可以在此处将整个项目作为包下载。
随访 2019 年 9 月
在这篇文章的后续文章中,我更新了这个可视化示例以包含流量模式数据并为其提供更好的视觉主题。请关注这里或点击下面的图片。此外,在本次更新中,我通过 github 公开共享了代码,并使用 SNMP 作为数据源,这是更受欢迎和请求的功能。
2017 年 12 月 12 日