|
12 | 12 | from . import _TopologyOutput |
13 | 13 | from ..utils import files as _files |
14 | 14 | from ..utils import log |
| 15 | +from ._graph import topology_graph,bgp_graph |
15 | 16 |
|
16 | 17 | ''' |
17 | 18 | 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 = '') -> |
47 | 48 | Add D2 styling information from d2.* link/node attributes |
48 | 49 | ''' |
49 | 50 | STYLE_MAP: Box |
| 51 | +IGNORE_KW: list = ['dir', 'type', 'name'] |
50 | 52 |
|
51 | 53 | def d2_style(f : typing.TextIO, obj: Box, indent: str) -> None: |
52 | 54 | if 'd2' not in obj: |
53 | 55 | 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) |
57 | 61 |
|
58 | 62 | ''' |
59 | 63 | 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 |
108 | 112 | Create a P2P connection between two nodes |
109 | 113 | ''' |
110 | 114 | 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) |
112 | 119 | d2_style(f,l,' ') |
113 | 120 | 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) |
116 | 124 | f.write("}\n") |
117 | 125 |
|
118 | 126 | ''' |
119 | | -Create a connection between a node and a LAN segment |
| 127 | +Create a group container (or ASN container) |
120 | 128 | ''' |
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: |
132 | 130 | f.write(f'{asn} {{\n') |
133 | 131 | copy_d2_attr(f,'container',settings,'- ') |
134 | 132 | asn = asn.replace('_',' ') |
135 | 133 | f.write(' label: '+ (f'{label} ({asn})' if label else asn)+'\n') |
136 | 134 |
|
137 | 135 | ''' |
138 | | -Create a topology graph as a set of containers (groups or ASNs) |
| 136 | +Create graph containers |
139 | 137 | ''' |
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(): |
144 | 142 | 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 |
146 | 144 | f.write('}\n') |
147 | 145 |
|
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) |
187 | 163 |
|
188 | 164 | def graph_topology(topology: Box, fname: str, settings: Box,g_format: typing.Optional[list]) -> bool: |
| 165 | + graph = topology_graph(topology,settings,'d2') |
189 | 166 | 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) |
219 | 167 |
|
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) |
222 | 171 |
|
223 | | -''' |
224 | | -Create a BGP session as a connection between two nodes |
| 172 | + if fname != '-': |
| 173 | + f.close() |
| 174 | + return True |
225 | 175 |
|
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' |
228 | 180 |
|
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: |
248 | 183 | return False |
249 | 184 |
|
250 | 185 | f = _files.open_output_file(fname) |
251 | 186 |
|
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) |
255 | 189 |
|
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 |
258 | 194 |
|
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) |
264 | 196 |
|
265 | | - f.close() |
| 197 | + if fname != '-': |
| 198 | + f.close() |
266 | 199 | return True |
267 | 200 |
|
268 | 201 | graph_dispatch = { |
|
0 commit comments