Skip to content

Commit cc8b8f5

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 fe65482 commit cc8b8f5

File tree

7 files changed

+319
-130
lines changed

7 files changed

+319
-130
lines changed

netsim/outputs/_graph.py

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
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+
must_be_list(
61+
parent=settings,
62+
key='groups',path='defaults.outputs.graph',
63+
true_value=list(topology.get('groups',{}).keys()),
64+
create_empty=True,
65+
module='graph')
66+
add_groups(graph,settings.groups,topology)
67+
elif 'bgp' in graph and settings.as_clusters:
68+
graph.clusters = graph.bgp
69+
70+
def get_graph_attributes(obj: Box, g_type: str, exclude: list = []) -> Box:
71+
global SHARED_GRAPH_ATTRIBUTES
72+
73+
# Get graph attributes shared in the 'graph' namespace
74+
#
75+
g_attr = { k:v for k,v in obj.get('graph',{}).items() if k in SHARED_GRAPH_ATTRIBUTES }
76+
g_dict = obj.get(g_type,{}) + g_attr # Get graph-specific attributes + shared attributes
77+
for kw in exclude:
78+
if kw in g_dict: # Remove unwanted attributes
79+
g_dict.pop(kw)
80+
81+
return g_dict
82+
83+
"""
84+
Get a graph attribute from shared or graph-specific dictionary
85+
"""
86+
def get_attr(obj: Box, g_type: str, attr: str, default: typing.Any) -> typing.Any:
87+
return obj.get(f'{g_type}.{attr}',obj.get(f'graph.{attr}',default))
88+
89+
def append_edge(graph: Box, if_a: Box, if_b: Box, g_type: str) -> None:
90+
nodes = []
91+
e_attr = get_empty_box()
92+
for intf in (if_a, if_b):
93+
addr = intf.ipv4 or intf.ipv6
94+
if addr and 'prefix' not in intf:
95+
addr = addr.split('/')[0]
96+
intf_attr = get_graph_attributes(intf,g_type)
97+
e_attr += intf_attr
98+
e_data = get_box({
99+
'node': intf.node,
100+
'attr': intf_attr,
101+
'label': addr})
102+
for kw in ['type']:
103+
if kw in intf:
104+
e_data[kw] = intf[kw]
105+
106+
nodes.append(e_data)
107+
108+
graph.edges.append({'nodes': nodes, 'attr': e_attr})
109+
110+
def topo_edges(graph: Box, topology: Box, settings: Box,g_type: str) -> None:
111+
graph.edges = []
112+
for link in sorted(topology.links,key=lambda x: get_attr(x,g_type,'linkorder',100)):
113+
l_attr = get_graph_attributes(link,g_type,['linkorder','type'])
114+
if l_attr:
115+
for intf in link.interfaces:
116+
intf[g_type] = l_attr + intf[g_type]
117+
118+
if len(link.interfaces) == 2 and link.get('graph.type','') != 'lan':
119+
append_edge(graph,link.interfaces[0],link.interfaces[1],g_type)
120+
else:
121+
link.node = link.get('bridge',f'{link.type}_{link.linkindex}')
122+
link.name = link.node
123+
graph.nodes[link.node] = link
124+
for af in ['ipv4','ipv6']:
125+
if af in link.prefix:
126+
link[af] = link.prefix[af]
127+
128+
for intf in link.interfaces:
129+
e_data = [ intf, link ]
130+
if get_attr(intf,g_type,'linkorder',100) > get_attr(link,g_type,'linkorder',101):
131+
e_data = [ link, intf ]
132+
append_edge(graph,e_data[0],e_data[1],g_type)
133+
134+
def topology_graph(topology: Box, settings: Box,g_type: str) -> Box:
135+
set_shared_attributes(topology)
136+
graph = build_nodes(topology)
137+
graph_clusters(graph,topology,settings)
138+
topo_edges(graph,topology,settings,g_type)
139+
return graph
140+
141+
def bgp_sessions(graph: Box, topology: Box, settings: Box, g_type: str, rr_sessions: bool) -> None:
142+
for n_name,n_data in topology.nodes.items():
143+
if 'bgp' not in n_data:
144+
continue
145+
for neighbor in n_data.bgp.get('neighbors',[]):
146+
if neighbor.name < n_name:
147+
continue
148+
149+
e_1 = get_box({ 'node': n_name, 'type': neighbor.type })
150+
e_2 = get_box({ 'node': neighbor.name, 'type': neighbor.type })
151+
dir = '<->'
152+
if 'ibgp' in neighbor.type:
153+
if n_data.bgp.get('rr',False) and not neighbor.get('rr',False):
154+
dir = '->'
155+
elif neighbor.get('rr',False) and not n_data.get('rr',False):
156+
dir = '->'
157+
e_1, e_2 = e_2, e_1
158+
159+
if rr_sessions:
160+
e_1.graph.dir = dir
161+
append_edge(graph,e_1,e_2,g_type)
162+
163+
def bgp_graph(topology: Box, settings: Box, g_type: str, rr_sessions: bool) -> typing.Optional[Box]:
164+
set_shared_attributes(topology)
165+
if 'bgp' not in topology.module:
166+
log.error(
167+
'Cannot build a BGP graph from a topology that does not use BGP',
168+
category=log.IncorrectType,
169+
module='graph')
170+
return None
171+
172+
graph = build_nodes(topology)
173+
graph.clusters = graph.bgp
174+
graph.edges = []
175+
bgp_sessions(graph,topology,settings,g_type,rr_sessions)
176+
return graph

0 commit comments

Comments
 (0)