class SendTimer(object): def __init__(self, session, holdtime): self.logger = Logger() self.session = session self.keepalive = holdtime.keepalive() self.last_sent = int(time.time()) self.last_print = 0 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: self.logger.debug('send-timer %d second(s) left' % left, source=self.session()) self.last_print = now if left <= 0: self.last_sent = now return True return False
def ready (io): logger = Logger() warned = False start = time.time() while True: try: _,w,_ = select.select([],[io,],[],0) if not w: if not warned and time.time()-start > 1.0: logger.debug('attempting to establish connection','network') warned = True yield False continue err = io.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) if not err: if warned: logger.warning('connection established','network') yield True return elif err in error.block: logger.warning('connect attempt failed, retrying, reason %s' % errno.errorcode[err],'network') yield False else: yield False return except select.error: yield False return
def ready(io): logger = Logger() warned = False start = time.time() while True: try: _, w, _ = select.select([], [ io, ], [], 0) if not w: if not warned and time.time() - start > 1.0: logger.debug('attempting to establish connection', 'network') warned = True yield False continue err = io.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) if not err: if warned: logger.warning('connection established', 'network') yield True return elif err in error.block: logger.warning( 'connect attempt failed, retrying, reason %s' % errno.errorcode[err], 'network') yield False else: yield False return except select.error: yield False return
class ReceiveTimer (object): def __init__ (self, session, holdtime, code, subcode, message=''): self.logger = Logger() self.session = session self.holdtime = holdtime self.last_print = time.time() self.last_read = time.time() self.code = code self.subcode = subcode self.message = message self.single = False def check_ka_timer (self, message=_NOP,ignore=_NOP.TYPE): if message.TYPE != ignore: self.last_read = time.time() if self.holdtime: left = int(self.last_read + self.holdtime - time.time()) if self.last_print != left: self.logger.debug('receive-timer %d second(s) left' % left,source='ka-'+self.session()) self.last_print = left if left <= 0: raise Notify(self.code,self.subcode,self.message) return message.TYPE != KeepAlive.TYPE return False def check_ka (self, message=_NOP,ignore=_NOP.TYPE): if self.check_ka_timer(message,ignore): return if self.single: raise Notify(2,6,'Negotiated holdtime was zero, it was invalid to send us a keepalive messages') self.single = True
class ReceiveTimer(object): def __init__(self, session, holdtime, code, subcode, message=''): self.logger = Logger() self.session = session self.holdtime = holdtime self.last_read = time.time() self.last_print = 0 self.code = code self.subcode = subcode self.message = message def check_ka(self, message=_NOP, ignore=_NOP.TYPE): if message.TYPE != ignore: self.last_read = time.time() if self.holdtime: left = int(self.last_read + self.holdtime - time.time()) if self.last_print != left: self.logger.debug('receive-timer %d second(s) left' % left, source=self.session()) self.last_print = left if left <= 0: raise Notify(self.code, self.subcode, self.message) elif message.TYPE == KeepAlive.TYPE: raise Notify( 2, 6, 'Negotiated holdtime was zero, it was invalid to send us a keepalive messages' )
class ASYNC(object): LIMIT = 50 def __init__(self): self.logger = Logger() self._async = deque() def ready(self): return not self._async def schedule(self, uid, command, callback): self.logger.debug('async | %s | %s' % (uid, command), 'reactor') self._async.append((uid, callback)) def clear(self, deluid=None): if not self._async: return if deluid is None: # We could delete all the generators just to be safe self._async = deque() return running = deque() for (uid, generator) in self._async: if uid != deluid: running.append((uid, generator)) self._async = running def run(self): if not self._async: return False # length = range(min(len(self._async),self.LIMIT)) length = range(self.LIMIT) uid, generator = self._async.popleft() for _ in length: try: six.next(generator) except KeyboardInterrupt: raise except StopIteration: if not self._async: return False uid, generator = self._async.popleft() except Exception as exc: self.logger.error('async | %s | problem with function' % uid, 'reactor') for line in str(exc).split('\n'): self.logger.error('async | %s | %s' % (uid, line), 'reactor') self._async.appendleft((uid, generator)) return True
def ready(io): logger = Logger() warned = False start = time.time() poller = select.poll() poller.register(io, select.POLLOUT | select.POLLNVAL | select.POLLERR) while True: try: found = False pulled = poller.poll(0) if not pulled: if not warned and time.time() - start > 1.0: logger.debug('attempting to establish connection', 'network') warned = True yield False continue for _, event in pulled: if event & select.POLLOUT: found = True elif event & select.POLLHUP: raise KeyboardInterrupt() elif event & select.POLLERR or event & select.POLLNVAL: logger.warning( 'connect attempt failed, issue with reading on the network, retrying', 'network') yield found if not found: continue err = io.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) if not err: if warned: logger.warning('connection established', 'network') yield True return elif err in error.block: logger.warning( 'connect attempt failed, retrying, reason %s' % errno.errorcode[err], 'network') yield False return else: yield False return except select.error: yield False return
class ASYNC (object): def __init__ (self): self.logger = Logger() self._async = [] def ready (self): return len(self._async) > 0 def schedule (self, uid, command, callback): self.logger.debug('async | %s | %s' % (uid,command),'reactor') if self._async: self._async[0].append((uid,callback)) else: self._async.append([(uid,callback),]) def clear (self, deluid=None): if not self._async: return if deluid is None: self._async = [] return running = [] for (uid,generator) in self._async[0]: if uid != deluid: running.append((uid,generator)) self._async.pop() if running: self._async.append(running) def run (self): if not self._async: return False running = [] for (uid,generator) in self._async[0]: try: six.next(generator) six.next(generator) running.append((uid,generator)) except StopIteration: pass except KeyboardInterrupt: raise except Exception as exc: self.logger.error('async | %s | problem with function' % uid,'reactor') for line in str(exc).split('\n'): self.logger.error('async | %s | %s' % (uid,line),'reactor') self._async.pop() if running: self._async.append(running) return True
class ASYNC (object): LIMIT = 500 def __init__ (self): self.logger = Logger() self._async = deque() def ready (self): return len(self._async) > 0 def schedule (self, uid, command, callback): self.logger.debug('async | %s | %s' % (uid,command),'reactor') self._async.append((uid,callback)) def clear (self, deluid=None): if not self._async: return if deluid is None: # We could delete all the generators just to be safe self._async = deque() return running = deque() for (uid,generator) in self._async: if uid != deluid: running.append((uid,generator)) self._async = running def run (self): if not self.ready(): return False length = range(min(len(self._async),self.LIMIT)) uid, generator = self._async.popleft() for _ in length: try: six.next(generator) six.next(generator) except StopIteration: if not self._async: return False uid, generator = self._async.popleft() except KeyboardInterrupt: raise except Exception as exc: self.logger.error('async | %s | problem with function' % uid,'reactor') for line in str(exc).split('\n'): self.logger.error('async | %s | %s' % (uid,line),'reactor') self._async.appendleft((uid, generator)) return True
class ASYNC(object): def __init__(self): self.logger = Logger() self._async = [] def ready(self): return len(self._async) > 0 def schedule(self, uid, command, callback): self.logger.debug('async | %s' % command, 'reactor') if self._async: self._async[0].append((uid, callback)) else: self._async.append([ (uid, callback), ]) def clear(self, deluid=None): if not self._async: return if deluid is None: self._async = [] return running = [] for (uid, generator) in self._async[0]: if uid != deluid: running.append((uid, generator)) self._async.pop() if running: self._async.append(running) def run(self): if not self._async: return False running = [] for (uid, generator) in self._async[0]: try: six.next(generator) six.next(generator) running.append((uid, generator)) except StopIteration: pass self._async.pop() if running: self._async.append(running) return True
class ReceiveTimer(object): def __init__(self, session, holdtime, code, subcode, message=''): self.logger = Logger() self.session = session self.holdtime = holdtime self.last_print = 0 self.last_read = int(time.time()) self.code = code self.subcode = subcode self.message = message self.single = False 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 self.logger.debug('receive-timer %d second(s) left' % left, source='ka-' + self.session()) self.last_print = now return True def check_ka(self, message=_NOP, ignore=_NOP.TYPE): if self.check_ka_timer(message, ignore): return if self.single: raise Notify( 2, 6, 'Negotiated holdtime was zero, it was invalid to send us a keepalive messages' ) self.single = True
class SendTimer (object): def __init__ (self, session, holdtime): self.logger = Logger() self.session = session self.keepalive = holdtime.keepalive() self.last_print = int(time.time()) self.last_sent = int(time.time()) 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: self.logger.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 run(env, comment, configurations, root, validate, pid=0): logger = Logger() logger.notice('Thank you for using ExaBGP', 'welcome') logger.notice('%s' % version, 'version') logger.notice('%s' % sys.version.replace('\n', ' '), 'interpreter') logger.notice('%s' % ' '.join(platform.uname()[:5]), 'os') logger.notice('%s' % root, 'installation') if comment: logger.notice(comment, 'advice') warning = warn() if warning: logger.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 logger.error( 'could not find the named pipes (%s.in and %s.out) required for the cli' % (pipename, pipename), 'cli') logger.error( 'we scanned the following folders (the number is your PID):', 'cli') for location in pipes: logger.error(' - %s' % location, 'cli control') logger.error( 'please make them in one of the folder with the following commands:', 'cli control') logger.error( '> mkfifo %s/run/%s.{in,out}' % (os.getcwd(), pipename), 'cli control') logger.error( '> chmod 600 %s/run/%s.{in,out}' % (os.getcwd(), pipename), 'cli control') if os.getuid() != 0: logger.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 logger.info('named pipes for the cli are:', 'cli control') logger.info('to send commands %s%s.in' % (pipe, pipename), 'cli control') logger.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() logger.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: 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.debug("-" * len(notice), 'reactor') logger.debug(notice, 'reactor') logger.debug("-" * len(notice), 'reactor') __exit(env.debug.memory, exit_code) else: logger.debug("-" * len(notice), 'reactor') logger.debug(notice, 'reactor') logger.debug("-" * len(notice), 'reactor') Reactor(configurations).run(validate, root) __exit(env.debug.memory, 1)
def main(): major = int(sys.version[0]) minor = int(sys.version[2]) if major <= 2 and minor < 5: sys.stdout.write( 'This program can not work (is not tested) with your python version (< 2.5)\n' ) sys.stdout.flush() sys.exit(1) cli_named_pipe = os.environ.get('exabgp_cli_pipe', '') if cli_named_pipe: from exabgp.application.control import main as control control(cli_named_pipe) sys.exit(0) options = docopt.docopt(usage, help=False) 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: sys.stdout.write(usage) sys.stdout.flush() sys.exit(0) return root = root_folder(options, [ '/bin/exabgp', '/sbin/exabgp', '/lib/exabgp/application/bgp.py', '/lib/exabgp/application/control.py' ]) prefix = '' if root == '/usr' else root etc = prefix + '/etc/exabgp' os.environ['EXABGP_ETC'] = etc # This is not most pretty if options["--version"]: sys.stdout.write('ExaBGP : %s\n' % version) sys.stdout.write('Python : %s\n' % sys.version.replace('\n', ' ')) sys.stdout.write('Uname : %s\n' % ' '.join(platform.uname()[:5])) sys.stdout.write('Root : %s\n' % root) sys.stdout.flush() sys.exit(0) envfile = get_envfile(options, etc) env = get_env(envfile) # 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() from exabgp.configuration.setup import environment if options["--decode"]: decode = ''.join(options["--decode"]).replace(':', '').replace(' ', '') if not is_bgp(decode): sys.stdout.write(usage) sys.stdout.write('Environment values are:\n%s\n\n' % '\n'.join(' - %s' % _ for _ in environment.default())) sys.stdout.write( 'The BGP message must be an hexadecimal string.\n\n') sys.stdout.write( 'All colons or spaces are ignored, for example:\n\n') sys.stdout.write(' --decode 001E0200000007900F0003000101\n') sys.stdout.write( ' --decode 001E:02:0000:0007:900F:0003:0001:01\n') sys.stdout.write( ' --decode FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF001E0200000007900F0003000101\n' ) sys.stdout.write( ' --decode FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:001E:02:0000:0007:900F:0003:0001:01\n' ) sys.stdout.write( ' --decode \'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF 001E02 00000007900F0003000101\n\'' ) sys.stdout.flush() sys.exit(1) else: decode = '' 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"]: sys.stdout.write(usage) sys.stdout.write('Environment values are:\n' + '\n'.join(' - %s' % _ for _ in environment.default())) sys.stdout.flush() 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(): sys.stdout.write('%s\n' % line) sys.stdout.flush() sys.exit(0) if options["--full-env"] or options["--fe"]: print() for line in environment.iter_env(): sys.stdout.write('%s\n' % line) sys.stdout.flush() sys.exit(0) if options["--diff-ini"] or options["--di"]: for line in environment.iter_ini(True): sys.stdout.write('%s\n' % line) sys.stdout.flush() sys.exit(0) if options["--diff-env"] or options["--de"]: for line in environment.iter_env(True): sys.stdout.write('%s\n' % line) sys.stdout.flush() 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(etc, f[11:]) if os.path.isfile(normalised): configurations.append(normalised) continue logger.debug( 'one of the arguments passed as configuration is not a file (%s)' % f, 'configuration') sys.exit(1) else: sys.stdout.write(usage) sys.stdout.write('Environment values are:\n%s\n\n' % '\n'.join(' - %s' % _ for _ in environment.default())) sys.stdout.write('no configuration file provided') sys.stdout.flush() 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, root, options["--validate"]) if not (env.log.destination in ('syslog', 'stdout', 'stderr') or env.log.destination.startswith('host:')): logger.error( 'can not log to files when running multiple configuration (as we fork)', 'configuration') 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], root, options["--validate"], 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.critical( 'can not fork, errno %d : %s' % (exc.errno, exc.strerror), 'reactor') sys.exit(1)
def check_update(neighbor, raw): logger = Logger() logger._option['parser'] = True logger.debug('\ndecoding routes in configuration', 'parser') neighbor = neighbor[list(neighbor)[0]] path = {} for f in NLRI.known_families(): if neighbor.add_path: path[f] = neighbor.add_path capa = Capabilities().new(neighbor, False) capa[Capability.CODE.ADD_PATH] = path capa[Capability.CODE.MULTIPROTOCOL] = neighbor.families() # capa[Capability.CODE.FOUR_BYTES_ASN] = True routerid_1 = str(neighbor.router_id) routerid_2 = '.'.join( str((int(_) + 1) % 250) for _ in str(neighbor.router_id).split('.', -1)) o1 = Open(Version(4), ASN(neighbor.local_as), HoldTime(180), RouterID(routerid_1), capa) o2 = Open(Version(4), ASN(neighbor.peer_as), HoldTime(180), RouterID(routerid_2), capa) negotiated = Negotiated(neighbor) negotiated.sent(o1) negotiated.received(o2) # grouped = False while raw: if raw.startswith(b'\xff' * 16): kind = ordinal(raw[18]) size = (ordinal(raw[16]) << 16) + (ordinal(raw[17])) injected, raw = raw[19:size], raw[size:] if kind == 2: logger.debug('the message is an update', 'parser') decoding = 'update' else: logger.debug( 'the message is not an update (%d) - aborting' % kind, 'parser') return False else: logger.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, negotiated) except KeyboardInterrupt: raise except Notify: logger.error('could not parse the message', 'parser') logger.error(traceback.format_exc(), 'parser') return False except StandardError: logger.error('could not parse the message', 'parser') logger.error(traceback.format_exc(), 'parser') return False logger.debug('', 'parser') # new line for number in range(len(update.nlris)): change = Change(update.nlris[number], update.attributes) logger.info( 'decoded %s %s %s' % (decoding, change.nlri.action, change.extensive()), 'parser') logger.info( 'update json %s' % Response.JSON(json_version).update( neighbor, 'in', update, None, '', ''), 'parser') return True
class Reactor(object): class Exit(object): normal = 0 validate = 0 listening = 1 configuration = 1 privileges = 1 log = 1 pid = 1 socket = 1 io_error = 1 process = 1 select = 1 unknown = 1 # [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.exit_code = self.Exit.unknown self.max_loop_time = environment.settings().reactor.speed self._sleep_time = self.max_loop_time / 100 self._busyspin = {} self._ratelimit = {} self.early_drop = environment.settings().daemon.drop self.processes = None self.configuration = Configuration(configurations) self.logger = Logger() self.asynchronous = 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 self._poller = select.poll() def _termination(self, reason, exit_code): self.exit_code = exit_code self.signal.received = Signal.SHUTDOWN self.logger.critical(reason, 'reactor') def _prevent_spin(self): second = int(time.time()) if not second in self._busyspin: self._busyspin = {second: 0} self._busyspin[second] += 1 if self._busyspin[second] > self.max_loop_time: time.sleep(self._sleep_time) return True return False def _rate_limited(self, peer, rate): if rate <= 0: return False second = int(time.time()) ratelimit = self._ratelimit.get(peer, {}) if not second in ratelimit: self._ratelimit[peer] = {second: rate - 1} return False if self._ratelimit[peer][second] > 0: self._ratelimit[peer][second] -= 1 return False return True def _wait_for_io(self, sleeptime): spin_prevention = False try: for fd, event in self._poller.poll(sleeptime): if event & select.POLLIN or event & select.POLLPRI: yield fd continue elif event & select.POLLHUP or event & select.POLLERR or event & select.POLLNVAL: spin_prevention = True continue if spin_prevention: self._prevent_spin() except KeyboardInterrupt: self._termination('^C received', self.Exit.normal) return except Exception: self._prevent_spin() return # peer related functions 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 established_peers(self): peers = set() for key, peer in self._peers.items(): if peer.fsm == FSM.ESTABLISHED: peers.add(key) return peers def peers(self): return list(self._peers) def handle_connection(self, peer_name, connection): peer = self._peers.get(peer_name, None) if not peer: self.logger.critical('could not find referenced peer', 'reactor') return peer.handle_connection(connection) def neighbor(self, peer_name): peer = self._peers.get(peer_name, None) if not peer: self.logger.critical('could not find referenced peer', 'reactor') return return peer.neighbor def neighbor_name(self, peer_name): peer = self._peers.get(peer_name, None) if not peer: self.logger.critical('could not find referenced peer', 'reactor') return "" return peer.neighbor.name() def neighbor_ip(self, peer_name): peer = self._peers.get(peer_name, None) if not peer: self.logger.critical('could not find referenced peer', 'reactor') return "" return str(peer.neighbor.peer_address) def neighbor_cli_data(self, peer_name): peer = self._peers.get(peer_name, None) if not peer: self.logger.critical('could not find referenced peer', 'reactor') return "" return peer.cli_data() def neighor_rib(self, peer_name, rib_name, advertised=False): peer = self._peers.get(peer_name, None) if not peer: self.logger.critical('could not find referenced peer', 'reactor') return [] families = None if advertised: families = peer.proto.negotiated.families if peer.proto else [] rib = peer.neighbor.rib.outgoing if rib_name == 'out' else peer.neighbor.rib.incoming return list(rib.cached_changes(families)) def neighbor_rib_resend(self, peer_name): peer = self._peers.get(peer_name, None) if not peer: self.logger.critical('could not find referenced peer', 'reactor') return peer.neighbor.rib.outgoing.resend(None, peer.neighbor.route_refresh) def neighbor_rib_out_withdraw(self, peer_name): peer = self._peers.get(peer_name, None) if not peer: self.logger.critical('could not find referenced peer', 'reactor') return peer.neighbor.rib.outgoing.withdraw(None, peer.neighbor.route_refresh) def neighbor_rib_in_clear(self, peer_name): peer = self._peers.get(peer_name, None) if not peer: self.logger.critical('could not find referenced peer', 'reactor') return peer.neighbor.rib.incoming.clear() # ... def _completed(self, peers): for peer in peers: if self._peers[peer].neighbor.rib.outgoing.pending(): return False return True 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.load(): return self.Exit.configuration if validate: # only validate configuration self.logger.warning('', 'configuration') self.logger.warning('parsed Neighbors, un-templated', 'configuration') self.logger.warning('------------------------------', 'configuration') self.logger.warning('', 'configuration') for key in self._peers: self.logger.warning(str(self._peers[key].neighbor), 'configuration') self.logger.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(): self.logger.critical( 'could not drop privileges to \'%s\' refusing to run as root' % self.daemon.user, 'reactor') self.logger.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 self.logger.restart(): self.logger.critical('could not setup the logger, aborting', 'reactor') return self.Exit.log if not self.daemon.savepid(): return self.Exit.pid # 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.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: 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.exit_code = self.Exit.normal self.shutdown() break if signaled == Signal.RESTART: self.restart() continue if not reload_completed: continue if signaled == Signal.FULL_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.asynchronous.schedule( str(uuid.uuid1()), 'checking for new connection(s)', self.listener.new_connections()) peers = self.active_peers() if self._completed(peers): reload_completed = True 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] # 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 register_peer(self, name, peer): self._peers[name] = peer def teardown_peer(self, name, code): self._peers[name].teardown(code) def shutdown(self): """Terminate all the current BGP connections""" self.logger.critical('performing shutdown', 'reactor') if self.listener: self.listener.stop() self.listener = None for key in self._peers.keys(): self._peers[key].shutdown() self.asynchronous.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.notice('performing reload of exabgp %s' % version, 'configuration') 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.error( 'not reloaded, no change found in the configuration', 'configuration') # Careful the string above is used but the QA code to check for sucess of failure # self.logger.error(str(self.configuration.error), 'configuration') return False for key, peer in self._peers.items(): if key not in self.configuration.neighbors: self.logger.debug('removing peer: %s' % peer.neighbor.name(), 'reactor') peer.remove() for key, neighbor in self.configuration.neighbors.items(): # new peer if key not in self._peers: self.logger.debug('new peer: %s' % neighbor.name(), 'reactor') peer = Peer(neighbor, self) self._peers[key] = peer # modified peer elif self._peers[key].neighbor != neighbor: self.logger.debug( 'peer definition change, establishing a new connection for %s' % str(key), 'reactor') 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.debug( 'peer definition identical, updating peer routes if required for %s' % str(key), 'reactor') 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.notice('loaded new configuration successfully', 'reactor') return True def restart(self): """Kill the BGP session and restart it""" self.logger.notice('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] self.logger.debug('removing peer %s' % peer.neighbor.name(), 'reactor') self._peers[key].remove() else: self._peers[key].reestablish() self.processes.start(self.configuration.processes, True)
class Processes (object): # how many time can a process can respawn in the time interval respawn_timemask = 0xFFFFFF - 0b111111 # '0b111111111111111111000000' (around a minute, 63 seconds) _dispatch = {} def __init__ (self): self.logger = Logger() self.clean() self.silence = False self._buffer = {} self._configuration = {} self.respawn_number = 5 if environment.settings().api.respawn else 0 self.terminate_on_error = environment.settings().api.terminate self.ack = environment.settings().api.ack def number (self): return len(self._process) def clean (self): self._process = {} self._encoder = {} self._broken = [] self._respawning = {} def _handle_problem (self, process): if self.respawn_number: self.logger.debug('issue with the process, restarting it','process') self._terminate(process) self._start(process) else: self.logger.debug('issue with the process, terminating it','process') self._terminate(process) def _terminate (self, process): self.logger.debug('terminating process %s' % process,'process') try: self._process[process].terminate() except OSError: # the process is most likely already dead pass self._process[process].wait() del self._process[process] 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: self._terminate(process) except OSError: # we most likely received a SIGTERM signal and our child is already dead self.logger.debug('child process %s was already dead' % process,'process') self.clean() def _start (self,process): try: if process in self._process: self.logger.debug('process already running','process') return if process not in self._configuration: self.logger.debug('can not start process, no configuration for it','process') return # Prevent some weird termcap data to be created at the start of the PIPE # \x1b[?1034h (no-eol) (esc) os.environ['TERM'] = 'dumb' configuration = self._configuration[process] run = configuration.get('run','') if run: api = configuration.get('encoder','') self._encoder[process] = Response.Text(text_version) if api == 'text' else Response.JSON(json_version) self._process[process] = subprocess.Popen( run, stdin=subprocess.PIPE, stdout=subprocess.PIPE, preexec_fn=preexec_helper # This flags exists for python 2.7.3 in the documentation but on on my MAC # creationflags=subprocess.CREATE_NEW_PROCESS_GROUP ) fcntl.fcntl(self._process[process].stdout.fileno(), fcntl.F_SETFL, os.O_NONBLOCK) self.logger.debug('forked process %s' % process,'process') around_now = int(time.time()) & self.respawn_timemask if process in self._respawning: if around_now in self._respawning[process]: self._respawning[process][around_now] += 1 # we are respawning too fast if self._respawning[process][around_now] > self.respawn_number: self.logger.critical( 'Too many death for %s (%d) terminating program' % (process,self.respawn_number), 'process' ) raise ProcessError() else: # reset long time since last respawn self._respawning[process] = {around_now: 1} else: # record respawing self._respawning[process] = {around_now: 1} except (subprocess.CalledProcessError,OSError,ValueError) as exc: self._broken.append(process) self.logger.debug('could not start process %s' % process,'process') self.logger.debug('reason: %s' % str(exc),'process') def start (self, configuration, restart=False): for process in list(self._process): if process not in configuration: self._terminate(process) self._configuration = configuration for process in configuration: if restart and process in list(self._process): self._terminate(process) self._start(process) def broken (self, neighbor): if self._broken: for process in self._configuration: if process in self._broken: return True return False def fds (self): return [self._process[process].stdout for process in self._process] def received (self): consumed_data = False for process in list(self._process): try: proc = self._process[process] poll = proc.poll() # proc.poll returns None if the process is still fine # -[signal], like -15, if the process was terminated if poll is not None: self._handle_problem(process) return r,_,_ = select.select([proc.stdout,],[],[],0) if r: try: # Calling next() on Linux and OSX works perfectly well # but not on OpenBSD where it always raise StopIteration # and only readline() works buf = str_ascii(proc.stdout.read(16384)) if buf == '' and poll is not None: # if proc.poll() is None then # process is fine, we received an empty line because # we're doing .readline() on a non-blocking pipe and # the process maybe has nothing to send yet self._handle_problem(process) continue raw = self._buffer.get(process,'') + buf while '\n' in raw: line,raw = raw.split('\n',1) line = line.rstrip() consumed_data = True self.logger.debug('command from process %s : %s ' % (process,line),'process') yield (process,formated(line)) self._buffer[process] = raw except IOError as exc: if not exc.errno or exc.errno in error.fatal: # if the program exits we can get an IOError with errno code zero ! self._handle_problem(process) elif exc.errno in error.block: # we often see errno.EINTR: call interrupted and # we most likely have data, we will try to read them a the next loop iteration pass else: self.logger.debug('unexpected errno received from forked process (%s)' % errstr(exc),'process') except StopIteration: if not consumed_data: self._handle_problem(process) except (subprocess.CalledProcessError,OSError,ValueError): self._handle_problem(process) 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_ascii('%s\n' % string)) except IOError as exc: self._broken.append(process) if exc.errno == errno.EPIPE: self._broken.append(process) self.logger.debug('issue while sending data to our helper program','process') raise ProcessError() else: # Could it have been caused by a signal ? What to do. self.logger.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. self.logger.debug('error received while FLUSHING data to helper program, retrying (%s)' % errstr(exc),'process') return True def answer (self, service, string, force=False): if force or self.ack: self.logger.debug('responding to %s : %s' % (service,string.replace('\n','\\n')),'process') self._write(service,string) def answer_done (self, service): self.answer(service,'done') def answer_error (self, service): self.answer(service,'error') def _notify (self, neighbor, event): for process in neighbor.api[event]: yield process # do not do anything if silenced # no-self-argument def silenced (function): def closure (self, *args): if self.silence: return return function(self,*args) return closure # invalid-name @silenced def up (self, neighbor): for process in self._notify(neighbor,'neighbor-changes'): self._write(process,self._encoder[process].up(neighbor),neighbor) @silenced def connected (self, neighbor): for process in self._notify(neighbor,'neighbor-changes'): self._write(process,self._encoder[process].connected(neighbor),neighbor) @silenced def down (self, neighbor, reason): for process in self._notify(neighbor,'neighbor-changes'): self._write(process,self._encoder[process].down(neighbor,reason),neighbor) @silenced def negotiated (self, neighbor, negotiated): for process in self._notify(neighbor,'negotiated'): self._write(process,self._encoder[process].negotiated(neighbor,negotiated),neighbor) @silenced def fsm (self, neighbor, fsm): for process in self._notify(neighbor,'fsm'): self._write(process,self._encoder[process].fsm(neighbor,fsm),neighbor) @silenced def signal (self, neighbor, signal): for process in self._notify(neighbor,'signal'): self._write(process,self._encoder[process].signal(neighbor,signal),neighbor) @silenced def packets (self, neighbor, direction, category, header, body): for process in self._notify(neighbor,'%s-packets' % direction): self._write(process,self._encoder[process].packets(neighbor,direction,category,header,body),neighbor) @silenced def notification (self, neighbor, direction, code, subcode, data, header, body): for process in self._notify(neighbor,'neighbor-changes'): self._write(process,self._encoder[process].notification(neighbor,direction,code,subcode,data,header,body),neighbor) @silenced def message (self, message_id, neighbor, direction, message, negotiated, header, *body): self._dispatch[message_id](self,neighbor,direction,message,negotiated,header,*body) # registering message functions # no-self-argument def register_process (message_id, storage=_dispatch): def closure (function): def wrap (*args): function(*args) storage[message_id] = wrap return wrap return closure # notifications are handled in the loop as they use different arguments @register_process(Message.CODE.OPEN) def _open (self, peer, direction, message, negotiated, header, body): for process in self._notify(peer,'%s-%s' % (direction,Message.CODE.OPEN.SHORT)): self._write(process,self._encoder[process].open(peer,direction,message,negotiated,header,body),peer) @register_process(Message.CODE.UPDATE) def _update (self, peer, direction, update, negotiated, header, body): for process in self._notify(peer,'%s-%s' % (direction,Message.CODE.UPDATE.SHORT)): self._write(process,self._encoder[process].update(peer,direction,update,negotiated,header,body),peer) @register_process(Message.CODE.NOTIFICATION) def _notification (self, peer, direction, message, negotiated, header, body): for process in self._notify(peer,'%s-%s' % (direction,Message.CODE.NOTIFICATION.SHORT)): self._write(process,self._encoder[process].notification(peer,direction,message,negotiated,header,body),peer) # unused-argument, must keep the API @register_process(Message.CODE.KEEPALIVE) def _keepalive (self, peer, direction, keepalive, negotiated, header, body): for process in self._notify(peer,'%s-%s' % (direction,Message.CODE.KEEPALIVE.SHORT)): self._write(process,self._encoder[process].keepalive(peer,direction,negotiated,header,body),peer) @register_process(Message.CODE.ROUTE_REFRESH) def _refresh (self, peer, direction, refresh, negotiated, header, body): for process in self._notify(peer,'%s-%s' % (direction,Message.CODE.ROUTE_REFRESH.SHORT)): self._write(process,self._encoder[process].refresh(peer,direction,refresh,negotiated,header,body),peer) @register_process(Message.CODE.OPERATIONAL) def _operational (self, peer, direction, operational, negotiated, header, body): for process in self._notify(peer,'%s-%s' % (direction,Message.CODE.OPERATIONAL.SHORT)): self._write(process,self._encoder[process].operational(peer,direction,operational.category,operational,negotiated,header,body),peer)
class Reactor (object): class Exit (object): normal = 0 validate = 0 listening = 1 configuration = 1 privileges = 1 log = 1 pid = 1 socket = 1 io_error = 1 process = 1 select = 1 unknown = 1 # [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.exit_code = self.Exit.unknown self.max_loop_time = environment.settings().reactor.speed self._sleep_time = self.max_loop_time / 100 self._busyspin = {} self.early_drop = environment.settings().daemon.drop self.processes = None self.configuration = Configuration(configurations) self.logger = Logger() self.asynchronous = 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, exit_code): self.exit_code = exit_code self.signal.received = Signal.SHUTDOWN self.logger.critical(reason,'reactor') def _prevent_spin(self): second = int(time.time()) if not second in self._busyspin: self._busyspin = {second: 0} self._busyspin[second] += 1 if self._busyspin[second] > self.max_loop_time: time.sleep(self._sleep_time) return True return False def _api_ready (self,sockets,sleeptime): 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 self._prevent_spin() 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 self._prevent_spin() return [] except ValueError as exc: # The peer closing the TCP connection lead to a negative file descritor self._prevent_spin() return [] except KeyboardInterrupt: self._termination('^C received',self.Exit.normal) return [] 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 _completed (self,peers): for peer in peers: if self.peers[peer].neighbor.rib.outgoing.pending(): return False return True 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.load(): return self.Exit.configuration if validate: # only validate configuration self.logger.warning('','configuration') self.logger.warning('parsed Neighbors, un-templated','configuration') self.logger.warning('------------------------------','configuration') self.logger.warning('','configuration') for key in self.peers: self.logger.warning(str(self.peers[key].neighbor),'configuration') self.logger.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(): self.logger.critical('could not drop privileges to \'%s\' refusing to run as root' % self.daemon.user,'reactor') self.logger.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 self.logger.restart(): self.logger.critical('could not setup the logger, aborting','reactor') return self.Exit.log if not self.daemon.savepid(): return self.Exit.pid # 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.debug('waiting for %d seconds before connecting' % sleeptime,'reactor') 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.FULL_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.asynchronous.schedule(str(uuid.uuid1()),'checking for new connection(s)',self.listener.new_connections()) peers = self._active_peers() if self._completed(peers): reload_completed = True sleep = self._sleep_time # do not attempt to listen on closed sockets even if the peer is still here for io in list(workers.keys()): if io.fileno() == -1: del workers[io] # 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) 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() for io in self._api_ready(list(workers),sleep): peers.add(workers[io]) del 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 shutdown (self): """Terminate all the current BGP connections""" self.logger.critical('performing shutdown','reactor') if self.listener: self.listener.stop() self.listener = None for key in self.peers.keys(): self.peers[key].shutdown() self.asynchronous.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.notice('performing reload of exabgp %s' % version,'configuration') 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.error('problem with the configuration file, no change done','configuration') # Careful the string above is used but the QA code to check for sucess of failure # self.logger.error(str(self.configuration.error),'configuration') return False for key, peer in self.peers.items(): if key not in self.configuration.neighbors: self.logger.debug('removing peer: %s' % peer.neighbor.name(),'reactor') peer.remove() for key, neighbor in self.configuration.neighbors.items(): # new peer if key not in self.peers: self.logger.debug('new peer: %s' % neighbor.name(),'reactor') peer = Peer(neighbor,self) self.peers[key] = peer # modified peer elif self.peers[key].neighbor != neighbor: self.logger.debug('peer definition change, establishing a new connection for %s' % str(key),'reactor') 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.debug('peer definition identical, updating peer routes if required for %s' % str(key),'reactor') 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.notice('loaded new configuration successfully','reactor') return True def restart (self): """Kill the BGP session and restart it""" self.logger.notice('performing restart of exabgp %s' % version,'reactor') self.configuration.reload() for key in self.peers.keys(): if key not in self.configuration.neighbors.keys(): neighbor = self.configuration.neighbors[key] self.logger.debug('removing Peer %s' % neighbor.name(),'reactor') self.peers[key].remove() else: self.peers[key].reestablish() self.processes.start(self.configuration.processes,True)
class Listener (object): _family_AFI_map = { socket.AF_INET: AFI.ipv4, socket.AF_INET6: AFI.ipv6, } def __init__ (self, reactor, backlog=200): self.serving = False self.logger = Logger() self._reactor = reactor self._backlog = backlog self._sockets = {} self._accepted = {} self._pending = 0 def _new_socket (self, ip): if ip.afi == AFI.ipv6: return socket.socket(socket.AF_INET6, socket.SOCK_STREAM, socket.IPPROTO_TCP) if ip.afi == AFI.ipv4: return socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP) raise NetworkError('Can not create socket for listening, family of IP %s is unknown' % ip) def _listen (self, local_ip, peer_ip, local_port, md5, md5_base64, ttl_in): self.serving = True for sock,(local,port,peer,md) in self._sockets.items(): if local_ip.top() != local: continue if local_port != port: continue MD5(sock,peer_ip.top(),0,md5,md5_base64) if ttl_in: MIN_TTL(sock,peer_ip,ttl_in) return try: sock = self._new_socket(local_ip) # MD5 must match the peer side of the TCP, not the local one MD5(sock,peer_ip.top(),0,md5,md5_base64) if ttl_in: MIN_TTL(sock,peer_ip,ttl_in) try: sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) if local_ip.ipv6(): sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1) except (socket.error,AttributeError): pass sock.setblocking(0) # s.settimeout(0.0) sock.bind((local_ip.top(),local_port)) sock.listen(self._backlog) self._sockets[sock] = (local_ip.top(),local_port,peer_ip.top(),md5) except socket.error as exc: if exc.args[0] == errno.EADDRINUSE: raise BindingError('could not listen on %s:%d, the port may already be in use by another application' % (local_ip,local_port)) elif exc.args[0] == errno.EADDRNOTAVAIL: raise BindingError('could not listen on %s:%d, this is an invalid address' % (local_ip,local_port)) raise NetworkError(str(exc)) except NetworkError as exc: self.logger.critical(str(exc),'network') raise exc 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) self.logger.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: self.logger.critical('can not bind to %s:%d, you may need to run ExaBGP as root' % (local_addr, port),'network') else: self.logger.critical('can not bind to %s:%d (%s)' % (local_addr, port,str(exc)),'network') self.logger.critical('unset exabgp.tcp.bind if you do not want listen for incoming connections','network') self.logger.critical('and check that no other daemon is already binding to port %d' % port,'network') return False def incoming (self): if not self.serving: return False for sock in self._sockets: if sock in self._accepted: continue try: io, _ = sock.accept() self._accepted[sock] = io self._pending += 1 except socket.error as exc: if exc.errno in error.block: continue self.logger.critical(str(exc),'network') if self._pending: self._pending -= 1 return True return False def _connected (self): try: for sock,io in list(self._accepted.items()): del self._accepted[sock] if sock.family == socket.AF_INET: local_ip = io.getpeername()[0] # local_ip,local_port remote_ip = io.getsockname()[0] # remote_ip,remote_port elif sock.family == socket.AF_INET6: local_ip = io.getpeername()[0] # local_ip,local_port,local_flow,local_scope remote_ip = io.getsockname()[0] # remote_ip,remote_port,remote_flow,remote_scope else: raise AcceptError('unexpected address family (%d)' % sock.family) fam = self._family_AFI_map[sock.family] yield Incoming(fam,remote_ip,local_ip,io) except NetworkError as exc: self.logger.critical(str(exc),'network') def new_connections (self): if not self.serving: return yield None reactor = self._reactor ranged_neighbor = [] for connection in self._connected(): self.logger.debug('new connection received %s' % connection.name(),'network') for key in reactor.peers: peer = reactor.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.debug('refused connection from %s due to the state machine' % connection.name(),'network') break self.logger.debug('accepted connection from %s' % connection.name(),'network') 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.debug('could not accept connection from %s (more than one neighbor match)' % connection.name(),'network') reactor.asynchronous.schedule(str(uuid.uuid1()), 'sending notification (6,5)', connection.notification( 6, 5, 'could not accept the connection (more than one neighbor match)')) return if not matched: self.logger.debug('no session configured for %s' % connection.name(),'network') reactor.asynchronous.schedule(str(uuid.uuid1()), 'sending notification (6,3)', connection.notification( 6, 3, '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_neighbor.router_id = RouterID.create(connection.local) new_peer = Peer(new_neighbor,self) denied = new_peer.handle_connection(connection) if denied: self.logger.debug('refused connection from %s due to the state machine' % connection.name(),'network') return reactor.peers[new_neighbor.name()] = new_peer return def stop (self): if not self.serving: return for sock,(ip,port,_,_) in self._sockets.items(): sock.close() self.logger.info('stopped listening on %s:%d' % (ip,port),'network') self._sockets = {} self.serving = False
def check_neighbor (neighbors): logger = Logger() logger._option['parser'] = True logger.notice('\ndecoding routes in configuration','parser') for name in neighbors.keys(): neighbor = copy.deepcopy(neighbors[name]) neighbor.local_as = neighbor.peer_as path = {} for f in NLRI.known_families(): if neighbor.add_path: path[f] = neighbor.add_path capa = Capabilities().new(neighbor,False) if path: capa[Capability.CODE.ADD_PATH] = path capa[Capability.CODE.MULTIPROTOCOL] = neighbor.families() routerid_1 = str(neighbor.router_id) routerid_2 = '.'.join(str((int(_)+1) % 250) for _ in str(neighbor.router_id).split('.',-1)) o1 = Open(Version(4),ASN(neighbor.local_as),HoldTime(180),RouterID(routerid_1),capa) o2 = Open(Version(4),ASN(neighbor.peer_as),HoldTime(180),RouterID(routerid_2),capa) negotiated = Negotiated(neighbor) negotiated.sent(o1) negotiated.received(o2) # grouped = False for _ in neighbor.rib.outgoing.updates(False): pass for change1 in neighbor.rib.outgoing.cached_changes(): str1 = change1.extensive() packed = list(Update([change1.nlri],change1.attributes).messages(negotiated)) pack1 = packed[0] logger.debug('parsed route requires %d updates' % len(packed),'parser') logger.debug('update size is %d' % len(pack1),'parser') logger.debug('parsed route %s' % str1,'parser') logger.debug('parsed hex %s' % od(pack1),'parser') # This does not take the BGP header - let's assume we will not break that :) try: logger.debug('') # new line pack1s = pack1[19:] if pack1.startswith(b'\xFF'*16) else pack1 update = Update.unpack_message(pack1s,negotiated) change2 = Change(update.nlris[0],update.attributes) str2 = change2.extensive() pack2 = list(Update([update.nlris[0]],update.attributes).messages(negotiated))[0] logger.debug('recoded route %s' % str2,'parser') logger.debug('recoded hex %s' % od(pack2),'parser') str1 = str1.replace('attribute [ 0x04 0x80 0x00000064 ]','med 100') str1r = str1.lower().replace(' med 100','').replace(' local-preference 100','').replace(' origin igp','') str2r = str2.lower().replace(' med 100','').replace(' local-preference 100','').replace(' origin igp','') str2r = str2r.replace('large-community [ 1:2:3 10:11:12 ]','attribute [ 0x20 0xc0 0x0000000100000002000000030000000a0000000b0000000c ]') if 'next-hop self' in str1r: if ':' in str1r: str1r = str1r.replace('next-hop self','next-hop ::1') else: str1r = str1r.replace('next-hop self','next-hop %s' % neighbor.local_address) if ' name ' in str1r: parts = str1r.split(' ') pos = parts.index('name') str1r = ' '.join(parts[:pos] + parts[pos+2:]) skip = False if str1r != str2r: if 'attribute [' in str1r and ' 0x00 ' in str1r: # we do not decode non-transitive attributes logger.debug('skipping string check on update with non-transitive attribute(s)','parser') skip = True else: logger.debug('strings are different:','parser') logger.debug('[%s]' % (str1r),'parser') logger.debug('[%s]' % (str2r),'parser') return False else: logger.debug('strings are fine','parser') if skip: logger.debug('skipping encoding for update with non-transitive attribute(s)','parser') elif pack1 != pack2: logger.debug('encoding are different','parser') logger.debug('[%s]' % (od(pack1)),'parser') logger.debug('[%s]' % (od(pack2)),'parser') return False else: logger.debug('encoding is fine','parser') logger.debug('----------------------------------------','parser') logger.debug('JSON nlri %s' % change1.nlri.json(),'parser') logger.debug('JSON attr %s' % change1.attributes.json(),'parser') except Notify as exc: logger.debug('----------------------------------------','parser') logger.debug(str(exc),'parser') logger.debug('----------------------------------------','parser') return False neighbor.rib.clear() return True
def check_update (neighbor, raw): logger = Logger() logger._option['parser'] = True logger.debug('\ndecoding routes in configuration','parser') neighbor = neighbor[list(neighbor)[0]] path = {} for f in NLRI.known_families(): if neighbor.add_path: path[f] = neighbor.add_path capa = Capabilities().new(neighbor,False) capa[Capability.CODE.ADD_PATH] = path capa[Capability.CODE.MULTIPROTOCOL] = neighbor.families() # capa[Capability.CODE.FOUR_BYTES_ASN] = True routerid_1 = str(neighbor.router_id) routerid_2 = '.'.join(str((int(_)+1) % 250) for _ in str(neighbor.router_id).split('.',-1)) o1 = Open(Version(4),ASN(neighbor.local_as),HoldTime(180),RouterID(routerid_1),capa) o2 = Open(Version(4),ASN(neighbor.peer_as),HoldTime(180),RouterID(routerid_2),capa) negotiated = Negotiated(neighbor) negotiated.sent(o1) negotiated.received(o2) # grouped = False while raw: if raw.startswith(b'\xff'*16): kind = ordinal(raw[18]) size = (ordinal(raw[16]) << 16) + (ordinal(raw[17])) injected,raw = raw[19:size],raw[size:] if kind == 2: logger.debug('the message is an update','parser') decoding = 'update' else: logger.debug('the message is not an update (%d) - aborting' % kind,'parser') return False else: logger.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,negotiated) except KeyboardInterrupt: raise except Notify: logger.error('could not parse the message','parser') logger.error(traceback.format_exc(),'parser') return False except StandardError: logger.error('could not parse the message','parser') logger.error(traceback.format_exc(),'parser') return False logger.debug('','parser') # new line for number in range(len(update.nlris)): change = Change(update.nlris[number],update.attributes) logger.info('decoded %s %s %s' % (decoding,change.nlri.action,change.extensive()),'parser') logger.info('update json %s' % Response.JSON(json_version).update(neighbor,'in',update,None,'',''),'parser') return True
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.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): self.logger.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: self.logger.warning("Can not create PIDfile %s" % self.pid,'daemon') return False self.logger.warning("Created PIDfile %s with value %d" % (self.pid,ownid),'daemon') 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.error("Can not remove PIDfile %s" % self.pid,'daemon') return self.logger.debug("Removed PIDfile %s" % self.pid,'daemon') 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.critical('ExaBGP can not fork when logs are going to %s' % log.destination.lower(),'daemon') return def fork_exit (): try: pid = os.fork() if pid > 0: os._exit(0) except OSError as exc: self.logger.critical('can not fork, errno %d : %s' % (exc.errno,exc.strerror),'daemon') # 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)
def unpack_message (cls, data, negotiated): logger = Logger() logger.debug(LazyFormat('parsing UPDATE',data),'parser') length = len(data) # This could be speed up massively by changing the order of the IF if length == 4 and data == b'\x00\x00\x00\x00': return EOR(AFI.ipv4,SAFI.unicast) # pylint: disable=E1101 if length == 11 and data.startswith(EOR.NLRI.PREFIX): return EOR.unpack_message(data,negotiated) withdrawn, _attributes, announced = cls.split(data) if not withdrawn: logger.debug('withdrawn NLRI none','parser') attributes = Attributes.unpack(_attributes,negotiated) if not announced: logger.debug('announced NLRI none','parser') # Is the peer going to send us some Path Information with the route (AddPath) addpath = negotiated.addpath.receive(AFI.ipv4,SAFI.unicast) # empty string for NoNextHop, the packed IP otherwise (without the 3/4 bytes of attributes headers) nexthop = attributes.get(Attribute.CODE.NEXT_HOP,NoNextHop) # nexthop = NextHop.unpack(_nexthop.ton()) # XXX: NEXTHOP MUST NOT be the IP address of the receiving speaker. nlris = [] while withdrawn: nlri,left = NLRI.unpack_nlri(AFI.ipv4,SAFI.unicast,withdrawn,IN.WITHDRAWN,addpath) logger.debug('withdrawn NLRI %s' % nlri,'parser') withdrawn = left nlris.append(nlri) while announced: nlri,left = NLRI.unpack_nlri(AFI.ipv4,SAFI.unicast,announced,IN.ANNOUNCED,addpath) nlri.nexthop = nexthop logger.debug('announced NLRI %s' % nlri,'parser') announced = left nlris.append(nlri) unreach = attributes.pop(MPURNLRI.ID,None) reach = attributes.pop(MPRNLRI.ID,None) if unreach is not None: nlris.extend(unreach.nlris) if reach is not None: nlris.extend(reach.nlris) if not attributes and not nlris: # Careful do not use == or != as the comparaison does not work if unreach is None and reach is None: return EOR(AFI.ipv4,SAFI.unicast) if unreach is not None: return EOR(unreach.afi,unreach.safi) if reach is not None: return EOR(reach.afi,reach.safi) raise RuntimeError('This was not expected') return Update(nlris,attributes)
def parse (self, data, negotiated): if not data: return self try: # We do not care if the attribute are transitive or not as we do not redistribute flag = Attribute.Flag(ordinal(data[0])) aid = Attribute.CODE(ordinal(data[1])) except IndexError: self.add(TreatAsWithdraw()) return self try: offset = 3 length = ordinal(data[2]) if flag & Attribute.Flag.EXTENDED_LENGTH: offset = 4 length = (length << 8) + ordinal(data[3]) except IndexError: self.add(TreatAsWithdraw(aid)) return self data = data[offset:] left = data[length:] attribute = data[:length] logger = Logger() logger.debug(LazyAttribute(flag,aid,length,data[:length]),'parser') # remove the PARTIAL bit before comparaison if the attribute is optional if aid in Attribute.attributes_optional: flag &= Attribute.Flag.MASK_PARTIAL & 0xFF # flag &= ~Attribute.Flag.PARTIAL & 0xFF # cleaner than above (python use signed integer for ~) if aid in self: if aid in self.NO_DUPLICATE: raise Notify(3,1,'multiple attribute for %s' % str(Attribute.CODE(attribute.ID))) logger.debug('duplicate attribute %s (flag 0x%02X, aid 0x%02X) skipping' % (Attribute.CODE.names.get(aid,'unset'),flag,aid),'parser') return self.parse(left,negotiated) # handle the attribute if we know it if Attribute.registered(aid,flag): if length == 0 and aid not in self.VALID_ZERO: self.add(TreatAsWithdraw(aid)) return self.parse(left,negotiated) try: decoded = Attribute.unpack(aid,flag,attribute,negotiated) except IndexError as exc: if aid in self.TREAT_AS_WITHDRAW: decoded = TreatAsWithdraw(aid) else: raise exc except Notify as exc: if aid in self.TREAT_AS_WITHDRAW: decoded = TreatAsWithdraw() elif aid in self.DISCARD: decoded = Discard() else: raise exc self.add(decoded) return self.parse(left,negotiated) # XXX: FIXME: we could use a fallback function here like capability # if we know the attribute but the flag is not what the RFC says. if aid in Attribute.attributes_known: if aid in self.TREAT_AS_WITHDRAW: logger.debug('invalid flag for attribute %s (flag 0x%02X, aid 0x%02X) treat as withdraw' % (Attribute.CODE.names.get(aid,'unset'),flag,aid),'parser') self.add(TreatAsWithdraw()) if aid in self.DISCARD: logger.debug('invalid flag for attribute %s (flag 0x%02X, aid 0x%02X) discard' % (Attribute.CODE.names.get(aid,'unset'),flag,aid),'parser') return self.parse(left,negotiated) # XXX: Check if we are missing any logger.debug('invalid flag for attribute %s (flag 0x%02X, aid 0x%02X) unspecified (should not happen)' % (Attribute.CODE.names.get(aid,'unset'),flag,aid),'parser') return self.parse(left,negotiated) # it is an unknown transitive attribute we need to pass on if flag & Attribute.Flag.TRANSITIVE: logger.debug('unknown transitive attribute (flag 0x%02X, aid 0x%02X)' % (flag,aid),'parser') try: decoded = GenericAttribute(aid,flag | Attribute.Flag.PARTIAL,attribute) except IndexError: decoded = TreatAsWithdraw(aid) self.add(decoded,attribute) return self.parse(left,negotiated) # it is an unknown non-transitive attribute we can ignore. logger.debug('ignoring unknown non-transitive attribute (flag 0x%02X, aid 0x%02X)' % (flag,aid),'parser') return self.parse(left,negotiated)
def run (env, comment, configurations, root, validate, pid=0): logger = Logger() logger.notice('Thank you for using ExaBGP','welcome') logger.notice('%s' % version,'version') logger.notice('%s' % sys.version.replace('\n',' '),'interpreter') logger.notice('%s' % ' '.join(platform.uname()[:5]),'os') logger.notice('%s' % root,'installation') if comment: logger.notice(comment,'advice') warning = warn() if warning: logger.warning(warning,'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','cli') logger.error('we scanned the following folders (the number is your PID):','cli') for location in pipes: logger.error(' - %s' % location,'cli control') logger.error('please make them in one of the folder with the following commands:','cli control') logger.error('> mkfifo %s/run/exabgp.{in,out}' % os.getcwd(),'cli control') logger.error('> chmod 600 %s/run/exabgp.{in,out}' % os.getcwd(),'cli control') if os.getuid() != 0: logger.error('> chown %d:%d %s/run/exabgp.{in,out}' % (os.getuid(),os.getgid(),os.getcwd()),'cli control') else: pipe = pipes[0] os.environ['exabgp_cli_pipe'] = pipe logger.info('named pipes for the cli are:','cli control') logger.info('to send commands %sexabgp.in' % pipe,'cli control') logger.info('to read responses %sexabgp.out' % pipe,'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.debug('profiling ....','reactor') 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.debug("-"*len(notice),'reactor') logger.debug(notice,'reactor') logger.debug("-"*len(notice),'reactor') __exit(env.debug.memory,0 if was_ok else 1) else: logger.debug("-"*len(notice),'reactor') logger.debug(notice,'reactor') logger.debug("-"*len(notice),'reactor') Reactor(configurations).run(validate,root) __exit(env.debug.memory,1)
def parse(self, data, negotiated): if not data: return self try: # We do not care if the attribute are transitive or not as we do not redistribute flag = Attribute.Flag(ordinal(data[0])) aid = Attribute.CODE(ordinal(data[1])) except IndexError: self.add(TreatAsWithdraw()) return self try: offset = 3 length = ordinal(data[2]) if flag & Attribute.Flag.EXTENDED_LENGTH: offset = 4 length = (length << 8) + ordinal(data[3]) except IndexError: self.add(TreatAsWithdraw(aid)) return self data = data[offset:] left = data[length:] attribute = data[:length] logger = Logger() logger.debug(LazyAttribute(flag, aid, length, data[:length]), 'parser') # remove the PARTIAL bit before comparaison if the attribute is optional if aid in Attribute.attributes_optional: flag &= Attribute.Flag.MASK_PARTIAL & 0xFF # flag &= ~Attribute.Flag.PARTIAL & 0xFF # cleaner than above (python use signed integer for ~) if aid in self: if aid in self.NO_DUPLICATE: raise Notify( 3, 1, 'multiple attribute for %s' % str(Attribute.CODE(attribute.ID))) logger.debug( 'duplicate attribute %s (flag 0x%02X, aid 0x%02X) skipping' % (Attribute.CODE.names.get(aid, 'unset'), flag, aid), 'parser') return self.parse(left, negotiated) # handle the attribute if we know it if Attribute.registered(aid, flag): if length == 0 and aid not in self.VALID_ZERO: self.add(TreatAsWithdraw(aid)) return self.parse(left, negotiated) try: decoded = Attribute.unpack(aid, flag, attribute, negotiated) except IndexError as exc: if aid in self.TREAT_AS_WITHDRAW: decoded = TreatAsWithdraw(aid) else: raise exc except Notify as exc: if aid in self.TREAT_AS_WITHDRAW: decoded = TreatAsWithdraw() elif aid in self.DISCARD: decoded = Discard() else: raise exc self.add(decoded) return self.parse(left, negotiated) # XXX: FIXME: we could use a fallback function here like capability # if we know the attribute but the flag is not what the RFC says. if aid in Attribute.attributes_known: if aid in self.TREAT_AS_WITHDRAW: logger.debug( 'invalid flag for attribute %s (flag 0x%02X, aid 0x%02X) treat as withdraw' % (Attribute.CODE.names.get(aid, 'unset'), flag, aid), 'parser') self.add(TreatAsWithdraw()) if aid in self.DISCARD: logger.debug( 'invalid flag for attribute %s (flag 0x%02X, aid 0x%02X) discard' % (Attribute.CODE.names.get(aid, 'unset'), flag, aid), 'parser') return self.parse(left, negotiated) # XXX: Check if we are missing any logger.debug( 'invalid flag for attribute %s (flag 0x%02X, aid 0x%02X) unspecified (should not happen)' % (Attribute.CODE.names.get(aid, 'unset'), flag, aid), 'parser') return self.parse(left, negotiated) # it is an unknown transitive attribute we need to pass on if flag & Attribute.Flag.TRANSITIVE: logger.debug( 'unknown transitive attribute (flag 0x%02X, aid 0x%02X)' % (flag, aid), 'parser') try: decoded = GenericAttribute(aid, flag | Attribute.Flag.PARTIAL, attribute) except IndexError: decoded = TreatAsWithdraw(aid) self.add(decoded, attribute) return self.parse(left, negotiated) # it is an unknown non-transitive attribute we can ignore. logger.debug( 'ignoring unknown non-transitive attribute (flag 0x%02X, aid 0x%02X)' % (flag, aid), 'parser') return self.parse(left, negotiated)
class Reactor(object): class Exit(object): normal = 0 validate = 0 listening = 1 configuration = 1 privileges = 1 log = 1 pid = 1 socket = 1 io_error = 1 process = 1 select = 1 unknown = 1 # [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.exit_code = self.Exit.unknown self.max_loop_time = environment.settings().reactor.speed self._sleep_time = self.max_loop_time / 100 self._busyspin = {} self.early_drop = environment.settings().daemon.drop self.processes = None self.configuration = Configuration(configurations) self.logger = Logger() self.asynchronous = 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, exit_code): self.exit_code = exit_code self.signal.received = Signal.SHUTDOWN self.logger.critical(reason, 'reactor') def _prevent_spin(self): second = int(time.time()) if not second in self._busyspin: self._busyspin = {second: 0} self._busyspin[second] += 1 if self._busyspin[second] > self.max_loop_time: time.sleep(self._sleep_time) return True return False def _api_ready(self, sockets, sleeptime): 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 self._prevent_spin() 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 self._prevent_spin() return [] except ValueError as exc: # The peer closing the TCP connection lead to a negative file descritor self._prevent_spin() return [] except KeyboardInterrupt: self._termination('^C received', self.Exit.normal) return [] 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 _completed(self, peers): for peer in peers: if self.peers[peer].neighbor.rib.outgoing.pending(): return False return True 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.load(): return self.Exit.configuration if validate: # only validate configuration self.logger.warning('', 'configuration') self.logger.warning('parsed Neighbors, un-templated', 'configuration') self.logger.warning('------------------------------', 'configuration') self.logger.warning('', 'configuration') for key in self.peers: self.logger.warning(str(self.peers[key].neighbor), 'configuration') self.logger.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(): self.logger.critical( 'could not drop privileges to \'%s\' refusing to run as root' % self.daemon.user, 'reactor') self.logger.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 self.logger.restart(): self.logger.critical('could not setup the logger, aborting', 'reactor') return self.Exit.log if not self.daemon.savepid(): return self.Exit.pid # 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.debug( 'waiting for %d seconds before connecting' % sleeptime, 'reactor') 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.FULL_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.asynchronous.schedule( str(uuid.uuid1()), 'checking for new connection(s)', self.listener.new_connections()) peers = self._active_peers() if self._completed(peers): reload_completed = True sleep = self._sleep_time # do not attempt to listen on closed sockets even if the peer is still here for io in list(workers.keys()): if io.fileno() == -1: del workers[io] # 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) 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() for io in self._api_ready(list(workers), sleep): peers.add(workers[io]) del 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 shutdown(self): """Terminate all the current BGP connections""" self.logger.critical('performing shutdown', 'reactor') if self.listener: self.listener.stop() self.listener = None for key in self.peers.keys(): self.peers[key].shutdown() self.asynchronous.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.notice('performing reload of exabgp %s' % version, 'configuration') 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.error( 'problem with the configuration file, no change done', 'configuration') # Careful the string above is used but the QA code to check for sucess of failure # self.logger.error(str(self.configuration.error), 'configuration') return False for key, peer in self.peers.items(): if key not in self.configuration.neighbors: self.logger.debug('removing peer: %s' % peer.neighbor.name(), 'reactor') peer.remove() for key, neighbor in self.configuration.neighbors.items(): # new peer if key not in self.peers: self.logger.debug('new peer: %s' % neighbor.name(), 'reactor') peer = Peer(neighbor, self) self.peers[key] = peer # modified peer elif self.peers[key].neighbor != neighbor: self.logger.debug( 'peer definition change, establishing a new connection for %s' % str(key), 'reactor') 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.debug( 'peer definition identical, updating peer routes if required for %s' % str(key), 'reactor') 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.notice('loaded new configuration successfully', 'reactor') return True def restart(self): """Kill the BGP session and restart it""" self.logger.notice('performing restart of exabgp %s' % version, 'reactor') self.configuration.reload() for key in self.peers.keys(): if key not in self.configuration.neighbors.keys(): neighbor = self.configuration.neighbors[key] self.logger.debug('removing Peer %s' % neighbor.name(), 'reactor') self.peers[key].remove() else: self.peers[key].reestablish() self.processes.start(self.configuration.processes, True)
class Listener(object): _family_AFI_map = { socket.AF_INET: AFI.ipv4, socket.AF_INET6: AFI.ipv6, } def __init__(self, reactor, backlog=200): self.serving = False self.logger = Logger() self._reactor = reactor self._backlog = backlog self._sockets = {} self._accepted = {} self._pending = 0 def _new_socket(self, ip): if ip.afi == AFI.ipv6: return socket.socket(socket.AF_INET6, socket.SOCK_STREAM, socket.IPPROTO_TCP) if ip.afi == AFI.ipv4: return socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP) raise NetworkError( 'Can not create socket for listening, family of IP %s is unknown' % ip) def _listen(self, local_ip, peer_ip, local_port, md5, md5_base64, ttl_in): self.serving = True for sock, (local, port, peer, md) in self._sockets.items(): if local_ip.top() != local: continue if local_port != port: continue MD5(sock, peer_ip.top(), 0, md5, md5_base64) if ttl_in: MIN_TTL(sock, peer_ip, ttl_in) return try: sock = self._new_socket(local_ip) # MD5 must match the peer side of the TCP, not the local one MD5(sock, peer_ip.top(), 0, md5, md5_base64) if ttl_in: MIN_TTL(sock, peer_ip, ttl_in) try: sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) if local_ip.ipv6(): sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1) except (socket.error, AttributeError): pass sock.setblocking(0) # s.settimeout(0.0) sock.bind((local_ip.top(), local_port)) sock.listen(self._backlog) self._sockets[sock] = (local_ip.top(), local_port, peer_ip.top(), md5) except socket.error as exc: if exc.args[0] == errno.EADDRINUSE: raise BindingError( 'could not listen on %s:%d, the port may already be in use by another application' % (local_ip, local_port)) elif exc.args[0] == errno.EADDRNOTAVAIL: raise BindingError( 'could not listen on %s:%d, this is an invalid address' % (local_ip, local_port)) raise NetworkError(str(exc)) except NetworkError as exc: self.logger.critical(str(exc), 'network') raise exc 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) self.logger.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: self.logger.critical( 'can not bind to %s:%d, you may need to run ExaBGP as root' % (local_addr, port), 'network') else: self.logger.critical( 'can not bind to %s:%d (%s)' % (local_addr, port, str(exc)), 'network') self.logger.critical( 'unset exabgp.tcp.bind if you do not want listen for incoming connections', 'network') self.logger.critical( 'and check that no other daemon is already binding to port %d' % port, 'network') return False def incoming(self): if not self.serving: return False for sock in self._sockets: if sock in self._accepted: continue try: io, _ = sock.accept() self._accepted[sock] = io self._pending += 1 except socket.error as exc: if exc.errno in error.block: continue self.logger.critical(str(exc), 'network') if self._pending: self._pending -= 1 return True return False def _connected(self): try: for sock, io in list(self._accepted.items()): del self._accepted[sock] if sock.family == socket.AF_INET: local_ip = io.getpeername()[0] # local_ip,local_port remote_ip = io.getsockname()[0] # remote_ip,remote_port elif sock.family == socket.AF_INET6: local_ip = io.getpeername()[ 0] # local_ip,local_port,local_flow,local_scope remote_ip = io.getsockname()[ 0] # remote_ip,remote_port,remote_flow,remote_scope else: raise AcceptError('unexpected address family (%d)' % sock.family) fam = self._family_AFI_map[sock.family] yield Incoming(fam, remote_ip, local_ip, io) except NetworkError as exc: self.logger.critical(str(exc), 'network') def new_connections(self): if not self.serving: return yield None reactor = self._reactor ranged_neighbor = [] for connection in self._connected(): for key in reactor.peers: peer = reactor.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.debug( 'refused connection from %s due to the state machine' % connection.name(), 'network') break self.logger.debug( 'accepted connection from %s' % connection.name(), 'network') 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.debug( 'could not accept connection from %s (more than one neighbor match)' % connection.name(), 'network') reactor. async .schedule( str(uuid.uuid1()), 'sending notification (6,5)', connection.notification( 6, 5, b'could not accept the connection (more than one neighbor match)' )) return if not matched: self.logger.debug( 'no session configured for %s' % connection.name(), 'network') reactor. async .schedule( str(uuid.uuid1()), 'sending notification (6,3)', 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.debug( 'refused connection from %s due to the state machine' % connection.name(), 'network') return reactor.peers[new_neighbor.name()] = new_peer return def stop(self): if not self.serving: return for sock, (ip, port, _, _) in self._sockets.items(): sock.close() self.logger.info('stopped listening on %s:%d' % (ip, port), 'network') self._sockets = {} self.serving = False
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.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): self.logger.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: self.logger.warning("Can not create PIDfile %s" % self.pid, 'daemon') return False self.logger.warning( "Created PIDfile %s with value %d" % (self.pid, ownid), 'daemon') 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.error("Can not remove PIDfile %s" % self.pid, 'daemon') return self.logger.debug("Removed PIDfile %s" % self.pid, 'daemon') 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.critical( 'ExaBGP can not fork when logs are going to %s' % log.destination.lower(), 'daemon') return def fork_exit(): try: pid = os.fork() if pid > 0: os._exit(0) except OSError as exc: self.logger.critical( 'can not fork, errno %d : %s' % (exc.errno, exc.strerror), 'daemon') # 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 Processes(object): # how many time can a process can respawn in the time interval respawn_timemask = 0xFFFFFF - 0b111111 # '0b111111111111111111000000' (around a minute, 63 seconds) _dispatch = {} def __init__(self): self.logger = Logger() self.clean() self.silence = False self._buffer = {} self._configuration = {} self.respawn_number = 5 if environment.settings().api.respawn else 0 self.terminate_on_error = environment.settings().api.terminate self.ack = environment.settings().api.ack def number(self): return len(self._process) def clean(self): self.fds = [] self._process = {} self._encoder = {} self._broken = [] self._respawning = {} def _handle_problem(self, process): if process not in self._process: return if self.respawn_number: self.logger.debug('issue with the process, restarting it', 'process') self._terminate(process) self._start(process) else: self.logger.debug('issue with the process, terminating it', 'process') self._terminate(process) def _terminate(self, process_name): self.logger.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 _terminate_run(self, process): try: process.terminate() process.wait() except (OSError, KeyError): # the process is most likely already dead pass 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 self.logger.debug( 'child process %s was already dead' % process, 'process') self.clean() def _start(self, process): try: if process in self._process: self.logger.debug('process already running', 'process') return if process not in self._configuration: self.logger.debug( 'can not start process, no configuration for it', 'process') return # Prevent some weird termcap data to be created at the start of the PIPE # \x1b[?1034h (no-eol) (esc) os.environ['TERM'] = 'dumb' configuration = self._configuration[process] run = configuration.get('run', '') if run: api = configuration.get('encoder', '') self._encoder[process] = Response.Text( text_version) if api == 'text' else Response.JSON( json_version) self._process[process] = subprocess.Popen( run, stdin=subprocess.PIPE, stdout=subprocess.PIPE, preexec_fn=preexec_helper # This flags exists for python 2.7.3 in the documentation but on on my MAC # creationflags=subprocess.CREATE_NEW_PROCESS_GROUP ) self._update_fds() fcntl.fcntl(self._process[process].stdout.fileno(), fcntl.F_SETFL, os.O_NONBLOCK) self.logger.debug('forked process %s' % process, 'process') around_now = int(time.time()) & self.respawn_timemask if process in self._respawning: if around_now in self._respawning[process]: self._respawning[process][around_now] += 1 # we are respawning too fast if self._respawning[process][ around_now] > self.respawn_number: self.logger.critical( 'Too many death for %s (%d) terminating program' % (process, self.respawn_number), 'process') raise ProcessError() else: # reset long time since last respawn self._respawning[process] = {around_now: 1} else: # record respawing self._respawning[process] = {around_now: 1} except (subprocess.CalledProcessError, OSError, ValueError) as exc: self._broken.append(process) self.logger.debug('could not start process %s' % process, 'process') self.logger.debug('reason: %s' % str(exc), 'process') def start(self, configuration, restart=False): for process in list(self._process): if process not in configuration: self._terminate(process) self._configuration = configuration for process in configuration: if restart and process in list(self._process): self._terminate(process) self._start(process) def broken(self, neighbor): if self._broken: for process in self._configuration: if process in self._broken: return True return False def _update_fds(self): self.fds = [ self._process[process].stdout.fileno() for process in self._process ] def received(self): consumed_data = False for process in list(self._process): try: proc = self._process[process] poll = proc.poll() # proc.poll returns None if the process is still fine # -[signal], like -15, if the process was terminated if poll is not None: self._handle_problem(process) return poller = select.poll() poller.register( proc.stdout, select.POLLIN | select.POLLPRI | select.POLLHUP | select.POLLNVAL | select.POLLERR) ready = False for _, event in poller.poll(0): if event & select.POLLIN or event & select.POLLPRI: ready = True elif event & select.POLLHUP or event & select.POLLRDHUP or event & select.POLLERR or event & select.POLLNVAL: self._handle_problem(process) if not ready: continue try: # Calling next() on Linux and OSX works perfectly well # but not on OpenBSD where it always raise StopIteration # and only readline() works buf = str_ascii(proc.stdout.read(16384)) if buf == '' and poll is not None: # if proc.poll() is None then # process is fine, we received an empty line because # we're doing .readline() on a non-blocking pipe and # the process maybe has nothing to send yet self._handle_problem(process) continue raw = self._buffer.get(process, '') + buf while '\n' in raw: line, raw = raw.split('\n', 1) line = line.rstrip() consumed_data = True self.logger.debug( 'command from process %s : %s ' % (process, line), 'process') yield (process, formated(line)) self._buffer[process] = raw except IOError as exc: if not exc.errno or exc.errno in error.fatal: # if the program exits we can get an IOError with errno code zero ! self._handle_problem(process) elif exc.errno in error.block: # we often see errno.EINTR: call interrupted and # we most likely have data, we will try to read them a the next loop iteration pass else: self.logger.debug( 'unexpected errno received from forked process (%s)' % errstr(exc), 'process') except StopIteration: if not consumed_data: self._handle_problem(process) except KeyError: pass except (subprocess.CalledProcessError, OSError, ValueError): self._handle_problem(process) 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_ascii('%s\n' % string)) except IOError as exc: self._broken.append(process) if exc.errno == errno.EPIPE: self._broken.append(process) self.logger.debug( 'issue while sending data to our helper program', 'process') raise ProcessError() else: # Could it have been caused by a signal ? What to do. self.logger.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. self.logger.debug( 'error received while FLUSHING data to helper program, retrying (%s)' % errstr(exc), 'process') return True def _answer(self, service, string, force=False): if force or self.ack: self.logger.debug( 'responding to %s : %s' % (service, string.replace('\n', '\\n')), 'process') self.write(service, string) def answer_done(self, service): self._answer(service, Answer.done) def answer_error(self, service): self._answer(service, Answer.error) def _notify(self, neighbor, event): for process in neighbor.api[event]: yield process # do not do anything if silenced # no-self-argument def silenced(function): def closure(self, *args): if self.silence: return return function(self, *args) return closure # invalid-name @silenced def up(self, neighbor): for process in self._notify(neighbor, 'neighbor-changes'): self.write(process, self._encoder[process].up(neighbor), neighbor) @silenced def connected(self, neighbor): for process in self._notify(neighbor, 'neighbor-changes'): self.write(process, self._encoder[process].connected(neighbor), neighbor) @silenced def down(self, neighbor, reason): for process in self._notify(neighbor, 'neighbor-changes'): self.write(process, self._encoder[process].down(neighbor, reason), neighbor) @silenced def negotiated(self, neighbor, negotiated): for process in self._notify(neighbor, 'negotiated'): self.write(process, self._encoder[process].negotiated(neighbor, negotiated), neighbor) @silenced def fsm(self, neighbor, fsm): for process in self._notify(neighbor, 'fsm'): self.write(process, self._encoder[process].fsm(neighbor, fsm), neighbor) @silenced def signal(self, neighbor, signal): for process in self._notify(neighbor, 'signal'): self.write(process, self._encoder[process].signal(neighbor, signal), neighbor) @silenced def packets(self, neighbor, direction, category, header, body): for process in self._notify(neighbor, '%s-packets' % direction): self.write( process, self._encoder[process].packets(neighbor, direction, category, header, body), neighbor) @silenced def notification(self, neighbor, direction, code, subcode, data, header, body): for process in self._notify(neighbor, 'neighbor-changes'): self.write( process, self._encoder[process].notification(neighbor, direction, code, subcode, data, header, body), neighbor) @silenced def message(self, message_id, neighbor, direction, message, negotiated, header, *body): self._dispatch[message_id](self, neighbor, direction, message, negotiated, header, *body) # registering message functions # no-self-argument def register_process(message_id, storage=_dispatch): def closure(function): def wrap(*args): function(*args) storage[message_id] = wrap return wrap return closure # notifications are handled in the loop as they use different arguments @register_process(Message.CODE.OPEN) def _open(self, peer, direction, message, negotiated, header, body): for process in self._notify( peer, '%s-%s' % (direction, Message.CODE.OPEN.SHORT)): self.write( process, self._encoder[process].open(peer, direction, message, negotiated, header, body), peer) @register_process(Message.CODE.UPDATE) def _update(self, peer, direction, update, negotiated, header, body): for process in self._notify( peer, '%s-%s' % (direction, Message.CODE.UPDATE.SHORT)): self.write( process, self._encoder[process].update(peer, direction, update, negotiated, header, body), peer) @register_process(Message.CODE.NOTIFICATION) def _notification(self, peer, direction, message, negotiated, header, body): for process in self._notify( peer, '%s-%s' % (direction, Message.CODE.NOTIFICATION.SHORT)): self.write( process, self._encoder[process].notification(peer, direction, message, negotiated, header, body), peer) # unused-argument, must keep the API @register_process(Message.CODE.KEEPALIVE) def _keepalive(self, peer, direction, keepalive, negotiated, header, body): for process in self._notify( peer, '%s-%s' % (direction, Message.CODE.KEEPALIVE.SHORT)): self.write( process, self._encoder[process].keepalive(peer, direction, negotiated, header, body), peer) @register_process(Message.CODE.ROUTE_REFRESH) def _refresh(self, peer, direction, refresh, negotiated, header, body): for process in self._notify( peer, '%s-%s' % (direction, Message.CODE.ROUTE_REFRESH.SHORT)): self.write( process, self._encoder[process].refresh(peer, direction, refresh, negotiated, header, body), peer) @register_process(Message.CODE.OPERATIONAL) def _operational(self, peer, direction, operational, negotiated, header, body): for process in self._notify( peer, '%s-%s' % (direction, Message.CODE.OPERATIONAL.SHORT)): self.write( process, self._encoder[process].operational(peer, direction, operational.category, operational, negotiated, header, body), peer)
def main (): major = int(sys.version[0]) minor = int(sys.version[2]) if major <= 2 and minor < 5: sys.stdout.write('This program can not work (is not tested) with your python version (< 2.5)\n') sys.stdout.flush() sys.exit(1) cli_named_pipe = os.environ.get('exabgp_cli_pipe','') if cli_named_pipe: from exabgp.application.control import main as control control(cli_named_pipe) sys.exit(0) options = docopt.docopt(usage, help=False) 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: sys.stdout.write(usage) sys.stdout.flush() sys.exit(0) return root = root_folder(options,['/bin/exabgp','/sbin/exabgp','/lib/exabgp/application/bgp.py','/lib/exabgp/application/control.py']) etc = root + '/etc/exabgp' os.environ['EXABGP_ETC'] = etc # This is not most pretty if options["--version"]: sys.stdout.write('ExaBGP : %s\n' % version) sys.stdout.write('Python : %s\n' % sys.version.replace('\n',' ')) sys.stdout.write('Uname : %s\n' % ' '.join(platform.uname()[:5])) sys.stdout.write('Root : %s\n' % root) sys.stdout.flush() sys.exit(0) envfile = get_envfile(options,etc) env = get_env(envfile) # 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() from exabgp.configuration.setup import environment if options["--decode"]: decode = ''.join(options["--decode"]).replace(':','').replace(' ','') if not is_bgp(decode): sys.stdout.write(usage) sys.stdout.write('Environment values are:\n%s\n\n' % '\n'.join(' - %s' % _ for _ in environment.default())) sys.stdout.write('The BGP message must be an hexadecimal string.\n\n') sys.stdout.write('All colons or spaces are ignored, for example:\n\n') sys.stdout.write(' --decode 001E0200000007900F0003000101\n') sys.stdout.write(' --decode 001E:02:0000:0007:900F:0003:0001:01\n') sys.stdout.write(' --decode FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF001E0200000007900F0003000101\n') sys.stdout.write(' --decode FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:001E:02:0000:0007:900F:0003:0001:01\n') sys.stdout.write(' --decode \'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF 001E02 00000007900F0003000101\n\'') sys.stdout.flush() sys.exit(1) else: decode = '' 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"]: sys.stdout.write(usage) sys.stdout.write('Environment values are:\n' + '\n'.join(' - %s' % _ for _ in environment.default())) sys.stdout.flush() 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(): sys.stdout.write('%s\n' % line) sys.stdout.flush() sys.exit(0) if options["--full-env"] or options["--fe"]: print() for line in environment.iter_env(): sys.stdout.write('%s\n' % line) sys.stdout.flush() sys.exit(0) if options["--diff-ini"] or options["--di"]: for line in environment.iter_ini(True): sys.stdout.write('%s\n' % line) sys.stdout.flush() sys.exit(0) if options["--diff-env"] or options["--de"]: for line in environment.iter_env(True): sys.stdout.write('%s\n' % line) sys.stdout.flush() 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(etc,f[11:]) if os.path.isfile(normalised): configurations.append(normalised) continue logger.debug('one of the arguments passed as configuration is not a file (%s)' % f,'configuration') sys.exit(1) else: sys.stdout.write(usage) sys.stdout.write('Environment values are:\n%s\n\n' % '\n'.join(' - %s' % _ for _ in environment.default())) sys.stdout.write('no configuration file provided') sys.stdout.flush() 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,root,options["--validate"]) if not (env.log.destination in ('syslog','stdout','stderr') or env.log.destination.startswith('host:')): logger.error('can not log to files when running multiple configuration (as we fork)','configuration') 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],root,options["--validate"],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.critical('can not fork, errno %d : %s' % (exc.errno,exc.strerror),'reactor') sys.exit(1)
def unpack_message(cls, data, negotiated): logger = Logger() logger.debug(LazyFormat('parsing UPDATE', data), 'parser') length = len(data) # This could be speed up massively by changing the order of the IF if length == 4 and data == b'\x00\x00\x00\x00': return EOR(AFI.ipv4, SAFI.unicast) # pylint: disable=E1101 if length == 11 and data.startswith(EOR.NLRI.PREFIX): return EOR.unpack_message(data, negotiated) withdrawn, _attributes, announced = cls.split(data) if not withdrawn: logger.debug('withdrawn NLRI none', 'routes') attributes = Attributes.unpack(_attributes, negotiated) if not announced: logger.debug('announced NLRI none', 'routes') # Is the peer going to send us some Path Information with the route (AddPath) addpath = negotiated.addpath.receive(AFI.ipv4, SAFI.unicast) # empty string for NoNextHop, the packed IP otherwise (without the 3/4 bytes of attributes headers) nexthop = attributes.get(Attribute.CODE.NEXT_HOP, NoNextHop) # nexthop = NextHop.unpack(_nexthop.ton()) # XXX: NEXTHOP MUST NOT be the IP address of the receiving speaker. nlris = [] while withdrawn: nlri, left = NLRI.unpack_nlri(AFI.ipv4, SAFI.unicast, withdrawn, IN.WITHDRAWN, addpath) logger.debug('withdrawn NLRI %s' % nlri, 'routes') withdrawn = left nlris.append(nlri) while announced: nlri, left = NLRI.unpack_nlri(AFI.ipv4, SAFI.unicast, announced, IN.ANNOUNCED, addpath) nlri.nexthop = nexthop logger.debug('announced NLRI %s' % nlri, 'routes') announced = left nlris.append(nlri) unreach = attributes.pop(MPURNLRI.ID, None) reach = attributes.pop(MPRNLRI.ID, None) if unreach is not None: nlris.extend(unreach.nlris) if reach is not None: nlris.extend(reach.nlris) if not attributes and not nlris: # Careful do not use == or != as the comparaison does not work if unreach is None and reach is None: return EOR(AFI.ipv4, SAFI.unicast) if unreach is not None: return EOR(unreach.afi, unreach.safi) if reach is not None: return EOR(reach.afi, reach.safi) raise RuntimeError('This was not expected') update = Update(nlris, attributes) def parsed(_): # we need the import in the function as otherwise we have an cyclic loop # as this function currently uses Update.. from exabgp.reactor.api.response import Response from exabgp.version import json as json_version return 'json %s' % Response.JSON(json_version).update( negotiated.neighbor, 'in', update, None, '', '') logger.debug(LazyFormat('decoded UPDATE', '', parsed), 'parser') return update
def check_neighbor(neighbors): logger = Logger() logger._option['parser'] = True logger.notice('\ndecoding routes in configuration', 'parser') for name in neighbors.keys(): neighbor = neighbors[name] path = {} for f in NLRI.known_families(): if neighbor.add_path: path[f] = neighbor.add_path capa = Capabilities().new(neighbor, False) if path: capa[Capability.CODE.ADD_PATH] = path capa[Capability.CODE.MULTIPROTOCOL] = neighbor.families() routerid_1 = str(neighbor.router_id) routerid_2 = '.'.join( str((int(_) + 1) % 250) for _ in str(neighbor.router_id).split('.', -1)) o1 = Open(Version(4), ASN(neighbor.local_as), HoldTime(180), RouterID(routerid_1), capa) o2 = Open(Version(4), ASN(neighbor.peer_as), HoldTime(180), RouterID(routerid_2), capa) negotiated = Negotiated(neighbor) negotiated.sent(o1) negotiated.received(o2) # grouped = False for _ in neighbor.rib.outgoing.updates(False): pass for change1 in neighbor.rib.outgoing.cached_changes(): str1 = change1.extensive() packed = list( Update([change1.nlri], change1.attributes).messages(negotiated)) pack1 = packed[0] logger.debug('parsed route requires %d updates' % len(packed), 'parser') logger.debug('update size is %d' % len(pack1), 'parser') logger.debug('parsed route %s' % str1, 'parser') logger.debug('parsed hex %s' % od(pack1), 'parser') # This does not take the BGP header - let's assume we will not break that :) try: logger.debug('') # new line pack1s = pack1[19:] if pack1.startswith(b'\xFF' * 16) else pack1 update = Update.unpack_message(pack1s, negotiated) change2 = Change(update.nlris[0], update.attributes) str2 = change2.extensive() pack2 = list( Update([update.nlris[0]], update.attributes).messages(negotiated))[0] logger.debug('recoded route %s' % str2, 'parser') logger.debug('recoded hex %s' % od(pack2), 'parser') str1 = str1.replace('attribute [ 0x04 0x80 0x00000064 ]', 'med 100') str1r = str1.lower().replace(' med 100', '').replace( ' local-preference 100', '').replace(' origin igp', '') str2r = str2.lower().replace(' med 100', '').replace( ' local-preference 100', '').replace(' origin igp', '') str2r = str2r.replace( 'large-community [ 1:2:3 10:11:12 ]', 'attribute [ 0x20 0xc0 0x0000000100000002000000030000000a0000000b0000000c ]' ) if 'next-hop self' in str1r: if ':' in str1r: str1r = str1r.replace('next-hop self', 'next-hop ::1') else: str1r = str1r.replace( 'next-hop self', 'next-hop %s' % neighbor.local_address) if ' name ' in str1r: parts = str1r.split(' ') pos = parts.index('name') str1r = ' '.join(parts[:pos] + parts[pos + 2:]) skip = False if str1r != str2r: if 'attribute [' in str1r and ' 0x00 ' in str1r: # we do not decode non-transitive attributes logger.debug( 'skipping string check on update with non-transitive attribute(s)', 'parser') skip = True else: logger.debug('strings are different:', 'parser') logger.debug('[%s]' % (str1r), 'parser') logger.debug('[%s]' % (str2r), 'parser') return False else: logger.debug('strings are fine', 'parser') if skip: logger.debug( 'skipping encoding for update with non-transitive attribute(s)', 'parser') elif pack1 != pack2: logger.debug('encoding are different', 'parser') logger.debug('[%s]' % (od(pack1)), 'parser') logger.debug('[%s]' % (od(pack2)), 'parser') return False else: logger.debug('encoding is fine', 'parser') logger.debug('----------------------------------------', 'parser') logger.debug('JSON nlri %s' % change1.nlri.json(), 'parser') logger.debug('JSON attr %s' % change1.attributes.json(), 'parser') except Notify as exc: logger.debug('----------------------------------------', 'parser') logger.debug(str(exc), 'parser') logger.debug('----------------------------------------', 'parser') return False neighbor.rib.clear() return True