|
| 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 | +SHARED_GRAPH_ATTRIBUTES: Box # Graph attributes shared between all graph output modules |
| 12 | + |
| 13 | +def set_shared_attributes(topology: Box) -> None: |
| 14 | + global SHARED_GRAPH_ATTRIBUTES |
| 15 | + SHARED_GRAPH_ATTRIBUTES = topology.defaults.outputs.graph.attributes.shared |
| 16 | + |
| 17 | +def build_nodes(topology: Box) -> Box: |
| 18 | + maps = Box({},default_box=True,box_dots=True) |
| 19 | + for name,n in topology.nodes.items(): |
| 20 | + maps.nodes[name] = n |
| 21 | + |
| 22 | + if 'bgp' in topology.get('module',[]): |
| 23 | + for name,n in topology.nodes.items(): |
| 24 | + bgp_as = n.get('bgp.as',None) |
| 25 | + if bgp_as: |
| 26 | + bgp_as = f'AS_{bgp_as}' |
| 27 | + maps.bgp[bgp_as].nodes[n.name] = n |
| 28 | + |
| 29 | + if 'bgp' in topology and 'as_list' in topology.bgp: |
| 30 | + for (asn,asdata) in topology.bgp.as_list.items(): |
| 31 | + if 'name' in asdata and asn in maps.bgp: |
| 32 | + maps.bgp[asn].name = asdata.name |
| 33 | + |
| 34 | + return maps |
| 35 | + |
| 36 | +''' |
| 37 | +add_groups -- use topology groups as graph clustering mechanism |
| 38 | +''' |
| 39 | +def add_groups(maps: Box, graph_groups: list, topology: Box) -> None: |
| 40 | + if not 'groups' in topology: |
| 41 | + return |
| 42 | + placed_hosts = [] |
| 43 | + |
| 44 | + for g_name,g_data in topology.groups.items(): |
| 45 | + if g_name not in graph_groups: |
| 46 | + continue |
| 47 | + for node in g_data.members: |
| 48 | + if node in placed_hosts: |
| 49 | + log.error( |
| 50 | + f'Cannot create overlapping graph clusters: node {node} is in two groups', |
| 51 | + log.IncorrectValue, |
| 52 | + 'graph') |
| 53 | + continue |
| 54 | + |
| 55 | + maps.clusters[g_name].nodes[node] = topology.nodes[node] |
| 56 | + placed_hosts.append(node) |
| 57 | + |
| 58 | +def graph_clusters(graph: Box, topology: Box, settings: Box) -> None: |
| 59 | + if 'groups' in settings: |
| 60 | + print(settings.groups) |
| 61 | + must_be_list( |
| 62 | + parent=settings, |
| 63 | + key='groups',path='defaults.outputs.graph', |
| 64 | + true_value=list(topology.get('groups',{}).keys()), |
| 65 | + create_empty=True, |
| 66 | + module='graph') |
| 67 | + add_groups(graph,settings.groups,topology) |
| 68 | + elif 'bgp' in graph and settings.as_clusters: |
| 69 | + graph.clusters = graph.bgp |
| 70 | + |
| 71 | +def get_graph_attributes(obj: Box, g_type: str, exclude: list = []) -> Box: |
| 72 | + global SHARED_GRAPH_ATTRIBUTES |
| 73 | + |
| 74 | + # Get graph attributes shared in the 'graph' namespace |
| 75 | + # |
| 76 | + g_attr = { k:v for k,v in obj.get('graph',{}).items() if k in SHARED_GRAPH_ATTRIBUTES } |
| 77 | + g_dict = obj.get(g_type,{}) + g_attr # Get graph-specific attributes + shared attributes |
| 78 | + for kw in exclude: |
| 79 | + if kw in g_dict: # Remove unwanted attributes |
| 80 | + g_dict.pop(kw) |
| 81 | + |
| 82 | + return g_dict |
| 83 | + |
| 84 | +""" |
| 85 | +Get a graph attribute from shared or graph-specific dictionary |
| 86 | +""" |
| 87 | +def get_attr(obj: Box, g_type: str, attr: str, default: typing.Any) -> typing.Any: |
| 88 | + return obj.get(f'{g_type}.{attr}',obj.get(f'graph.{attr}',default)) |
| 89 | + |
| 90 | +def append_edge(graph: Box, if_a: Box, if_b: Box, g_type: str) -> None: |
| 91 | + nodes = [] |
| 92 | + e_attr = get_empty_box() |
| 93 | + for intf in (if_a, if_b): |
| 94 | + addr = intf.ipv4 or intf.ipv6 |
| 95 | + if addr and 'prefix' not in intf: |
| 96 | + addr = addr.split('/')[0] |
| 97 | + intf_attr = get_graph_attributes(intf,g_type) |
| 98 | + e_attr += intf_attr |
| 99 | + e_data = get_box({ |
| 100 | + 'node': intf.node, |
| 101 | + 'attr': intf_attr, |
| 102 | + 'label': addr}) |
| 103 | + for kw in ['type','_subnet']: |
| 104 | + if kw in intf: |
| 105 | + e_data[kw] = intf[kw] |
| 106 | + |
| 107 | + nodes.append(e_data) |
| 108 | + |
| 109 | + graph.edges.append({'nodes': nodes, 'attr': e_attr}) |
| 110 | + |
| 111 | +def topo_edges(graph: Box, topology: Box, settings: Box,g_type: str) -> None: |
| 112 | + graph.edges = [] |
| 113 | + for link in sorted(topology.links,key=lambda x: get_attr(x,g_type,'linkorder',100)): |
| 114 | + l_attr = get_graph_attributes(link,g_type,['linkorder','type']) |
| 115 | + if l_attr: |
| 116 | + for intf in link.interfaces: |
| 117 | + intf[g_type] = l_attr + intf[g_type] |
| 118 | + |
| 119 | + if len(link.interfaces) == 2 and link.get('graph.type','') != 'lan': |
| 120 | + append_edge(graph,link.interfaces[0],link.interfaces[1],g_type) |
| 121 | + else: |
| 122 | + link.node = link.get('bridge',f'{link.type}_{link.linkindex}') |
| 123 | + link.name = link.node |
| 124 | + graph.nodes[link.node] = link |
| 125 | + for af in ['ipv4','ipv6']: |
| 126 | + if af in link.prefix: |
| 127 | + link[af] = link.prefix[af] |
| 128 | + link._subnet = True |
| 129 | + |
| 130 | + for intf in link.interfaces: |
| 131 | + e_data = [ intf, link ] |
| 132 | + if get_attr(intf,g_type,'linkorder',100) > get_attr(link,g_type,'linkorder',101): |
| 133 | + e_data = [ link, intf ] |
| 134 | + append_edge(graph,e_data[0],e_data[1],g_type) |
| 135 | + |
| 136 | +def topology_graph(topology: Box, settings: Box,g_type: str) -> Box: |
| 137 | + set_shared_attributes(topology) |
| 138 | + graph = build_nodes(topology) |
| 139 | + graph_clusters(graph,topology,settings) |
| 140 | + topo_edges(graph,topology,settings,g_type) |
| 141 | + log.exit_on_error() |
| 142 | + return graph |
| 143 | + |
| 144 | +def bgp_sessions(graph: Box, topology: Box, settings: Box, g_type: str, rr_sessions: bool) -> None: |
| 145 | + for n_name,n_data in topology.nodes.items(): |
| 146 | + if 'bgp' not in n_data: |
| 147 | + continue |
| 148 | + for neighbor in n_data.bgp.get('neighbors',[]): |
| 149 | + if neighbor.name < n_name: |
| 150 | + continue |
| 151 | + |
| 152 | + e_1 = get_box({ 'node': n_name, 'type': neighbor.type }) |
| 153 | + e_2 = get_box({ 'node': neighbor.name, 'type': neighbor.type }) |
| 154 | + dir = '<->' |
| 155 | + if 'ibgp' in neighbor.type: |
| 156 | + if n_data.bgp.get('rr',False) and not neighbor.get('rr',False): |
| 157 | + dir = '->' |
| 158 | + elif neighbor.get('rr',False) and not n_data.get('rr',False): |
| 159 | + dir = '->' |
| 160 | + e_1, e_2 = e_2, e_1 |
| 161 | + |
| 162 | + if rr_sessions: |
| 163 | + e_1.graph.dir = dir |
| 164 | + append_edge(graph,e_1,e_2,g_type) |
| 165 | + |
| 166 | +def bgp_graph(topology: Box, settings: Box, g_type: str, rr_sessions: bool) -> typing.Optional[Box]: |
| 167 | + set_shared_attributes(topology) |
| 168 | + if 'bgp' not in topology.module: |
| 169 | + log.error( |
| 170 | + 'Cannot build a BGP graph from a topology that does not use BGP', |
| 171 | + category=log.IncorrectType, |
| 172 | + module='graph') |
| 173 | + return None |
| 174 | + |
| 175 | + graph = build_nodes(topology) |
| 176 | + graph.clusters = graph.bgp |
| 177 | + graph.edges = [] |
| 178 | + bgp_sessions(graph,topology,settings,g_type,rr_sessions) |
| 179 | + log.exit_on_error() |
| 180 | + return graph |
0 commit comments