Skip to content

Commit 2cc57d7

Browse files
committed
Add 'color', 'width' and 'fill' attributes to GraphViz and D2 graphs
* Add 'color', 'width' and 'fill' to shared attributes, so D2 graphs can use the 'graph' values * Map the well-known graph attributes to Graph Language (GL)- specific node/edge attributes (D2 and GraphViz) * Use the 'style_map' graph settings to map well-known attributes to GL-specific attributes, giving the users infinite extensibility. * Add support for GraphViz node and edge formatting attributes
1 parent 736edb5 commit 2cc57d7

File tree

9 files changed

+98
-22
lines changed

9 files changed

+98
-22
lines changed

docs/outputs/d2.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ A single formatting modifier can be used to specify the graph type:
1111
* **bgp** -- Include autonomous systems, nodes, and BGP sessions. With the **rr** option (specified with `netlab create -o graph:bgp:rr`), RR-client sessions are drawn as directed arrows.
1212

1313
```{tip}
14-
The network topology graph description contains nodes and links, but no placement information. D2 is pretty good at figuring out how to draw the required graph, but it pays off to test out the [layout engines](https://d2lang.com/tour/layouts). Changing the [order of links](outputs-d2-style-attributes) might also unclutter the diagrams.
14+
The network topology graph description contains nodes and links, but no placement information. D2 is pretty good at figuring out how to draw the required graph, but it pays off to test out the [layout engines](https://d2lang.com/tour/layouts). Changing the [order of links](outputs-d2-link-node-attributes) might also unclutter the diagrams.
1515
```
1616

1717
## Modifying Graph Attributes
@@ -27,15 +27,20 @@ Graphing routines use **[default](topo-defaults)** topology settings to modify t
2727

2828
[^DG]: The results look disgusting. If you find a better way to get it done, please submit a PR. Thank you!
2929

30-
(outputs-d2-style-attributes)=
30+
(outputs-d2-link-node-attributes)=
3131
## Modifying Link and Node Attributes
3232

3333
You can use the **d2** link and node attributes to change the style of individual nodes or links. The following attributes are built into _netlab_ (but see also [](outputs-d2-styles)):
3434

3535
* **d2.color** -- line color (*stroke* in D2 lingo)
36-
* **d2.width** -- line width (*stroke-width*) in D2 lingo)
36+
* **d2.fill** -- fill color (*fill* in D2 lingo)
37+
* **d2.width** -- line width (*stroke-width* in D2 lingo)
3738

38-
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** value (default: 100) appear earlier in the list of links.
39+
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.
40+
41+
```{tip}
42+
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.
43+
```
3944

4045
## Modifying Shape and Connection Attributes
4146

@@ -133,10 +138,10 @@ You can define your own link/node style attributes:
133138
defaults.outputs.d2.attributes.node.background: str
134139
```
135140

136-
* Define a mapping between your attribute and D2 style attribute within the **defaults.outputs.d2.styles** dictionary. For example, your **d2.background** attribute maps into D2 **style.fill** attribute:
141+
* Define a mapping between your attribute and D2 style attribute within the **defaults.outputs.d2.style_map** dictionary. For example, your **d2.background** attribute maps into D2 **style.fill** attribute:
137142

138143
```
139-
defaults.outputs.d2.styles.background: fill
144+
defaults.outputs.d2.style_map.background: fill
140145
```
141146

142147
[^AD]: See [](dev-attribute-validation) and [](dev-valid-data-types) for more details.

docs/outputs/graph.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,22 @@ Graphing routines use **[default](topo-defaults)** topology settings to modify t
2121
* **outputs.graphs.node_address_label** (default: *True*) -- add node loopback IP addresses or IP addresses of the first interface (for hosts) to node labels.
2222
* **outputs.graph.rr_sessions** (default: *False*) -- draw IBGP sessions between BGP route reflectors and clients as directional connections.
2323

24-
You can also change the formatting of individual graph objects with the **outputs.styles._object_** defaults:
24+
(outputs-graph-link-node-attributes)=
25+
## Modifying Link and Node Attributes
26+
27+
You can use the **graph** link and node attributes to change the style of individual nodes or links. The following attributes are built into _netlab_[^XS]:
28+
29+
* **graph.color** -- line color (*color* GraphViz attribute)
30+
* **graph.fill** -- fill color (*fillcolor* GraphViz attribute)
31+
* **graph.width** -- line width (*penwidth* GraphViz attribute)
32+
33+
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.
34+
35+
[^XS]: You can extend the GraphViz styling capabilities and add new **graph** attributes. See [](outputs-d2-styles) for details.
36+
37+
## Object Styles
38+
39+
You can also change the formatting of individual graph objects with the **outputs.graph.styles._object_** defaults:
2540

2641
| Object | Description |
2742
|--------|-------------|

docs/release/2.0.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Release 2.0.1 includes [bug](bug-fixes-2.0.1) and [documentation](doc-fixes-2.0.
2828
* SRv6: [BGP L3VPN support](module-srv6)
2929
* Use the **routing** module to configure static routes on [**host** devices](node-role-host). VRF-aware devices can use a default route; other devices get more specific routes for address pools and named prefixes.
3030
* Multiple [EVPN import/export route targets](evpn-vlan-service) allow you to build complex EVPN-based services like *common services* or *hub-and-spoke connectivity*
31-
* Implement [node/link styles for D2 graphs](outputs-d2-style-attributes)
31+
* Implement [node/link styles for D2 graphs](outputs-d2-link-node-attributes)
3232
* Implement 'delete communities matching a list' in [routing policies](generic-routing-policies)
3333

3434
**Retirements**

netsim/outputs/_graph.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,28 @@ def set_shared_attributes(topology: Box) -> None:
1414
global SHARED_GRAPH_ATTRIBUTES
1515
SHARED_GRAPH_ATTRIBUTES = topology.defaults.outputs.graph.attributes.shared
1616

17-
def build_nodes(topology: Box) -> Box:
17+
"""
18+
Utility function: map shared attributes (color, fill, width) into GL-specific attributes
19+
"""
20+
def map_style(g_attr: Box, style_map: Box) -> Box:
21+
return get_box({ style_map[k]:v for k,v in g_attr.items() if k in style_map })
22+
23+
"""
24+
Build graph nodes data structure (first step in graph-building)
25+
26+
* Copy shared graph attributes into GL-specific ones
27+
* Collect nodes into graph.nodes dict
28+
* Collect BGP AS into graph.bgp dict
29+
* Return the new graph data structure
30+
"""
31+
def build_nodes(topology: Box, g_type: str) -> Box:
32+
global SHARED_GRAPH_ATTRIBUTES
1833
maps = Box({},default_box=True,box_dots=True)
1934
for name,n in topology.nodes.items():
35+
if g_type != 'graph':
36+
for kw in SHARED_GRAPH_ATTRIBUTES:
37+
if kw in n.get('graph',{}) and kw not in n.get(g_type,{}):
38+
n[g_type][kw] = n.graph[kw]
2039
maps.nodes[name] = n
2140

2241
if 'bgp' in topology.get('module',[]):
@@ -55,6 +74,12 @@ def add_groups(maps: Box, graph_groups: list, topology: Box) -> None:
5574
maps.clusters[g_name].nodes[node] = topology.nodes[node]
5675
placed_hosts.append(node)
5776

77+
"""
78+
Create graph clusters:
79+
80+
* Use 'groups' when available
81+
* Use BGP AS otherwise (if the topology is using BGP)
82+
"""
5883
def graph_clusters(graph: Box, topology: Box, settings: Box) -> None:
5984
if 'groups' in settings:
6085
must_be_list(
@@ -134,7 +159,7 @@ def topo_edges(graph: Box, topology: Box, settings: Box,g_type: str) -> None:
134159

135160
def topology_graph(topology: Box, settings: Box,g_type: str) -> Box:
136161
set_shared_attributes(topology)
137-
graph = build_nodes(topology)
162+
graph = build_nodes(topology,g_type)
138163
graph_clusters(graph,topology,settings)
139164
topo_edges(graph,topology,settings,g_type)
140165
log.exit_on_error()
@@ -171,7 +196,7 @@ def bgp_graph(topology: Box, settings: Box, g_type: str, rr_sessions: bool) -> t
171196
module='graph')
172197
return None
173198

174-
graph = build_nodes(topology)
199+
graph = build_nodes(topology,g_type)
175200
graph.clusters = graph.bgp
176201
graph.edges = []
177202
bgp_sessions(graph,topology,settings,g_type,rr_sessions)

netsim/outputs/d2.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +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
15+
from ._graph import topology_graph,bgp_graph,map_style
1616

1717
'''
1818
Copy default settings into a D2 map converting Python dictionaries into
@@ -53,7 +53,7 @@ def d2_node_attr(f : typing.TextIO, n: Box, settings: Box, indent: str = '') ->
5353
def d2_style(f : typing.TextIO, obj: Box, indent: str) -> None:
5454
if 'd2' not in obj:
5555
return
56-
d2_style = { STYLE_MAP[k]:v for k,v in obj.d2.items() if k in STYLE_MAP }
56+
d2_style = map_style(obj.d2,STYLE_MAP)
5757
d2_extra = obj.get('d2.format',{})
5858

5959
if d2_style or d2_extra:
@@ -208,7 +208,7 @@ def graph_bgp(topology: Box, fname: str, settings: Box, g_format: typing.Optiona
208208
'''
209209
def set_d2_attr(topology: Box) -> None:
210210
global STYLE_MAP
211-
STYLE_MAP = topology.defaults.outputs.d2.styles
211+
STYLE_MAP = topology.defaults.outputs.d2.style_map
212212

213213
for n,ndata in topology.nodes.items():
214214
dev_data = topology.defaults.devices[ndata.device]

netsim/outputs/d2.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,15 @@ ebgp:
4040
target-arrowhead:
4141
shape: arrow
4242

43-
styles:
43+
style_map:
4444
color: stroke
4545
width: stroke-width
46+
fill: fill
4647

4748
attributes:
4849
node:
4950
color: str
51+
fill: str
5052
width: { type: int, min_value: 1, max_value: 32 }
5153
link:
5254
color: str

netsim/outputs/graph.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +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
15+
from ._graph import topology_graph,bgp_graph,map_style
1616

1717
def edge_label(f : typing.TextIO, direction: str, data: Box, subnet: bool = True) -> None:
1818
addr = data.ipv4 or data.ipv6
@@ -29,6 +29,12 @@ def edge_node_net(f : typing.TextIO, link: Box, ifdata: Box, labels: typing.Opti
2929
edge_label(f,'tail',ifdata,subnet=False)
3030
f.write(' ]\n')
3131

32+
'''
33+
Add graph styling information from graph.* link/node attributes
34+
'''
35+
STYLE_MAP: Box
36+
IGNORE_KW: list = ['dir', 'type', 'name']
37+
3238
def get_gv_attr(c_data: Box, o_type: str, settings: Box) -> None:
3339
c_data.graph.format = settings.styles[o_type] + c_data.graph.format
3440

@@ -81,6 +87,8 @@ def gv_end(f : typing.TextIO, fname: str) -> None:
8187
f.close()
8288

8389
def gv_node(f : typing.TextIO, n: Box, settings: Box, indent: int = 0) -> None:
90+
global STYLE_MAP
91+
8492
f.write(f'{" "*indent}"{n.name}" [\n')
8593
node_ip_str = ""
8694
if settings.node_address_label:
@@ -90,7 +98,10 @@ def gv_node(f : typing.TextIO, n: Box, settings: Box, indent: int = 0) -> None:
9098
if node_ip:
9199
node_ip_str = f'{node_ip}'
92100

93-
f.write(f'{" "*indent} label="{n.name} [{n.device}]\\n{node_ip_str}"\n')
101+
gv_attr = n.get('graph.format',{})
102+
gv_attr.label = f"{n.name} [{n.device}]\\n{node_ip_str}"
103+
gv_attr += map_style(n.get('graph',{}),STYLE_MAP)
104+
gv_multiline_attr(f,attr=gv_attr,indent=indent + 2)
94105
f.write(f'{" "*indent}]\n')
95106

96107
def gv_network(f : typing.TextIO, n: Box, settings: Box, indent: int = 0) -> None:
@@ -126,10 +137,12 @@ def gv_nodes(f: typing.TextIO, graph: Box, topology: Box, settings: Box) -> None
126137
gv_node(f,n_data,settings,indent=2)
127138

128139
def gv_links(f: typing.TextIO, graph: Box, topology: Box, settings: Box) -> None:
140+
global STYLE_MAP
129141
for edge in graph.edges:
130142
dir = edge.nodes[0].get('attr.dir','--')
131143
f.write(f' "{edge.nodes[0].node}" -- "{edge.nodes[1].node}"')
132-
attr = get_empty_box()
144+
145+
attr = map_style(edge.attr,STYLE_MAP) + edge.attr.get('format',{})
133146
for n_data in edge.nodes:
134147
if 'type' in n_data:
135148
attr = attr + settings.styles[n_data.type]
@@ -209,6 +222,9 @@ class Graph(_TopologyOutput):
209222
DESCRIPTION :str = 'Topology graph in graphviz format'
210223

211224
def write(self, topology: Box) -> None:
225+
global STYLE_MAP
226+
STYLE_MAP = topology.defaults.outputs.graph.style_map
227+
212228
graphfile = self.settings.filename or 'graph.dot'
213229
output_format = 'topology'
214230

netsim/outputs/graph.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,21 @@ styles:
3737
labelfontsize: 8
3838
labeldistance: 1.5
3939

40+
style_map:
41+
color: color
42+
width: penwidth
43+
fill: fillcolor
44+
4045
attributes:
46+
node:
47+
color: str
48+
fill: str
49+
width: { type: int, min_value: 1, max_value: 32 }
4150
link:
4251
type: dict
4352
_keys:
4453
linkorder: { type: int, min_value: 1, max_value: 200 }
4554
type: { type: str, valid_values: ['lan'] }
46-
shared: [ linkorder ]
55+
color: str
56+
width: { type: int, min_value: 1, max_value: 32 }
57+
shared: [ linkorder, color, fill, width ]

tests/platform-integration/graph/topo.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ groups:
66
device: frr
77
members: [ fabric ]
88
fabric:
9-
# d2.color: '#ff8080'
10-
# d2.width: 3
9+
graph.color: '#ff8080'
10+
graph.width: 3
11+
graph.fill: '#ffc0c0'
1112
bgp.as: 65000
1213
members: [ s1, s2, l1, l2 ]
1314
host:
1415
members: [ h1, h2, h3, h4 ]
1516
device: linux
16-
# d2.color: '#e0e0e0'
1717

1818
module: [ ospf, bgp ]
1919

@@ -27,6 +27,8 @@ links:
2727
graph.linkorder: 200
2828
h2:
2929
graph.linkorder: 200
30+
graph.color: "#FF0000"
31+
graph.width: 3
3032
- interfaces: [ l2, h3 ]
3133
- interfaces: [ l2, h4 ]
3234
graph.type: lan

0 commit comments

Comments
 (0)