Skip to content

Commit 5941ba0

Browse files
authored
Make 'graph.linkorder' and 'graph.rank' easier to use (#3330)
A number of small changes were made to the graph creation code to make the node positioning features easier to use: * 'graph.rank' and 'graph.linkorder' can be used on nodes, links, or interfaces * Setting node 'graph.linkorder' to a high value will consistently place the node below the connected subnet. * The 'Influencing Graph Layout' documentation section was expanded with several examples. Bug fixes: * Cleanup the 'get attribute' code and use a shared function in all sorting procedures * Do not copy 'graph.rank' from link to interface, allowing 'graph.rank' to be used for node sorting when the link 'graph.linkorder' is set to 50 (node default value).
1 parent abb5214 commit 5941ba0

4 files changed

Lines changed: 62 additions & 16 deletions

File tree

docs/outputs/graph.md

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,43 @@ _netlab_ uses the node **graph.rank** attribute to:
7373

7474
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.
7575

76-
You can also use the **graph.rank** on links to influence how Graphviz draws multi-access links.
76+
You can also use the **graph.rank** on links to influence how Graphviz positions multi-access links relative to other links and nodes.
7777

78-
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.
78+
Finally, the **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). **graph.rank** is used to sort objects with the same **graph.linkorder**.
79+
80+
The default value of **graph.linkorder** places subnets (multi-access links) below all nodes attached to them. For example, the following lab topology results in a graph with the subnet below sw, h1, and h2:
81+
82+
```
83+
nodes: [ sw, h1, h2 ]
84+
links:
85+
- interfaces: [ sw, h1, h2 ]
86+
```
87+
88+
You can change the **graph.linkorder** on the host interfaces (h1, h2) to put the host nodes below the subnet:
89+
90+
```
91+
nodes: [ sw, h1, h2 ]
92+
links:
93+
- sw:
94+
h1:
95+
graph.linkorder: 200
96+
h2:
97+
graph.linkorder: 200
98+
```
99+
100+
You can also change the node **graph.linkorder** attribute to put nodes below subnets *on all links they're connected to*. For example, you could use a `hosts` group to set the **graph.linkorder** for all hosts to a value higher than the default link value (100):
101+
102+
```
103+
groups:
104+
hosts:
105+
members: [ h1, h2 ]
106+
graph.linkorder: 200
107+
108+
nodes: [ r1, h1, h2 ]
109+
110+
links:
111+
- interfaces: [ r1, h1, h2 ]
112+
```
79113

80114
(outputs-graph-appearance)=
81115
## Modifying Graph Appearance

netsim/outputs/_graph.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,20 @@ def get_graph_attributes(obj: Box, g_type: str, exclude: list = []) -> Box:
112112
"""
113113
Get a graph attribute from shared or graph-specific dictionary
114114
"""
115-
def get_attr(obj: Box, g_type: str, attr: str, default: typing.Any) -> typing.Any:
116-
return obj.get(f'{g_type}.{attr}',obj.get(f'graph.{attr}',default))
115+
def get_attr(obj: Box, topology: Box, g_type: str, attr: str, default: typing.Any) -> typing.Any:
116+
kw_list = [ f'{g_type}.{attr}', f'graph.{attr}' ] # Try graph-specific (graph/d2) and shared graph attributes
117+
for kw in kw_list: # Try to find the value in interface/link object
118+
if kw in obj:
119+
return obj[kw]
120+
121+
if 'node' in obj: # Try to get node data from object.node (if present)
122+
ndata = topology.nodes.get(obj.node,None)
123+
if ndata: # If we found node data, try to get the keyword value from it
124+
for kw in kw_list:
125+
if kw in ndata:
126+
return ndata[kw]
127+
128+
return default # Nothing works, return the default value
117129

118130
"""
119131
Propagate link graph attributes to interfaces
@@ -182,22 +194,22 @@ def append_segment(graph: Box,link: Box, g_type: str, topology: Box) -> None:
182194

183195
for intf in link.interfaces:
184196
e_data = [ intf, link ]
185-
e_data.sort(key = lambda x: x.get('graph.rank',topology.nodes[intf.node].get('graph.rank',100)))
186-
e_data.sort(key = lambda x: get_attr(x,g_type,'linkorder',50))
197+
e_data.sort(key = lambda x: get_attr(x,topology,g_type,'rank',100))
198+
e_data.sort(key = lambda x: get_attr(x,topology,g_type,'linkorder',50))
187199
append_edge(graph,e_data[0],e_data[1],g_type)
188200

189201
def topo_edges(graph: Box, topology: Box, settings: Box,g_type: str) -> None:
190202
graph.edges = []
191-
for link in sorted(topology.links,key=lambda x: get_attr(x,g_type,'linkorder',100)):
192-
propagate_link_attributes(link,g_type,['linkorder','type'])
203+
for link in sorted(topology.links,key=lambda x: get_attr(x,topology,g_type,'linkorder',100)):
204+
propagate_link_attributes(link,g_type,['linkorder','rank','type'])
193205

194206
if settings.get('topology.vlan',False):
195207
for intf in link.interfaces:
196208
set_vlan_type(intf)
197209

198210
if len(link.interfaces) == 2 and link.get('graph.type','') != 'lan':
199211
intf_list = sorted(link.interfaces,key=lambda intf: topology.nodes[intf.node].get('graph.rank',100))
200-
intf_list.sort(key = lambda x: get_attr(x,g_type,'linkorder',50))
212+
intf_list.sort(key = lambda x: get_attr(x,topology,g_type,'linkorder',50))
201213
append_edge(graph,intf_list[0],intf_list[1],g_type)
202214
else:
203215
append_segment(graph,link,g_type,topology)
@@ -300,8 +312,8 @@ def get_node_interface(topology: Box, intf: Box) -> Box:
300312

301313
def isis_edges(topology: Box, graph: Box, g_type: str) -> None:
302314
graph.edges = []
303-
for link in sorted(topology.links,key=lambda x: get_attr(x,g_type,'linkorder',100)):
304-
propagate_link_attributes(link,g_type,['linkorder','type'])
315+
for link in sorted(topology.links,key=lambda x: get_attr(x,topology,g_type,'linkorder',100)):
316+
propagate_link_attributes(link,g_type,['linkorder','rank','type'])
305317

306318
isis_nodes = [ intf.node
307319
for intf in link.interfaces

netsim/outputs/graph.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ attributes:
8181
fill: str
8282
width: { type: int, min_value: 1, max_value: 32 }
8383
rank: { type: int, min_value: 1, max_value: 200 }
84+
linkorder: { type: int, min_value: 1, max_value: 200 }
8485
format: dict
8586
class: { type: list, _subtype: str}
8687
link:

tests/platform-integration/graph/01-topo.yml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ groups:
1616
host:
1717
members: [ h1, h2, h3, h4 ]
1818
device: linux
19+
edge_host:
20+
members: [ h1, h2 ]
21+
graph.linkorder: 200
1922
spine:
2023
members: [ s-1, s-b-2 ]
2124
graph.rank: 1
@@ -42,11 +45,7 @@ links:
4245
l2:
4346
graph.format.style: dashed
4447
d2.format.style.stroke-dash: 5
45-
- l1:
46-
h1:
47-
graph.linkorder: 200
48-
h2:
49-
graph.linkorder: 200
48+
- interfaces: [ l1, h1, h2 ]
5049
graph.color: "#FF0000"
5150
graph.width: 3
5251
d2.format.shape: oval

0 commit comments

Comments
 (0)