Skip to content

Commit 679c47d

Browse files
committed
Refactor D2 graph generation
Implementing ideas from #2535, add code that creates generic graph data structure for topology and BGP graphs, and use that data structure to generate D2 graphs with generic primitives (d2_cluster, d2_node, and d2_edge)
1 parent e7b4e60 commit 679c47d

File tree

7 files changed

+328
-135
lines changed

7 files changed

+328
-135
lines changed

netsim/outputs/_graph.py

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
#
2+
# Create generic graph from lab topology
3+
#
4+
import typing
5+
from box import Box
6+
7+
from ..data import get_box,get_empty_box
8+
from ..data.types import must_be_list
9+
from ..utils import log
10+
11+
SHARED_GRAPH_ATTRIBUTES: Box # Graph attributes shared between all graph output modules
12+
13+
def set_shared_attributes(topology: Box) -> None:
14+
global SHARED_GRAPH_ATTRIBUTES
15+
SHARED_GRAPH_ATTRIBUTES = topology.defaults.outputs.graph.attributes.shared
16+
17+
def build_nodes(topology: Box) -> Box:
18+
maps = Box({},default_box=True,box_dots=True)
19+
for name,n in topology.nodes.items():
20+
maps.nodes[name] = n
21+
22+
if 'bgp' in topology.get('module',[]):
23+
for name,n in topology.nodes.items():
24+
bgp_as = n.get('bgp.as',None)
25+
if bgp_as:
26+
bgp_as = f'AS_{bgp_as}'
27+
maps.bgp[bgp_as].nodes[n.name] = n
28+
29+
if 'bgp' in topology and 'as_list' in topology.bgp:
30+
for (asn,asdata) in topology.bgp.as_list.items():
31+
if 'name' in asdata and asn in maps.bgp:
32+
maps.bgp[asn].name = asdata.name
33+
34+
return maps
35+
36+
'''
37+
add_groups -- use topology groups as graph clustering mechanism
38+
'''
39+
def add_groups(maps: Box, graph_groups: list, topology: Box) -> None:
40+
if not 'groups' in topology:
41+
return
42+
placed_hosts = []
43+
44+
for g_name,g_data in topology.groups.items():
45+
if g_name not in graph_groups:
46+
continue
47+
for node in g_data.members:
48+
if node in placed_hosts:
49+
log.error(
50+
f'Cannot create overlapping graph clusters: node {node} is in two groups',
51+
log.IncorrectValue,
52+
'graph')
53+
continue
54+
55+
maps.clusters[g_name].nodes[node] = topology.nodes[node]
56+
placed_hosts.append(node)
57+
58+
def graph_clusters(graph: Box, topology: Box, settings: Box) -> None:
59+
if 'groups' in settings:
60+
print(settings.groups)
61+
must_be_list(
62+
parent=settings,
63+
key='groups',path='defaults.outputs.graph',
64+
true_value=list(topology.get('groups',{}).keys()),
65+
create_empty=True,
66+
module='graph')
67+
add_groups(graph,settings.groups,topology)
68+
elif 'bgp' in graph and settings.as_clusters:
69+
graph.clusters = graph.bgp
70+
71+
def get_graph_attributes(obj: Box, g_type: str, exclude: list = []) -> Box:
72+
global SHARED_GRAPH_ATTRIBUTES
73+
74+
# Get graph attributes shared in the 'graph' namespace
75+
#
76+
g_attr = { k:v for k,v in obj.get('graph',{}).items() if k in SHARED_GRAPH_ATTRIBUTES }
77+
g_dict = obj.get(g_type,{}) + g_attr # Get graph-specific attributes + shared attributes
78+
for kw in exclude:
79+
if kw in g_dict: # Remove unwanted attributes
80+
g_dict.pop(kw)
81+
82+
return g_dict
83+
84+
"""
85+
Get a graph attribute from shared or graph-specific dictionary
86+
"""
87+
def get_attr(obj: Box, g_type: str, attr: str, default: typing.Any) -> typing.Any:
88+
return obj.get(f'{g_type}.{attr}',obj.get(f'graph.{attr}',default))
89+
90+
def append_edge(graph: Box, if_a: Box, if_b: Box, g_type: str) -> None:
91+
nodes = []
92+
e_attr = get_empty_box()
93+
for intf in (if_a, if_b):
94+
addr = intf.ipv4 or intf.ipv6
95+
if addr and 'prefix' not in intf:
96+
addr = addr.split('/')[0]
97+
intf_attr = get_graph_attributes(intf,g_type)
98+
e_attr += intf_attr
99+
e_data = get_box({
100+
'node': intf.node,
101+
'attr': intf_attr,
102+
'label': addr})
103+
for kw in ['type','_subnet']:
104+
if kw in intf:
105+
e_data[kw] = intf[kw]
106+
107+
nodes.append(e_data)
108+
109+
graph.edges.append({'nodes': nodes, 'attr': e_attr})
110+
111+
def topo_edges(graph: Box, topology: Box, settings: Box,g_type: str) -> None:
112+
graph.edges = []
113+
for link in sorted(topology.links,key=lambda x: get_attr(x,g_type,'linkorder',100)):
114+
l_attr = get_graph_attributes(link,g_type,['linkorder','type'])
115+
if l_attr:
116+
for intf in link.interfaces:
117+
intf[g_type] = l_attr + intf[g_type]
118+
119+
if len(link.interfaces) == 2 and link.get('graph.type','') != 'lan':
120+
append_edge(graph,link.interfaces[0],link.interfaces[1],g_type)
121+
else:
122+
link.node = link.get('bridge',f'{link.type}_{link.linkindex}')
123+
link.name = link.node
124+
graph.nodes[link.node] = link
125+
for af in ['ipv4','ipv6']:
126+
if af in link.prefix:
127+
link[af] = link.prefix[af]
128+
link._subnet = True
129+
130+
for intf in link.interfaces:
131+
e_data = [ intf, link ]
132+
if get_attr(intf,g_type,'linkorder',100) > get_attr(link,g_type,'linkorder',101):
133+
e_data = [ link, intf ]
134+
append_edge(graph,e_data[0],e_data[1],g_type)
135+
136+
def topology_graph(topology: Box, settings: Box,g_type: str) -> Box:
137+
set_shared_attributes(topology)
138+
graph = build_nodes(topology)
139+
graph_clusters(graph,topology,settings)
140+
topo_edges(graph,topology,settings,g_type)
141+
log.exit_on_error()
142+
return graph
143+
144+
def bgp_sessions(graph: Box, topology: Box, settings: Box, g_type: str, rr_sessions: bool) -> None:
145+
for n_name,n_data in topology.nodes.items():
146+
if 'bgp' not in n_data:
147+
continue
148+
for neighbor in n_data.bgp.get('neighbors',[]):
149+
if neighbor.name < n_name:
150+
continue
151+
152+
e_1 = get_box({ 'node': n_name, 'type': neighbor.type })
153+
e_2 = get_box({ 'node': neighbor.name, 'type': neighbor.type })
154+
dir = '<->'
155+
if 'ibgp' in neighbor.type:
156+
if n_data.bgp.get('rr',False) and not neighbor.get('rr',False):
157+
dir = '->'
158+
elif neighbor.get('rr',False) and not n_data.get('rr',False):
159+
dir = '->'
160+
e_1, e_2 = e_2, e_1
161+
162+
if rr_sessions:
163+
e_1.graph.dir = dir
164+
append_edge(graph,e_1,e_2,g_type)
165+
166+
def bgp_graph(topology: Box, settings: Box, g_type: str, rr_sessions: bool) -> typing.Optional[Box]:
167+
set_shared_attributes(topology)
168+
if 'bgp' not in topology.module:
169+
log.error(
170+
'Cannot build a BGP graph from a topology that does not use BGP',
171+
category=log.IncorrectType,
172+
module='graph')
173+
return None
174+
175+
graph = build_nodes(topology)
176+
graph.clusters = graph.bgp
177+
graph.edges = []
178+
bgp_sessions(graph,topology,settings,g_type,rr_sessions)
179+
log.exit_on_error()
180+
return graph

0 commit comments

Comments
 (0)