def proxy_add(self, points): """ :param points: (source, fwd, cost, prefix)* """ log.info('Shapeshifter added attraction points: %s', points) for prefix, route in self._get_proxy_routes(points): self.install_route(prefix, route, self.leader)
def update_graph(self, new_graph): self.leader_watchdog.check_leader(self.get_leader()) added_edges = new_graph.difference(self.graph) removed_edges = self.graph.difference(new_graph) node_prop_diff = {n: data for n, data in new_graph.nodes_iter(data=True) if n not in self.graph or (data.viewitems() - self.graph.node[n].viewitems())} # Propagate differences if added_edges or removed_edges or node_prop_diff: log.debug('Pushing changes') for u, v in added_edges: self.for_all_listeners('add_edge', u, v, new_graph.export_edge_data(u, v)) for u, v in removed_edges: self.for_all_listeners('remove_edge', u, v) if node_prop_diff: self.for_all_listeners('update_node_properties', **node_prop_diff) if CFG.getboolean(DEFAULTSECT, 'draw_graph'): new_graph.draw(CFG.get(DEFAULTSECT, 'graph_loc')) self.graph = new_graph log.info('LSA update yielded +%d -%d edges changes, ' '%d node property changes', len(added_edges), len(removed_edges), len(node_prop_diff)) self.for_all_listeners('commit')
def proxy_remove(self, points): """ :param points: (source, fwd, cost, prefix)* """ log.info('Shapeshifter removed attraction points: %s', points) for prefix, route in self._get_proxy_routes(points): # We don't need the cost self.remove_route_part(prefix, self.leader, *(r[0] for r in route))
def check_leader(self, instance): was_leader = self.leader self.leader = self.instance == self.lsdb.get_leader() if self.leader and not was_leader: # We are the new leader log.info('Elected as leader') for route in self.routes.values(): route.advertize() elif was_leader and not self.leader: log.info('No longer leader')
def _get_diff_lsas(self): self._refresh_augmented_topo() new_lsas = set(self.optimizer.get_fake_lsas()) log.info('New LSA set: %s', new_lsas) to_add = new_lsas.difference(self.current_lsas) to_rem = self.current_lsas.difference(new_lsas) log.info('Removing LSA set: %s', to_rem) self.current_lsas = new_lsas return to_add, to_rem
def solve(self, topo, requirement_dags): # a list of tuples with info on the node to be attracted, # the forwarding address, the cost to be set in the fake LSA, # and the respective destinations self.fake_ospf_lsas = [] self.reqs = requirement_dags self.igp_graph = topo self.igp_paths = ssu.all_shortest_paths(self.igp_graph) # process input forwarding DAGs, one at the time for dest, dag in requirement_dags.iteritems(): logger.debug('Solving DAG for dest %s', dest) self.dest, self.dag = dest, dag self.add_dest_to_graphs(dest, dag) if dest not in topo: sinks = dag.predecessors(dest) for s in sinks: logger.info('Adding edge (%s, %s) in the graph', s, self.dest) topo.add_edge(s, dest, weight=self.new_edge_weight) for n in topo.nodes_iter(): if n == dest: # dest is a path in itself self.igp_paths[n] = ([[n]], 0) continue paths = [] cost = sys.maxint for s in sinks: if s not in self.igp_paths[n][0]: # no path to sink continue c = self.igp_paths[n][1][s] p = self.igp_paths[n][0][s] if c < cost: # new spt paths = list(ssu.extend_paths_list(p, dest)) cost = c if c == cost: # ecmp paths.extend(ssu.extend_paths_list(p, dest)) if paths: _t = self.igp_paths[n] _t[0][dest] = paths _t[1][dest] = cost self.complete_dag() # Add temporarily the destination to the igp graph and/or req dags if not self.solvable(dest, dag): continue for node in nx.topological_sort(dag, reverse=True)[1:]: nhs, original_nhs = self.nhs_for(node, dag, dest) if not self.require_fake_node(nhs, original_nhs): logger.debug('%s does not require a fake node (%s - %s)', node, nhs, original_nhs) continue for req_nh in nhs: logger.debug('Placing a fake node for nh %s', req_nh) self.fake_ospf_lsas.append(ssu.LSA(node=node, nh=req_nh, cost=-1, dest=dest)) return self.fake_ospf_lsas
def retract(self, address, advertize): try: point = self.attraction_points.pop(address) if advertize: point.retract(self.prefix) return point.node except KeyError: log.info('Unkown attraction point %s for prefix %s', address, self.prefix) return None
def remove_session_routes(self, session=None): """Removes the routes that were forced by the application in the session. This function is ment to be invoked when application disconnects, but for the moment we will use it in self.proxy_connected TODO: Keep track of which routes were forced by the application in the current session TODO: Make this function be invoked in the SJMPServer when the application disconnects """ log.info("Removing routes from current session...") map(self.remove_route, self.routes.keys())
def register_change_listener(self, listener): try: del self.listener[listener] log.info('Shapeshifter disconnected.') except KeyError: log.info('Shapeshifter connected.') l = ProxyCloner(ShapeshifterProxy, listener) self.listener[listener] = l l.boostrap_graph(graph=[(u, v, d.get('metric', -1)) for u, v, d in self.graph.edges(data=True)])
def create_node(self, id=None, cls=Router): """ Create a new node :param id: The name of the new node :param cls: The class to use to instantiate it """ n = cls(id=id, prefix=self.name, namespaced=True) self.nodes[n.id] = n log.info('Created node %s', n.id) return n
def refresh_augmented_topo(self): log.info('Solving topologies') try: self.optimizer.solve(self.igp_graph, self.fwd_dags) except Exception as e: log.exception(e) return self.advertized_lsa else: return set(self.optimizer.get_fake_lsas())
def refresh_augmented_topo(self): log.info('Solving topologies') if not self.json_proxy.alive() or not self.has_initial_topo: log.debug('Skipping as we do not yet have a topology') return self.advertized_lsa try: self.optimizer.solve(self.igp_graph, self.fwd_dags) except Exception as e: log.exception(e) return self.advertized_lsa else: return set(self.optimizer.get_fake_lsas())
def solve(self, topo, requirement_dags): # a list of tuples with info on the node to be attracted, # the forwarding address, the cost to be set in the fake LSA, # and the respective destinations self.fake_ospf_lsas = [] self.reqs = requirement_dags self.igp_graph = topo self.igp_paths = ShortestPath(self.igp_graph) # process input forwarding DAGs, one at the time for dest, dag in requirement_dags.iteritems(): log.info('Solving DAG for dest %s', dest) self.dest, self.dag = dest, dag log.debug('Checking dest in dag') ssu.add_dest_to_graph(dest, dag) log.debug('Checking dest in igp graph') ssu.add_dest_to_graph(dest, topo, edges_src=dag.predecessors, spt=self.igp_paths, metric=self.new_edge_metric) ssu.complete_dag(dag, topo, dest, self.igp_paths, skip=self.reqs.keys()) # Add temporarily the destination to the igp graph and/or req dags if not ssu.solvable(dag, topo): log.warning('Skipping requirement for dest: %s', dest) continue for node in dag: nhs = self.nhs_for(node, dest, dag) if not nhs: continue for req_nh in nhs: log.debug('Placing a fake node for %s->%s', node, req_nh) for i in xrange(get_edge_multiplicity(dag, node, req_nh)): self.fake_ospf_lsas.append( ssu.LSA(node=node, nh=req_nh, cost=(-1 - i), dest=dest)) # Check whether we need to include one more fake node to handle # the case where we create a new route from scratch. for p in dag.predecessors_iter(dest): if not is_fake(topo, p, dest): continue log.debug( '%s is a terminal node towards %s but had no prior ' 'route to it! Adding a synthetic route', p, dest) self.fake_ospf_lsas.append( ssu.GlobalLie(dest, self.new_edge_metric, p)) return self.fake_ospf_lsas
def register_change_listener(self, listener): try: del self.listener[listener] log.info('Shapeshifter disconnected.') except KeyError: log.info('Shapeshifter connected.') l = ProxyCloner(ShapeshifterProxy, listener) self.listener[listener] = l l.bootstrap_graph(graph=[(u, v, d) for u, v, d in self.graph.export_edges() ], node_properties={n: data for n, data in self.graph.nodes_iter(data=True) })
def update_graph(self, new_graph): added_edges = graph_diff(new_graph, self.graph) removed_edges = graph_diff(self.graph, new_graph) # Propagate differences if len(added_edges) > 0 or len(removed_edges) > 0: log.debug('Pushing changes') for u, v in added_edges: self.listener_add_edge(u, v, new_graph[u][v]['metric']) for u, v in removed_edges: self.listener_remove_edge(u, v) if CFG.getboolean(DEFAULTSECT, 'draw_graph'): draw_graph(new_graph) self.graph = new_graph log.info('LSA update yielded +%d -%d edges changes' % (len(added_edges), len(removed_edges)))
def print_routes(self): """ Log all fibbing routes that are installed in the network """ log.info('----------------------') log.info('%s fibbing routes', len(self.routes)) log.info('----------------------') for route in self.routes.values(): log.info(route)
def solve(self, topo, requirement_dags): # a list of tuples with info on the node to be attracted, # the forwarding address, the cost to be set in the fake LSA, # and the respective destinations self.fake_ospf_lsas = [] self.reqs = requirement_dags self.igp_graph = topo self.igp_paths = ShortestPath(self.igp_graph) # process input forwarding DAGs, one at the time for dest, dag in requirement_dags.iteritems(): log.info('Solving DAG for dest %s', dest) self.dest, self.dag = dest, dag log.debug('Checking dest in dag') ssu.add_dest_to_graph(dest, dag) log.debug('Checking dest in igp graph') ssu.add_dest_to_graph(dest, topo, edges_src=dag.predecessors, spt=self.igp_paths, metric=self.new_edge_metric) ssu.complete_dag(dag, topo, dest, self.igp_paths, skip=self.reqs.keys()) # Add temporarily the destination to the igp graph and/or req dags if not ssu.solvable(dag, topo): log.warning('Skipping requirement for dest: %s', dest) continue for node in dag: nhs = self.nhs_for(node, dest, dag) if not nhs: continue for req_nh in nhs: log.debug('Placing a fake node for %s->%s', node, req_nh) for i in xrange(get_edge_multiplicity(dag, node, req_nh)): self.fake_ospf_lsas.append(ssu.LSA(node=node, nh=req_nh, cost=(-1 - i), dest=dest)) # Check whether we need to include one more fake node to handle # the case where we create a new route from scratch. for p in dag.predecessors_iter(dest): if not is_fake(topo, p, dest): continue log.debug('%s is a terminal node towards %s but had no prior ' 'route to it! Adding a synthetic route', p, dest) self.fake_ospf_lsas.append( ssu.GlobalLie(dest, self.new_edge_metric, p)) return self.fake_ospf_lsas
def add_dest_to_graph(dest, graph, edges_src=None, spt=None, node_data_gen=None, **kw): """Add dest to the graph, possibly updating the shortest paths object :param dest: The destination node, will be set as a prefix :param graph: The graph to which dest must be added if not present :param edges_src: The source of edges to add in order to add dest, if None, defaults to the sinks in the graph, otherwise it is a function returning a list of edges and taking dest as argument :param spt: The ShortestPath object to update to account for the new node if applicable :param node_data_gen: A function that will generate data for the new node if needed :param kw: Extra parameters for the edges if any""" added = None if dest in graph and not _is_fake_dest(graph, dest): # Unless dest was only announced through fake links, we don't touch it log.debug('%s is already in the graph', dest) in_dag = True else: in_dag = False if not edges_src: added = [] sinks = find_sink(graph) if not sinks: log.info('No sinks found in the graph!') for node in sinks: if node == dest: continue log.info('Connected %s to %s in the graph', node, dest) _add_fake_route(graph, node, dest, **kw) added.append(node) else: added = edges_src(dest) log.info('Connecting edges sources %s to the graph to %s', dest, added) for s in added: _add_fake_route(graph, s, dest, **kw) graph.add_edges_from((s, dest) for s in added, **kw) ndata = {} if not node_data_gen else node_data_gen() # Only update the dest node if explicitely requested if node_data_gen or not in_dag: graph.add_node(dest, prefix=True, **ndata) if added and spt: log.info('Updating SPT') _update_paths_towards(spt, graph, dest, added)
def add_dest_to_graph(dest, graph, edges_src=None, spt=None, node_data_gen=None, **kw): """Add dest to the graph, possibly updating the shortest paths object :param dest: The destination node, will be set as a prefix :param graph: The graph to which dest must be added if not present :param edges_src: The source of edges to add in order to add dest, if None, defaults to the sinks in the graph, otherwise it is a function returning a list of edges and taking dest as argument :param spt: The ShortestPath object to update to account for the new node if applicable :param node_data_gen: A function that will generate data for the new node if needed :param kw: Extra parameters for the edges if any""" added = None if dest in graph and not _is_fake_dest(graph, dest): # Unless dest was only announced through fake links, we don't touch it log.debug("%s is already in the graph", dest) in_dag = True else: in_dag = False if not edges_src: added = [] sinks = find_sink(graph) if not sinks: log.info("No sinks found in the graph!") for node in sinks: if node == dest: continue log.info("Connected %s to %s in the graph", node, dest) _add_fake_route(graph, node, dest, **kw) added.append(node) else: added = edges_src(dest) log.info("Connecting edges sources %s to the graph to %s", dest, added) for s in added: _add_fake_route(graph, s, dest, **kw) graph.add_edges_from((s, dest) for s in added, **kw) ndata = {} if not node_data_gen else node_data_gen() # Only update the dest node if explicitely requested if node_data_gen or not in_dag: graph.add_node(dest, prefix=True, **ndata) if added and spt: log.info("Updating SPT") _update_paths_towards(spt, graph, dest, added)
def add_dest_to_graph(dest, graph, edges_src=None, spt=None, node_data_gen=None, **kw): """Add dest to the graph, possibly updating the shortest paths object :param dest: The destination node, will be set as a prefix :param graph: The graph to which dest must be added if not present :param edges_src: The source of edges to add in order to add dest, if None, defaults to the sinks in the graph, otherwise it is a function returning a list of edges and taking dest as argument :param spt: The ShortestPath object to update to account for the new node if applicable :param node_data_gen: A function that will generate data for the new node if needed :param kw: Extra parameters for the edges if any""" if dest in graph: log.debug('%s is already in the graph', dest) return if not edges_src: added = [] sinks = find_sink(graph) if not sinks: log.info('No sinks found in the graph!') for node in sinks: log.info('Connected %s to %s in the graph', node, dest) # TODO cleanup, atm. some places use DiGraph other IGPGraph ... graph.add_edge(node, dest, **kw) added.append(node) else: added = edges_src(dest) log.info('Adding edges sources %s to the graph', added) graph.add_edges_from((s, dest) for s in added, **kw) ndata = {} if not node_data_gen else node_data_gen() graph.add_node(dest, prefix=True, **ndata) if added and spt: log.info('Updating SPT') _update_paths_towards(spt, graph, dest, added)
def add_edge(self, source, destination, metric): log.info("Adding %s-%s @ %s", source, destination, metric) self.graph.add_edge(source, destination, cost=metric)
def add_dest_to_graphs(self, dest, dag): if dest not in dag: for node in ssu.find_sink(dag): logger.info('Connected %s to %s in the DAG', node, dest) dag.add_edge(node, dest)
def remove_edge(self, source, destination): log.info('Removing %s-%s', source, destination) self.graph.remove_edge(source, destination)
def boostrap_graph(self, graph): log.info('Received graph: %s', graph) for u, v, m in graph: self.graph.add_edge(u, v, cost=m)
def run(self): log.info('Connecting to server ...') self.json_proxy.communicate()
def add_edge(self, source, destination, metric): log.info('Adding %s-%s @ %s', source, destination, metric) self.graph.add_edge(source, destination, cost=metric)
def remove_edge(self, source, destination): log.info("Removing %s-%s", source, destination) self.graph.remove_edge(source, destination)
def solve(self, graph, requirements): """Compute the augmented topology for a given graph and a set of requirements. :type graph: IGPGraph :type requirements: { dest: IGPGraph } :param requirements: the set of requirement DAG on a per dest. basis :return: list of fake LSAs""" self.reqs = requirements log.info('Preparing IGP graph') self.g = prepare_graph(graph, requirements) log.info('Computing SPT') self._p = ShortestPath(graph) lsa = [] for dest, dag in requirements.iteritems(): self.dest, self.dag = dest, dag self.ecmp.clear() log.info('Evaluating requirement %s', dest) log.info('Ensuring the consistency of the DAG') self.check_dest() ssu.complete_dag(self.dag, self.g, self.dest, self._p, skip=self.reqs.keys()) log.info('Computing original and required next-hop sets') for n, node in self.nodes(): node.forced_nhs = set(self.dag.successors(n)) node.original_nhs = set([p[1] for p in self._p.default_path(n, self.dest)]) if not ssu.solvable(self.dag, self.g): log.warning('Consistency check failed, skipping %s', dest) continue log.info('Placing initial fake nodes') self.place_fake_nodes() log.info('Initializing fake nodes') self.initialize_fake_nodes() log.info('Propagating initial lower bounds') self.propagate_lb() log.debug('Fake node bounds: %s', [n for _, n in self.nodes() if n.has_any_fake_node()]) log.info('Reducing the augmented topology') self.merge_fake_nodes() self.remove_redundant_fake_nodes() log.info('Generating LSAs') lsas = self.create_fake_lsa() log.info('Solved the DAG for destination %s with LSA set: %s', self.dest, lsas) lsa.extend(lsas) return lsa
def boostrap_graph(self, graph): log.info("Received graph: %s", graph) for u, v, m in graph: self.graph.add_edge(u, v, cost=m)
def print_net(self): """ Log a representation of the fibbing network state """ log.info('----------------------') log.info('Network of %s nodes', len(self.nodes)) log.info('----------------------') log.info('') r_info = str(self.root) for l in r_info.split('\n'): log.info(l) log.info('|') bid_str = '%s --+' % self.bridge.id prefix = ' ' * (len(bid_str) - 1) log.info(bid_str) for n in self.nodes.values(): if n == self.root: continue log.info('%s|', prefix) log.info('%s+--- %s', prefix, n)
def run(self): """Connect the the southbound controller. This call will not return unless the connection is halted.""" log.info('Connecting to server ...') self.json_proxy.communicate()
def check_fwd_dags(fwd_req, topo, lsas, solver): correct = True topo = topo.copy() # Check that the topology/dag contain the destinations, otherwise add it for dest, dag in fwd_req.iteritems(): dest_in_dag = dest in dag dest_in_graph = dest in topo if not dest_in_dag or not dest_in_graph: if not dest_in_dag: sinks = ssu.find_sink(dag) else: sinks = dag.predecessors(dest) for s in sinks: if not dest_in_dag: dag.add_edge(s, dest) if not dest_in_graph: topo.add_edge(s, dest, metric=solver.new_edge_metric) fake_nodes = {} local_fake_nodes = collections.defaultdict(list) f_ids = set() for lsa in lsas: if lsa.cost > 0: f_id = '__f_%s_%s_%s' % (lsa.node, lsa.nh, lsa.dest) f_ids.add(f_id) fake_nodes[(lsa.node, f_id, lsa.dest)] = lsa.nh cost = topo[lsa.node][lsa.nh]['metric'] topo.add_edge(lsa.node, f_id, metric=cost) topo.add_edge(f_id, lsa.dest, metric=lsa.cost - cost) log.debug( 'Added a globally-visible fake node: ' '%s - %s - %s - %s - %s [-> %s]', lsa.node, cost, f_id, lsa.cost - cost, lsa.dest, lsa.nh) else: local_fake_nodes[(lsa.node, lsa.dest)].append(lsa.nh) log.debug('Added a locally-visible fake node: %s -> %s', lsa.node, lsa.nh) spt = ssu.all_shortest_paths(topo, metric='metric') for dest, req_dag in fwd_req.iteritems(): log.info('Validating requirements for dest %s', dest) dag = IGPGraph() for n in filter(lambda n: n not in fwd_req, topo): if n in f_ids: continue log.debug('Checking paths of %s', n) for p in spt[n][0][dest]: log.debug('Reported path: %s', p) for u, v in zip(p[:-1], p[1:]): try: # Are we using a globally-visible fake node? nh = fake_nodes[(u, v, dest)] log.debug( '%s uses the globally-visible fake node %s ' 'to get to %s', u, v, nh) dag.add_edge(u, nh) # Replace by correct next-hop break except KeyError: # Are we using a locally-visible one? nh = local_fake_nodes[(u, dest)] if nh: log.debug( '%s uses a locally-visible fake node' ' to get to %s', u, nh) for h in nh: dag.add_edge(u, h) # Replace by true nh break else: dag.add_edge(u, v) # Otherwise follow the SP # Now that we have the current fwing dag, compare to the requirements for n in req_dag: successors = set(dag.successors(n)) req_succ = set(req_dag.successors(n)) if successors ^ req_succ: log.error( 'The successor sets for node %s differ, ' 'REQ: %s, CURRENT: %s', n, req_succ, successors) correct = False predecessors = set(dag.predecessors(n)) req_pred = set(req_dag.predecessors(n)) # Also requires to have a non-null successor sets to take into # account the fact that the destination will have new adjacencies # through fake nodes if predecessors ^ req_pred and successors: log.error( 'The predecessors sets for %s differ, ' 'REQ: %s, CURRENT: %s', n, req_pred, predecessors) correct = False if correct: log.info('All forwarding requirements are enforced!') return correct
def apply_merge(self, n, s, lb, ub, nh): """Try to apply a given merge, n->s, with new lb/ub for s, and corresponding to the nexthop of n nh""" undos = [] propagation_failure = [] def undo_all(): log.debug('Undoing all changes') for (f, args, kw) in reversed(undos): f(*args, **kw) def record_undo(f, *args, **kw): undos.append((f, args, kw)) def propagation_fail(n): log.debug('The propagation failed on node %s, aborting merge!', n) propagation_failure.append(False) return True def propagation_assign(node, lb): record_undo(setattr, node, 'lb', node.lb) log.debug('Propagation caused the LB of %s to increase by %s', node, lb) Node.increase_lb(node, lb) log.debug('Trying to apply merge, n: %s, s:%s, lb:%s, ub:%s, nh:%s', n, s, lb, ub, nh) # Remove the fake node node = self.node(n) node.forced_nhs.remove(nh) record_undo(node.forced_nhs.add, nh) # Update the values in its successor succ_node = self.node(s) path_cost_increase = (self._p.default_cost(n, s) + succ_node.lb - node.lb) record_undo(setattr, succ_node, 'lb', succ_node.lb) record_undo(setattr, succ_node, 'ub', succ_node.ub) succ_node.lb = lb succ_node.ub = ub ecmp_deps = list(self.ecmp_dep(n)) log.debug('Checking merge effect on ECMP dependencies of %s: %s', n, ecmp_deps) if s in ecmp_deps: log.debug( 'Aborting merge has %s and %s are ECMP dependent: ' 'Merging them would make it impossible to keep both path' ' with the same cost!', n, s) undo_all() return remove_n = not node.has_fake_node(Node.GLOBAL) if remove_n: record_undo(node.add_fake_node, node.fake) node.remove_fake_node() log.debug( 'Also removing %s from its ECMP deps has it no longer ' 'has a fake node.', n) deps = self.ecmp[s] for e in ecmp_deps: e_node = self.node(e) e_deps = self.ecmp[e] if remove_n: e_deps.remove(n) record_undo(e_deps.add, n) if e == n: continue if e not in deps: deps.add(e) record_undo(deps.remove, e) if s not in e_deps: e_deps.add(s) record_undo(e_deps.remove, s) new_lb = e_node.lb + path_cost_increase if not self.valid_range(e, new_lb, e_node.ub): log.debug( 'Cannot increase the ECMP ecmp dep %s of %s by %s. ' 'Aborting merge!', e, n, path_cost_increase) undo_all() return else: log.debug('Increased %s to %s', e, new_lb) record_undo(setattr, e_node, 'lb', e_node.lb) e_node.lb = new_lb ecmp_deps.append(s) log.debug('Propagating LB changes') self.propagate_lb(assign=propagation_assign, fail_func=propagation_fail, initial_nodes=ecmp_deps) if propagation_failure: undo_all() else: log.info('Merged %s into %s', n, s)
def apply_merge(self, n, s, lb, ub, nh): """Try to apply a given merge, n->s, with new lb/ub for s, and corresponding to the nexthop of n nh""" undos = [] propagation_failure = [] def undo_all(): log.debug('Undoing all changes') for (f, args, kw) in reversed(undos): f(*args, **kw) def record_undo(f, *args, **kw): undos.append((f, args, kw)) def propagation_fail(n): log.debug('The propagation failed on node %s, aborting merge!', n) propagation_failure.append(False) return True def propagation_assign(node, lb): record_undo(setattr, node, 'lb', node.lb) log.debug('Propagation caused the LB of %s to increase by %s', node, lb) Node.increase_lb(node, lb) log.debug('Trying to apply merge, n: %s, s:%s, lb:%s, ub:%s, nh:%s', n, s, lb, ub, nh) # Remove the fake node node = self.node(n) node.forced_nhs.remove(nh) record_undo(node.forced_nhs.add, nh) # Update the values in its successor succ_node = self.node(s) path_cost_increase = (self._p.default_cost(n, s) + succ_node.lb - node.lb) record_undo(setattr, succ_node, 'lb', succ_node.lb) record_undo(setattr, succ_node, 'ub', succ_node.ub) succ_node.lb = lb succ_node.ub = ub ecmp_deps = list(self.ecmp_dep(n)) log.debug('Checking merge effect on ECMP dependencies of %s: %s', n, ecmp_deps) if s in ecmp_deps: log.debug('Aborting merge has %s and %s are ECMP dependent: ' 'Merging them would make it impossible to keep both path' ' with the same cost!', n, s) undo_all() return remove_n = not node.has_fake_node(Node.GLOBAL) if remove_n: record_undo(node.add_fake_node, node.fake) node.remove_fake_node() log.debug('Also removing %s from its ECMP deps has it no longer ' 'has a fake node.', n) deps = self.ecmp[s] for e in ecmp_deps: e_node = self.node(e) e_deps = self.ecmp[e] if remove_n: e_deps.remove(n) record_undo(e_deps.add, n) if e == n: continue if e not in deps: deps.add(e) record_undo(deps.remove, e) if s not in e_deps: e_deps.add(s) record_undo(e_deps.remove, s) new_lb = e_node.lb + path_cost_increase if not self.valid_range(e, new_lb, e_node.ub): log.debug('Cannot increase the ECMP ecmp dep %s of %s by %s. ' 'Aborting merge!', e, n, path_cost_increase) undo_all() return else: log.debug('Increased %s to %s', e, new_lb) record_undo(setattr, e_node, 'lb', e_node.lb) e_node.lb = new_lb ecmp_deps.append(s) log.debug('Propagating LB changes') self.propagate_lb(assign=propagation_assign, fail_func=propagation_fail, initial_nodes=ecmp_deps) if propagation_failure: undo_all() else: log.info('Merged %s into %s', n, s)
def solve(self, graph, requirements): """Compute the augmented topology for a given graph and a set of requirements. :type graph: IGPGraph :type requirements: { dest: IGPGraph } :param requirements: the set of requirement DAG on a per dest. basis :return: list of fake LSAs""" self.reqs = requirements log.info('Preparing IGP graph') self.g = prepare_graph(graph, requirements) log.info('Computing SPT') self._p = ShortestPath(graph) lsa = [] for dest, dag in requirements.iteritems(): self.dest, self.dag = dest, dag self.ecmp.clear() log.info('Evaluating requirement %s', dest) log.info('Ensuring the consistency of the DAG') self.check_dest() ssu.complete_dag(self.dag, self.g, self.dest, self._p, skip=self.reqs.keys()) log.info('Computing original and required next-hop sets') for n, node in self.nodes(): node.forced_nhs = set(self.dag.successors(n)) node.original_nhs = set( [p[1] for p in self._p.default_path(n, self.dest)]) if not ssu.solvable(self.dag, self.g): log.warning('Consistency check failed, skipping %s', dest) continue log.info('Placing initial fake nodes') self.place_fake_nodes() log.info('Initializing fake nodes') self.initialize_fake_nodes() log.info('Propagating initial lower bounds') self.propagate_lb() log.debug('Fake node bounds: %s', [n for _, n in self.nodes() if n.has_any_fake_node()]) log.info('Reducing the augmented topology') self.merge_fake_nodes() self.remove_redundant_fake_nodes() log.info('Generating LSAs') lsas = self.create_fake_lsa() log.info('Solved the DAG for destination %s with LSA set: %s', self.dest, lsas) lsa.extend(lsas) return lsa
def check_fwd_dags(fwd_req, topo, lsas, solver): correct = True topo = topo.copy() # Check that the topology/dag contain the destinations, otherwise add it for dest, dag in fwd_req.iteritems(): dest_in_dag = dest in dag dest_in_graph = dest in topo if not dest_in_dag or not dest_in_graph: if not dest_in_dag: sinks = ssu.find_sink(dag) else: sinks = dag.predecessors(dest) for s in sinks: if not dest_in_dag: dag.add_edge(s, dest) if not dest_in_graph: topo.add_edge(s, dest, metric=solver.new_edge_metric) fake_nodes = {} local_fake_nodes = collections.defaultdict(list) f_ids = set() for lsa in lsas: if lsa.cost > 0: if not lsa.node: # We added a pure fake LSA continue f_id = '__f_%s_%s_%s' % (lsa.node, lsa.nh, lsa.dest) f_ids.add(f_id) fake_nodes[(lsa.node, f_id, lsa.dest)] = lsa.nh cost = topo[lsa.node][lsa.nh]['metric'] topo.add_edge(lsa.node, f_id, metric=cost) topo.add_edge(f_id, lsa.dest, metric=lsa.cost - cost) log.debug('Added a globally-visible fake node: ' '%s - %s - %s - %s - %s [-> %s]', lsa.node, cost, f_id, lsa.cost - cost, lsa.dest, lsa.nh) else: local_fake_nodes[(lsa.node, lsa.dest)].append(lsa.nh) log.debug('Added a locally-visible fake node: %s -> %s', lsa.node, lsa.nh) spt = ssu.all_shortest_paths(topo, metric='metric') for dest, req_dag in fwd_req.iteritems(): log.info('Validating requirements for dest %s', dest) dag = IGPGraph() for n in filter(lambda n: n not in fwd_req, topo): if n in f_ids: continue log.debug('Checking paths of %s', n) for p in spt[n][0][dest]: log.debug('Reported path: %s', p) for u, v in zip(p[:-1], p[1:]): try: # Are we using a globally-visible fake node? nh = fake_nodes[(u, v, dest)] log.debug('%s uses the globally-visible fake node %s ' 'to get to %s', u, v, nh) dag.add_edge(u, nh) # Replace by correct next-hop break except KeyError: # Are we using a locally-visible one? nh = local_fake_nodes[(u, dest)] if nh: log.debug('%s uses a locally-visible fake node' ' to get to %s', u, nh) for h in nh: dag.add_edge(u, h) # Replace by true nh break else: dag.add_edge(u, v) # Otherwise follow the SP # Now that we have the current fwing dag, compare to the requirements for n in req_dag: successors = set(dag.successors(n)) req_succ = set(req_dag.successors(n)) if successors ^ req_succ: log.error('The successor sets for node %s differ, ' 'REQ: %s, CURRENT: %s', n, req_succ, successors) correct = False predecessors = set(dag.predecessors(n)) req_pred = set(req_dag.predecessors(n)) # Also requires to have a non-null successor sets to take into # account the fact that the destination will have new adjacencies # through fake nodes if predecessors ^ req_pred and successors: log.error('The predecessors sets for %s differ, ' 'REQ: %s, CURRENT: %s', n, req_pred, predecessors) correct = False if correct: log.info('All forwarding requirements are enforced!') return correct
def _refresh_augmented_topo(self): log.info('Solving topologies') self.optimizer.solve(self.igp_graph, self.fwd_dags)