def check_nlri(neighbor, routes): option.enabled['parser'] = True announced = _hexa(routes) negotiated = _negotiated(neighbor) afi, safi = neighbor.families()[0] # Is the peer going to send us some Path Information with the route (AddPath) addpath = negotiated.addpath.send(afi, safi) nlris = [] try: while announced: log.debug('parsing NLRI %s' % announced, 'parser') nlri, announced = NLRI.unpack_nlri(afi, safi, announced, IN.ANNOUNCED, addpath) nlris.append(nlri) except Exception as exc: log.error('could not parse the nlri', 'parser') from exabgp.debug import string_exception log.error(string_exception(exc), 'parser') if getenv().debug.pdb: raise return False log.debug('', 'parser') # new line for nlri in nlris: log.info('nlri json %s' % nlri.json(), 'parser') return True
def run(self): if self.reactor.processes.broken(self.neighbor): # XXX: we should perhaps try to restart the process ?? log.error('ExaBGP lost the helper process for this peer - stopping', 'process') if self.reactor.processes.terminate_on_error: self.reactor.api_shutdown() else: self.stop() return True if self.generator: try: # This generator only stops when it raises # otherwise return one of the ACTION return next(self.generator) except StopIteration: # Trying to run a closed loop, no point continuing self.generator = None if self._restart: return ACTION.LATER return ACTION.CLOSE elif self.generator is None: if self.fsm in [FSM.OPENCONFIRM, FSM.ESTABLISHED]: log.debug('stopping, other connection is established', self.id()) self.generator = False return ACTION.LATER if self._delay.backoff(): return ACTION.LATER if self._restart: log.debug('initialising connection to %s' % self.id(), 'reactor') self.generator = self._run() return ACTION.LATER # make sure we go through a clean loop return ACTION.CLOSE
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 close(self, reason='protocol closed, reason unspecified'): if self.connection: log.debug(reason, self.connection.session()) self.peer.stats['down'] = self.peer.stats.get('down', 0) + 1 self.connection.close() self.connection = None
def _enter(self, name): location = self.tokeniser.iterate() log.debug("> %-16s | %s" % (location, self.tokeniser.params()), 'configuration') if location not in self._structure[name]['sections']: return self.error.set('section %s is invalid in %s, %s' % (location, name, self.scope.location())) self.scope.enter(location) self.scope.to_context() class_name = self._structure[name]['sections'][location] instance = self._structure[class_name].get('class', None) if not instance: raise RuntimeError('This should not be happening, debug time !') if not instance.pre(): return False if not self.dispatch(self._structure[name]['sections'][location]): return False if not instance.post(): return False left = self.scope.leave() if not left: return self.error.set('closing too many parenthesis') self.scope.to_context() log.debug("< %-16s | %s" % (left, self.tokeniser.params()), 'configuration') return True
def listen_on(self, local_addr, remote_addr, port, md5_password, md5_base64, ttl_in): try: if not remote_addr: remote_addr = IP.create( '0.0.0.0') if local_addr.ipv4() else IP.create('::') self._listen(local_addr, remote_addr, port, md5_password, md5_base64, ttl_in) log.debug( 'listening for BGP session(s) on %s:%d%s' % (local_addr, port, ' with MD5' if md5_password else ''), 'network', ) return True except NetworkError as exc: if os.geteuid() != 0 and port <= 1024: log.critical( 'can not bind to %s:%d, you may need to run ExaBGP as root' % (local_addr, port), 'network') else: log.critical( 'can not bind to %s:%d (%s)' % (local_addr, port, str(exc)), 'network') log.critical( 'unset exabgp.tcp.bind if you do not want listen for incoming connections', 'network') log.critical( 'and check that no other daemon is already binding to port %d' % port, 'network') return False
def unpack_nlri(cls, afi, safi, data, action, addpath): a, s = AFI.create(afi), SAFI.create(safi) log.debug(LazyNLRI(a, s, addpath, data), 'parser') key = '%s/%s' % (a, s) if key in cls.registered_nlri: return cls.registered_nlri[key].unpack_nlri(a, s, data, action, addpath) raise Notify(3, 0, 'trying to decode unknown family %s/%s' % (a, s))
def _terminate(self, process_name): log.debug('terminating process %s' % process_name, 'process') process = self._process[process_name] del self._process[process_name] self._update_fds() thread = Thread(target=self._terminate_run, args=(process, )) thread.start() return thread
def _run(self, name): command = self.tokeniser.iterate() log.debug(". %-16s | %s" % (command, self.tokeniser.params()), 'configuration') if not self.run(name, command): return False return True
def new_notification(self, notification): for _ in self.write(notification): yield _NOP log.debug( '>> NOTIFICATION (%d,%d,"%s")' % (notification.code, notification.subcode, notification.data.decode('utf-8')), self.connection.session(), ) yield notification
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 new_keepalive(self, comment=''): keepalive = KeepAlive() for _ in self.write(keepalive): yield _NOP log.debug('>> KEEPALIVE%s' % (' (%s)' % comment if comment else ''), self.connection.session()) yield keepalive
def _handle_problem(self, process): if process not in self._process: return if self.respawn_number and self._restart[process]: log.debug('process %s ended, restarting it' % process, 'process') self._terminate(process) self._start(process) else: log.debug('process %s ended' % process, 'process') self._terminate(process)
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 new_update(self, include_withdraw): updates = self.neighbor.rib.outgoing.updates(self.neighbor['group-updates']) number = 0 for update in updates: for message in update.messages(self.negotiated, include_withdraw): number += 1 for boolean in self.send(message): # boolean is a transient network error we already announced yield _NOP if number: log.debug('>> %d UPDATE(s)' % number, self.connection.session()) yield _UPDATE
def read_open(self, ip): for received_open in self.read_message(): if received_open.TYPE == NOP.TYPE: yield received_open else: break if received_open.TYPE != Open.TYPE: raise Notify(5, 1, 'The first packet received is not an open message (%s)' % received_open) log.debug('<< %s' % received_open, self.connection.session()) yield received_open
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: log.error("Can not remove PIDfile %s" % self.pid, 'daemon') return log.debug("Removed PIDfile %s" % self.pid, 'daemon')
def __init__(self, afi, peer, local, io): Connection.__init__(self, afi, peer, local) log.debug('connection from %s' % self.peer, 'network') try: self.io = io asynchronous(self.io, self.peer) nagle(self.io, self.peer) self.success() except NetworkError as exc: self.close() raise NotConnected(errstr(exc))
def _terminate_run(self, process, process_name): try: process.terminate() try: process.wait(timeout=2) except subprocess.TimeoutExpired: log.debug('force kill unresponsive %s' % process_name, 'process') process.kill() process.wait(timeout=1) except (OSError, KeyError, subprocess.TimeoutExpired): # the process is most likely already dead pass
def close(self, reason='protocol closed, reason unspecified'): if self.connection: log.debug(reason, self.connection.session()) # must be first otherwise we could have a loop caused by the raise in the below self.connection.close() self.connection = None self.peer.stats['down'] = self.peer.stats.get('down', 0) + 1 try: if self.peer.neighbor.api['neighbor-changes']: self.peer.reactor.processes.down(self.peer.neighbor, reason) except ProcessError: log.debug('could not send notification of neighbor close to API', self.connection.session())
def check_ka_timer(self, message=_NOP, ignore=_NOP.TYPE): if self.holdtime == 0: return message.TYPE != KeepAlive.TYPE now = int(time.time()) if message.TYPE != ignore: self.last_read = now elapsed = now - self.last_read if elapsed > self.holdtime: raise Notify(self.code, self.subcode, self.message) if self.last_print != now: left = self.holdtime - elapsed log.debug('receive-timer %d second(s) left' % left, source='ka-' + self.session()) self.last_print = now return True
def restart(self): """Kill the BGP session and restart it""" log.info('performing restart of exabgp %s' % version, 'reactor') # XXX: FIXME: Could return False, in case there is interference with old config... reloaded = self.configuration.reload() for key in self._peers.keys(): if key not in self.configuration.neighbors.keys(): peer = self._peers[key] log.debug('removing peer %s' % peer.neighbor.name(), 'reactor') self._peers[key].remove() else: self._peers[key].reestablish() self.processes.start(self.configuration.processes, True)
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 = syslog.LOG_DEBUG log.init() if cmdarg.pdb: env.debug.pdb = True if cmdarg.verbose: env.log.parser = True for configuration in cmdarg.configuration: log.notice(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 = Reactor([location]).configuration if not config.reload(): log.critical(f'{configuration} is not a valid config file', 'configuration') sys.exit(1) log.info(f'\u2713 loading', 'configuration') if cmdarg.neighbor: log.notice(f'checking neighbors', 'configuration') for name, neighbor in config.neighbors.items(): reparsed = neighbor.string() for line in reparsed.split('\n'): log.debug(line, configuration) log.info(f'\u2713 neighbor {name.split()[1]}', 'configuration') if cmdarg.route: log.notice(f'checking routes', 'configuration') if not check_generation(config.neighbors): log.critical(f'{configuration} has an invalid route', 'configuration') sys.exit(1) log.info(f'\u2713 routes', 'configuration')
def need_ka(self): if not self.keepalive: return False now = int(time.time()) left = self.last_sent + self.keepalive - now if now != self.last_print: log.debug('send-timer %d second(s) left' % left, source='ka-' + self.session()) self.last_print = now if left <= 0: self.last_sent = now return True return False
def check_update(neighbor, raw): option.enabled['parser'] = True negotiated = _negotiated(neighbor) while raw: if raw.startswith(b'\xff' * 16): kind = raw[18] size = (raw[16] << 16) + raw[17] injected, raw = raw[19:size], raw[size:] if kind == 2: log.debug('the message is an update', 'parser') decoding = 'update' else: log.debug( 'the message is not an update (%d) - aborting' % kind, 'parser') return False else: log.debug('header missing, assuming this message is ONE update', 'parser') decoding = 'update' injected, raw = raw, '' try: # This does not take the BGP header - let's assume we will not break that :) update = Update.unpack_message(injected, Direction.IN, negotiated) except Notify: import traceback log.error('could not parse the message', 'parser') log.error(traceback.format_exc(), 'parser') if getenv().debug.pdb: raise return False except Exception: import traceback log.error('could not parse the message', 'parser') log.error(traceback.format_exc(), 'parser') if getenv().debug.pdb: raise return False log.debug('', 'parser') # new line for number in range(len(update.nlris)): change = Change(update.nlris[number], update.attributes) log.info( 'decoded %s %s %s' % (decoding, change.nlri.action, change.extensive()), 'parser') log.info( 'update json %s' % Response.JSON(json_version).update( neighbor, 'in', update, None, '', ''), 'parser') return True
def terminate(self): for process in list(self._process): if not self.silence: try: self.write(process, self._encoder[process].shutdown()) except ProcessError: pass self.silence = True # waiting a little to make sure IO is flushed to the pipes # we are using unbuffered IO but still .. time.sleep(0.1) for process in list(self._process): try: t = self._terminate(process) t.join() except OSError: # we most likely received a SIGTERM signal and our child is already dead log.debug('child process %s was already dead' % process, 'process') self.clean()
def partial(self, section, text, action='announce'): self._cleanup( ) # this perform a big cleanup (may be able to be smarter) self._clear() self.tokeniser.set_api( text if text.endswith(';') or text.endswith('}') else text + ' ;') self.tokeniser.set_action(action) if self.parseSection(section) is not True: self._rollback_reload() log.debug( "\n" "syntax error in api command %s\n" "line %d: %s\n" "\n%s" % (self.scope.location(), self.tokeniser.number, ' '.join(self.tokeniser.line), str(self.error)), 'configuration', ) return False return True
def handle_connection(self, connection): log.debug("state machine for the peer is %s" % self.fsm.name(), self.id()) # if the other side fails, we go back to idle if self.fsm == FSM.ESTABLISHED: log.debug( 'we already have a peer in state established for %s' % connection.name(), self.id()) return connection.notification( 6, 7, 'could not accept the connection, already established') # 6.8 The convention is to compare the BGP Identifiers of the peers # involved in the collision and to retain only the connection initiated # by the BGP speaker with the higher-valued BGP Identifier. # FSM.IDLE , FSM.ACTIVE , FSM.CONNECT , FSM.OPENSENT , FSM.OPENCONFIRM , FSM.ESTABLISHED if self.fsm == FSM.OPENCONFIRM: # We cheat: we are not really reading the OPEN, we use the data we have instead # it does not matter as the open message will be the same anyway local_id = self.neighbor.router_id.pack() remote_id = self.proto.negotiated.received_open.router_id.pack() if remote_id < local_id: log.debug( 'closing incoming connection as we have an outgoing connection with higher router-id for %s' % connection.name(), self.id(), ) return connection.notification( 6, 7, 'could not accept the connection, as another connection is already in open-confirm and will go through', ) # accept the connection if self.proto: log.debug( 'closing outgoing connection as we have another incoming on with higher router-id for %s' % connection.name(), self.id(), ) self.proto.close( 'closing outgoing connection as we have another incoming on with higher router-id' ) self.proto = Protocol(self).accept(connection) self.generator = None # Let's make sure we do some work with this connection self._delay.reset() return None
def write(self, process, string, neighbor=None): if string is None: return True # XXX: FIXME: This is potentially blocking while True: try: self._process[process].stdin.write( bytes('%s\n' % string, 'ascii')) except IOError as exc: self._broken.append(process) if exc.errno == errno.EPIPE: self._broken.append(process) log.debug('issue while sending data to our helper program', 'process') raise ProcessError() else: # Could it have been caused by a signal ? What to do. log.debug( 'error received while sending data to helper program, retrying (%s)' % errstr(exc), 'process') continue break try: self._process[process].stdin.flush() except IOError as exc: # AFAIK, the buffer should be flushed at the next attempt. log.debug( 'error received while FLUSHING data to helper program, retrying (%s)' % errstr(exc), 'process') return True
def establish(self): last = time.time() - 2.0 while True: notify = time.time() - last > 1.0 if notify: last = time.time() if notify: log.debug('attempting connection to %s:%d' % (self.peer, self.port), self.session()) connect_issue = self._connect() if connect_issue: if notify: log.debug('connection to %s:%d failed' % (self.peer, self.port), self.session()) log.debug(str(connect_issue), self.session()) yield False continue connected = False for r, message in ready(self.io): if not r: yield False continue connected = True if connected: self.success() if not self.local: self.local = self.io.getsockname()[0] yield True return self._setup()