def testSquareWithThreeConsecutiveChangesAndMultipleRequirements( self, expected_lsa_count=5): self.log_test_name() dag = IGPGraph([('D2', 'B1'), ('B1', 'T1'), ('T1', 'T2'), ('T2', 'B2'), ('B2', 'D1')]) self._test(self.gadgets.square, { '3_8': dag, '8_3': dag.reverse(copy=True) }, expected_lsa_count)
def testSquareWithThreeConsecutiveChangesAndMultipleRequirements(self): log.warning('Testing SquareWithThreeConsecutiveChanges' 'AndMultipleRequirements') dag = IGPGraph([('D2', 'B1'), ('B1', 'T1'), ('T1', 'T2'), ('T2', 'B2'), ('B2', 'D1')]) self._test(self.gadgets.square, { '3_8': dag, '8_3': dag.reverse(copy=True) }, 5)
def __init__(self, *args, **kwargs): super(SouthboundListener, self).__init__(*args, **kwargs) self.igp_graph = IGPGraph() self.dirty = False self.json_proxy = SJMPClient(hostname=CFG.get(DEFAULTSECT, 'json_hostname'), port=CFG.getint(DEFAULTSECT, 'json_port'), target=self) self.quagga_manager = ProxyCloner(FakeNodeProxy, self.json_proxy)
def testSquareWithThreeConsecutiveChangesAndMultipleRequirements(self): log.warning('Testing SquareWithThreeConsecutiveChanges' 'AndMultipleRequirements') dag = IGPGraph([('D2', 'B1'), ('B1', 'T1'), ('T1', 'T2'), ('T2', 'B2'), ('B2', 'D1')]) self._test(self.gadgets.square, {'3_8': dag, '8_3': dag.reverse(copy=True)}, 5)
def testSquareWithThreeConsecutiveChangesAndMultipleRequirements( self, expected_lsa_count=5): self.log_test_name() dag = IGPGraph([('D2', 'B1'), ('B1', 'T1'), ('T1', 'T2'), ('T2', 'B2'), ('B2', 'D1')]) self._test(self.gadgets.square, {'3_8': dag, '8_3': dag.reverse(copy=True)}, expected_lsa_count)
def _setUpDoubleDiamond(self): # + --------19--------- + # | | # H1 ---10--- Y1 | # \ | | # 15 5 | # \ | | # Y2 -10- X --100-- D --1000-- 1/8 # | | # H2---2---+ | # / | # 6 | # / | # A -------- 17 --------+ self.ddiamond = g = IGPGraph() self._add_edge(g, 'H1', 'D', metric=19) self._add_edge(g, 'H1', 'Y1', metric=10) self._add_edge(g, 'Y1', 'X', metric=5) self._add_edge(g, 'H1', 'Y2', metric=15) self._add_edge(g, 'Y2', 'X', metric=10) self._add_edge(g, 'A', 'H2', metric=6) self._add_edge(g, 'H2', 'X', metric=2) self._add_edge(g, 'A', 'D', metric=17) self._add_edge(g, 'X', 'D', metric=100) for _, data in g.nodes_iter(data=True): data['router'] = True
def _setUpPaperGadget(self): # H1 -- 19 -- A1 ---------+ # | | # +-- 10 ----+ 2 # | | # H2 -- 2 -- X -- 100 -- Y # | / \ | # 6 H3 -- 2 \ | # | | 8 | # | 6----+ / 17 # | | / | # +--------A2------------+ # self.paper_gadget = g = IGPGraph() self._add_edge(g, 'H1', 'A1', 19) self._add_edge(g, 'H1', 'X', 10) self._add_edge(g, 'A1', 'Y', 2) self._add_edge(g, 'X', 'Y', 100) self._add_edge(g, 'X', 'H2', 2) self._add_edge(g, 'X', 'H3', 2) self._add_edge(g, 'X', 'A2', 8) self._add_edge(g, 'H3', 'A2', 6) self._add_edge(g, 'H2', 'A2', 6) self._add_edge(g, 'Y', 'A2', 17) for _, data in g.nodes_iter(data=True): data['router'] = True
def testSquareWithThreeConsecutiveChanges(self, expected_lsa_count=3): self.log_test_name() self._test( self.gadgets.square, { '3_8': IGPGraph([('D2', 'B1'), ('B1', 'T1'), ('T1', 'T2'), ('T2', 'B2'), ('B2', 'D1')]) }, expected_lsa_count)
def testDiamond(self, expected_lsa_count=2): self.log_test_name() self._test( self.gadgets.diamond, { '3_8': IGPGraph([('A', 'Y1'), ('A', 'Y2'), ('Y2', 'X'), ('Y1', 'X'), ('X', 'D'), ('O', 'D')]) }, expected_lsa_count)
def testDiamond(self): log.warning('Testing Diamond') self._test( self.gadgets.diamond, { '3_8': IGPGraph([('A', 'Y1'), ('A', 'Y2'), ('Y2', 'X'), ('Y1', 'X'), ('X', 'D'), ('O', 'D')]) }, 2)
def testSquareWithThreeConsecutiveChanges(self): log.warning('Testing SquareWithThreeConsecutiveChanges') self._test( self.gadgets.square, { '3_8': IGPGraph([('D2', 'B1'), ('B1', 'T1'), ('T1', 'T2'), ('T2', 'B2'), ('B2', 'D1')]) }, 3)
def testDoubleDiamond(self): log.warning('Testing DoubleDiamond') self._test( self.gadgets.ddiamond, { '1_8': IGPGraph([('H1', 'Y1'), ('H1', 'Y2'), ('Y1', 'X'), ('Y2', 'X'), ('H2', 'X'), ('X', 'D')]) }, 3)
def testPaperGadget(self, expected_lsa_count=1): self.log_test_name() self._test( self.gadgets.paper_gadget, { '3_8': IGPGraph([('H1', 'X'), ('H2', 'X'), ('H3', 'X'), ('X', 'Y'), ('A1', 'Y'), ('A2', 'Y')]) }, expected_lsa_count)
def testPaperGadget(self): log.warning('Testing PaperGadget') self._test( self.gadgets.paper_gadget, { '3_8': IGPGraph([('H1', 'X'), ('H2', 'X'), ('H3', 'X'), ('X', 'Y'), ('A1', 'Y'), ('A2', 'Y')]) }, 1)
def testDoubleDiamond(self, expected_lsa_count=3): self.log_test_name() self._test( self.gadgets.ddiamond, { '1_8': IGPGraph([('H1', 'Y1'), ('H1', 'Y2'), ('Y1', 'X'), ('Y2', 'X'), ('H2', 'X'), ('X', 'D')]) }, expected_lsa_count)
def __init__(self): self.BASE_NET = ip_network(CFG.get(DEFAULTSECT, 'base_net')) self.private_addresses = PrivateAddressStore(CFG.get(DEFAULTSECT, 'private_ips')) self.last_line = '' self.leader_watchdog = None self.transaction = False self.uncommitted_changes = 0 self.graph = IGPGraph() self._lsdb = {NetworkLSA.TYPE: {}, RouterLSA.TYPE: {}, ASExtLSA.TYPE: {}} self.controllers = defaultdict(list) # controller nr : ip_list self.listener = {} self.keep_running = True self.queue = Queue() self.processing_thread = start_daemon_thread( target=self.process_lsa, name='lsa processing thread')
def testParallel(self): log.warning('Testing Parallel') self._test( self.gadgets.parallel, { '3_8': IGPGraph([('A2', 'B2'), ('B2', 'C2'), ('C2', 'D2'), ('D2', 'D1'), ('D1', 'C1'), ('C1', 'B1'), ('B1', 'A1'), ('A1', 'D')]) }, 4)
def simple_path_requirement(self, prefix, path): """Add a path requirement for the given prefix. :param path: The ordered list of routerid composing the path. E.g. for path = [A, B, C], the following edges will be used as requirements: [](A, B), (B, C), (C, D)]""" self.fwd_dags[prefix] = IGPGraph( [(s, d) for s, d in zip(path[:-1], path[1:])]) self.refresh_lsas()
def testParallel(self, expected_lsa_count=4): self.log_test_name() self._test( self.gadgets.parallel, { '3_8': IGPGraph([('A2', 'B2'), ('B2', 'C2'), ('C2', 'D2'), ('D2', 'D1'), ('D1', 'C1'), ('C1', 'B1'), ('B1', 'A1'), ('A1', 'D')]) }, expected_lsa_count)
def __init__(self): self.BASE_NET = ip_network(CFG.get(DEFAULTSECT, 'base_net')) self.private_addresses = PrivateAddressStore( CFG.get(DEFAULTSECT, 'private_ips')) self.last_line = '' self.leader_watchdog = None self.transaction = None self.graph = IGPGraph() self.routers = {} # router-id : lsa self.networks = {} # DR IP : lsa self.ext_networks = {} # (router-id, dest) : lsa self.controllers = defaultdict(list) # controller nr : ip_list self.listener = {} self.keep_running = True self.queue = Queue() self.processing_thread = Thread(target=self.process_lsa, name="lsa_processing_thread") self.processing_thread.setDaemon(True) self.processing_thread.start()
def build_graph(self): self.controllers.clear() new_graph = IGPGraph() # Rebuild the graph from the LSDB for lsa in chain(self.routers.itervalues(), self.networks.itervalues(), self.ext_networks.itervalues()): if is_expired_lsa(lsa): log.debug("LSA %s is too old (%d) ignoring it!", lsa, lsa.age) else: lsa.apply(new_graph, self) # Contract all IPs to their respective router-id for rlsa in self.routers.itervalues(): rlsa.contract_graph(new_graph, self.private_addresses .addresses_of(rlsa.routerid)) # Figure out the controllers layout controller_prefix = CFG.getint(DEFAULTSECT, 'controller_prefixlen') # Group by controller and log them for ip in new_graph.nodes_iter(): try: addr = ip_address(ip) except ValueError: continue # Have a prefix if addr in self.BASE_NET: """1. Compute address diff to remove base_net 2. Right shift to remove host bits 3. Mask with controller mask""" cid = (((int(addr) - int(self.BASE_NET.network_address)) >> self.BASE_NET.max_prefixlen - controller_prefix) & ((1 << controller_prefix) - 1)) self.controllers[cid].append(ip) # Contract them on the graph for id, ips in self.controllers.iteritems(): cname = 'C_%s' % id new_graph.add_controller(cname) new_graph.contract(cname, ips) # Remove generated self loops new_graph.remove_edges_from(new_graph.selfloop_edges()) self.apply_secondary_addresses(new_graph) return new_graph
def _setUpWeird(self): # +-----D-----+ # / | \ # 2 2 2 # / | \ # A -- 4 -- B -- 2 -- C self.weird = g = IGPGraph() self._add_edge(g, 'A', 'B', 4) self._add_edge(g, 'B', 'C', 2) self._add_edge(g, 'D', 'C', 2) self._add_edge(g, 'D', 'B', 2) self._add_edge(g, 'D', 'A', 2)
def _setUpTrapezoid(self): # R1 -- 100 -- E1 -- 10 -+ # | | # 100 D # | | # R2 -- 10 -- E2 -- 10 -+ self.trap = g = IGPGraph() self._add_edge(g, 'R1', 'E1', metric=100) self._add_edge(g, 'R1', 'R2', metric=100) self._add_edge(g, 'R2', 'E2', metric=10) self._add_edge(g, 'E1', 'D', metric=10) self._add_edge(g, 'E2', 'D', metric=10)
def _setUpWeird(self): # +-----D-----+ # / | \ # 2 2 2 # / | \ # A -- 4 -- B -- 2 -- C self.weird = g = IGPGraph() self._add_edge(g, 'A', 'B', 4) self._add_edge(g, 'B', 'C', 2) self._add_edge(g, 'D', 'C', 2) self._add_edge(g, 'D', 'B', 2) self._add_edge(g, 'D', 'A', 2) for _, data in g.nodes_iter(data=True): data['router'] = True
def _setUpTrapezoid(self): # R1 -- 100 -- E1 -- 10 -+ # | | # 100 D # | | # R2 -- 10 -- E2 -- 10 -+ self.trap = g = IGPGraph() self._add_edge(g, 'R1', 'E1', metric=100) self._add_edge(g, 'R1', 'R2', metric=100) self._add_edge(g, 'R2', 'E2', metric=10) self._add_edge(g, 'E1', 'D', metric=10) self._add_edge(g, 'E2', 'D', metric=10) for _, data in g.nodes_iter(data=True): data['router'] = True
def testTrapezoidWithEcmp(self, expected_lsa_count=3): self.log_test_name() self._test( self.gadgets.trap, { '2_8': IGPGraph([ ('R1', 'R2'), ('R2', 'E2'), ('E2', 'D'), # ECMP on E1 ('E1', 'D'), ('E1', 'R1') ]) }, expected_lsa_count)
def testTrapezoidWithEcmp(self): log.warning('Testing TrapezoidWithEcmp') self._test( self.gadgets.trap, { '2_8': IGPGraph([ ('R1', 'R2'), ('R2', 'E2'), ('E2', 'D'), # ECMP on E1 ('E1', 'D'), ('E1', 'R1') ]) }, 3)
def _setUpParallelTracks(self): # A2--B2--C2--D2 # /| | | | # D-A1--B1--C1--D1 self.parallel = g = IGPGraph() self._add_edge(g, 'D', 'A1', 2) self._add_edge(g, 'D', 'A2', 2) self._add_edge(g, 'B2', 'A2', 2) self._add_edge(g, 'B1', 'A1', 2) self._add_edge(g, 'B1', 'C1', 2) self._add_edge(g, 'B2', 'C2', 2) self._add_edge(g, 'C2', 'D2', 2) self._add_edge(g, 'C1', 'D1', 2) self._add_edge(g, 'D2', 'D1', 2) self._add_edge(g, 'C2', 'C1', 2) self._add_edge(g, 'B2', 'B1', 2) self._add_edge(g, 'A2', 'A1', 2)
def _setUpSquare(self): self.square = g = IGPGraph() # T1 --10-- T2 # | \ | # 10 5 100 # | \ | # B1 --3-- B2 --100--D1 # | # 100 # | # D2 self._add_edge(g, 'B1', 'B2', metric=3) self._add_edge(g, 'T1', 'B1', metric=10) self._add_edge(g, 'T2', 'T1', metric=10) self._add_edge(g, 'B2', 'T1', metric=5) self._add_edge(g, 'T2', 'B2', metric=100) self._add_edge(g, 'D1', 'B2', metric=100) self._add_edge(g, 'D2', 'B1', metric=100)
def commit_change(self): """ @API commit the changes, and applied the requirements entered for the current session """ for prefix in self.change_pfx: tmp = [] if not self.simple_req[prefix]: self.remove_dag_requirement(prefix) else: for item in self.simple_req[prefix]: for s, d in zip(item.path[:-1], item.path[1:]): if (s, d) not in tmp: tmp.append((s, d)) LOG.debug('add_dag_requirement') self.add_dag_requirement(prefix, IGPGraph(tmp)) del self.change_pfx[:] self.refresh_augmented_topo()
def _setUpParallelTracks(self): # A2--B2--C2--D2 # /| | | | # D-A1--B1--C1--D1 self.parallel = g = IGPGraph() self._add_edge(g, 'D', 'A1', 2) self._add_edge(g, 'D', 'A2', 2) self._add_edge(g, 'B2', 'A2', 2) self._add_edge(g, 'B1', 'A1', 2) self._add_edge(g, 'B1', 'C1', 2) self._add_edge(g, 'B2', 'C2', 2) self._add_edge(g, 'C2', 'D2', 2) self._add_edge(g, 'C1', 'D1', 2) self._add_edge(g, 'D2', 'D1', 2) self._add_edge(g, 'C2', 'C1', 2) self._add_edge(g, 'B2', 'B1', 2) self._add_edge(g, 'A2', 'A1', 2) for _, data in g.nodes_iter(data=True): data['router'] = True
def __init__(self): self.BASE_NET = ip_network(CFG.get(DEFAULTSECT, 'base_net')) self.private_addresses = PrivateAddressStore(CFG.get(DEFAULTSECT, 'private_ips')) self.last_line = '' self.leader_watchdog = None self.transaction = None self.graph = IGPGraph() self.routers = {} # router-id : lsa self.networks = {} # DR IP : lsa self.ext_networks = {} # (router-id, dest) : lsa self.controllers = defaultdict(list) # controller nr : ip_list self.listener = {} self.keep_running = True self.queue = Queue() self.processing_thread = Thread(target=self.process_lsa, name="lsa_processing_thread") self.processing_thread.setDaemon(True) self.processing_thread.start()
def _setUpDiamond(self): # A ---5--- Y1 # | \ | # | 10 10 # | \ | # | Y2 -15-- X ---50--- D # | | | # 25 +--30----+ | # | / | # O -------- 10 ---------+ self.diamond = g = IGPGraph() self._add_edge(g, 'A', 'Y1', metric=5) self._add_edge(g, 'Y1', 'X', metric=10) self._add_edge(g, 'A', 'Y2', metric=10) self._add_edge(g, 'Y2', 'X', metric=15) self._add_edge(g, 'X', 'D', metric=50) self._add_edge(g, 'A', 'O', metric=25) self._add_edge(g, 'X', 'O', metric=30) self._add_edge(g, 'D', 'O', metric=10)
def __init__(self): self.BASE_NET = ip_network(CFG.get(DEFAULTSECT, 'base_net')) self.private_address_network = ip_network(CFG.get(DEFAULTSECT, 'private_net')) try: with open(CFG.get(DEFAULTSECT, 'private_ips'), 'r') as f: self.private_address_binding = json.load(f) self.router_private_address = {} for subnets in self.private_address_binding.itervalues(): for rid, ip in subnets.iteritems(): try: iplist = self.router_private_address[rid] except KeyError: iplist = self.router_private_address[rid] = [] # Enable single private address as string if isinstance(ip, str): ip = [ip] iplist.extend(ip) except Exception as e: log.warning('Incorrect private IP addresses binding file') log.warning(str(e)) self.private_address_binding = {} self.router_private_address = {} self.last_line = '' self.leader_watchdog = None self.transaction = None self.graph = IGPGraph() self.routers = {} # router-id : lsa self.networks = {} # DR IP : lsa self.ext_networks = {} # (router-id, dest) : lsa self.controllers = defaultdict(list) # controller nr : ip_list self.listener = {} self.keep_running = True self.queue = Queue() self.processing_thread = Thread(target=self.process_lsa, name="lsa_processing_thread") self.processing_thread.setDaemon(True) self.processing_thread.start()
class SouthboundListener(ShapeshifterProxy): """This basic controller maintains a structure describing the IGP topology and listens for changes.""" def __init__(self, *args, **kwargs): super(SouthboundListener, self).__init__(*args, **kwargs) self.igp_graph = IGPGraph() self.dirty = False self.json_proxy = SJMPClient(hostname=CFG.get(DEFAULTSECT, 'json_hostname'), port=CFG.getint(DEFAULTSECT, 'json_port'), target=self) self.quagga_manager = ProxyCloner(FakeNodeProxy, self.json_proxy) 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 stop(self): """Stop the connection to the southbound controller""" self.json_proxy.stop() def bootstrap_graph(self, graph, node_properties): self.igp_graph.clear() self.igp_graph.add_edges_from(graph) for _, _, d in self.igp_graph.edges_iter(data=True): sanitize_edge_data(d) self.update_node_properties(**node_properties) log.debug('Bootstrapped graph with edges: %s and properties: %s', self.igp_graph.edges(data=True), node_properties) self.received_initial_graph() self.graph_changed() def received_initial_graph(self): """Called when the initial graph has been bootstrapped, before calling graph_changed""" pass def add_edge(self, source, destination, properties={'metric': 1}): properties = sanitize_edge_data(properties) # metric is added twice to support backward-compat. self.igp_graph.add_edge(source, destination, properties) log.debug('Added edge: %s-%s@%s', source, destination, properties) # Only trigger an update if the link is bidirectional self.dirty = self.igp_graph.has_edge(destination, source) def commit(self): log.debug('End of graph update') if self.dirty: self.dirty = False self.graph_changed() @abc.abstractmethod def graph_changed(self): """Called when the IGP graph has changed.""" def remove_edge(self, source, destination): # TODO: pay attention to re-add the symmetric edge if only one way # crashed try: self.igp_graph.remove_edge(source, destination) log.debug('Removed edge %s-%s', source, destination) self.igp_graph.remove_edge(destination, source) log.debug('Removed edge %s-%s', destination, source) except nx.NetworkXError: # This means that we had already removed both side of the edge # earlier or that the adjacency was not fully established before # going down pass else: self.dirty = True def update_node_properties(self, **properties): log.debug('Updating node propeties: %s', properties) for node, data in properties.iteritems(): self.igp_graph.node[node].update(data) self.dirty = self.dirty or properties
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
class LSDB(object): def __init__(self): self.BASE_NET = ip_network(CFG.get(DEFAULTSECT, 'base_net')) self.private_addresses = PrivateAddressStore(CFG.get(DEFAULTSECT, 'private_ips')) self.last_line = '' self.leader_watchdog = None self.transaction = False self.uncommitted_changes = 0 self.graph = IGPGraph() self._lsdb = {NetworkLSA.TYPE: {}, RouterLSA.TYPE: {}, ASExtLSA.TYPE: {}} self.controllers = defaultdict(list) # controller nr : ip_list self.listener = {} self.keep_running = True self.queue = Queue() self.processing_thread = start_daemon_thread( target=self.process_lsa, name='lsa processing thread') @property def routers(self): return self._lsdb[RouterLSA.TYPE] @property def networks(self): return self._lsdb[NetworkLSA.TYPE] @property def ext_networks(self): return self._lsdb[ASExtLSA.TYPE] def set_leader_watchdog(self, wd): self.leader_watchdog = wd def get_leader(self): return min(self.controllers.iterkeys()) if self.controllers else None def stop(self): for l in self.listener.values(): l.session.stop() self.keep_running = False self.queue.put('') def lsdb(self, lsa): return self._lsdb.get(lsa.TYPE, None) 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 commit_change(self, line): # Check that this is not a duplicate of a previous update ... if self.last_line == line or not line: return self.queue.put(line) def forwarding_address_of(self, src, dst): """ Return the forwarding address for a src, dst pair. If src is specified, return the private 'link-local' address of the src-dst link, otherwise return a 'public' IP belonging to dst :param src: the source node of the link towards the FA, possibly null :param dst: the node owning the forwarding address :return: forwarding address (str) or None if no compatible address was found """ # If we have a src address, we want the set of private IPs # Otherwise we want any IP of dst if src: try: return self.graph[src][dst]['dst_address'] except KeyError as e: log.error("Couldn't resolve local forwarding of %s-%s, missing" " key %s", src, dst, e) else: try: data = filter(lambda v: v is not None, (self.graph[dst][succ].get('src_address', None) for succ in self.graph.successors_iter(dst))) if data: return min(data) log.error("Cannot use %s as nexthop as it has no physical " "link to other routers!", dst) except KeyError: log.error("Couldn't find nexthop %s when resolving global " "forwarding address", dst) return None def remove_lsa(self, lsa): lsdb = self.lsdb(lsa) try: del lsdb[lsa.key()] except (KeyError, TypeError): # LSA not found, lsdb is None pass def add_lsa(self, lsa): lsdb = self.lsdb(lsa) try: lsdb[lsa.key()] = lsa except TypeError: # LSDB is None pass def get_current_seq_number(self, lsa): try: return self.lsdb(lsa)[lsa.key()].seqnum except (KeyError, TypeError): # LSA not found, LSDB is None return None def is_old_seqnum(self, lsa): """Return whether the lsa is older than the copy in the lsdb is any""" c_seqnum = self.get_current_seq_number(lsa) new_seqnum = lsa.seqnum return (c_seqnum and # LSA already present in the LSDB not is_newer_seqnum(new_seqnum, c_seqnum) and # We allow duplicate as they are used to flush LSAs new_seqnum != c_seqnum) def handle_lsa_line(self, line): """We received a line describing an lsa, handle it""" action, lsa_info = line.split(SEP_ACTION) if action == BEGIN: self.start_transaction() elif action == COMMIT: self.reset_transaction() else: # ADD/REM LSA messages lsa = parse_lsa(lsa_info) log.debug('Parsed %s: %s [%d]', action, lsa, lsa.seqnum) # Sanity checks if self.is_old_seqnum(lsa): log.debug("OLD seqnum for LSA, ignoring it...") action = None # Perform the update if it is still applicable if action == REM: self.remove_lsa(lsa) self.uncommitted_changes += 1 elif action == ADD: self.add_lsa(lsa) self.uncommitted_changes += 1 def process_lsa(self): """Parse new LSAs, and update the graph if needed""" while self.keep_running: try: line = self.queue.get(timeout=5) self.queue.task_done() if not line: continue self.handle_lsa_line(line) except Empty: self.reset_transaction() if (self.queue.empty() and # Try to empty the queue before update not self.transaction and self.uncommitted_changes): self.commit() def reset_transaction(self): """Reset the transaction""" self.transaction = False def start_transaction(self): """Record a new LSA transaction""" self.transaction = True def commit(self): """Updates have been made on the LSDB, update the graph""" # Update graph accordingly new_graph = self.build_graph() # Compute graph difference and update it self.update_graph(new_graph) self.uncommitted_changes = 0 def __str__(self): strs = [str(lsa) for lsa in chain(self.routers.values(), self.networks.values(), self.ext_networks.values())] strs.insert(0, '* LSDB Content [%d]:' % len(strs)) return '\n'.join(strs) def build_graph(self): self.controllers.clear() new_graph = IGPGraph() # Rebuild the graph from the LSDB for lsa in chain(self.routers.itervalues(), self.networks.itervalues(), self.ext_networks.itervalues()): if is_expired_lsa(lsa): log.debug("LSA %s is too old (%d) ignoring it!", lsa, lsa.age) else: lsa.apply(new_graph, self) # Contract all IPs to their respective router-id for rlsa in self.routers.itervalues(): rlsa.contract_graph(new_graph, self.private_addresses .addresses_of(rlsa.routerid)) # Figure out the controllers layout controller_prefix = CFG.getint(DEFAULTSECT, 'controller_prefixlen') # Group by controller and log them for ip in new_graph.nodes_iter(): try: addr = ip_address(ip) except ValueError: continue # Have a prefix if addr in self.BASE_NET: """1. Compute address diff to remove base_net 2. Right shift to remove host bits 3. Mask with controller mask""" cid = (((int(addr) - int(self.BASE_NET.network_address)) >> self.BASE_NET.max_prefixlen - controller_prefix) & ((1 << controller_prefix) - 1)) self.controllers[cid].append(ip) # Contract them on the graph for id, ips in self.controllers.iteritems(): cname = 'C_%s' % id new_graph.add_controller(cname) new_graph.contract(cname, ips) # Remove generated self loops new_graph.remove_edges_from(new_graph.selfloop_edges()) self.apply_secondary_addresses(new_graph) return new_graph 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 for_all_listeners(self, funcname, *args, **kwargs): """Apply funcname to all listeners""" f = methodcaller(funcname, *args, **kwargs) map(f, self.listener.itervalues()) def apply_secondary_addresses(self, graph): for src, dst in graph.router_links: try: graph[src][dst]['dst_address'] = self.private_addresses\ .addresses_of(dst, src) except KeyError: log.debug('%(src)-%(dst)s does not yet exists on the graph' ', ignoring private addresses.', locals()) pass
class LSDB(object): BASE_NET = ip_network(CFG.get(DEFAULTSECT, 'base_net')) def __init__(self): self.BASE_NET = ip_network(CFG.get(DEFAULTSECT, 'base_net')) self.private_address_network = ip_network(CFG.get(DEFAULTSECT, 'private_net')) try: with open(CFG.get(DEFAULTSECT, 'private_ips'), 'r') as f: self.private_address_binding = json.load(f) self.router_private_address = {} for subnets in self.private_address_binding.itervalues(): for rid, ip in subnets.iteritems(): try: iplist = self.router_private_address[rid] except KeyError: iplist = self.router_private_address[rid] = [] # Enable single private address as string if isinstance(ip, str): ip = [ip] iplist.extend(ip) except Exception as e: log.warning('Incorrect private IP addresses binding file') log.warning(str(e)) self.private_address_binding = {} self.router_private_address = {} self.last_line = '' self.leader_watchdog = None self.transaction = None self.graph = IGPGraph() self.routers = {} # router-id : lsa self.networks = {} # DR IP : lsa self.ext_networks = {} # (router-id, dest) : lsa self.controllers = defaultdict(list) # controller nr : ip_list self.listener = {} self.keep_running = True self.queue = Queue() self.processing_thread = Thread(target=self.process_lsa, name="lsa_processing_thread") self.processing_thread.setDaemon(True) self.processing_thread.start() def set_leader_watchdog(self, wd): self.leader_watchdog = wd def get_leader(self): return min(self.controllers.iterkeys()) if self.controllers else None def stop(self): for l in self.listener.values(): l.session.stop() self.keep_running = False self.queue.put('') def lsdb(self, lsa): if lsa.TYPE == RouterLSA.TYPE: return self.routers elif lsa.TYPE == NetworkLSA.TYPE: return self.networks elif lsa.TYPE == ASExtLSA.TYPE: return self.ext_networks 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.get('metric', -1)) for u, v, d in self.graph.edges(data=True) ], node_properties={n: data for n, data in self.graph.nodes_iter(data=True) }) @staticmethod def extract_lsa_properties(lsa_part): d = {} for prop in lsa_part.split(SEP_INTER_FIELD): if not prop: continue key, val = prop.split(SEP_INTRA_FIELD) d[key] = val return d def commit_change(self, line): # Check that this is not a duplicate of a previous update ... if self.last_line == line: return self.queue.put(line) def forwarding_address_of(self, src, dst): """ Return the forwarding address for a src, dst pair. If src is specified, return the private 'link-local' address of the src-dst link, otherwise return a 'public' IP belonging to dst :param src: the source node of the link towards the FA, possibly null :param dst: the node owning the forwarding address :return: forwarding address (str) or None if no compatible address was found """ try: return self.graph[src][dst]['dst_address'] if src \ else self.graph[dst][self.graph.neighbors(dst)[0]]['src_address'] except KeyError: log.debug('%s-%s not found in graph', src, dst) return None def remove_lsa(self, lsa): lsdb = self.lsdb(lsa) try: del lsdb[lsa.key()] except KeyError: pass def add_lsa(self, lsa): lsdb = self.lsdb(lsa) lsdb[lsa.key()] = lsa def process_lsa(self): while self.keep_running: commit = False try: line = self.queue.get(timeout=5) if not line: self.queue.task_done() continue # Start parsing the LSA log action, lsa_info = line.split(SEP_ACTION) if action == BEGIN: self.transaction = Transaction() elif action == COMMIT: if self.transaction: self.transaction.commit(self) self.transaction = None commit = True else: lsa_parts = [self.extract_lsa_properties(part) for part in lsa_info.split(SEP_GROUP) if part] lsa = LSA.parse(LSAHeader.parse(lsa_parts.pop(0)), lsa_parts) log.debug('Parsed %s: %s', action, lsa) if action == REM: if not self.transaction: self.remove_lsa(lsa) else: self.transaction.remove_lsa(lsa) elif action == ADD: if not self.transaction: self.add_lsa(lsa) else: self.transaction.add_lsa(lsa) if lsa.push_update_on_remove() or not action == REM: commit = True self.queue.task_done() except Empty: if self.transaction: log.debug('Splitting transaction due to timeout') self.transaction.commit(self) self.transaction = Transaction() commit = True if commit: # Update graph accordingly new_graph = self.build_graph() # Compute graph difference and update it self.update_graph(new_graph) def __str__(self): strs = [str(lsa) for lsa in chain(self.routers.values(), self.networks.values(), self.ext_networks.values())] strs.insert(0, '* LSDB Content [%d]:' % len(strs)) return '\n'.join(strs) def build_graph(self): self.controllers.clear() new_graph = IGPGraph() # Rebuild the graph from the LSDB for lsa in chain(self.routers.values(), self.networks.values(), self.ext_networks.values()): lsa.apply(new_graph, self) # Contract all IPs to their respective router-id for lsa in self.routers.values(): lsa.contract_graph(new_graph, self.router_private_address.get( lsa.routerid, [])) # Figure out the controllers layout controller_prefix = CFG.getint(DEFAULTSECT, 'controller_prefixlen') # Group by controller and log them for ip in new_graph.nodes_iter(): try: addr = ip_address(ip) except ValueError: continue # Have a prefix if addr in self.BASE_NET: """1. Compute address diff to remove base_net 2. Right shift to remove host bits 3. Mask with controller mask """ id = (((int(addr) - int(self.BASE_NET.network_address)) >> self.BASE_NET.max_prefixlen - controller_prefix) & ((1 << controller_prefix) - 1)) self.controllers[id].append(ip) # Contract them on the graph for id, ips in self.controllers.iteritems(): cname = 'C_%s' % id new_graph.add_controller(cname) new_graph.contract(cname, ips) # Remove generated self loops new_graph.remove_edges_from(new_graph.selfloop_edges()) self.apply_secondary_addresses(new_graph) return new_graph 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, metric=new_graph.metric(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 for_all_listeners(self, funcname, *args, **kwargs): """Apply funcname to all listeners""" for i in self.listener.itervalues(): getattr(i, funcname)(*args, **kwargs) def apply_secondary_addresses(self, graph): for subnet in self.private_address_binding.itervalues(): for dst, ip in subnet.iteritems(): for src in subnet.iterkeys(): if src == dst: continue try: graph[src][dst]['dst_address'] = ip except KeyError: pass