Skip to content

Commit 23d7887

Browse files
authored
Implement node rank in graph definition files (ipspace#2711)
The 'graph.rank' node attribute allows you to group nodes into layers in GraphViz graphs, and to ensure the node positioning is more closely aligned with the network topology in D2 graphs (D2 does not have a shape rank concept)
1 parent bc45dad commit 23d7887

File tree

9 files changed

+81
-19
lines changed

9 files changed

+81
-19
lines changed

docs/outputs/d2.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,21 @@ You can use the **d2** link and node attributes to change the style of individua
2626
* **d2.fill** -- fill color (*fill* in D2 lingo)
2727
* **d2.width** -- line width (*stroke-width* in D2 lingo)
2828

29-
You can also use the **d2.linkorder** link attribute to change the order of links in the D2 graph description file, which can sometimes improve the diagrams' appearance. Links with lower **d2.linkorder** values (default: 100) appear earlier in the list of links.
30-
3129
```{tip}
3230
The generic link- and node attributes can also be specified as **graph._attribute_** (for example, **graph.color**) values to use the same settings for GraphViz and D2 graphs.
3331
```
3432

33+
(outputs-d2-layout)=
34+
## Influencing the Graph Layout
35+
36+
_netlab_ uses the node **graph.rank** attribute to sort nodes in link definitions to ensure the graph edges are always defined as going from nodes with a lower rank to nodes with a higher rank. The order of nodes in the graph edges influences the D2 layout engine.
37+
38+
The default value of the **graph.rank** attribute is 100, allowing you to push some nodes (with rank below 100) toward the top of the graph and others (with rank above 100) toward the bottom.
39+
40+
You can also use the **graph.rank** on links to influence how GraphViz draws multi-access links.
41+
42+
Finally, the link/interface **graph.linkorder** attribute allows you to specify the node order in individual links. The default **graph.linkorder** value is 50 for interfaces and 100 for subnets (multi-access links), resulting in subnets being "below" nodes unless you change the link- or interface **graph.linkorder** value.
43+
3544
(outputs-d2-graph-appearance)=
3645
## Modifying Graph Appearance
3746

@@ -116,8 +125,6 @@ vrf:
116125
_netlab_ releases 25.07 and older specified D2 style attributes in the **‌defaults.outputs.d2** dictionary. The style attributes recognized by those releases are automatically migrated into the **‌defaults.outputs.d2.styles** dictionary.
117126
```
118127

119-
## Specifying D2 Attributes
120-
121128
You could specify D2 attributes in your [topology file](defaults-topology) (where you would have to prefix them with **defaults**), in [per-user topology defaults](defaults-user-file), or with [environment variables](defaults-env) (even [more details](../defaults.md)). You could also specify them with the `-s` parameter of the **[netlab create](netlab-create)** command, yet again prefixed with **defaults** ([more details](netlab-create-set)).
122129

123130
Use the **netlab show defaults outputs.d2** [command](netlab-show-defaults) to show the current D2 defaults, including topology file defaults and user defaults.

docs/outputs/graph.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ The *graph* output module is invoked with the **[netlab graph](netlab-graph)** c
99

1010
A single formatting modifier can be used to specify the graph type:
1111

12-
* **topology** (default) -- Display inter-node links, multi-access- and stub subnets. When the network topology contains BGP information, the graph groups nodes into autonomous systems. Alternatively, you could set **defaults.outputs.graph.groups** attribute to use topology **[groups](topo-groups)** to group graph nodes.
12+
* **topology** (default) -- Display inter-node links, multi-access-, and stub subnets. When the network topology contains BGP information, the graph groups nodes into autonomous systems. Alternatively, you could set **defaults.outputs.graph.groups** attribute to use topology **[groups](topo-groups)** to group graph nodes.
1313
* **bgp** -- Include autonomous systems, nodes, and BGP sessions. The formatting modifier can include [BGP formatting parameters](outputs-graph-bgp-parameters). For example, `netlab create -o graph:bgp:rr` draws RR-client sessions as directed arrows.
1414
* **isis** -- Create a diagram of IS-IS routing, including areas, color-coded circuit types, and edge subnets (does not work with IS-IS running over VLANs)
1515

@@ -24,10 +24,22 @@ You can use the **graph** link and node attributes to change the style of indivi
2424
* **graph.fill** -- fill color (*fillcolor* GraphViz attribute)
2525
* **graph.width** -- line width (*penwidth* GraphViz attribute)
2626

27-
You can also use the **graph.linkorder** link attribute to change the order of links in the D2 graph description file, which can sometimes improve the diagrams' appearance. Links with lower **graph.linkorder** values (default: 100) appear earlier in the list of links.
28-
2927
[^XS]: You can extend the GraphViz styling capabilities and add new **graph** attributes. See [](outputs-d2-styles) for details.
3028

29+
(outputs-graph-layout)=
30+
## Influencing the Graph Layout
31+
32+
_netlab_ uses the node **graph.rank** attribute to:
33+
34+
1. Tell GraphViz that nodes with the same **graph.rank** should be at the same rank (vertical position) in the graph
35+
2. Sort nodes in link definitions to ensure the graph edges are always defined as going from nodes with a lower rank to nodes with a higher rank. The order of nodes in the graph edges influences the GraphViz layout engine.
36+
37+
The default value of the **graph.rank** attribute is 100, allowing you to push some nodes (with rank below 100) toward the top of the graph and others (with rank above 100) toward the bottom.
38+
39+
You can also use the **graph.rank** on links to influence how GraphViz draws multi-access links.
40+
41+
Finally, the link/interface **graph.linkorder** attribute allows you to specify the node order in individual links. The default **graph.linkorder** value is 50 for interfaces and 100 for subnets (multi-access links), resulting in subnets being "below" nodes unless you change the link- or interface **graph.linkorder** value.
42+
3143
(outputs-graph-appearance)=
3244
## Modifying Graph Appearance
3345

netsim/outputs/_graph.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,16 @@ def append_edge(graph: Box, if_a: Box, if_b: Box, g_type: str) -> None:
153153
"""
154154
Append a LAN segment to the graph
155155
"""
156-
def append_segment(graph: Box,link: Box, g_type: str) -> None:
156+
LINK_DEFAULTS: dict = { 'graph.rank': 100, 'graph.linkorder': 100 }
157+
158+
def append_segment(graph: Box,link: Box, g_type: str, topology: Box) -> None:
157159
link.node = link.get('bridge',f'{link.type}_{link.linkindex}')
158160
link.name = link.node
161+
link.device = 'link'
162+
for dk,dv in LINK_DEFAULTS.items():
163+
if dk not in link:
164+
link[dk] = dv
165+
159166
graph.nodes[link.node] = link
160167
for af in ['ipv4','ipv6']:
161168
if af in link.prefix:
@@ -164,8 +171,8 @@ def append_segment(graph: Box,link: Box, g_type: str) -> None:
164171

165172
for intf in link.interfaces:
166173
e_data = [ intf, link ]
167-
if get_attr(intf,g_type,'linkorder',100) > get_attr(link,g_type,'linkorder',101):
168-
e_data = [ link, intf ]
174+
e_data.sort(key = lambda x: x.get('graph.rank',topology.nodes[intf.node].get('graph.rank',100)))
175+
e_data.sort(key = lambda x: get_attr(x,g_type,'linkorder',50))
169176
append_edge(graph,e_data[0],e_data[1],g_type)
170177

171178
def topo_edges(graph: Box, topology: Box, settings: Box,g_type: str) -> None:
@@ -174,9 +181,11 @@ def topo_edges(graph: Box, topology: Box, settings: Box,g_type: str) -> None:
174181
propagate_link_attributes(link,g_type,['linkorder','type'])
175182

176183
if len(link.interfaces) == 2 and link.get('graph.type','') != 'lan':
177-
append_edge(graph,link.interfaces[0],link.interfaces[1],g_type)
184+
intf_list = sorted(link.interfaces,key=lambda intf: topology.nodes[intf.node].get('graph.rank',100))
185+
intf_list.sort(key = lambda x: get_attr(x,g_type,'linkorder',50))
186+
append_edge(graph,intf_list[0],intf_list[1],g_type)
178187
else:
179-
append_segment(graph,link,g_type)
188+
append_segment(graph,link,g_type,topology)
180189

181190
def topology_graph(topology: Box, settings: Box,g_type: str) -> Box:
182191
set_shared_attributes(topology)
@@ -191,6 +200,7 @@ def bgp_sessions(graph: Box, topology: Box, settings: Box, g_type: str) -> None:
191200
no_vrf = settings.get('bgp.novrf',None)
192201
add_vrf = settings.get('bgp.vrf',None)
193202
bgp_af = settings.get('bgp.af')
203+
node_order = list(topology.nodes.keys())
194204
for n_name,n_data in topology.nodes.items():
195205
if 'bgp' not in n_data:
196206
continue
@@ -217,6 +227,9 @@ def bgp_sessions(graph: Box, topology: Box, settings: Box, g_type: str) -> None:
217227
elif neighbor.get('rr',False) and not n_data.get('rr',False):
218228
dir = '->'
219229
e_1, e_2 = e_2, e_1
230+
else:
231+
(e_1, e_2) = sorted([e_1, e_2],key=lambda x: node_order.index(x.node))
232+
(e_1, e_2) = sorted([e_1, e_2],key=lambda x: topology.nodes[x.node].get('graph.rank',100))
220233

221234
if rr_sessions:
222235
e_1[g_type].dir = dir
@@ -289,7 +302,7 @@ def isis_edges(topology: Box, graph: Box, g_type: str) -> None:
289302
append_edge(graph,link.interfaces[0],link.interfaces[1],g_type)
290303
continue
291304

292-
append_segment(graph,link,g_type)
305+
append_segment(graph,link,g_type,topology)
293306
isis_areas = set([ topology.nodes[node].get('isis.area') for node in isis_nodes ])
294307
if len(isis_areas) == 1:
295308
area_id = isis_areas.pop().replace('.','_')

netsim/outputs/graph.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def gv_line_attr(
7070
if newline:
7171
f.write("\n")
7272

73-
def gv_start(f : typing.TextIO, settings: Box, topology: Box) -> None:
73+
def gv_start(f : typing.TextIO, graph: Box, topology: Box, settings: Box) -> None:
7474
title = topology.get('graph.title') or topology.get('defaults.graph.title')
7575
f.write('graph {\n')
7676
gv_multiline_attr(f,attr=settings.styles.graph,indent=2)
@@ -82,6 +82,16 @@ def gv_start(f : typing.TextIO, settings: Box, topology: Box) -> None:
8282
gv_line_attr(f,attr=settings.styles.node,newline=True)
8383
f.write(' edge')
8484
gv_line_attr(f,attr=settings.styles.edge,newline=True)
85+
rank = { node.graph.rank
86+
for node in graph.nodes.values()
87+
if 'graph.rank' in node and node.device != 'link' }
88+
if rank:
89+
f.write (' newrank=true;\n')
90+
for r_val in sorted(list(rank)):
91+
r_nodes = [ node.name +"; "
92+
for node in graph.nodes.values()
93+
if node.get('graph.rank',None) == r_val and node.device != 'link' ]
94+
f.write(f' {{ rank=same; {"".join(r_nodes)}}}\n')
8595

8696
def gv_end(f : typing.TextIO, fname: str) -> None:
8797
f.write('}\n')
@@ -135,7 +145,7 @@ def gv_clusters(f : typing.TextIO, graph: Box, topology: Box, settings: Box) ->
135145
f.write(' }\n')
136146

137147
def gv_nodes(f: typing.TextIO, graph: Box, topology: Box, settings: Box) -> None:
138-
for n_name,n_data in graph.nodes.items():
148+
for _,n_data in graph.nodes.items():
139149
if 'graph.cluster' not in n_data:
140150
if 'prefix' in n_data:
141151
gv_network(f,n_data,settings,indent=2)
@@ -190,7 +200,7 @@ def gv_migrate_styles(settings: Box) -> None:
190200
def draw_graph(topology: Box, settings: Box, graph: Box, fname: str) -> None:
191201
f = _files.open_output_file(fname)
192202
gv_migrate_styles(settings)
193-
gv_start(f,settings,topology)
203+
gv_start(f,graph,topology,settings)
194204

195205
gv_clusters(f,graph,topology,settings)
196206
gv_nodes(f,graph,topology,settings)

netsim/outputs/graph.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,13 @@ attributes:
7373
color: str
7474
fill: str
7575
width: { type: int, min_value: 1, max_value: 32 }
76+
rank: { type: int, min_value: 1, max_value: 200 }
7677
link:
7778
type: dict
7879
_keys:
7980
linkorder: { type: int, min_value: 1, max_value: 200 }
8081
type: { type: str, valid_values: ['lan'] }
8182
color: str
8283
width: { type: int, min_value: 1, max_value: 32 }
84+
rank: { type: int, min_value: 1, max_value: 200 }
8385
shared: [ linkorder, color, fill, width ]

tests/platform-integration/graph/bgp.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ module: [ ospf, bgp, vrf, vlan, vxlan, evpn ]
55
plugin: [ files ]
66
defaults.vrf.warnings.inactive: False
77

8+
groups:
9+
core:
10+
members: [ r2, r5 ]
811
bgp.as_list:
912
65000:
1013
members: [ r1, r2, r3 ]
@@ -16,6 +19,7 @@ nodes:
1619
r1:
1720
r2:
1821
device: eos
22+
graph.rank: 1
1923
r3:
2024
r4:
2125
r5:

tests/platform-integration/graph/create-graphviz.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ if [[ "$OPTS" == *"topo"* ]]; then
1313
NETLAB_OUTPUTS_GRAPH_GROUPS=[fabric,host] graph topo.yml dot-topo-groups.svg ""
1414
fi
1515
if [[ "$OPTS" == *"bgp"* ]]; then
16-
graph bgp.yml dot-bgp-default.svg :bgp "Default BGP graph"
16+
NETLAB_GROUPS_CORE_GRAPH_RANK=1 graph bgp.yml dot-bgp-default.svg :bgp "Default BGP graph"
1717
graph bgp.yml dot-bgp-rr.svg :bgp:rr "BGP graph with RR sessions"
1818
graph bgp.yml dot-bgp-vrf.svg :bgp:vrf "BGP graph with VRF sessions"
1919
graph bgp.yml dot-bgp-novrf.svg :bgp:novrf "No VRF sessions in the BGP graph"

tests/platform-integration/graph/isis.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,18 @@ plugin: [ files ]
88
isis.area: '49.0100'
99
isis.type: level-2
1010

11+
groups:
12+
l2_is:
13+
members: [ c1, c2, x1, x2 ]
14+
graph.rank: 1
15+
1116
nodes:
17+
x1:
18+
isis.area: "49.0001"
1219
c1:
1320
isis.type: level-1-2
1421
c2:
1522
isis.type: level-1-2
16-
x1:
17-
isis.area: "49.0001"
1823
x2:
1924
isis.area: "49.0002"
2025
r1:

tests/platform-integration/graph/topo.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ groups:
1515
host:
1616
members: [ h1, h2, h3, h4 ]
1717
device: linux
18+
spine:
19+
members: [ s1, s2 ]
20+
graph.rank: 1
21+
leaf:
22+
members: [ l1, l2 ]
23+
graph.rank: 2
24+
sa_host:
25+
members: [ h3, h4 ]
26+
graph.rank: 50
1827

1928
module: [ ospf, bgp ]
2029

0 commit comments

Comments
 (0)