Skip to content

Commit 0e43d55

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 c0317a0 commit 0e43d55

File tree

2 files changed

+209
-128
lines changed

2 files changed

+209
-128
lines changed

netsim/outputs/_graph.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
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+
def build_nodes(topology: Box) -> Box:
12+
maps = Box({},default_box=True,box_dots=True)
13+
for name,n in topology.nodes.items():
14+
maps.nodes[name] = n
15+
16+
if 'bgp' in topology.get('module',[]):
17+
for name,n in topology.nodes.items():
18+
bgp_as = n.get('bgp.as',None)
19+
if bgp_as:
20+
bgp_as = f'AS_{bgp_as}'
21+
maps.bgp[bgp_as].nodes[n.name] = n
22+
23+
if 'bgp' in topology and 'as_list' in topology.bgp:
24+
for (asn,asdata) in topology.bgp.as_list.items():
25+
if 'name' in asdata and asn in maps.bgp:
26+
maps.bgp[asn].name = asdata.name
27+
28+
return maps
29+
30+
'''
31+
add_groups -- use topology groups as graph clustering mechanism
32+
'''
33+
def add_groups(maps: Box, graph_groups: list, topology: Box) -> None:
34+
if not 'groups' in topology:
35+
return
36+
placed_hosts = []
37+
38+
for g_name,g_data in topology.groups.items():
39+
if g_name not in graph_groups:
40+
continue
41+
for node in g_data.members:
42+
if node in placed_hosts:
43+
log.error(
44+
f'Cannot create overlapping graph clusters: node {node} is in two groups',
45+
log.IncorrectValue,
46+
'graph')
47+
continue
48+
49+
maps.clusters[g_name].nodes[node] = topology.nodes[node]
50+
placed_hosts.append(node)
51+
52+
def graph_clusters(graph: Box, topology: Box, settings: Box) -> None:
53+
if 'groups' in settings:
54+
must_be_list(
55+
parent=settings,
56+
key='groups',path='defaults.outputs.graph',
57+
true_value=list(topology.get('groups',{}).keys()),
58+
create_empty=True,
59+
module='graph')
60+
add_groups(graph,settings.groups,topology)
61+
elif 'bgp' in graph and settings.as_clusters:
62+
graph.clusters = graph.bgp
63+
64+
def append_edge(graph: Box, if_a: Box, if_b: Box, g_type: str) -> None:
65+
nodes = []
66+
e_attr = get_empty_box()
67+
for intf in (if_a, if_b):
68+
addr = intf.ipv4 or intf.ipv6
69+
if addr and 'prefix' not in intf:
70+
addr = addr.split('/')[0]
71+
intf_attr = intf.get('graph',{}) + intf.get(g_type,{})
72+
e_attr += intf_attr
73+
e_data = get_box({
74+
'node': intf.node,
75+
'attr': intf_attr,
76+
'label': addr})
77+
for kw in ['type']:
78+
if kw in intf:
79+
e_data[kw] = intf[kw]
80+
81+
nodes.append(e_data)
82+
83+
graph.edges.append({'nodes': nodes, 'attr': e_attr})
84+
85+
def topo_edges(graph: Box, topology: Box, settings: Box,g_type: str) -> None:
86+
graph.edges = []
87+
for link in sorted(topology.links,key=lambda x: x.get(f'{g_type}.linkorder',100)):
88+
l_attr = link.get(g_type,None)
89+
if l_attr:
90+
for intf in link.interfaces:
91+
intf[g_type] = l_attr + intf[g_type]
92+
93+
if len(link.interfaces) == 2:
94+
append_edge(graph,link.interfaces[0],link.interfaces[1],g_type)
95+
else:
96+
link.node = link.name or f'{link.type}_{link.linkindex}'
97+
graph.nodes[link.node] = link
98+
for af in ['ipv4','ipv6']:
99+
if af in link.prefix:
100+
link[af] = link.prefix[af]
101+
102+
for intf in link.interfaces:
103+
e_data = [ intf, link ]
104+
if intf.get(f'{g_type}.linkorder',100) > link.get(f'{g_type}.linkorder',101):
105+
e_data = [ link, intf ]
106+
append_edge(graph,e_data[0],e_data[1],g_type)
107+
108+
def topology_graph(topology: Box, settings: Box,g_type: str) -> Box:
109+
graph = build_nodes(topology)
110+
graph_clusters(graph,topology,settings)
111+
topo_edges(graph,topology,settings,g_type)
112+
return graph
113+
114+
def bgp_sessions(graph: Box, topology: Box, settings: Box, g_type: str, rr_sessions: bool) -> None:
115+
for n_name,n_data in topology.nodes.items():
116+
if 'bgp' not in n_data:
117+
continue
118+
for neighbor in n_data.bgp.get('neighbors',[]):
119+
if neighbor.name < n_name:
120+
continue
121+
122+
e_1 = get_box({ 'node': n_name, 'type': neighbor.type })
123+
e_2 = get_box({ 'node': neighbor.name, 'type': neighbor.type })
124+
dir = '<->'
125+
if 'ibgp' in neighbor.type:
126+
if n_data.bgp.get('rr',False) and not neighbor.get('rr',False):
127+
dir = '->'
128+
elif neighbor.get('rr',False) and not n_data.get('rr',False):
129+
dir = '->'
130+
e_1, e_2 = e_2, e_1
131+
132+
if rr_sessions:
133+
e_1.graph.dir = dir
134+
append_edge(graph,e_1,e_2,g_type)
135+
136+
def bgp_graph(topology: Box, settings: Box, g_type: str, rr_sessions: bool) -> typing.Optional[Box]:
137+
if 'bgp' not in topology.module:
138+
log.error(
139+
'Cannot build a BGP graph from a topology that does not use BGP',
140+
category=log.IncorrectType,
141+
module='graph')
142+
return None
143+
144+
graph = build_nodes(topology)
145+
graph.clusters = graph.bgp
146+
graph.edges = []
147+
bgp_sessions(graph,topology,settings,g_type,rr_sessions)
148+
return graph

netsim/outputs/d2.py

Lines changed: 61 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from . import _TopologyOutput
1313
from ..utils import files as _files
1414
from ..utils import log
15+
from ._graph import topology_graph,bgp_graph
1516

1617
'''
1718
Copy default settings into a D2 map converting Python dictionaries into
@@ -47,13 +48,16 @@ def d2_node_attr(f : typing.TextIO, n: Box, settings: Box, indent: str = '') ->
4748
Add D2 styling information from d2.* link/node attributes
4849
'''
4950
STYLE_MAP: Box
51+
IGNORE_KW: list = ['dir', 'type', 'name']
5052

5153
def d2_style(f : typing.TextIO, obj: Box, indent: str) -> None:
5254
if 'd2' not in obj:
5355
return
54-
d2_data = { STYLE_MAP[k]:v for k,v in obj.d2.items() if k in STYLE_MAP }
55-
if d2_data:
56-
dump_d2_dict(f,{ 'style': d2_data },indent)
56+
d2_style = { STYLE_MAP[k]:v for k,v in obj.d2.items() if k in STYLE_MAP }
57+
d2_extra = get_box({ k:v for k,v in obj.d2.items() if k not in STYLE_MAP and k not in IGNORE_KW })
58+
59+
if d2_style or d2_extra:
60+
dump_d2_dict(f,d2_extra + { 'style': d2_style },indent)
5761

5862
'''
5963
Create a node in D2 graph and add a label and styling attributes to it
@@ -108,161 +112,90 @@ def edge_label(f : typing.TextIO, direction: str, data: Box, subnet: bool = True
108112
Create a P2P connection between two nodes
109113
'''
110114
def edge_p2p(f : typing.TextIO, l: Box, labels: typing.Optional[bool] = False) -> None:
111-
f.write(f"{l.interfaces[0].node} -- {l.interfaces[1].node} {{\n")
115+
e_direction = ('source','target')
116+
dir = l.interfaces[0].get('attr.dir','--')
117+
f.write(f"{l.interfaces[0].d2.name} {dir} {l.interfaces[1].d2.name} {{\n")
118+
print(l)
112119
d2_style(f,l,' ')
113120
if labels:
114-
edge_label(f,'source',l.interfaces[0],True)
115-
edge_label(f,'target',l.interfaces[1],True)
121+
for e_idx,intf in enumerate(l.interfaces):
122+
if 'prefix' not in intf:
123+
edge_label(f,e_direction[e_idx],intf,True)
116124
f.write("}\n")
117125

118126
'''
119-
Create a connection between a node and a LAN segment
127+
Create a group container (or ASN container)
120128
'''
121-
def edge_node_net(f : typing.TextIO, link: Box, ifdata: Box, labels: typing.Optional[bool] = False) -> None:
122-
f.write(f"{ifdata.node} -> {link.bridge} {{\n")
123-
d2_style(f,link,' ')
124-
if labels:
125-
edge_label(f,'source',ifdata,False)
126-
f.write("}\n")
127-
128-
'''
129-
Create an ASN or group container
130-
'''
131-
def as_start(f : typing.TextIO, asn: str, label: typing.Optional[str], settings: Box) -> None:
129+
def d2_cluster_start(f : typing.TextIO, asn: str, label: typing.Optional[str], settings: Box) -> None:
132130
f.write(f'{asn} {{\n')
133131
copy_d2_attr(f,'container',settings,'- ')
134132
asn = asn.replace('_',' ')
135133
f.write(' label: '+ (f'{label} ({asn})' if label else asn)+'\n')
136134

137135
'''
138-
Create a topology graph as a set of containers (groups or ASNs)
136+
Create graph containers
139137
'''
140-
def graph_clusters(f : typing.TextIO, clusters: Box, settings: Box, nodes: Box) -> None:
141-
for asn in clusters.keys():
142-
as_start(f,asn,clusters[asn].get('name',None),settings)
143-
for n in clusters[asn].nodes.values():
138+
def d2_clusters(f: typing.TextIO, graph: Box, topology: Box, settings: Box) -> None:
139+
for c_name,c_data in graph.clusters.items():
140+
d2_cluster_start(f,c_name,c_data.get('name',None),settings)
141+
for n in c_data.nodes.values():
144142
node_with_label(f,n,settings,' ') # Create a node within a cluster
145-
nodes[n.name].d2.name = f'{asn}.{n.name}' # And change the D2 node name that will be used in connections
143+
topology.nodes[n.name].d2.name = f'{c_name}.{n.name}' # And change the D2 node name that will be used in connections
146144
f.write('}\n')
147145

148-
def build_maps(topology: Box) -> Box:
149-
maps = Box({},default_box=True,box_dots=True)
150-
for name,n in topology.nodes.items():
151-
maps.nodes[name] = n
152-
153-
if 'bgp' in topology.get('module',[]):
154-
for name,n in topology.nodes.items():
155-
bgp_as = n.get('bgp.as',None)
156-
if bgp_as:
157-
bgp_as = f'AS_{bgp_as}'
158-
maps.bgp[bgp_as].nodes[n.name] = n
159-
160-
if 'bgp' in topology and 'as_list' in topology.bgp:
161-
for (asn,asdata) in topology.bgp.as_list.items():
162-
if 'name' in asdata and asn in maps.bgp:
163-
maps.bgp[asn].name = asdata.name
164-
165-
return maps
166-
167-
'''
168-
add_groups -- use topology groups as graph clustering mechanism
169-
'''
170-
def add_groups(maps: Box, groups: list, topology: Box) -> None:
171-
if not 'groups' in topology:
172-
return
173-
174-
placed_hosts = []
175-
for g,v in topology.groups.items():
176-
if g in groups:
177-
for n in v.members:
178-
if n in placed_hosts:
179-
log.error(
180-
f'Cannot create overlapping graph clusters: node {n} is in two groups',
181-
log.IncorrectValue,
182-
'graph')
183-
continue
184-
else:
185-
maps.groups[g].nodes[n] = topology.nodes[n]
186-
placed_hosts.append(n)
146+
def d2_nodes(f: typing.TextIO, graph: Box, topology: Box, settings: Box) -> None:
147+
for n_name,n_data in graph.nodes.items():
148+
if 'd2.name' not in n_data:
149+
n_data.d2.name = n_data.name
150+
if 'prefix' in n_data:
151+
network_with_label(f,n_data,settings)
152+
else:
153+
node_with_label(f,n_data,settings)
154+
155+
def d2_links(f: typing.TextIO, graph: Box, topology: Box, settings: Box) -> None:
156+
for edge in graph.edges:
157+
fake_link = get_box({ 'interfaces': edge.nodes, 'd2': edge.attr })
158+
for intf in edge.nodes:
159+
intf.d2.name = intf.node
160+
if intf.node in topology.nodes:
161+
intf.d2.name = topology.nodes[intf.node].d2.name
162+
edge_p2p(f,fake_link,settings.interface_labels)
187163

188164
def graph_topology(topology: Box, fname: str, settings: Box,g_format: typing.Optional[list]) -> bool:
165+
graph = topology_graph(topology,settings,'d2')
189166
f = _files.open_output_file(fname)
190-
maps = build_maps(topology)
191-
192-
if 'groups' in settings:
193-
must_be_list(
194-
parent=settings,
195-
key='groups',path='defaults.outputs.graph',
196-
true_value=list(topology.get('groups',{}).keys()),
197-
create_empty=True,
198-
module='graph')
199-
add_groups(maps,settings.groups,topology)
200-
graph_clusters(f,maps.groups,settings,topology.nodes)
201-
elif 'bgp' in maps and settings.as_clusters:
202-
graph_clusters(f,maps.bgp,settings,topology.nodes)
203-
else:
204-
for name,n in topology.nodes.items():
205-
node_with_label(f,n,settings)
206-
207-
for l in sorted(topology.links,key=lambda x: x.get('d2.linkorder',100)):
208-
for intf in l.interfaces:
209-
intf._topo_node = intf.node
210-
intf.node = topology.nodes[intf.node].d2.name
211-
if l.type == "p2p":
212-
edge_p2p(f,l,settings.interface_labels)
213-
else:
214-
l.bridge = l.name or f'{l.type}_{l.linkindex}'
215-
network_with_label(f,l,settings)
216-
for ifdata in l.interfaces:
217-
if ifdata._topo_node in maps.nodes:
218-
edge_node_net(f,l,ifdata,settings.interface_labels)
219167

220-
f.close()
221-
return True
168+
d2_clusters(f,graph,topology,settings)
169+
d2_nodes(f,graph,topology,settings)
170+
d2_links(f,graph,topology,settings)
222171

223-
'''
224-
Create a BGP session as a connection between two nodes
172+
if fname != '-':
173+
f.close()
174+
return True
225175

226-
* Select the connection (arrow) type based on whether a connection is a RR-to-client session
227-
* Copy IBGP or EBGP attributes to the connection
176+
def graph_bgp(topology: Box, fname: str, settings: Box, g_format: typing.Optional[list]) -> bool:
177+
rr_session = settings.get('rr_sessions',False)
178+
if g_format is not None and len(g_format) > 1:
179+
rr_session = g_format[1] == 'rr'
228180

229-
Please note that we call this function once for every pair of nodes, so it has to deal with
230-
RR being the first (node) or the second (session) connection endpoint.
231-
'''
232-
def bgp_session(f : typing.TextIO, node: Box, session: Box, settings: Box, rr_session: bool, nodes: Box) -> None:
233-
arrow_dir = '<->'
234-
if rr_session:
235-
if session.type == 'ibgp':
236-
if 'rr' in node.bgp and node.bgp.rr and not 'rr' in session:
237-
arrow_dir = '->'
238-
if not 'rr' in node.bgp and 'rr' in session:
239-
arrow_dir = '<-'
240-
241-
f.write(f'{node.d2.name} {arrow_dir} {nodes[session.name].d2.name} {{\n')
242-
copy_d2_attr(f,session.type,settings,' ')
243-
f.write('}\n')
244-
245-
def graph_bgp(topology: Box, fname: str, settings: Box,g_format: typing.Optional[list]) -> bool:
246-
if not 'bgp' in topology.get('module',{}):
247-
log.error('BGP graph format can only be used to draw topologies using BGP',module='d2')
181+
graph = bgp_graph(topology,settings,'d2',rr_sessions=rr_session)
182+
if graph is None:
248183
return False
249184

250185
f = _files.open_output_file(fname)
251186

252-
rr_session = settings.get('rr_sessions',False)
253-
if g_format is not None and len(g_format) > 1:
254-
rr_session = g_format[1] == 'rr'
187+
d2_clusters(f,graph,topology,settings)
188+
d2_nodes(f,graph,topology,settings)
255189

256-
maps = build_maps(topology)
257-
graph_clusters(f,maps.bgp,settings,topology.nodes)
190+
for edge in graph.edges:
191+
print(edge)
192+
if edge.nodes[0].type in settings:
193+
edge.attr = settings[edge.nodes[0].type] + edge.attr
258194

259-
for name,n in topology.nodes.items():
260-
if 'bgp' in n:
261-
for neighbor in n.bgp.get('neighbors',[]):
262-
if neighbor.name > n.name:
263-
bgp_session(f,n,neighbor,settings,rr_session,topology.nodes)
195+
d2_links(f,graph,topology,settings)
264196

265-
f.close()
197+
if fname != '-':
198+
f.close()
266199
return True
267200

268201
graph_dispatch = {

0 commit comments

Comments
 (0)