def validate_open(self): error = self.negotiated.validate(self.neighbor) if error is not None: raise Notify(*error) if self.neighbor.api['negotiated']: self.peer.reactor.processes.negotiated(self.peer.neighbor, self.negotiated) if self.negotiated.mismatch: log.warning( '--------------------------------------------------------------------', self.connection.session()) log.warning( 'the connection can not carry the following family/families', self.connection.session()) for reason, (afi, safi) in self.negotiated.mismatch: log.warning( ' - %s is not configured for %s/%s' % (reason, afi, safi), self.connection.session()) log.warning( 'therefore no routes of this kind can be announced on the connection', self.connection.session()) log.warning( '--------------------------------------------------------------------', self.connection.session())
def savepid(self): 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)): log.debug("PIDfile already exists and program still running %s" % self.pid, 'daemon') 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): log.debug("issue accessing PID file %s (most likely permission or ownership)" % self.pid, 'daemon') return False try: f = os.fdopen(fd, 'w') line = "%d\n" % ownid f.write(line) f.close() self._saved_pid = True except IOError: log.warning("Can not create PIDfile %s" % self.pid, 'daemon') return False log.warning("Created PIDfile %s with value %d" % (self.pid, ownid), 'daemon') return True
def text(self, reactor, service, command): for registered in self.functions: if registered == command or command.endswith(' ' + registered) or registered + ' ' in command: return self.callback['text'][registered](self, reactor, service, command) reactor.processes.answer_error(service) log.warning('command from process not understood : %s' % command, 'api') return False
def close(self): try: log.warning('%s, closing connection' % self.name(), source=self.session()) if self.io: self.io.close() self.io = None except Exception: self.io = None
def rate_limit(tokeniser): # README: We are setting the ASN as zero as that what Juniper (and Arbor) did when we created a local flow route speed = int(tokeniser()) if speed < 9600 and speed != 0: log.warning("rate-limiting flow under 9600 bytes per seconds may not work", 'configuration') if speed > 1000000000000: speed = 1000000000000 log.warning("rate-limiting changed for 1 000 000 000 000 bytes from %s" % speed, 'configuration') return ExtendedCommunities().add(TrafficRate(ASN(0), speed))
def writer(self, data): if not self.io: # XXX: FIXME: Make sure it does not hold the cleanup during the closing of the peering session yield True return while not self.writing(): yield False logfunc.debug(lazyformat('sending TCP payload', data), self.session()) # The first while is here to setup the try/catch block once as it is very expensive while True: try: while True: if self.defensive and random.randint(0, 2): raise socket.error(errno.EAGAIN, 'raising network error on purpose') # we can not use sendall as in case of network buffer filling # it does raise and does not let you know how much was sent number = self.io.send(data) if not number: self.close() log.warning( '%s %s lost TCP connection with peer' % (self.name(), self.peer), self.session()) raise LostConnection('lost the TCP connection') data = data[number:] if not data: yield True return yield False except socket.error as exc: if exc.args[0] in error.block: log.debug( '%s %s blocking io problem mid-way through writing a message %s, trying to complete' % (self.name(), self.peer, errstr(exc)), self.session(), ) yield False elif exc.errno == errno.EPIPE: # The TCP connection is gone. self.close() raise NetworkError('Broken TCP connection') elif exc.args[0] in error.fatal: self.close() log.critical( '%s %s problem sending message (%s)' % (self.name(), self.peer, errstr(exc)), self.session()) raise NetworkError( 'Problem while writing data to the network (%s)' % errstr(exc)) # what error could it be ! else: log.critical( '%s %s undefined error writing on socket' % (self.name(), self.peer), self.session()) yield False
def _reader(self, number): # The function must not be called if it does not return with no data with a smaller size as parameter if not self.io: self.close() raise NotConnected('Trying to read on a closed TCP connection') if number == 0: yield b'' return while not self.reading(): yield b'' data = b'' reported = '' while True: try: while True: if self.defensive and random.randint(0, 2): raise socket.error(errno.EAGAIN, 'raising network error on purpose') read = self.io.recv(number) if not read: self.close() log.warning('%s %s lost TCP session with peer' % (self.name(), self.peer), self.session()) raise LostConnection('the TCP connection was closed by the remote end') data += read number -= len(read) if not number: log.debug(LazyFormat('received TCP payload', data), self.session()) yield data return yield b'' except socket.timeout as exc: self.close() log.warning('%s %s peer is too slow' % (self.name(), self.peer), self.session()) raise TooSlowError('Timeout while reading data from the network (%s)' % errstr(exc)) except socket.error as exc: if exc.args[0] in error.block: message = '%s %s blocking io problem mid-way through reading a message %s, trying to complete' % ( self.name(), self.peer, errstr(exc), ) if message != reported: reported = message log.debug(message, self.session()) yield b'' elif exc.args[0] in error.fatal: self.close() raise LostConnection('issue reading on the socket: %s' % errstr(exc)) # what error could it be ! else: log.critical('%s %s undefined error reading on socket' % (self.name(), self.peer), self.session()) raise NetworkError('Problem while reading data from the network (%s)' % errstr(exc))
def cmdline(cmdarg): env = getenv() # Must be done before setting the logger as it modify its behaviour if cmdarg.verbose: env.log.all = True env.log.level = 'DEBUG' if cmdarg.pdb: env.debug.pdb = True log.init(env) trace_interceptor(env.debug.pdb) if cmdarg.verbose: env.log.parser = True for configuration in cmdarg.configuration: log.info(f'loading {configuration}', 'configuration') location = getconf(configuration) if not location: log.critical(f'{configuration} is not an exabgp config file', 'configuration') sys.exit(1) config = Configuration([location]) if not config.reload(): log.critical(f'{configuration} is not a valid config file', 'configuration') sys.exit(1) log.info('\u2713 loading', 'configuration') if cmdarg.neighbor: log.warning('checking neighbors', 'configuration') for name, neighbor in config.neighbors.items(): reparsed = neighbor.string() log.debug(reparsed, configuration) log.info(f'\u2713 neighbor {name.split()[1]}', 'configuration') if cmdarg.route: log.warning('checking routes', 'configuration') if not check_generation(config.neighbors): log.critical(f'{configuration} has an invalid route', 'configuration') sys.exit(1) log.info('\u2713 routes', 'configuration')
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 self.Exit.listening if not self.reload(): return self.Exit.configuration if validate: # only validate configuration log.warning('', 'configuration') log.warning('parsed Neighbors, un-templated', 'configuration') log.warning('------------------------------', 'configuration') log.warning('', 'configuration') for key in self._peers: log.warning(str(self._peers[key].neighbor), 'configuration') log.warning('', 'configuration') return self.Exit.validate 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 self.Exit.listening if not self.early_drop: self.processes.start(self.configuration.processes) if not self.daemon.drop_privileges(): log.critical( 'could not drop privileges to \'%s\' refusing to run as root' % self.daemon.user, 'reactor') log.critical( 'set the environmemnt value exabgp.daemon.user to change the unprivileged user', 'reactor') return self.Exit.privileges 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 log.restart(): log.critical('could not setup the logger, aborting', 'reactor') return self.Exit.log if not self.daemon.savepid(): return self.Exit.pid wait = getenv().tcp.delay if wait: sleeptime = (wait * 60) - int(time.time()) % (wait * 60) log.debug('waiting for %d seconds before connecting' % sleeptime, 'reactor') time.sleep(float(sleeptime)) workers = {} peers = set() api_fds = [] ms_sleep = int(self._sleep_time * 1000) while True: try: if self.signal.received: signaled = self.signal.received # report that we received a signal 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) self.signal.rearm() # we always want to exit if signaled == Signal.SHUTDOWN: self.exit_code = self.Exit.normal self.shutdown() break # it does mot matter what we did if we are restarting # as the peers and network stack are replaced by new ones if signaled == Signal.RESTART: self.restart() continue # did we complete the run of updates caused by the last SIGUSR1/SIGUSR2 ? if self._pending_adjribout(): continue if signaled == Signal.RELOAD: self.reload() self.processes.start(self.configuration.processes, False) continue if signaled == Signal.FULL_RELOAD: self.reload() self.processes.start(self.configuration.processes, True) continue if self.listener.incoming(): # check all incoming connection self.asynchronous.schedule( str(uuid.uuid1()), 'checking for new connection(s)', self.listener.new_connections()) sleep = ms_sleep # do not attempt to listen on closed sockets even if the peer is still here for io in list(workers.keys()): if io == -1: self._poller.unregister(io) del workers[io] peers = self.active_peers() # give a turn to all the peers for key in list(peers): peer = self._peers[key] # limit the number of message handling per second if self._rate_limited(key, peer.neighbor.rate_limit): peers.discard(key) continue # handle the peer 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: io = peer.socket() if io != -1: self._poller.register( io, select.POLLIN | select.POLLPRI | select.POLLHUP | select.POLLNVAL | select.POLLERR) workers[io] = key # no need to come back to it before a a full cycle peers.discard(key) elif action == ACTION.NOW: sleep = 0 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) sleep = 0 self.asynchronous.run() if api_fds != self.processes.fds: for fd in api_fds: if fd == -1: continue if fd not in self.processes.fds: self._poller.unregister(fd) for fd in self.processes.fds: if fd == -1: continue if fd not in api_fds: self._poller.register( fd, select.POLLIN | select.POLLPRI | select.POLLHUP | select.POLLNVAL | select.POLLERR) api_fds = self.processes.fds for io in self._wait_for_io(sleep): if io not in api_fds: peers.add(workers[io]) if self._stopping and not self._peers.keys(): self._termination('exiting on peer termination', self.Exit.normal) except KeyboardInterrupt: self._termination('^C received', self.Exit.normal) except SystemExit: self._termination('exiting', self.Exit.normal) # socket.error is a subclass of IOError (so catch it first) except socket.error: self._termination('socket error received', self.Exit.socket) except IOError: self._termination( 'I/O Error received, most likely ^C during IO', self.Exit.io_error) except ProcessError: self._termination( 'Problem when sending message(s) to helper program, stopping', self.Exit.process) except select.error: self._termination('problem using select, stopping', self.Exit.select) return self.exit_code
def __del__(self): if self.io: self.close() log.warning('connection to %s closed' % self.peer, self.session())
def run(comment, configurations, validate, pid=0): env = getenv() log.notice('Thank you for using ExaBGP', 'welcome') log.notice('%s' % version, 'version') log.notice('%s' % sys.version.replace('\n', ' '), 'interpreter') log.notice('%s' % ' '.join(platform.uname()[:5]), 'os') log.notice('%s' % ROOT, 'installation') if comment: log.notice(comment, 'advice') warning = warn() if warning: log.warning(warning, 'advice') if env.api.cli: pipename = 'exabgp' if env.api.pipename is None else env.api.pipename pipes = named_pipe(ROOT, pipename) if len(pipes) != 1: env.api.cli = False log.error( 'could not find the named pipes (%s.in and %s.out) required for the cli' % (pipename, pipename), 'cli' ) log.error('we scanned the following folders (the number is your PID):', 'cli') for location in pipes: log.error(' - %s' % location, 'cli control') log.error('please make them in one of the folder with the following commands:', 'cli control') log.error('> mkfifo %s/run/%s.{in,out}' % (os.getcwd(), pipename), 'cli control') log.error('> chmod 600 %s/run/%s.{in,out}' % (os.getcwd(), pipename), 'cli control') if os.getuid() != 0: log.error( '> chown %d:%d %s/run/%s.{in,out}' % (os.getuid(), os.getgid(), os.getcwd(), pipename), 'cli control', ) else: pipe = pipes[0] os.environ['exabgp_cli_pipe'] = pipe os.environ['exabgp_api_pipename'] = pipename log.info('named pipes for the cli are:', 'cli control') log.info('to send commands %s%s.in' % (pipe, pipename), 'cli control') log.info('to read responses %s%s.out' % (pipe, pipename), 'cli control') if not env.profile.enable: exit_code = Reactor(configurations).run(validate, ROOT) __exit(env.debug.memory, exit_code) 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)) exit_code = profile.run(profiled) __exit(env.debug.memory, exit_code) 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() log.debug('profiling ....', 'reactor') profiler = profile.Profile() profiler.enable() try: exit_code = Reactor(configurations).run(validate, ROOT) except Exception: exit_code = Reactor.Exit.unknown raise finally: from exabgp.vendoring import lsprofcalltree 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 log.debug("-" * len(notice), 'reactor') log.debug(notice, 'reactor') log.debug("-" * len(notice), 'reactor') __exit(env.debug.memory, exit_code) else: log.debug("-" * len(notice), 'reactor') log.debug(notice, 'reactor') log.debug("-" * len(notice), 'reactor') Reactor(configurations).run(validate, ROOT) __exit(env.debug.memory, 1)