class Handler (object): callback = { 'text': {}, 'json': {}, } # need to sort and reverse, in order for the shorter command to not used by error # "show neighbor" should not match "show neighbors" functions = sorted([ 'withdraw watchdog', 'withdraw vpls', 'withdraw route', 'withdraw flow', 'withdraw attribute', 'version', 'teardown', 'shutdown', 'show routes extensive', 'show routes', 'show neighbors', 'show neighbor', 'restart', 'reload', 'flush route', 'announce watchdog', 'announce vpls', 'announce route-refresh', 'announce route', 'announce operational', 'announce flow', 'announce eor', 'announce attribute' ],reverse=True) def __init__ (self): self.logger = Logger() self.parser = Parser.Text() try: for name in self.functions: self.callback['text'][name] = Command.Text.callback[name] except KeyError: raise RuntimeError('The code does not have an implementation for "%s", please code it !' % name) def text (self, reactor, service, command): for registered in self.functions: if registered in command: self.logger.reactor("callback | handling '%s' with %s" % (command,self.callback['text'][registered].func_name),'warning') self.callback['text'][registered](self,reactor,service,command) return True self.logger.reactor("Command from process not understood : %s" % command,'warning') return False
def run (env, comment, configurations, pid=0): from exabgp.logger import Logger logger = Logger() if comment: logger.configuration(comment) if not env.profile.enable: ok = Reactor(configurations).run() __exit(env.debug.memory,0 if ok else 1) try: import cProfile as profile except ImportError: import profile if not env.profile.file or env.profile.file == 'stdout': ok = profile.run('Reactor(configurations).run()') __exit(env.debug.memory,0 if ok else 1) if pid: profile_name = "%s-pid-%d" % (env.profile.file,pid) else: profile_name = env.profile.file notice = '' if os.path.isdir(profile_name): notice = 'profile can not use this filename as outpout, it is not a directory (%s)' % profile_name if os.path.exists(profile_name): notice = 'profile can not use this filename as outpout, it already exists (%s)' % profile_name if not notice: logger.reactor('profiling ....') profiler = profile.Profile() profiler.enable() try: ok = Reactor(configurations).run() except Exception: raise finally: profiler.disable() kprofile = lsprofcalltree.KCacheGrind(profiler) with open(profile_name, 'w+') as write: kprofile.output(write) __exit(env.debug.memory,0 if ok else 1) else: logger.reactor("-"*len(notice)) logger.reactor(notice) logger.reactor("-"*len(notice)) Reactor(configurations).run() __exit(env.debug.memory,1)
class Decoder (object): storage = {} def __init__ (self): self.logger = Logger() self.format = Text() # callaback code @classmethod def register_command (cls, command, function): cls.storage[command] = function return function def parse_command (self, reactor, service, command): # it must be reversed so longer command are found before the shorter # "show neighbor" should not match "show neighbors" for registered in sorted(self.storage, reverse=True): if registered in command: return self.storage[registered](self,reactor,service,command) self.logger.reactor("Command from process not understood : %s" % command,'warning') return False
class Decoder (object): storage = {} def __init__ (self): self.logger = Logger() self.format = Text() # callaback code @classmethod def register_command (cls, command, function): cls.storage[command] = function return function def parse_command (self, reactor, service, command): # it must be reversed so longer command are found before the shorter # "show neighbor" should not match "show neighbors" for registered in sorted(self.storage, reverse=True): if registered in command: return self.storage[registered](self,reactor,service,command) self.logger.reactor("Command from process not understood : %s" % command,'warning') return False
for configuration in configurations: pid = os.fork() if pid == 0: run(env,comment,[configuration],os.getpid()) else: pids.append(pid) # If we get a ^C / SIGTERM, ignore just continue waiting for our child process import signal signal.signal(signal.SIGINT, signal.SIG_IGN) # wait for the forked processes for pid in pids: os.waitpid(pid,0) except OSError,exc: logger.reactor('Can not fork, errno %d : %s' % (exc.errno,exc.strerror),'critical') sys.exit(1) def run (env, comment, configurations, pid=0): logger = Logger() if comment: logger.configuration(comment) if not env.profile.enable: ok = Reactor(configurations).run() __exit(env.debug.memory,0 if ok else 1) try: import cProfile as profile except ImportError:
class Daemon (object): def __init__ (self, reactor): self.pid = environment.settings().daemon.pid self.user = environment.settings().daemon.user self.daemonize = environment.settings().daemon.daemonize self.umask = environment.settings().daemon.umask self.logger = Logger() self.reactor = reactor os.chdir('/') os.umask(self.umask) def check_pid (self,pid): if pid < 0: # user input error return False if pid == 0: # all processes return False try: os.kill(pid, 0) return True except OSError as err: if err.errno == errno.EPERM: # a process we were denied access to return True if err.errno == errno.ESRCH: # No such process return False # should never happen return False def savepid (self): self._saved_pid = False if not self.pid: return True ownid = os.getpid() flags = os.O_CREAT | os.O_EXCL | os.O_WRONLY mode = ((os.R_OK | os.W_OK) << 6) | (os.R_OK << 3) | os.R_OK try: fd = os.open(self.pid,flags,mode) except OSError: try: pid = open(self.pid,'r').readline().strip() if self.check_pid(int(pid)): self.logger.daemon("PIDfile already exists and program still running %s" % self.pid) return False else: # If pid is not running, reopen file without O_EXCL fd = os.open(self.pid,flags ^ os.O_EXCL,mode) except (OSError,IOError,ValueError): pass try: f = os.fdopen(fd,'w') line = "%d\n" % ownid f.write(line) f.close() self._saved_pid = True except IOError: self.logger.daemon("Can not create PIDfile %s" % self.pid,'warning') return False self.logger.daemon("Created PIDfile %s with value %d" % (self.pid,ownid),'warning') return True def removepid (self): if not self.pid or not self._saved_pid: return try: os.remove(self.pid) except OSError as exc: if exc.errno == errno.ENOENT: pass else: self.logger.daemon("Can not remove PIDfile %s" % self.pid,'error') return self.logger.daemon("Removed PIDfile %s" % self.pid) def drop_privileges (self): """return true if we are left with insecure privileges""" # os.name can be ['posix', 'nt', 'os2', 'ce', 'java', 'riscos'] if os.name not in ['posix',]: return True uid = os.getuid() gid = os.getgid() if uid and gid: return True try: user = pwd.getpwnam(self.user) nuid = int(user.pw_uid) ngid = int(user.pw_gid) except KeyError: return False # not sure you can change your gid if you do not have a pid of zero try: # we must change the GID first otherwise it may fail after change UID if not gid: os.setgid(ngid) if not uid: os.setuid(nuid) cuid = os.getuid() ceid = os.geteuid() cgid = os.getgid() if cuid < 0: cuid += (1 << 32) if cgid < 0: cgid += (1 << 32) if ceid < 0: ceid += (1 << 32) if nuid != cuid or nuid != ceid or ngid != cgid: return False except OSError: return False return True def _is_socket (self, fd): try: s = socket.fromfd(fd, socket.AF_INET, socket.SOCK_RAW) except ValueError: # The file descriptor is closed return False try: s.getsockopt(socket.SOL_SOCKET, socket.SO_TYPE) except socket.error as exc: # It is look like one but it is not a socket ... if exc.args[0] == errno.ENOTSOCK: return False return True def daemonise (self): if not self.daemonize: return log = environment.settings().log if log.enable and log.destination.lower() in ('stdout','stderr'): self.logger.daemon('ExaBGP can not fork when logs are going to %s' % log.destination.lower(),'critical') return def fork_exit (): try: pid = os.fork() if pid > 0: os._exit(0) except OSError as exc: self.logger.reactor('Can not fork, errno %d : %s' % (exc.errno,exc.strerror),'critical') # do not detach if we are already supervised or run by init like process if self._is_socket(sys.__stdin__.fileno()) or os.getppid() == 1: return fork_exit() os.setsid() fork_exit() self.silence() def silence (self): # closing more would close the log file too if open maxfd = 3 for fd in range(0, maxfd): try: os.close(fd) except OSError: pass os.open("/dev/null", os.O_RDWR) os.dup2(0, 1) os.dup2(0, 2)
class API (object): callback = { 'text': {}, 'json': {}, } # need to sort and reverse, in order for the shorter command to not used by error # "show neighbor" should not match "show neighbors" functions = sorted([ 'show neighbor', 'show neighbors', 'show neighbor status', 'show routes', 'show routes static', 'show routes flow', 'show routes l2vpn', 'show routes extensive', 'announce operational', 'announce attributes', 'announce eor', 'announce flow', 'announce route', 'announce route-refresh', 'announce vpls', 'announce watchdog', 'withdraw attributes', 'withdraw flow', 'withdraw route', 'withdraw vpls', 'withdraw watchdog', 'flush route', 'teardown', 'version', 'restart', 'reload', 'shutdown', '#', ],reverse=True) def __init__ (self,reactor): self.reactor = reactor self.logger = Logger() self.parser = Parser.Text(reactor) try: for name in self.functions: self.callback['text'][name] = Command.Text.callback[name] except KeyError: raise RuntimeError('The code does not have an implementation for "%s", please code it !' % name) def log_message (self, message, level='info'): self.logger.reactor(message,level) def log_failure (self, message, level='error'): error = str(self.parser.configuration.tokeniser.error) report = '%s\nreason: %s' % (message, error) if error else message self.logger.reactor(report,level) def text (self, reactor, service, command): for registered in self.functions: if registered in command: self.logger.reactor("callback | handling '%s' with %s" % (command,self.callback['text'][registered].func_name),'warning') # XXX: should we not test the return value ? self.callback['text'][registered](self,reactor,service,command) # reactor.plan(self.callback['text'][registered](self,reactor,service,command),registered) return True self.logger.reactor("Command from process not understood : %s" % command,'warning') return False def shutdown (self): self.reactor.api_shutdown() return True def reload (self): self.reactor.api_reload() return True def restart (self): self.reactor.api_restart() return True
def run(env, comment, configurations, root, validate, pid=0): logger = Logger() logger.info('Thank you for using ExaBGP', source='welcome') logger.info('%s' % version, source='version') logger.info('%s' % sys.version.replace('\n', ' '), source='interpreter') logger.info('%s' % ' '.join(platform.uname()[:5]), source='os') logger.info('%s' % root, source='installation') if comment: logger.info(comment, source='advice') warning = warn() if warning: logger.info(warning, source='advice') if env.api.cli: pipes = named_pipe(root) if len(pipes) != 1: env.api.cli = False logger.error( 'Could not find the named pipes (exabgp.in and exabgp.out) required for the cli', source='cli') logger.error( 'We scanned the following folders (the number is your PID):', source='cli') for location in pipes: logger.error(' - %s' % location, source='cli control') logger.error( 'please make them in one of the folder with the following commands:', source='cli control') logger.error('> mkfifo %s/run/exabgp.{in,out}' % os.getcwd(), source='cli control') logger.error('> chmod 600 %s/run/exabgp.{in,out}' % os.getcwd(), source='cli control') if os.getuid() != 0: logger.error('> chown %d:%d %s/run/exabgp.{in,out}' % (os.getuid(), os.getgid(), os.getcwd()), source='cli control') else: pipe = pipes[0] os.environ['exabgp_cli_pipe'] = pipe logger.info('named pipes for the cli are:', source='cli control') logger.info('to send commands %sexabgp.in' % pipe, source='cli control') logger.info('to read responses %sexabgp.out' % pipe, source='cli control') if not env.profile.enable: was_ok = Reactor(configurations).run(validate, root) __exit(env.debug.memory, 0 if was_ok else 1) try: import cProfile as profile except ImportError: import profile if env.profile.file == 'stdout': profiled = 'Reactor(%s).run(%s,"%s")' % (str(configurations), str(validate), str(root)) was_ok = profile.run(profiled) __exit(env.debug.memory, 0 if was_ok else 1) if pid: profile_name = "%s-pid-%d" % (env.profile.file, pid) else: profile_name = env.profile.file notice = '' if os.path.isdir(profile_name): notice = 'profile can not use this filename as output, it is not a directory (%s)' % profile_name if os.path.exists(profile_name): notice = 'profile can not use this filename as output, it already exists (%s)' % profile_name if not notice: cwd = os.getcwd() logger.reactor('profiling ....') profiler = profile.Profile() profiler.enable() try: was_ok = Reactor(configurations).run(validate, root) except Exception: was_ok = False raise finally: profiler.disable() kprofile = lsprofcalltree.KCacheGrind(profiler) try: destination = profile_name if profile_name.startswith( '/') else os.path.join(cwd, profile_name) with open(destination, 'w+') as write: kprofile.output(write) except IOError: notice = 'could not save profiling in formation at: ' + destination logger.reactor("-" * len(notice)) logger.reactor(notice) logger.reactor("-" * len(notice)) __exit(env.debug.memory, 0 if was_ok else 1) else: logger.reactor("-" * len(notice)) logger.reactor(notice) logger.reactor("-" * len(notice)) Reactor(configurations).run(validate, root) __exit(env.debug.memory, 1)
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))
class API (object): callback = { 'text': {}, 'json': {}, } # need to sort and reverse, in order for the shorter command to not used by error # "show neighbor" should not match "show neighbors" functions = sorted([ 'withdraw watchdog', 'withdraw vpls', 'withdraw route', 'withdraw flow', 'withdraw attribute', 'version', 'teardown', 'shutdown', 'show routes extensive', 'show routes', 'show neighbors', 'show neighbor', 'restart', 'reload', 'flush route', 'announce watchdog', 'announce vpls', 'announce route-refresh', 'announce route', 'announce flow', 'announce eor', 'announce attribute', 'announce operational', ],reverse=True) def __init__ (self,reactor): self.reactor = reactor self.logger = Logger() self.parser = Parser.Text() try: for name in self.functions: self.callback['text'][name] = Command.Text.callback[name] except KeyError: raise RuntimeError('The code does not have an implementation for "%s", please code it !' % name) def text (self, reactor, service, command): for registered in self.functions: if registered in command: self.logger.reactor("callback | handling '%s' with %s" % (command,self.callback['text'][registered].func_name),'warning') # XXX: should we not test the return value ? self.callback['text'][registered](self,reactor,service,command) return True self.logger.reactor("Command from process not understood : %s" % command,'warning') return False def change_to_peers (self, change, peers): neighbors = self.reactor.configuration.neighbor.neighbors result = True for neighbor in neighbors: if neighbor in peers: if change.nlri.family() in neighbors[neighbor].families(): neighbors[neighbor].rib.outgoing.insert_announced(change) else: self.logger.configuration('the route family is not configured on neighbor','error') result = False return result def eor_to_peers (self, family, peers): neighbors = self.reactor.configuration.neighbor.neighbors result = False for neighbor in neighbors: if neighbor in peers: result = True neighbors[neighbor].eor.append(family) return result def operational_to_peers (self, operational, peers): neighbors = self.reactor.configuration.neighbor.neighbors result = True for neighbor in neighbors: if neighbor in peers: if operational.family() in neighbors[neighbor].families(): if operational.name == 'ASM': neighbors[neighbor].asm[operational.family()] = operational neighbors[neighbor].messages.append(operational) else: self.logger.configuration('the route family is not configured on neighbor','error') result = False return result def refresh_to_peers (self, refresh, peers): neighbors = self.reactor.configuration.neighbor.neighbors result = True for neighbor in neighbors: if neighbor in peers: family = (refresh.afi,refresh.safi) if family in neighbors[neighbor].families(): neighbors[neighbor].refresh.append(refresh.__class__(refresh.afi,refresh.safi)) else: result = False return result def shutdown (self): self.reactor.api_shutdown() return True def reload (self): self.reactor.api_reload() return True def restart (self): self.reactor.api_restart() return True
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))
def run(env, comment, configurations, validate, pid=0): logger = Logger() logger.error('', source='ExaBGP') logger.error('%s' % version, source='version') logger.error('%s' % sys.version.replace('\n', ' '), source='interpreter') logger.error('%s' % ' '.join(platform.uname()[:5]), source='os') logger.error('', source='ExaBGP') if comment: logger.configuration(comment) warning = warn() if warning: logger.configuration(warning) if not env.profile.enable: ok = Reactor(configurations).run(validate) __exit(env.debug.memory, 0 if ok else 1) try: import cProfile as profile except ImportError: import profile if not env.profile.file or env.profile.file == 'stdout': ok = profile.run('Reactor(configurations).run(validate)') __exit(env.debug.memory, 0 if ok else 1) if pid: profile_name = "%s-pid-%d" % (env.profile.file, pid) else: profile_name = env.profile.file notice = '' if os.path.isdir(profile_name): notice = 'profile can not use this filename as output, it is not a directory (%s)' % profile_name if os.path.exists(profile_name): notice = 'profile can not use this filename as output, it already exists (%s)' % profile_name if not notice: logger.reactor('profiling ....') profiler = profile.Profile() profiler.enable() try: ok = Reactor(configurations).run() except Exception: raise finally: profiler.disable() kprofile = lsprofcalltree.KCacheGrind(profiler) with open(profile_name, 'w+') as write: kprofile.output(write) __exit(env.debug.memory, 0 if ok else 1) else: logger.reactor("-" * len(notice)) logger.reactor(notice) logger.reactor("-" * len(notice)) Reactor(configurations).run() __exit(env.debug.memory, 1)
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._stopping = environment.settings().tcp.once self.max_loop_time = environment.settings().reactor.speed self.early_drop = environment.settings().daemon.drop self.processes = None self.configuration = Configuration(configurations) self.logger = Logger() self. async = ASYNC() self.signal = Signal() self.daemon = Daemon(self) self.listener = Listener(self) self.api = API(self) self.peers = {} self._reload_processes = False self._saved_pid = False def _termination(self, reason): self.signal.received = Signal.SHUTDOWN self.logger.reactor(reason, 'warning') def _api_ready(self, sockets): sleeptime = self.max_loop_time / 100 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: err_no, message = exc.args # pylint: disable=W0633 if err_no not in error.block: raise exc return [] except socket.error as exc: # python 3 does not raise on closed FD, but python2 does # we have lost a peer and it is causing the select # to complain, the code will self-heal, ignore the issue # (EBADF from python2 must be ignored if when checkign error.fatal) # otherwise sending notification causes TCP to drop and cause # this code to kill ExaBGP return [] except ValueError as exc: # The peer closing the TCP connection lead to a negative file descritor return [] except KeyboardInterrupt: self._termination('^C received') return [] def schedule_rib_check(self): self.logger.reactor('performing dynamic route update') for key in self.configuration.neighbors.keys(): self.peers[key].schedule_rib_check() def _active_peers(self): peers = set() for key, peer in self.peers.items(): if not peer.neighbor.passive or peer.proto: peers.add(key) return peers def run(self, validate, root): 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() # 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.listener.listen_on(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.listener.listen_on( 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(self.configuration.processes) 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(self.configuration.processes) # 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 = False 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() while True: try: if self.signal.received: for key in self.peers: if self.peers[key].neighbor.api['signal']: self.peers[key].reactor.processes.signal( self.peers[key].neighbor, self.signal.number) signaled = self.signal.received self.signal.rearm() if signaled == Signal.SHUTDOWN: self.shutdown() break if signaled == Signal.RESTART: self.restart() continue if not reload_completed: continue if signaled == Signal.RELOAD: self._reload_processes = True if signaled in (Signal.RELOAD, Signal.FULL_RELOAD): self.load() self.processes.start(self.configuration.processes, self._reload_processes) self._reload_processes = False continue if self.listener.incoming(): # check all incoming connection self. async .schedule(str(uuid.uuid1()), 'check new connection', self.listener.new_connections()) peers = self._active_peers() if not peers: reload_completed = True # give a turn to all the 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: if key in self.peers: del self.peers[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: break # 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) self. async .run() 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. async .clear() 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.listener.listen_on(ip, neighbor.peer_address, self._port, neighbor.md5_password, neighbor.md5_base64, None) self.logger.configuration('loaded new configuration successfully', 'info') return True 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.start(self.configuration.processes, True)
def main (): options = docopt.docopt(usage, help=False) major = int(sys.version[0]) minor = int(sys.version[2]) if major != 2 or minor < 5: sys.exit('This program can not work (is not tested) with your python version (< 2.5 or >= 3.0)') if options["--version"]: print('ExaBGP : %s' % version) print('Python : %s' % sys.version.replace('\n',' ')) print('Uname : %s' % ' '.join(platform.uname()[:5])) sys.exit(0) if options["--folder"]: folder = os.path.realpath(os.path.normpath(options["--folder"])) elif sys.argv[0].endswith('/bin/exabgp'): folder = sys.argv[0][:-len('/bin/exabgp')] + '/etc/exabgp' elif sys.argv[0].endswith('/sbin/exabgp'): folder = sys.argv[0][:-len('/sbin/exabgp')] + '/etc/exabgp' else: folder = '/etc/exabgp' os.environ['EXABGP_ETC'] = folder # This is not most pretty if options["--run"]: sys.argv = sys.argv[sys.argv.index('--run')+1:] if sys.argv[0] == 'healthcheck': from exabgp.application import run_healthcheck run_healthcheck() elif sys.argv[0] == 'cli': from exabgp.application import run_cli run_cli() else: print(usage) sys.exit(0) return envfile = 'exabgp.env' if not options["--env"] else options["--env"] if not envfile.startswith('/'): envfile = '%s/%s' % (folder, envfile) from exabgp.configuration.setup import environment try: env = environment.setup(envfile) except environment.Error as exc: print(usage) print('\nconfiguration issue,', str(exc)) sys.exit(1) # Must be done before setting the logger as it modify its behaviour if options["--debug"]: env.log.all = True env.log.level = syslog.LOG_DEBUG logger = Logger() named_pipe = os.environ.get('NAMED_PIPE','') if named_pipe: from exabgp.application.control import main as control control(named_pipe) sys.exit(0) if options["--decode"]: decode = ''.join(options["--decode"]).replace(':','').replace(' ','') if not is_bgp(decode): print(usage) print('Environment values are:\n' + '\n'.join(' - %s' % _ for _ in environment.default())) print("") print("The BGP message must be an hexadecimal string.") print("") print("All colons or spaces are ignored, for example:") print("") print(" --decode 001E0200000007900F0003000101") print(" --decode 001E:02:0000:0007:900F:0003:0001:01") print(" --decode FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF001E0200000007900F0003000101") print(" --decode FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:001E:02:0000:0007:900F:0003:0001:01") print(" --decode 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF 001E02 00000007900F0003000101'") sys.exit(1) else: decode = '' # Make sure our child has a named pipe name if env.api.file: os.environ['NAMED_PIPE'] = env.api.file duration = options["--signal"] if duration and duration.isdigit(): pid = os.fork() if pid: import time import signal try: time.sleep(int(duration)) os.kill(pid,signal.SIGUSR1) except KeyboardInterrupt: pass try: pid,code = os.wait() sys.exit(code) except KeyboardInterrupt: try: pid,code = os.wait() sys.exit(code) except Exception: sys.exit(0) if options["--help"]: print(usage) print('Environment values are:\n' + '\n'.join(' - %s' % _ for _ in environment.default())) sys.exit(0) if options["--decode"]: env.log.parser = True env.debug.route = decode env.tcp.bind = '' if options["--profile"]: env.profile.enable = True if options["--profile"].lower() in ['1','true']: env.profile.file = True elif options["--profile"].lower() in ['0','false']: env.profile.file = False else: env.profile.file = options["--profile"] if envfile and not os.path.isfile(envfile): comment = 'environment file missing\ngenerate it using "exabgp --fi > %s"' % envfile else: comment = '' if options["--full-ini"] or options["--fi"]: for line in environment.iter_ini(): print(line) sys.exit(0) if options["--full-env"] or options["--fe"]: print() for line in environment.iter_env(): print(line) sys.exit(0) if options["--diff-ini"] or options["--di"]: for line in environment.iter_ini(True): print(line) sys.exit(0) if options["--diff-env"] or options["--de"]: for line in environment.iter_env(True): print(line) sys.exit(0) if options["--once"]: env.tcp.once = True if options["--pdb"]: # The following may fail on old version of python (but is required for debug.py) os.environ['PDB'] = 'true' env.debug.pdb = True if options["--test"]: env.debug.selfcheck = True env.log.parser = True if options["--memory"]: env.debug.memory = True configurations = [] # check the file only once that we have parsed all the command line options and allowed them to run if options["<configuration>"]: for f in options["<configuration>"]: normalised = os.path.realpath(os.path.normpath(f)) if os.path.isfile(normalised): configurations.append(normalised) continue if f.startswith('etc/exabgp'): normalised = os.path.join(folder,f[11:]) if os.path.isfile(normalised): configurations.append(normalised) continue logger.configuration('one of the arguments passed as configuration is not a file (%s)' % f,'error') sys.exit(1) else: print(usage) print('Environment values are:\n' + '\n'.join(' - %s' % _ for _ in environment.default())) print('\nno configuration file provided') sys.exit(1) from exabgp.bgp.message.update.attribute import Attribute Attribute.caching = env.cache.attributes if env.debug.rotate or len(configurations) == 1: run(env,comment,configurations) if not (env.log.destination in ('syslog','stdout','stderr') or env.log.destination.startswith('host:')): logger.configuration('can not log to files when running multiple configuration (as we fork)','error') sys.exit(1) try: # run each configuration in its own process pids = [] for configuration in configurations: pid = os.fork() if pid == 0: run(env,comment,[configuration],os.getpid()) else: pids.append(pid) # If we get a ^C / SIGTERM, ignore just continue waiting for our child process import signal signal.signal(signal.SIGINT, signal.SIG_IGN) # wait for the forked processes for pid in pids: os.waitpid(pid,0) except OSError as exc: logger.reactor('Can not fork, errno %d : %s' % (exc.errno,exc.strerror),'critical') sys.exit(1)
class Decoder (object): _dispatch = {} _order = {} def __init__ (self): self.logger = Logger() self.format = Text() # callaback code def register_command (command,storage,order): def closure (f): def wrap (*args): f(*args) storage[command] = wrap order = sorted(storage.keys(),key=len) return wrap return closure def parse_command (self,reactor,service,command): for registered in sorted(self._dispatch, reverse=True): if registered in command: return self._dispatch[registered](self,reactor,service,command) self.logger.reactor("Command from process not understood : %s" % command,'warning') return False # @staticmethod def extract_neighbors (command): """return a list of neighbor definition : the neighbor definition is a list of string which are in the neighbor indexing string""" # This function returns a list and a string # The first list contains parsed neighbor to match against our defined peers # The string is the command to be run for those peers # The parsed neighbor is a list of the element making the neighbor string so each part can be checked against the neighbor name returned = [] neighbor,remaining = command.split(' ',1) if neighbor != 'neighbor': return [],command ip,command = remaining.split(' ',1) definition = ['neighbor %s' % (ip)] while True: try: key,value,remaining = command.split(' ',2) except ValueError: key,value = command.split(' ',1) if key == ',': returned.append(definition) _,command = command.split(' ',1) definition = [] continue if key not in ['neighbor','local-ip','local-as','peer-as','router-id','family-allowed']: if definition: returned.append(definition) break definition.append('%s %s' % (key,value)) command = remaining return returned,command # @register_command('shutdown',_dispatch,_order) def _shutdown (self,reactor,service,command): reactor.api_shutdown() reactor.answer(service,'shutdown in progress') return True @register_command('reload',_dispatch,_order) def _reload (self,reactor,service,command): reactor.api_reload() reactor.answer(service,'reload in progress') return True @register_command('reload',_dispatch,_order) def _restart (self,reactor,service,command): reactor.api_restart() reactor.answer(service,'restart in progress') return True @register_command('version',_dispatch,_order) def _version (self,reactor,service,command): reactor.answer(service,'exabgp %s' % version) return True # teardown @register_command('teardown',_dispatch,_order) def _t (self,reactor,service,command): try: descriptions,command = Decoder.extract_neighbors(command) _,code = command.split(' ',1) for key in reactor.peers: for description in descriptions: if reactor.match_neighbor(description,key): reactor.peers[key].teardown(int(code)) self.logger.reactor('teardown scheduled for %s' % ' '.join(description)) return True except ValueError: return False except IndexError: return False # show neighbor(s) @register_command('show neighbor',_dispatch,_order) def _show_neighbor (self,reactor,service,command): def _callback (): for key in reactor.configuration.neighbor.keys(): neighbor = reactor.configuration.neighbor[key] for line in str(neighbor).split('\n'): reactor.answer(service,line) yield True reactor.plan(_callback()) return True @register_command('show neighbors',_dispatch,_order) def _show_neighbors (self,reactor,service,command): def _callback (): for key in reactor.configuration.neighbor.keys(): neighbor = reactor.configuration.neighbor[key] for line in str(neighbor).split('\n'): reactor.answer(service,line) yield True reactor.plan(_callback()) return True # show route(s) @register_command('show routes',_dispatch,_order) def _show_routes (self,reactor,service,command): def _callback (): for key in reactor.configuration.neighbor.keys(): neighbor = reactor.configuration.neighbor[key] for change in list(neighbor.rib.outgoing.sent_changes()): reactor.answer(service,'neighbor %s %s' % (neighbor.local_address,str(change.nlri))) yield True reactor.plan(_callback()) return True @register_command('show routes extensive',_dispatch,_order) def _show_routes_extensive (self,reactor,service,command): def _callback (): for key in reactor.configuration.neighbor.keys(): neighbor = reactor.configuration.neighbor[key] for change in list(neighbor.rib.outgoing.sent_changes()): reactor.answer(service,'neighbor %s %s' % (neighbor.name(),change.extensive())) yield True reactor.plan(_callback()) return True # watchdogs @register_command('announce watchdog',_dispatch,_order) def _announce_watchdog (self,reactor,service,command): def _callback (name): for neighbor in reactor.configuration.neighbor: reactor.configuration.neighbor[neighbor].rib.outgoing.announce_watchdog(name) yield False reactor.route_update = True try: name = command.split(' ')[2] except IndexError: name = service reactor.plan(_callback(name)) return True @register_command('withdraw watchdog',_dispatch,_order) def _withdraw_watchdog (self,reactor,service,command): def _callback (name): for neighbor in reactor.configuration.neighbor: reactor.configuration.neighbor[neighbor].rib.outgoing.withdraw_watchdog(name) yield False reactor.route_update = True try: name = command.split(' ')[2] except IndexError: name = service reactor.plan(_callback(name)) return True # flush routes @register_command('flush route',_dispatch,_order) def _flush_route (self,reactor,service,command): def _callback (self,peers): self.logger.reactor("Flushing routes for %s" % ', '.join(peers if peers else []) if peers is not None else 'all peers') yield True reactor.route_update = True try: descriptions,command = Decoder.extract_neighbors(command) peers = reactor.match_neighbors(descriptions) if not peers: self.logger.reactor('no neighbor matching the command : %s' % command,'warning') return False reactor.plan(_callback(self,peers)) return True except ValueError: return False except IndexError: return False # route @register_command('announce route',_dispatch,_order) def _announce_route (self,reactor,service,command): def _callback (self,command,nexthops): changes = self.format.parse_api_route(command,nexthops,'announce') if not changes: self.logger.reactor("Command could not parse route in : %s" % command,'warning') yield True else: peers = [] for (peer,change) in changes: peers.append(peer) reactor.configuration.change_to_peers(change,[peer,]) yield False self.logger.reactor("Route added to %s : %s" % (', '.join(peers if peers else []) if peers is not None else 'all peers',change.extensive())) reactor.route_update = True try: descriptions,command = Decoder.extract_neighbors(command) peers = reactor.match_neighbors(descriptions) if not peers: self.logger.reactor('no neighbor matching the command : %s' % command,'warning') return False reactor.plan(_callback(self,command,reactor.nexthops(peers))) return True except ValueError: return False except IndexError: return False @register_command('withdraw route',_dispatch,_order) def _withdraw_route (self,reactor,service,command): def _callback (self,command,nexthops): changes = self.format.parse_api_route(command,nexthops,'withdraw') if not changes: self.logger.reactor("Command could not parse route in : %s" % command,'warning') yield True else: for (peer,change) in changes: if reactor.configuration.change_to_peers(change,[peer,]): self.logger.reactor("Route removed : %s" % change.extensive()) yield False else: self.logger.reactor("Could not find therefore remove route : %s" % change.extensive(),'warning') yield False reactor.route_update = True try: descriptions,command = Decoder.extract_neighbors(command) peers = reactor.match_neighbors(descriptions) if not peers: self.logger.reactor('no neighbor matching the command : %s' % command,'warning') return False reactor.plan(_callback(self,command,reactor.nexthops(peers))) return True except ValueError: return False except IndexError: return False # vpls @register_command('announce vpls',_dispatch,_order) def _announce_vpls (self,reactor,service,command): def _callback (self,command,nexthops): changes = self.format.parse_api_vpls(command,nexthops,'announce') if not changes: self.logger.reactor("Command could not parse vpls in : %s" % command,'warning') yield True else: peers = [] for (peer,change) in changes: peers.append(peer) reactor.configuration.change_to_peers(change,[peer,]) yield False self.logger.reactor("vpls added to %s : %s" % (', '.join(peers if peers else []) if peers is not None else 'all peers',change.extensive())) reactor.route_update = True try: descriptions,command = Decoder.extract_neighbors(command) peers = reactor.match_neighbors(descriptions) if not peers: self.logger.reactor('no neighbor matching the command : %s' % command,'warning') return False reactor.plan(_callback(self,command,reactor.nexthops(peers))) return True except ValueError: return False except IndexError: return False @register_command('withdraw vpls',_dispatch,_order) def _withdraw_change (self,reactor,service,command): def _callback (self,command,nexthops): changes = self.format.parse_api_vpls(command,nexthops,'withdraw') if not changes: self.logger.reactor("Command could not parse vpls in : %s" % command,'warning') yield True else: for (peer,change) in changes: if reactor.configuration.change_to_peers(change,[peer,]): self.logger.reactor("vpls removed : %s" % change.extensive()) yield False else: self.logger.reactor("Could not find therefore remove vpls : %s" % change.extensive(),'warning') yield False reactor.route_update = True try: descriptions,command = Decoder.extract_neighbors(command) peers = reactor.match_neighbors(descriptions) if not peers: self.logger.reactor('no neighbor matching the command : %s' % command,'warning') return False reactor.plan(_callback(self,command,reactor.nexthops(peers))) return True except ValueError: return False except IndexError: return False # attribute @register_command('announce attribute',_dispatch,_order) def _announce_attribute (self,reactor,service,command): def _callback (self,command,nexthops): changes = self.format.parse_api_attribute(command,nexthops,'announce') if not changes: self.logger.reactor("Command could not parse attribute in : %s" % command,'warning') yield True else: for (peers,change) in changes: reactor.configuration.change_to_peers(change,peers) self.logger.reactor("Route added to %s : %s" % (', '.join(peers if peers else []) if peers is not None else 'all peers',change.extensive())) yield False reactor.route_update = True try: descriptions,command = Decoder.extract_neighbors(command) peers = reactor.match_neighbors(descriptions) if not peers: self.logger.reactor('no neighbor matching the command : %s' % command,'warning') return False reactor.plan(_callback(self,command,reactor.nexthops(peers))) return True except ValueError: return False except IndexError: return False @register_command('withdraw attribute',_dispatch,_order) def _withdraw_attribute (self,reactor,service,command): def _callback (self,command,nexthops): changes = self.format.parse_api_attribute(command,nexthops,'withdraw') if not changes: self.logger.reactor("Command could not parse attribute in : %s" % command,'warning') yield True else: for (peers,change) in changes: if reactor.configuration.change_to_peers(change,peers): self.logger.reactor("Route removed : %s" % change.extensive()) yield False else: self.logger.reactor("Could not find therefore remove route : %s" % change.extensive(),'warning') yield False reactor.route_update = True try: descriptions,command = Decoder.extract_neighbors(command) peers = reactor.match_neighbors(descriptions) if not peers: self.logger.reactor('no neighbor matching the command : %s' % command,'warning') return False reactor.plan(_callback(self,command,reactor.nexthops(peers))) return True except ValueError: return False except IndexError: return False # flow @register_command('announce flow',_dispatch,_order) def _announce_flow (self,reactor,service,command): def _callback (self,command,peers): changes = self.format.parse_api_flow(command,'announce') if not changes: self.logger.reactor("Command could not parse flow in : %s" % command) yield True else: for change in changes: reactor.configuration.change_to_peers(change,peers) self.logger.reactor("Flow added to %s : %s" % (', '.join(peers if peers else []) if peers is not None else 'all peers',change.extensive())) yield False reactor.route_update = True try: descriptions,command = Decoder.extract_neighbors(command) peers = reactor.match_neighbors(descriptions) if not peers: self.logger.reactor('no neighbor matching the command : %s' % command,'warning') return False reactor.plan(_callback(self,command,peers)) return True except ValueError: return False except IndexError: return False @register_command('withdraw flow',_dispatch,_order) def _withdraw_flow (self,reactor,service,command): def _callback (self,command,peers): changes = self.format.parse_api_flow(command,'withdraw') if not changes: self.logger.reactor("Command could not parse flow in : %s" % command) yield True else: for change in changes: if reactor.configuration.change_to_peers(change,peers): self.logger.reactor("Flow found and removed : %s" % change.extensive()) yield False else: self.logger.reactor("Could not find therefore remove flow : %s" % change.extensive(),'warning') yield False reactor.route_update = True try: descriptions,command = Decoder.extract_neighbors(command) peers = reactor.match_neighbors(descriptions) if not peers: self.logger.reactor('no neighbor matching the command : %s' % command,'warning') return False reactor.plan(_callback(self,command,peers)) return True except ValueError: return False except IndexError: return False # eor @register_command('announce eor',_dispatch,_order) def _announce_eor (self,reactor,service,command): def _callback (self,command,peers): family = self.format.parse_api_eor(command) if not family: self.logger.reactor("Command could not parse eor : %s" % command) yield True else: reactor.configuration.eor_to_peers(family,peers) self.logger.reactor("Sent to %s : %s" % (', '.join(peers if peers else []) if peers is not None else 'all peers',family.extensive())) yield False reactor.route_update = True try: descriptions,command = Decoder.extract_neighbors(command) peers = reactor.match_neighbors(descriptions) if not peers: self.logger.reactor('no neighbor matching the command : %s' % command,'warning') return False reactor.plan(_callback(self,command,peers)) return True except ValueError: return False except IndexError: return False # route-refresh @register_command('announce route-refresh',_dispatch,_order) def _announce_refresh (self,reactor,service,command): def _callback (self,command,peers): rr = self.format.parse_api_refresh(command) if not rr: self.logger.reactor("Command could not parse flow in : %s" % command) yield True else: reactor.configuration.refresh_to_peers(rr,peers) self.logger.reactor("Sent to %s : %s" % (', '.join(peers if peers else []) if peers is not None else 'all peers',rr.extensive())) yield False reactor.route_update = True try: descriptions,command = Decoder.extract_neighbors(command) peers = reactor.match_neighbors(descriptions) if not peers: self.logger.reactor('no neighbor matching the command : %s' % command,'warning') return False reactor.plan(_callback(self,command,peers)) return True except ValueError: return False except IndexError: return False # operational @register_command('operational',_dispatch,_order) def _announce_operational (self,reactor,service,command): def _callback (self,command,peers): operational = self.format.parse_api_operational(command) if not operational: self.logger.reactor("Command could not parse operational command : %s" % command) yield True else: reactor.configuration.operational_to_peers(operational,peers) self.logger.reactor("operational message sent to %s : %s" % (', '.join(peers if peers else []) if peers is not None else 'all peers',operational.extensive())) yield False reactor.route_update = True if (command.split() + ['safe'])[1].lower() not in ('asm','adm','rpcq','rpcp','apcq','apcp','lpcq','lpcp'): return False try: descriptions,command = Decoder.extract_neighbors(command) peers = reactor.match_neighbors(descriptions) if not peers: self.logger.reactor('no neighbor matching the command : %s' % command,'warning') return False reactor.plan(_callback(self,command,peers)) return True except ValueError: return False except IndexError: return False
class Reactor (object): # [hex(ord(c)) for c in os.popen('clear').read()] clear = ''.join([chr(int(c,16)) for c in ['0x1b', '0x5b', '0x48', '0x1b', '0x5b', '0x32', '0x4a']]) def __init__ (self,configuration): self.ip = environment.settings().tcp.bind self.port = environment.settings().tcp.port self.max_loop_time = environment.settings().reactor.speed self.half_loop_time = self.max_loop_time / 2 self.logger = Logger() self.daemon = Daemon(self) self.processes = None self.listener = None self.configuration = Configuration(configuration) self._peers = {} self._shutdown = False self._reload = False self._reload_processes = False self._restart = False self._route_update = False self._saved_pid = False self._commands = [] self._pending = [] 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 run (self): if self.ip: try: self.listener = Listener([self.ip,],self.port) self.listener.start() except NetworkError,e: 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(e)),'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) self.logger.reactor("Listening for BGP session(s) on %s:%d" % (self.ip,self.port)) if 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 self.daemon.daemonise() if not self.daemon.savepid(): self.logger.reactor('could not update PID, not starting','error') # Make sure we create processes one we have dropped privileges and closed file descriptor self.processes = Processes(self) self.reload() # 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)) while True: try: while self._peers: start = time.time() if self._shutdown: self._shutdown = False self.shutdown() elif self._reload and reload_completed: self._reload = False self.reload(self._reload_processes) self._reload_processes = False elif self._restart: self._restart = False self.restart() elif self._route_update: self._route_update = False self.route_update() while self.schedule(self.processes.received()) or self._pending: self._pending = list(self.run_pending(self._pending)) duration = time.time() - start if duration >= self.half_loop_time: break # Handle all connection peers = self._peers.keys() ios = [] while peers: for key in peers[:]: peer = self._peers[key] action = peer.run() # .run() returns: # * True if it wants to be called again # * None if it should be called again but has no work atm # * False if it is finished and is closing down, or restarting if action == ACTION.close: self.unschedule(peer) peers.remove(key) elif action == ACTION.later: ios.extend(peer.sockets()) # no need to come back to it before a a full cycle peers.remove(key) duration = time.time() - start if duration >= self.max_loop_time: ios=[] break if not peers: reload_completed = True # append here after reading as if read fails due to a dead process # we may respawn the process which changes the FD ios.extend(self.processes.fds()) # RFC state that we MUST not send more than one KEEPALIVE / sec # And doing less could cause the session to drop while self.schedule(self.processes.received()) or self._pending: self._pending = list(self.run_pending(self._pending)) duration = time.time() - start if duration >= self.max_loop_time: break 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() if ios: delay = max(start+self.max_loop_time-time.time(),0.0) try: read,_,_ = select.select(ios,[],[],delay) except select.error,e: errno,message = e.args if not errno in error.block: raise e delay = max(start+self.max_loop_time-time.time(),0.0) if delay: time.sleep(delay) self.processes.terminate() self.daemon.removepid() break except KeyboardInterrupt: while True: try: self._shutdown = True self.logger.reactor("^C received") break except KeyboardInterrupt: pass
class Signal(object): NONE = 0 SHUTDOWN = 1 RESTART = 2 RELOAD = 4 FULL_RELOAD = 8 def __init__(self): self.logger = Logger() self.received = self.NONE self.number = 0 self.rearm() def rearm(self): self.received = Signal.NONE self.number = 0 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') if self.received: self.logger.reactor('ignoring - still handling previous signal') return self.logger.reactor('scheduling shutdown') self.received = self.SHUTDOWN self.number = signum def sighup(self, signum, frame): self.logger.reactor('SIG HUP received') if self.received: self.logger.reactor('ignoring - still handling previous signal') return self.logger.reactor('scheduling shutdown') self.received = self.SHUTDOWN self.number = signum def sigalrm(self, signum, frame): self.logger.reactor('SIG ALRM received') if self.received: self.logger.reactor('ignoring - still handling previous signal') return self.logger.reactor('scheduling restart') self.received = self.RESTART self.number = signum def sigusr1(self, signum, frame): self.logger.reactor('SIG USR1 received') if self.received: self.logger.reactor('ignoring - still handling previous signal') return self.logger.reactor('scheduling reload of configuration') self.received = self.RELOAD self.number = signum def sigusr2(self, signum, frame): self.logger.reactor('SIG USR1 received') if self.received: self.logger.reactor('ignoring - still handling previous signal') return self.logger.reactor('scheduling reload of configuration and processes') self.received = self.FULL_RELOAD self.number = signum