class Reactor(object): # [hex(ord(c)) for c in os.popen('clear').read()] clear = concat_bytes_i( character(int(c, 16)) for c in ['0x1b', '0x5b', '0x48', '0x1b', '0x5b', '0x32', '0x4a']) def __init__(self, configurations): self.ips = environment.settings().tcp.bind self.port = environment.settings().tcp.port self.ack = environment.settings().api.ack self.max_loop_time = environment.settings().reactor.speed self.early_drop = environment.settings().daemon.drop self.logger = Logger() self.daemon = Daemon(self) self.processes = None self.listener = None self.configuration = Configuration(configurations) self.api = API(self) self.peers = {} self.route_update = False self._stopping = environment.settings().tcp.once self._shutdown = False self._reload = False self._reload_processes = False self._restart = False self._saved_pid = False self._running = None self._pending = deque() self._async = deque() self._signal = {} signal.signal(signal.SIGTERM, self.sigterm) signal.signal(signal.SIGHUP, self.sighup) signal.signal(signal.SIGALRM, self.sigalrm) signal.signal(signal.SIGUSR1, self.sigusr1) signal.signal(signal.SIGUSR2, self.sigusr2) def _termination(self, reason): while True: try: self._shutdown = True self.logger.reactor(reason, 'warning') break except KeyboardInterrupt: pass def sigterm(self, signum, frame): self.logger.reactor('SIG TERM received - shutdown') self._shutdown = True for key in self.peers: if self.peers[key].neighbor.api['signal']: self._signal[key] = signum def sighup(self, signum, frame): self.logger.reactor('SIG HUP received - shutdown') self._shutdown = True for key in self.peers: if self.peers[key].neighbor.api['signal']: self._signal[key] = signum def sigalrm(self, signum, frame): self.logger.reactor('SIG ALRM received - restart') self._restart = True for key in self.peers: if self.peers[key].neighbor.api['signal']: self._signal[key] = signum def sigusr1(self, signum, frame): self.logger.reactor('SIG USR1 received - reload configuration') self._reload = True for key in self.peers: if self.peers[key].neighbor.api['signal']: self._signal[key] = signum def sigusr2(self, signum, frame): self.logger.reactor( 'SIG USR2 received - reload configuration and processes') self._reload = True self._reload_processes = True for key in self.peers: if self.peers[key].neighbor.api['signal']: self._signal[key] = signum def _api_ready(self, sockets): sleeptime = self.max_loop_time / 20 fds = self.processes.fds() ios = fds + sockets try: read, _, _ = select.select(ios, [], [], sleeptime) for fd in fds: if fd in read: read.remove(fd) return read except select.error as exc: errno, message = exc.args # pylint: disable=W0633 if errno not in error.block: raise exc return [] except socket.error as exc: if exc.errno in error.fatal: raise exc return [] except KeyboardInterrupt: self._termination('^C received') return [] def _setup_listener(self, local_addr, remote_addr, port, md5_password, md5_base64, ttl_in): try: if not self.listener: self.listener = Listener() if not remote_addr: remote_addr = IP.create( '0.0.0.0') if local_addr.ipv4() else IP.create('::') self.listener.listen(local_addr, remote_addr, port, md5_password, md5_base64, ttl_in) self.logger.reactor( 'Listening for BGP session(s) on %s:%d%s' % (local_addr, port, ' with MD5' if md5_password else '')) return True except NetworkError as exc: if os.geteuid() != 0 and port <= 1024: self.logger.reactor( 'Can not bind to %s:%d, you may need to run ExaBGP as root' % (local_addr, port), 'critical') else: self.logger.reactor( 'Can not bind to %s:%d (%s)' % (local_addr, port, str(exc)), 'critical') self.logger.reactor( 'unset exabgp.tcp.bind if you do not want listen for incoming connections', 'critical') self.logger.reactor( 'and check that no other daemon is already binding to port %d' % port, 'critical') return False def _handle_listener(self): if not self.listener: return ranged_neighbor = [] for connection in self.listener.connected(): for key in self.peers: peer = self.peers[key] neighbor = peer.neighbor connection_local = IP.create(connection.local).address() neighbor_peer_start = neighbor.peer_address.address() neighbor_peer_next = neighbor_peer_start + neighbor.range_size if not neighbor_peer_start <= connection_local < neighbor_peer_next: continue connection_peer = IP.create(connection.peer).address() neighbor_local = neighbor.local_address.address() if connection_peer != neighbor_local: if not neighbor.auto_discovery: continue # we found a range matching for this connection # but the peer may already have connected, so # we need to iterate all individual peers before # handling "range" peers if neighbor.range_size > 1: ranged_neighbor.append(peer.neighbor) continue denied = peer.handle_connection(connection) if denied: self.logger.reactor( 'refused connection from %s due to the state machine' % connection.name()) self._async.append(denied) break self.logger.reactor('accepted connection from %s' % connection.name()) break else: # we did not break (and nothign was found/done or we have group match) matched = len(ranged_neighbor) if matched > 1: self.logger.reactor( 'could not accept connection from %s (more than one neighbor match)' % connection.name()) self._async.append( connection.notification( 6, 5, b'could not accept the connection (more than one neighbor match)' )) return if not matched: self.logger.reactor('no session configured for %s' % connection.name()) self._async.append( connection.notification( 6, 3, b'no session configured for the peer')) return new_neighbor = copy.copy(ranged_neighbor[0]) new_neighbor.range_size = 1 new_neighbor.generated = True new_neighbor.local_address = IP.create(connection.peer) new_neighbor.peer_address = IP.create(connection.local) new_peer = Peer(new_neighbor, self) denied = new_peer.handle_connection(connection) if denied: self.logger.reactor( 'refused connection from %s due to the state machine' % connection.name()) self._async.append(denied) return self.peers[new_neighbor.name()] = new_peer return def run(self, validate): self.daemon.daemonise() # Make sure we create processes once we have closed file descriptor # unfortunately, this must be done before reading the configuration file # so we can not do it with dropped privileges self.processes = Processes(self) # we have to read the configuration possibly with root privileges # as we need the MD5 information when we bind, and root is needed # to bind to a port < 1024 # this is undesirable as : # - handling user generated data as root should be avoided # - we may not be able to reload the configuration once the privileges are dropped # but I can not see any way to avoid it for ip in self.ips: if not self._setup_listener(ip, None, self.port, None, False, None): return False if not self.load(): return False if validate: # only validate configuration self.logger.configuration('') self.logger.configuration('Parsed Neighbors, un-templated') self.logger.configuration('------------------------------') self.logger.configuration('') for key in self.peers: self.logger.configuration(str(self.peers[key].neighbor)) self.logger.configuration('') return True for neighbor in self.configuration.neighbors.values(): if neighbor.listen: if not self._setup_listener( neighbor.md5_ip, neighbor.peer_address, neighbor.listen, neighbor.md5_password, neighbor.md5_base64, neighbor.ttl_in): return False if not self.early_drop: self.processes.start() if not self.daemon.drop_privileges(): self.logger.reactor( 'Could not drop privileges to \'%s\' refusing to run as root' % self.daemon.user, 'critical') self.logger.reactor( 'Set the environmemnt value exabgp.daemon.user to change the unprivileged user', 'critical') return if self.early_drop: self.processes.start() # This is required to make sure we can write in the log location as we now have dropped root privileges if not self.logger.restart(): self.logger.reactor('Could not setup the logger, aborting', 'critical') return if not self.daemon.savepid(): return # did we complete the run of updates caused by the last SIGUSR1/SIGUSR2 ? reload_completed = True wait = environment.settings().tcp.delay if wait: sleeptime = (wait * 60) - int(time.time()) % (wait * 60) self.logger.reactor('waiting for %d seconds before connecting' % sleeptime) time.sleep(float(sleeptime)) workers = {} peers = set() busy = False while True: try: start = time.time() end = start + self.max_loop_time if self._shutdown: self._shutdown = False self.shutdown() break if self._reload and reload_completed: self._reload = False self.load() self.processes.start(self._reload_processes) self._reload_processes = False elif self._restart: self._restart = False self.restart() # We got some API routes to announce if self.route_update: self.route_update = False self.route_send() for key, peer in self.peers.items(): if not peer.neighbor.passive or peer.proto: peers.add(key) if key in self._signal: self.peers[key].reactor.processes.signal( self.peers[key].neighbor, self._signal[key]) self._signal = {} # check all incoming connection self._handle_listener() # give a turn to all the peers while start < time.time() < end: for key in list(peers): peer = self.peers[key] action = peer.run() # .run() returns an ACTION enum: # * immediate if it wants to be called again # * later if it should be called again but has no work atm # * close if it is finished and is closing down, or restarting if action == ACTION.CLOSE: self._unschedule(key) peers.discard(key) # we are loosing this peer, not point to schedule more process work elif action == ACTION.LATER: for io in peer.sockets(): workers[io] = key # no need to come back to it before a a full cycle peers.discard(key) # handle API calls busy = self._scheduled_api() # handle new connections busy |= self._scheduled_listener() if not peers and not busy: break if not peers: reload_completed = True for io in self._api_ready(list(workers)): peers.add(workers[io]) del workers[io] if self._stopping and not self.peers.keys(): break except KeyboardInterrupt: self._termination('^C received') # socket.error is a subclass of IOError (so catch it first) except socket.error: self._termination('socket error received') except IOError: self._termination( 'I/O Error received, most likely ^C during IO') except SystemExit: self._termination('exiting') except ProcessError: self._termination( 'Problem when sending message(s) to helper program, stopping' ) except select.error: self._termination('problem using select, stopping') def shutdown(self): """terminate all the current BGP connections""" self.logger.reactor('performing shutdown') if self.listener: self.listener.stop() self.listener = None for key in self.peers.keys(): self.peers[key].stop() self.processes.terminate() self.daemon.removepid() self._stopping = True def load(self): """reload the configuration and send to the peer the route which changed""" self.logger.reactor('performing reload of exabgp %s' % version) reloaded = self.configuration.reload() if not reloaded: # # Careful the string below is used but the QA code to check for sucess of failure self.logger.configuration( 'problem with the configuration file, no change done', 'error') # Careful the string above is used but the QA code to check for sucess of failure # self.logger.configuration(str(self.configuration.error), 'error') return False for key, peer in self.peers.items(): if key not in self.configuration.neighbors: self.logger.reactor('removing peer: %s' % peer.neighbor.name()) peer.stop() for key, neighbor in self.configuration.neighbors.items(): # new peer if key not in self.peers: self.logger.reactor('new peer: %s' % neighbor.name()) peer = Peer(neighbor, self) self.peers[key] = peer # modified peer elif self.peers[key].neighbor != neighbor: self.logger.reactor( 'peer definition change, establishing a new connection for %s' % str(key)) self.peers[key].reestablish(neighbor) # same peer but perhaps not the routes else: # finding what route changed and sending the delta is not obvious self.logger.reactor( 'peer definition identical, updating peer routes if required for %s' % str(key)) self.peers[key].reconfigure(neighbor) for ip in self.ips: if ip.afi == neighbor.peer_address.afi: self._setup_listener(ip, neighbor.peer_address, self.port, neighbor.md5_password, neighbor.md5_base64, None) self.logger.configuration('loaded new configuration successfully', 'info') return True def _scheduled_listener(self, flipflop=[]): try: for generator in self._async: try: six.next(generator) six.next(generator) flipflop.append(generator) except StopIteration: pass self._async, flipflop = flipflop, self._async return len(self._async) > 0 except KeyboardInterrupt: self._termination('^C received') return False def _scheduled_api(self): try: # read at least on message per process if there is some and parse it for service, command in self.processes.received(): self.api.text(self, service, command) # if we have nothing to do, return or save the work if not self._running: if not self._pending: return False self._running, name = self._pending.popleft() self.logger.reactor('callback | installing %s' % name) if self._running: # run it try: self.logger.reactor('callback | running') six.next(self._running) # run # should raise StopIteration in most case # and prevent us to have to run twice to run one command six.next(self._running) # run except StopIteration: self._running = None self.logger.reactor('callback | removing') return True return False except KeyboardInterrupt: self._termination('^C received') return False def route_send(self): """the process ran and we need to figure what routes to changes""" self.logger.reactor('performing dynamic route update') for key in self.configuration.neighbors.keys(): self.peers[key].send_new() self.logger.reactor('updated peers dynamic routes successfully') def restart(self): """kill the BGP session and restart it""" self.logger.reactor('performing restart of exabgp %s' % version) self.configuration.reload() for key in self.peers.keys(): if key not in self.configuration.neighbors.keys(): neighbor = self.configuration.neighbors[key] self.logger.reactor('removing Peer %s' % neighbor.name()) self.peers[key].stop() else: self.peers[key].reestablish() self.processes.terminate() self.processes.start() def _unschedule(self, peer): if peer in self.peers: del self.peers[peer] def answer(self, service, string): if self.ack: self.always_answer(service, string) def always_answer(self, service, string): self.processes.write(service, string) self.logger.reactor('responding to %s : %s' % (service, string.replace('\n', '\\n'))) def api_shutdown(self): self._shutdown = True self._pending = deque() self._running = None def api_reload(self): self._reload = True self._pending = deque() self._running = None def api_restart(self): self._restart = True self._pending = deque() self._running = None @staticmethod def match_neighbor(description, name): for string in description: if re.search(r'(^|[\s])%s($|[\s,])' % re.escape(string), name) is None: return False return True def match_neighbors(self, descriptions): """return the sublist of peers matching the description passed, or None if no description is given""" if not descriptions: return self.peers.keys() returned = [] for key in self.peers: for description in descriptions: if Reactor.match_neighbor(description, key): if key not in returned: returned.append(key) return returned def nexthops(self, peers): return dict( (peer, self.peers[peer].neighbor.local_address) for peer in peers) def plan(self, callback, name): self._pending.append((callback, name))
class Reactor(object): # [hex(ord(c)) for c in os.popen('clear').read()] clear = concat_bytes_i( character(int(c, 16)) for c in ['0x1b', '0x5b', '0x48', '0x1b', '0x5b', '0x32', '0x4a']) def __init__(self, configurations): self.ip = environment.settings().tcp.bind self.port = environment.settings().tcp.port self.respawn = environment.settings().api.respawn self.max_loop_time = environment.settings().reactor.speed self.early_drop = environment.settings().daemon.drop self.logger = Logger() self.daemon = Daemon(self) self.processes = None self.listener = None self.configuration = Configuration(configurations) self.api = API(self) self.peers = {} self.route_update = False self._stopping = environment.settings().tcp.once self._shutdown = False self._reload = False self._reload_processes = False self._restart = False self._saved_pid = False self._pending = deque() self._running = None signal.signal(signal.SIGTERM, self.sigterm) signal.signal(signal.SIGHUP, self.sighup) signal.signal(signal.SIGALRM, self.sigalrm) signal.signal(signal.SIGUSR1, self.sigusr1) signal.signal(signal.SIGUSR2, self.sigusr2) def sigterm(self, signum, frame): self.logger.reactor('SIG TERM received - shutdown') self._shutdown = True def sighup(self, signum, frame): self.logger.reactor('SIG HUP received - shutdown') self._shutdown = True def sigalrm(self, signum, frame): self.logger.reactor('SIG ALRM received - restart') self._restart = True def sigusr1(self, signum, frame): self.logger.reactor('SIG USR1 received - reload configuration') self._reload = True def sigusr2(self, signum, frame): self.logger.reactor( 'SIG USR2 received - reload configuration and processes') self._reload = True self._reload_processes = True def ready(self, sockets, ios, sleeptime=0): # never sleep a negative number of second (if the rounding is negative somewhere) # never sleep more than one second (should the clock time change during two time.time calls) sleeptime = min(max(0.0, sleeptime), 1.0) if not ios: time.sleep(sleeptime) return [] try: read, _, _ = select.select(sockets + ios, [], [], sleeptime) return read except select.error as exc: errno, message = exc.args # pylint: disable=W0633 if errno not in error.block: raise exc return [] except socket.error as exc: if exc.errno in error.fatal: raise exc return [] def run(self): self.daemon.daemonise() # Make sure we create processes once we have closed file descriptor # unfortunately, this must be done before reading the configuration file # so we can not do it with dropped privileges self.processes = Processes(self) # we have to read the configuration possibly with root privileges # as we need the MD5 information when we bind, and root is needed # to bind to a port < 1024 # this is undesirable as : # - handling user generated data as root should be avoided # - we may not be able to reload the configuration once the privileges are dropped # but I can not see any way to avoid it if not self.load(): return False try: self.listener = Listener() if self.ip: self.listener.listen(IP.create(self.ip), IP.create('0.0.0.0'), self.port, None, False, None) self.logger.reactor('Listening for BGP session(s) on %s:%d' % (self.ip, self.port)) for neighbor in self.configuration.neighbors.values(): if neighbor.listen: self.listener.listen(neighbor.md5_ip, neighbor.peer_address, neighbor.listen, neighbor.md5_password, neighbor.md5_base64, neighbor.ttl_in) self.logger.reactor( 'Listening for BGP session(s) on %s:%d%s' % (neighbor.md5_ip, neighbor.listen, ' with MD5' if neighbor.md5_password else '')) except NetworkError as exc: self.listener = None if os.geteuid() != 0 and self.port <= 1024: self.logger.reactor( 'Can not bind to %s:%d, you may need to run ExaBGP as root' % (self.ip, self.port), 'critical') else: self.logger.reactor( 'Can not bind to %s:%d (%s)' % (self.ip, self.port, str(exc)), 'critical') self.logger.reactor( 'unset exabgp.tcp.bind if you do not want listen for incoming connections', 'critical') self.logger.reactor( 'and check that no other daemon is already binding to port %d' % self.port, 'critical') sys.exit(1) if not self.early_drop: self.processes.start() if not self.daemon.drop_privileges(): self.logger.reactor( 'Could not drop privileges to \'%s\' refusing to run as root' % self.daemon.user, 'critical') self.logger.reactor( 'Set the environmemnt value exabgp.daemon.user to change the unprivileged user', 'critical') return if self.early_drop: self.processes.start() # This is required to make sure we can write in the log location as we now have dropped root privileges if not self.logger.restart(): self.logger.reactor('Could not setup the logger, aborting', 'critical') return if not self.daemon.savepid(): return # did we complete the run of updates caused by the last SIGUSR1/SIGUSR2 ? reload_completed = True wait = environment.settings().tcp.delay if wait: sleeptime = (wait * 60) - int(time.time()) % (wait * 60) self.logger.reactor('waiting for %d seconds before connecting' % sleeptime) time.sleep(float(sleeptime)) workers = {} peers = set() scheduled = False while True: try: finished = False start = time.time() end = start + self.max_loop_time if self._shutdown: self._shutdown = False self.shutdown() break if self._reload and reload_completed: self._reload = False self.load() self.processes.start(self._reload_processes) self._reload_processes = False elif self._restart: self._restart = False self.restart() # We got some API routes to announce if self.route_update: self.route_update = False self.route_send() for peer in self.peers.keys(): peers.add(peer) while start < time.time() < end and not finished: if self.peers: for key in list(peers): peer = self.peers[key] action = peer.run() # .run() returns an ACTION enum: # * immediate if it wants to be called again # * later if it should be called again but has no work atm # * close if it is finished and is closing down, or restarting if action == ACTION.CLOSE: self.unschedule(key) peers.discard(key) # we are loosing this peer, not point to schedule more process work elif action == ACTION.LATER: for io in peer.sockets(): workers[io] = key # no need to come back to it before a a full cycle peers.discard(key) if not peers: reload_completed = True if self.listener: for connection in self.listener.connected(): # found # * False, not peer found for this TCP connection # * True, peer found # * None, conflict found for this TCP connections found = False for key in self.peers: peer = self.peers[key] neighbor = peer.neighbor # XXX: FIXME: Inet can only be compared to Inet if connection.local == str( neighbor.peer_address) and ( neighbor.auto_discovery or connection.peer == str( neighbor.local_address)): if peer.incoming(connection): found = True break found = None break if found: self.logger.reactor( 'accepted connection from %s - %s' % (connection.local, connection.peer)) elif found is False: self.logger.reactor( 'no session configured for %s - %s' % (connection.local, connection.peer)) connection.notification( 6, 3, 'no session configured for the peer') connection.close() elif found is None: self.logger.reactor( 'connection refused (already connected to the peer) %s - %s' % (connection.local, connection.peer)) connection.notification( 6, 5, 'could not accept the connection') connection.close() scheduled = self.schedule() finished = not peers and not scheduled # RFC state that we MUST not send more than one KEEPALIVE / sec # And doing less could cause the session to drop if finished: for io in self.ready(list(peers), self.processes.fds(), end - time.time()): if io in workers: peers.add(workers[io]) del workers[io] if self._stopping and not self.peers.keys(): break except KeyboardInterrupt: while True: try: self._shutdown = True self.logger.reactor('^C received') break except KeyboardInterrupt: pass # socket.error is a subclass of IOError (so catch it first) except socket.error: try: self._shutdown = True self.logger.reactor('socket error received', 'warning') break except KeyboardInterrupt: pass except IOError: while True: try: self._shutdown = True self.logger.reactor( 'I/O Error received, most likely ^C during IO', 'warning') break except KeyboardInterrupt: pass except SystemExit: try: self._shutdown = True self.logger.reactor('exiting') break except KeyboardInterrupt: pass except ProcessError: try: self._shutdown = True self.logger.reactor( 'Problem when sending message(s) to helper program, stopping', 'error') except KeyboardInterrupt: pass except select.error: try: self._shutdown = True self.logger.reactor('problem using select, stopping', 'error') except KeyboardInterrupt: pass # from exabgp.leak import objgraph # print objgraph.show_most_common_types(limit=20) # import random # obj = objgraph.by_type('Route')[random.randint(0,2000)] # objgraph.show_backrefs([obj], max_depth=10) def shutdown(self): """terminate all the current BGP connections""" self.logger.reactor('performing shutdown') if self.listener: self.listener.stop() self.listener = None for key in self.peers.keys(): self.peers[key].stop() self.processes.terminate() self.daemon.removepid() self._stopping = True def load(self): """reload the configuration and send to the peer the route which changed""" self.logger.reactor('performing reload of exabgp %s' % version) reloaded = self.configuration.reload() if not reloaded: # # Careful the string below is used but the QA code to check for sucess of failure self.logger.configuration( 'problem with the configuration file, no change done', 'error') # Careful the string above is used but the QA code to check for sucess of failure # self.logger.configuration(str(self.configuration.error), 'error') return False for key, peer in self.peers.items(): if key not in self.configuration.neighbors: self.logger.reactor('removing peer: %s' % peer.neighbor.name()) peer.stop() for key, neighbor in self.configuration.neighbors.items(): # new peer if key not in self.peers: self.logger.reactor('new peer: %s' % neighbor.name()) peer = Peer(neighbor, self) self.peers[key] = peer # modified peer elif self.peers[key].neighbor != neighbor: self.logger.reactor( 'peer definition change, establishing a new connection for %s' % str(key)) self.peers[key].reestablish(neighbor) # same peer but perhaps not the routes else: # finding what route changed and sending the delta is not obvious self.logger.reactor( 'peer definition identical, updating peer routes if required for %s' % str(key)) self.peers[key].reconfigure(neighbor) self.logger.configuration('loaded new configuration successfully', 'info') return True def schedule(self): try: # read at least on message per process if there is some and parse it for service, command in self.processes.received(): self.api.text(self, service, command) # if we have nothing to do, return or save the work if not self._running: if not self._pending: return False self._running, name = self._pending.popleft() self.logger.reactor('callback | installing %s' % name) if self._running: # run it try: self.logger.reactor('callback | running') six.next(self._running) # run # should raise StopIteration in most case # and prevent us to have to run twice to run one command six.next(self._running) # run except StopIteration: self._running = None self.logger.reactor('callback | removing') return True except StopIteration: pass except KeyboardInterrupt: self._shutdown = True self.logger.reactor('^C received', 'error') def route_send(self): """the process ran and we need to figure what routes to changes""" self.logger.reactor('performing dynamic route update') for key in self.configuration.neighbors.keys(): self.peers[key].send_new() self.logger.reactor('updated peers dynamic routes successfully') def restart(self): """kill the BGP session and restart it""" self.logger.reactor('performing restart of exabgp %s' % version) self.configuration.reload() for key in self.peers.keys(): if key not in self.configuration.neighbors.keys(): neighbor = self.configuration.neighbors[key] self.logger.reactor('removing Peer %s' % neighbor.name()) self.peers[key].stop() else: self.peers[key].reestablish() self.processes.terminate() self.processes.start() def unschedule(self, peer): if peer in self.peers: del self.peers[peer] def answer(self, service, string): self.processes.write(service, string) self.logger.reactor('responding to %s : %s' % (service, string.replace('\n', '\\n'))) def api_shutdown(self): self._shutdown = True self._pending = deque() self._running = None def api_reload(self): self._reload = True self._pending = deque() self._running = None def api_restart(self): self._restart = True self._pending = deque() self._running = None @staticmethod def match_neighbor(description, name): for string in description: if re.search(r'(^|[\s])%s($|[\s,])' % re.escape(string), name) is None: return False return True def match_neighbors(self, descriptions): """return the sublist of peers matching the description passed, or None if no description is given""" if not descriptions: return self.peers.keys() returned = [] for key in self.peers: for description in descriptions: if Reactor.match_neighbor(description, key): if key not in returned: returned.append(key) return returned def nexthops(self, peers): return dict( (peer, self.peers[peer].neighbor.local_address) for peer in peers) def plan(self, callback, name): self._pending.append((callback, name))
class Reactor (object): # [hex(ord(c)) for c in os.popen('clear').read()] clear = b''.join([chr_(int(c,16)) for c in ['0x1b', '0x5b', '0x48', '0x1b', '0x5b', '0x32', '0x4a']]) def __init__ (self, configurations): self.ip = environment.settings().tcp.bind self.port = environment.settings().tcp.port self.respawn = environment.settings().api.respawn self.max_loop_time = environment.settings().reactor.speed self.early_drop = environment.settings().daemon.drop self.logger = Logger() self.daemon = Daemon(self) self.processes = None self.listener = None self.configuration = Configuration(configurations) self.api = API(self) self.peers = {} self.route_update = False self._stopping = environment.settings().tcp.once self._shutdown = False self._reload = False self._reload_processes = False self._restart = False self._saved_pid = False self._pending = deque() self._running = None signal.signal(signal.SIGTERM, self.sigterm) signal.signal(signal.SIGHUP, self.sighup) signal.signal(signal.SIGALRM, self.sigalrm) signal.signal(signal.SIGUSR1, self.sigusr1) signal.signal(signal.SIGUSR2, self.sigusr2) def sigterm (self, signum, frame): self.logger.reactor('SIG TERM received - shutdown') self._shutdown = True def sighup (self, signum, frame): self.logger.reactor('SIG HUP received - shutdown') self._shutdown = True def sigalrm (self, signum, frame): self.logger.reactor('SIG ALRM received - restart') self._restart = True def sigusr1 (self, signum, frame): self.logger.reactor('SIG USR1 received - reload configuration') self._reload = True def sigusr2 (self, signum, frame): self.logger.reactor('SIG USR2 received - reload configuration and processes') self._reload = True self._reload_processes = True def ready (self, sockets, ios, sleeptime=0): # never sleep a negative number of second (if the rounding is negative somewhere) # never sleep more than one second (should the clock time change during two time.time calls) sleeptime = min(max(0.0,sleeptime),1.0) if not ios: time.sleep(sleeptime) return [] try: read,_,_ = select.select(sockets+ios,[],[],sleeptime) return read except select.error as exc: errno,message = exc.args # pylint: disable=W0633 if errno not in error.block: raise exc return [] except socket.error as exc: if exc.errno in error.fatal: raise exc return [] def run (self): self.daemon.daemonise() # Make sure we create processes once we have closed file descriptor # unfortunately, this must be done before reading the configuration file # so we can not do it with dropped privileges self.processes = Processes(self) # we have to read the configuration possibly with root privileges # as we need the MD5 information when we bind, and root is needed # to bind to a port < 1024 # this is undesirable as : # - handling user generated data as root should be avoided # - we may not be able to reload the configuration once the privileges are dropped # but I can not see any way to avoid it if not self.load(): return False try: self.listener = Listener() if self.ip: self.listener.listen(IP.create(self.ip),IP.create('0.0.0.0'),self.port,None,None) self.logger.reactor('Listening for BGP session(s) on %s:%d' % (self.ip,self.port)) for neighbor in self.configuration.neighbors.values(): if neighbor.listen: self.listener.listen(neighbor.md5_ip,neighbor.peer_address,neighbor.listen,neighbor.md5_password,neighbor.ttl_in) self.logger.reactor('Listening for BGP session(s) on %s:%d%s' % (neighbor.md5_ip,neighbor.listen,' with MD5' if neighbor.md5_password else '')) except NetworkError as exc: self.listener = None if os.geteuid() != 0 and self.port <= 1024: self.logger.reactor('Can not bind to %s:%d, you may need to run ExaBGP as root' % (self.ip,self.port),'critical') else: self.logger.reactor('Can not bind to %s:%d (%s)' % (self.ip,self.port,str(exc)),'critical') self.logger.reactor('unset exabgp.tcp.bind if you do not want listen for incoming connections','critical') self.logger.reactor('and check that no other daemon is already binding to port %d' % self.port,'critical') sys.exit(1) if not self.early_drop: self.processes.start() if not self.daemon.drop_privileges(): self.logger.reactor('Could not drop privileges to \'%s\' refusing to run as root' % self.daemon.user,'critical') self.logger.reactor('Set the environmemnt value exabgp.daemon.user to change the unprivileged user','critical') return if self.early_drop: self.processes.start() # This is required to make sure we can write in the log location as we now have dropped root privileges if not self.logger.restart(): self.logger.reactor('Could not setup the logger, aborting','critical') return if not self.daemon.savepid(): return # did we complete the run of updates caused by the last SIGUSR1/SIGUSR2 ? reload_completed = True wait = environment.settings().tcp.delay if wait: sleeptime = (wait * 60) - int(time.time()) % (wait * 60) self.logger.reactor('waiting for %d seconds before connecting' % sleeptime) time.sleep(float(sleeptime)) workers = {} peers = set() scheduled = False while True: try: finished = False start = time.time() end = start + self.max_loop_time if self._shutdown: self._shutdown = False self.shutdown() break if self._reload and reload_completed: self._reload = False self.load() self.processes.start(self._reload_processes) self._reload_processes = False elif self._restart: self._restart = False self.restart() # We got some API routes to announce if self.route_update: self.route_update = False self.route_send() for peer in self.peers.keys(): peers.add(peer) while start < time.time() < end and not finished: if self.peers: for key in list(peers): peer = self.peers[key] action = peer.run() # .run() returns an ACTION enum: # * immediate if it wants to be called again # * later if it should be called again but has no work atm # * close if it is finished and is closing down, or restarting if action == ACTION.CLOSE: self.unschedule(peer) peers.discard(key) # we are loosing this peer, not point to schedule more process work elif action == ACTION.LATER: for io in peer.sockets(): workers[io] = key # no need to come back to it before a a full cycle peers.discard(key) if not peers: reload_completed = True if self.listener: for connection in self.listener.connected(): # found # * False, not peer found for this TCP connection # * True, peer found # * None, conflict found for this TCP connections found = False for key in self.peers: peer = self.peers[key] neighbor = peer.neighbor # XXX: FIXME: Inet can only be compared to Inet if connection.local == str(neighbor.peer_address) and connection.peer == str(neighbor.local_address): if peer.incoming(connection): found = True break found = None break if found: self.logger.reactor('accepted connection from %s - %s' % (connection.local,connection.peer)) elif found is False: self.logger.reactor('no session configured for %s - %s' % (connection.local,connection.peer)) connection.notification(6,3,'no session configured for the peer') connection.close() elif found is None: self.logger.reactor('connection refused (already connected to the peer) %s - %s' % (connection.local,connection.peer)) connection.notification(6,5,'could not accept the connection') connection.close() scheduled = self.schedule() finished = not peers and not scheduled # RFC state that we MUST not send more than one KEEPALIVE / sec # And doing less could cause the session to drop if finished: for io in self.ready(list(peers),self.processes.fds(),end-time.time()): if io in workers: peers.add(workers[io]) del workers[io] if self._stopping and not self.peers.keys(): break except KeyboardInterrupt: while True: try: self._shutdown = True self.logger.reactor('^C received') break except KeyboardInterrupt: pass # socket.error is a subclass of IOError (so catch it first) except socket.error: try: self._shutdown = True self.logger.reactor('socket error received','warning') break except KeyboardInterrupt: pass except IOError: while True: try: self._shutdown = True self.logger.reactor('I/O Error received, most likely ^C during IO','warning') break except KeyboardInterrupt: pass except SystemExit: try: self._shutdown = True self.logger.reactor('exiting') break except KeyboardInterrupt: pass except ProcessError: try: self._shutdown = True self.logger.reactor('Problem when sending message(s) to helper program, stopping','error') except KeyboardInterrupt: pass except select.error: try: self._shutdown = True self.logger.reactor('problem using select, stopping','error') except KeyboardInterrupt: pass # from exabgp.leak import objgraph # print objgraph.show_most_common_types(limit=20) # import random # obj = objgraph.by_type('Route')[random.randint(0,2000)] # objgraph.show_backrefs([obj], max_depth=10) def shutdown (self): """terminate all the current BGP connections""" self.logger.reactor('performing shutdown') if self.listener: self.listener.stop() self.listener = None for key in self.peers.keys(): self.peers[key].stop() self.processes.terminate() self.daemon.removepid() self._stopping = True def load (self): """reload the configuration and send to the peer the route which changed""" self.logger.reactor('performing reload of exabgp %s' % version) reloaded = self.configuration.reload() if not reloaded: # # Careful the string below is used but the QA code to check for sucess of failure self.logger.configuration('problem with the configuration file, no change done','error') # Careful the string above is used but the QA code to check for sucess of failure # self.logger.configuration(str(self.configuration.error),'error') return False for key, peer in self.peers.items(): if key not in self.configuration.neighbors: self.logger.reactor('removing peer: %s' % peer.neighbor.name()) peer.stop() for key, neighbor in self.configuration.neighbors.items(): # new peer if key not in self.peers: self.logger.reactor('new peer: %s' % neighbor.name()) peer = Peer(neighbor,self) self.peers[key] = peer # modified peer elif self.peers[key].neighbor != neighbor: self.logger.reactor('peer definition change, establishing a new connection for %s' % str(key)) self.peers[key].reestablish(neighbor) # same peer but perhaps not the routes else: # finding what route changed and sending the delta is not obvious self.logger.reactor('peer definition identical, updating peer routes if required for %s' % str(key)) self.peers[key].reconfigure(neighbor) self.logger.configuration('loaded new configuration successfully','info') return True def schedule (self): try: # read at least on message per process if there is some and parse it for service,command in self.processes.received(): self.api.text(self,service,command) # if we have nothing to do, return or save the work if not self._running: if not self._pending: return False self._running,name = self._pending.popleft() self.logger.reactor('callback | installing %s' % name) if self._running: # run it try: self.logger.reactor('callback | running') six.next(self._running) # run # should raise StopIteration in most case # and prevent us to have to run twice to run one command six.next(self._running) # run except StopIteration: self._running = None self.logger.reactor('callback | removing') return True except StopIteration: pass except KeyboardInterrupt: self._shutdown = True self.logger.reactor('^C received','error') def route_send (self): """the process ran and we need to figure what routes to changes""" self.logger.reactor('performing dynamic route update') for key in self.configuration.neighbors.keys(): self.peers[key].send_new() self.logger.reactor('updated peers dynamic routes successfully') def restart (self): """kill the BGP session and restart it""" self.logger.reactor('performing restart of exabgp %s' % version) self.configuration.reload() for key in self.peers.keys(): if key not in self.configuration.neighbors.keys(): neighbor = self.configuration.neighbors[key] self.logger.reactor('removing Peer %s' % neighbor.name()) self.peers[key].stop() else: self.peers[key].reestablish() self.processes.terminate() self.processes.start() def unschedule (self, peer): key = peer.neighbor.name() if key in self.peers: del self.peers[key] def answer (self, service, string): self.processes.write(service,string) self.logger.reactor('responding to %s : %s' % (service,string.replace('\n','\\n'))) def api_shutdown (self): self._shutdown = True self._pending = deque() self._running = None def api_reload (self): self._reload = True self._pending = deque() self._running = None def api_restart (self): self._restart = True self._pending = deque() self._running = None @staticmethod def match_neighbor (description, name): for string in description: if re.search(r'(^|[\s])%s($|[\s,])' % re.escape(string), name) is None: return False return True def match_neighbors (self, descriptions): """return the sublist of peers matching the description passed, or None if no description is given""" if not descriptions: return self.peers.keys() returned = [] for key in self.peers: for description in descriptions: if Reactor.match_neighbor(description,key): if key not in returned: returned.append(key) return returned def nexthops (self, peers): return dict((peer,self.peers[peer].neighbor.local_address) for peer in peers) def plan (self, callback,name): self._pending.append((callback,name))