Example #1
0
class Handler (object):
	callback = {
		'text': {},
		'json': {},
	}

	# need to sort and reverse, in order for the shorter command to not used by error
	# "show neighbor" should not match "show neighbors"
	functions = sorted([
		'withdraw watchdog',
		'withdraw vpls',
		'withdraw route',
		'withdraw flow',
		'withdraw attribute',
		'version',
		'teardown',
		'shutdown',
		'show routes extensive',
		'show routes',
		'show neighbors',
		'show neighbor',
		'restart',
		'reload',
		'flush route',
		'announce watchdog',
		'announce vpls',
		'announce route-refresh',
		'announce route',
		'announce operational',
		'announce flow',
		'announce eor',
		'announce attribute'
	],reverse=True)

	def __init__ (self):
		self.logger = Logger()
		self.parser = Parser.Text()

		try:
			for name in self.functions:
				self.callback['text'][name] = Command.Text.callback[name]
		except KeyError:
			raise RuntimeError('The code does not have an implementation for "%s", please code it !' % name)

	def text (self, reactor, service, command):
		for registered in self.functions:
			if registered in command:
				self.logger.reactor("callback | handling '%s' with %s" % (command,self.callback['text'][registered].func_name),'warning')
				self.callback['text'][registered](self,reactor,service,command)
				return True
		self.logger.reactor("Command from process not understood : %s" % command,'warning')
		return False
Example #2
0
File: bgp.py Project: fobser/exabgp
def run (env, comment, configurations, pid=0):
	from exabgp.logger import Logger
	logger = Logger()

	if comment:
		logger.configuration(comment)

	if not env.profile.enable:
		ok = Reactor(configurations).run()
		__exit(env.debug.memory,0 if ok else 1)

	try:
		import cProfile as profile
	except ImportError:
		import profile

	if not env.profile.file or env.profile.file == 'stdout':
		ok = profile.run('Reactor(configurations).run()')
		__exit(env.debug.memory,0 if ok else 1)

	if pid:
		profile_name = "%s-pid-%d" % (env.profile.file,pid)
	else:
		profile_name = env.profile.file

	notice = ''
	if os.path.isdir(profile_name):
		notice = 'profile can not use this filename as outpout, it is not a directory (%s)' % profile_name
	if os.path.exists(profile_name):
		notice = 'profile can not use this filename as outpout, it already exists (%s)' % profile_name

	if not notice:
		logger.reactor('profiling ....')
		profiler = profile.Profile()
		profiler.enable()
		try:
			ok = Reactor(configurations).run()
		except Exception:
			raise
		finally:
			profiler.disable()
			kprofile = lsprofcalltree.KCacheGrind(profiler)

			with open(profile_name, 'w+') as write:
				kprofile.output(write)

			__exit(env.debug.memory,0 if ok else 1)
	else:
		logger.reactor("-"*len(notice))
		logger.reactor(notice)
		logger.reactor("-"*len(notice))
		Reactor(configurations).run()
		__exit(env.debug.memory,1)
Example #3
0
class Decoder (object):
	storage = {}

	def __init__ (self):
		self.logger = Logger()
		self.format = Text()

	# callaback code

	@classmethod
	def register_command (cls, command, function):
		cls.storage[command] = function
		return function

	def parse_command (self, reactor, service, command):
		# it must be reversed so longer command are found before the shorter
		# "show neighbor" should not match "show neighbors"
		for registered in sorted(self.storage, reverse=True):
			if registered in command:
				return self.storage[registered](self,reactor,service,command)
		self.logger.reactor("Command from process not understood : %s" % command,'warning')
		return False
Example #4
0
class Decoder (object):
	storage = {}

	def __init__ (self):
		self.logger = Logger()
		self.format = Text()

	# callaback code

	@classmethod
	def register_command (cls, command, function):
		cls.storage[command] = function
		return function

	def parse_command (self, reactor, service, command):
		# it must be reversed so longer command are found before the shorter
		# "show neighbor" should not match "show neighbors"
		for registered in sorted(self.storage, reverse=True):
			if registered in command:
				return self.storage[registered](self,reactor,service,command)
		self.logger.reactor("Command from process not understood : %s" % command,'warning')
		return False
Example #5
0
		for configuration in configurations:
			pid = os.fork()
			if pid == 0:
				run(env,comment,[configuration],os.getpid())
			else:
				pids.append(pid)

		# If we get a ^C / SIGTERM, ignore just continue waiting for our child process
		import signal
		signal.signal(signal.SIGINT, signal.SIG_IGN)

		# wait for the forked processes
		for pid in pids:
			os.waitpid(pid,0)
	except OSError,exc:
		logger.reactor('Can not fork, errno %d : %s' % (exc.errno,exc.strerror),'critical')
		sys.exit(1)

def run (env, comment, configurations, pid=0):
	logger = Logger()

	if comment:
		logger.configuration(comment)

	if not env.profile.enable:
		ok = Reactor(configurations).run()
		__exit(env.debug.memory,0 if ok else 1)

	try:
		import cProfile as profile
	except ImportError:
Example #6
0
class Daemon (object):

	def __init__ (self, reactor):
		self.pid = environment.settings().daemon.pid
		self.user = environment.settings().daemon.user
		self.daemonize = environment.settings().daemon.daemonize
		self.umask = environment.settings().daemon.umask

		self.logger = Logger()

		self.reactor = reactor

		os.chdir('/')
		os.umask(self.umask)

	def check_pid (self,pid):
		if pid < 0:  # user input error
			return False
		if pid == 0:  # all processes
			return False
		try:
			os.kill(pid, 0)
			return True
		except OSError as err:
			if err.errno == errno.EPERM:  # a process we were denied access to
				return True
			if err.errno == errno.ESRCH:  # No such process
				return False
			# should never happen
			return False

	def savepid (self):
		self._saved_pid = False

		if not self.pid:
			return True

		ownid = os.getpid()

		flags = os.O_CREAT | os.O_EXCL | os.O_WRONLY
		mode = ((os.R_OK | os.W_OK) << 6) | (os.R_OK << 3) | os.R_OK

		try:
			fd = os.open(self.pid,flags,mode)
		except OSError:
			try:
				pid = open(self.pid,'r').readline().strip()
				if self.check_pid(int(pid)):
					self.logger.daemon("PIDfile already exists and program still running %s" % self.pid)
					return False
				else:
					# If pid is not running, reopen file without O_EXCL
					fd = os.open(self.pid,flags ^ os.O_EXCL,mode)
			except (OSError,IOError,ValueError):
				pass

		try:
			f = os.fdopen(fd,'w')
			line = "%d\n" % ownid
			f.write(line)
			f.close()
			self._saved_pid = True
		except IOError:
			self.logger.daemon("Can not create PIDfile %s" % self.pid,'warning')
			return False
		self.logger.daemon("Created PIDfile %s with value %d" % (self.pid,ownid),'warning')
		return True

	def removepid (self):
		if not self.pid or not self._saved_pid:
			return
		try:
			os.remove(self.pid)
		except OSError as exc:
			if exc.errno == errno.ENOENT:
				pass
			else:
				self.logger.daemon("Can not remove PIDfile %s" % self.pid,'error')
				return
		self.logger.daemon("Removed PIDfile %s" % self.pid)

	def drop_privileges (self):
		"""return true if we are left with insecure privileges"""
		# os.name can be ['posix', 'nt', 'os2', 'ce', 'java', 'riscos']
		if os.name not in ['posix',]:
			return True

		uid = os.getuid()
		gid = os.getgid()

		if uid and gid:
			return True

		try:
			user = pwd.getpwnam(self.user)
			nuid = int(user.pw_uid)
			ngid = int(user.pw_gid)
		except KeyError:
			return False

		# not sure you can change your gid if you do not have a pid of zero
		try:
			# we must change the GID first otherwise it may fail after change UID
			if not gid:
				os.setgid(ngid)
			if not uid:
				os.setuid(nuid)

			cuid = os.getuid()
			ceid = os.geteuid()
			cgid = os.getgid()

			if cuid < 0:
				cuid += (1 << 32)

			if cgid < 0:
				cgid += (1 << 32)

			if ceid < 0:
				ceid += (1 << 32)

			if nuid != cuid or nuid != ceid or ngid != cgid:
				return False

		except OSError:
			return False

		return True

	def _is_socket (self, fd):
		try:
			s = socket.fromfd(fd, socket.AF_INET, socket.SOCK_RAW)
		except ValueError:
			# The file descriptor is closed
			return False
		try:
			s.getsockopt(socket.SOL_SOCKET, socket.SO_TYPE)
		except socket.error as exc:
			# It is look like one but it is not a socket ...
			if exc.args[0] == errno.ENOTSOCK:
				return False
		return True

	def daemonise (self):
		if not self.daemonize:
			return

		log = environment.settings().log
		if log.enable and log.destination.lower() in ('stdout','stderr'):
			self.logger.daemon('ExaBGP can not fork when logs are going to %s' % log.destination.lower(),'critical')
			return

		def fork_exit ():
			try:
				pid = os.fork()
				if pid > 0:
					os._exit(0)
			except OSError as exc:
				self.logger.reactor('Can not fork, errno %d : %s' % (exc.errno,exc.strerror),'critical')

		# do not detach if we are already supervised or run by init like process
		if self._is_socket(sys.__stdin__.fileno()) or os.getppid() == 1:
			return

		fork_exit()
		os.setsid()
		fork_exit()
		self.silence()

	def silence (self):
		# closing more would close the log file too if open
		maxfd = 3

		for fd in range(0, maxfd):
			try:
				os.close(fd)
			except OSError:
				pass
		os.open("/dev/null", os.O_RDWR)
		os.dup2(0, 1)
		os.dup2(0, 2)
Example #7
0
class API (object):
	callback = {
		'text': {},
		'json': {},
	}

	# need to sort and reverse, in order for the shorter command to not used by error
	# "show neighbor" should not match "show neighbors"
	functions = sorted([
		'show neighbor',
		'show neighbors',
		'show neighbor status',
		'show routes',
		'show routes static',
		'show routes flow',
		'show routes l2vpn',
		'show routes extensive',
		'announce operational',
		'announce attributes',
		'announce eor',
		'announce flow',
		'announce route',
		'announce route-refresh',
		'announce vpls',
		'announce watchdog',
		'withdraw attributes',
		'withdraw flow',
		'withdraw route',
		'withdraw vpls',
		'withdraw watchdog',
		'flush route',
		'teardown',
		'version',
		'restart',
		'reload',
		'shutdown',
		'#',
	],reverse=True)

	def __init__ (self,reactor):
		self.reactor = reactor
		self.logger = Logger()
		self.parser = Parser.Text(reactor)

		try:
			for name in self.functions:
				self.callback['text'][name] = Command.Text.callback[name]
		except KeyError:
			raise RuntimeError('The code does not have an implementation for "%s", please code it !' % name)

	def log_message (self, message, level='info'):
		self.logger.reactor(message,level)

	def log_failure (self, message, level='error'):
		error = str(self.parser.configuration.tokeniser.error)
		report = '%s\nreason: %s' % (message, error) if error else message
		self.logger.reactor(report,level)

	def text (self, reactor, service, command):
		for registered in self.functions:
			if registered in command:
				self.logger.reactor("callback | handling '%s' with %s" % (command,self.callback['text'][registered].func_name),'warning')
				# XXX: should we not test the return value ?
				self.callback['text'][registered](self,reactor,service,command)
				# reactor.plan(self.callback['text'][registered](self,reactor,service,command),registered)
				return True
		self.logger.reactor("Command from process not understood : %s" % command,'warning')
		return False

	def shutdown (self):
		self.reactor.api_shutdown()
		return True

	def reload (self):
		self.reactor.api_reload()
		return True

	def restart (self):
		self.reactor.api_restart()
		return True
Example #8
0
def run(env, comment, configurations, root, validate, pid=0):
    logger = Logger()

    logger.info('Thank you for using ExaBGP', source='welcome')
    logger.info('%s' % version, source='version')
    logger.info('%s' % sys.version.replace('\n', ' '), source='interpreter')
    logger.info('%s' % ' '.join(platform.uname()[:5]), source='os')
    logger.info('%s' % root, source='installation')

    if comment:
        logger.info(comment, source='advice')

    warning = warn()
    if warning:
        logger.info(warning, source='advice')

    if env.api.cli:
        pipes = named_pipe(root)
        if len(pipes) != 1:
            env.api.cli = False
            logger.error(
                'Could not find the named pipes (exabgp.in and exabgp.out) required for the cli',
                source='cli')
            logger.error(
                'We scanned the following folders (the number is your PID):',
                source='cli')
            for location in pipes:
                logger.error(' - %s' % location, source='cli control')
            logger.error(
                'please make them in one of the folder with the following commands:',
                source='cli control')
            logger.error('> mkfifo %s/run/exabgp.{in,out}' % os.getcwd(),
                         source='cli control')
            logger.error('> chmod 600 %s/run/exabgp.{in,out}' % os.getcwd(),
                         source='cli control')
            if os.getuid() != 0:
                logger.error('> chown %d:%d %s/run/exabgp.{in,out}' %
                             (os.getuid(), os.getgid(), os.getcwd()),
                             source='cli control')
        else:
            pipe = pipes[0]
            os.environ['exabgp_cli_pipe'] = pipe

            logger.info('named pipes for the cli are:', source='cli control')
            logger.info('to send commands  %sexabgp.in' % pipe,
                        source='cli control')
            logger.info('to read responses %sexabgp.out' % pipe,
                        source='cli control')

    if not env.profile.enable:
        was_ok = Reactor(configurations).run(validate, root)
        __exit(env.debug.memory, 0 if was_ok else 1)

    try:
        import cProfile as profile
    except ImportError:
        import profile

    if env.profile.file == 'stdout':
        profiled = 'Reactor(%s).run(%s,"%s")' % (str(configurations),
                                                 str(validate), str(root))
        was_ok = profile.run(profiled)
        __exit(env.debug.memory, 0 if was_ok else 1)

    if pid:
        profile_name = "%s-pid-%d" % (env.profile.file, pid)
    else:
        profile_name = env.profile.file

    notice = ''
    if os.path.isdir(profile_name):
        notice = 'profile can not use this filename as output, it is not a directory (%s)' % profile_name
    if os.path.exists(profile_name):
        notice = 'profile can not use this filename as output, it already exists (%s)' % profile_name

    if not notice:
        cwd = os.getcwd()
        logger.reactor('profiling ....')
        profiler = profile.Profile()
        profiler.enable()
        try:
            was_ok = Reactor(configurations).run(validate, root)
        except Exception:
            was_ok = False
            raise
        finally:
            profiler.disable()
            kprofile = lsprofcalltree.KCacheGrind(profiler)
            try:
                destination = profile_name if profile_name.startswith(
                    '/') else os.path.join(cwd, profile_name)
                with open(destination, 'w+') as write:
                    kprofile.output(write)
            except IOError:
                notice = 'could not save profiling in formation at: ' + destination
                logger.reactor("-" * len(notice))
                logger.reactor(notice)
                logger.reactor("-" * len(notice))
            __exit(env.debug.memory, 0 if was_ok else 1)
    else:
        logger.reactor("-" * len(notice))
        logger.reactor(notice)
        logger.reactor("-" * len(notice))
        Reactor(configurations).run(validate, root)
        __exit(env.debug.memory, 1)
Example #9
0
class Reactor (object):
	# [hex(ord(c)) for c in os.popen('clear').read()]
	clear = b''.join([chr_(int(c,16)) for c in ['0x1b', '0x5b', '0x48', '0x1b', '0x5b', '0x32', '0x4a']])

	def __init__ (self, configurations):
		self.ip = environment.settings().tcp.bind
		self.port = environment.settings().tcp.port
		self.respawn = environment.settings().api.respawn

		self.max_loop_time = environment.settings().reactor.speed
		self.early_drop = environment.settings().daemon.drop

		self.logger = Logger()
		self.daemon = Daemon(self)
		self.processes = None
		self.listener = None
		self.configuration = Configuration(configurations)
		self.api = API(self)

		self.peers = {}
		self.route_update = False

		self._stopping = environment.settings().tcp.once
		self._shutdown = False
		self._reload = False
		self._reload_processes = False
		self._restart = False
		self._saved_pid = False
		self._pending = deque()
		self._running = None

		signal.signal(signal.SIGTERM, self.sigterm)
		signal.signal(signal.SIGHUP, self.sighup)
		signal.signal(signal.SIGALRM, self.sigalrm)
		signal.signal(signal.SIGUSR1, self.sigusr1)
		signal.signal(signal.SIGUSR2, self.sigusr2)

	def sigterm (self, signum, frame):
		self.logger.reactor('SIG TERM received - shutdown')
		self._shutdown = True

	def sighup (self, signum, frame):
		self.logger.reactor('SIG HUP received - shutdown')
		self._shutdown = True

	def sigalrm (self, signum, frame):
		self.logger.reactor('SIG ALRM received - restart')
		self._restart = True

	def sigusr1 (self, signum, frame):
		self.logger.reactor('SIG USR1 received - reload configuration')
		self._reload = True

	def sigusr2 (self, signum, frame):
		self.logger.reactor('SIG USR2 received - reload configuration and processes')
		self._reload = True
		self._reload_processes = True

	def ready (self, sockets, ios, sleeptime=0):
		# never sleep a negative number of second (if the rounding is negative somewhere)
		# never sleep more than one second (should the clock time change during two time.time calls)
		sleeptime = min(max(0.0,sleeptime),1.0)
		if not ios:
			time.sleep(sleeptime)
			return []
		try:
			read,_,_ = select.select(sockets+ios,[],[],sleeptime)
			return read
		except select.error as exc:
			errno,message = exc.args  # pylint: disable=W0633
			if errno not in error.block:
				raise exc
			return []
		except socket.error as exc:
			if exc.errno in error.fatal:
				raise exc
			return []

	def run (self):
		self.daemon.daemonise()

		# Make sure we create processes once we have closed file descriptor
		# unfortunately, this must be done before reading the configuration file
		# so we can not do it with dropped privileges
		self.processes = Processes(self)

		# we have to read the configuration possibly with root privileges
		# as we need the MD5 information when we bind, and root is needed
		# to bind to a port < 1024

		# this is undesirable as :
		# - handling user generated data as root should be avoided
		# - we may not be able to reload the configuration once the privileges are dropped

		# but I can not see any way to avoid it

		if not self.load():
			return False

		try:
			self.listener = Listener()

			if self.ip:
				self.listener.listen(IP.create(self.ip),IP.create('0.0.0.0'),self.port,None,None)
				self.logger.reactor('Listening for BGP session(s) on %s:%d' % (self.ip,self.port))

			for neighbor in self.configuration.neighbors.values():
				if neighbor.listen:
					self.listener.listen(neighbor.md5_ip,neighbor.peer_address,neighbor.listen,neighbor.md5_password,neighbor.ttl_in)
					self.logger.reactor('Listening for BGP session(s) on %s:%d%s' % (neighbor.md5_ip,neighbor.listen,' with MD5' if neighbor.md5_password else ''))
		except NetworkError as exc:
			self.listener = None
			if os.geteuid() != 0 and self.port <= 1024:
				self.logger.reactor('Can not bind to %s:%d, you may need to run ExaBGP as root' % (self.ip,self.port),'critical')
			else:
				self.logger.reactor('Can not bind to %s:%d (%s)' % (self.ip,self.port,str(exc)),'critical')
			self.logger.reactor('unset exabgp.tcp.bind if you do not want listen for incoming connections','critical')
			self.logger.reactor('and check that no other daemon is already binding to port %d' % self.port,'critical')
			sys.exit(1)

		if not self.early_drop:
			self.processes.start()

		if not self.daemon.drop_privileges():
			self.logger.reactor('Could not drop privileges to \'%s\' refusing to run as root' % self.daemon.user,'critical')
			self.logger.reactor('Set the environmemnt value exabgp.daemon.user to change the unprivileged user','critical')
			return

		if self.early_drop:
			self.processes.start()

		# This is required to make sure we can write in the log location as we now have dropped root privileges
		if not self.logger.restart():
			self.logger.reactor('Could not setup the logger, aborting','critical')
			return

		if not self.daemon.savepid():
			return

		# did we complete the run of updates caused by the last SIGUSR1/SIGUSR2 ?
		reload_completed = True

		wait = environment.settings().tcp.delay
		if wait:
			sleeptime = (wait * 60) - int(time.time()) % (wait * 60)
			self.logger.reactor('waiting for %d seconds before connecting' % sleeptime)
			time.sleep(float(sleeptime))

		workers = {}
		peers = set()
		scheduled = False

		while True:
			try:
				finished = False
				start = time.time()
				end = start + self.max_loop_time

				if self._shutdown:
					self._shutdown = False
					self.shutdown()
					break

				if self._reload and reload_completed:
					self._reload = False
					self.load()
					self.processes.start(self._reload_processes)
					self._reload_processes = False
				elif self._restart:
					self._restart = False
					self.restart()

				# We got some API routes to announce
				if self.route_update:
					self.route_update = False
					self.route_send()

				for peer in self.peers.keys():
					peers.add(peer)

				while start < time.time() < end and not finished:
					if self.peers:
						for key in list(peers):
							peer = self.peers[key]
							action = peer.run()

							# .run() returns an ACTION enum:
							# * immediate if it wants to be called again
							# * later if it should be called again but has no work atm
							# * close if it is finished and is closing down, or restarting
							if action == ACTION.CLOSE:
								self.unschedule(peer)
								peers.discard(key)
							# we are loosing this peer, not point to schedule more process work
							elif action == ACTION.LATER:
								for io in peer.sockets():
									workers[io] = key
								# no need to come back to it before a a full cycle
								peers.discard(key)

					if not peers:
						reload_completed = True

					if self.listener:
						for connection in self.listener.connected():
							# found
							# * False, not peer found for this TCP connection
							# * True, peer found
							# * None, conflict found for this TCP connections
							found = False
							for key in self.peers:
								peer = self.peers[key]
								neighbor = peer.neighbor
								# XXX: FIXME: Inet can only be compared to Inet
								if connection.local == str(neighbor.peer_address) and connection.peer == str(neighbor.local_address):
									if peer.incoming(connection):
										found = True
										break
									found = None
									break

							if found:
								self.logger.reactor('accepted connection from  %s - %s' % (connection.local,connection.peer))
							elif found is False:
								self.logger.reactor('no session configured for  %s - %s' % (connection.local,connection.peer))
								connection.notification(6,3,'no session configured for the peer')
								connection.close()
							elif found is None:
								self.logger.reactor('connection refused (already connected to the peer) %s - %s' % (connection.local,connection.peer))
								connection.notification(6,5,'could not accept the connection')
								connection.close()

					scheduled = self.schedule()
					finished = not peers and not scheduled

				# RFC state that we MUST not send more than one KEEPALIVE / sec
				# And doing less could cause the session to drop

				if finished:
					for io in self.ready(list(peers),self.processes.fds(),end-time.time()):
						if io in workers:
							peers.add(workers[io])
							del workers[io]

				if self._stopping and not self.peers.keys():
					break

			except KeyboardInterrupt:
				while True:
					try:
						self._shutdown = True
						self.logger.reactor('^C received')
						break
					except KeyboardInterrupt:
						pass
			# socket.error is a subclass of IOError (so catch it first)
			except socket.error:
				try:
					self._shutdown = True
					self.logger.reactor('socket error received','warning')
					break
				except KeyboardInterrupt:
					pass
			except IOError:
				while True:
					try:
						self._shutdown = True
						self.logger.reactor('I/O Error received, most likely ^C during IO','warning')
						break
					except KeyboardInterrupt:
						pass
			except SystemExit:
				try:
					self._shutdown = True
					self.logger.reactor('exiting')
					break
				except KeyboardInterrupt:
					pass
			except ProcessError:
				try:
					self._shutdown = True
					self.logger.reactor('Problem when sending message(s) to helper program, stopping','error')
				except KeyboardInterrupt:
					pass
			except select.error:
				try:
					self._shutdown = True
					self.logger.reactor('problem using select, stopping','error')
				except KeyboardInterrupt:
					pass
				# from exabgp.leak import objgraph
				# print objgraph.show_most_common_types(limit=20)
				# import random
				# obj = objgraph.by_type('Route')[random.randint(0,2000)]
				# objgraph.show_backrefs([obj], max_depth=10)

	def shutdown (self):
		"""terminate all the current BGP connections"""
		self.logger.reactor('performing shutdown')
		if self.listener:
			self.listener.stop()
			self.listener = None
		for key in self.peers.keys():
			self.peers[key].stop()
		self.processes.terminate()
		self.daemon.removepid()
		self._stopping = True

	def load (self):
		"""reload the configuration and send to the peer the route which changed"""
		self.logger.reactor('performing reload of exabgp %s' % version)

		reloaded = self.configuration.reload()

		if not reloaded:
			#
			# Careful the string below is used but the QA code to check for sucess of failure
			self.logger.configuration('problem with the configuration file, no change done','error')
			# Careful the string above is used but the QA code to check for sucess of failure
			#
			self.logger.configuration(str(self.configuration.error),'error')
			return False

		for key, peer in self.peers.items():
			if key not in self.configuration.neighbors:
				self.logger.reactor('removing peer: %s' % peer.neighbor.name())
				peer.stop()

		for key, neighbor in self.configuration.neighbors.items():
			# new peer
			if key not in self.peers:
				self.logger.reactor('new peer: %s' % neighbor.name())
				peer = Peer(neighbor,self)
				self.peers[key] = peer
			# modified peer
			elif self.peers[key].neighbor != neighbor:
				self.logger.reactor('peer definition change, establishing a new connection for %s' % str(key))
				self.peers[key].reestablish(neighbor)
			# same peer but perhaps not the routes
			else:
				# finding what route changed and sending the delta is not obvious
				self.logger.reactor('peer definition identical, updating peer routes if required for %s' % str(key))
				self.peers[key].reconfigure(neighbor)
		self.logger.configuration('loaded new configuration successfully','info')

		return True

	def schedule (self):
		try:
			# read at least on message per process if there is some and parse it
			for service,command in self.processes.received():
				self.api.text(self,service,command)

			# if we have nothing to do, return or save the work
			if not self._running:
				if not self._pending:
					return False
				self._running,name = self._pending.popleft()
				self.logger.reactor('callback | installing %s' % name)

			if self._running:
				# run it
				try:
					self.logger.reactor('callback | running')
					six.next(self._running)  # run
					# should raise StopIteration in most case
					# and prevent us to have to run twice to run one command
					six.next(self._running)  # run
				except StopIteration:
					self._running = None
					self.logger.reactor('callback | removing')
				return True

		except StopIteration:
			pass
		except KeyboardInterrupt:
			self._shutdown = True
			self.logger.reactor('^C received','error')

	def route_send (self):
		"""the process ran and we need to figure what routes to changes"""
		self.logger.reactor('performing dynamic route update')
		for key in self.configuration.neighbors.keys():
			self.peers[key].send_new()
		self.logger.reactor('updated peers dynamic routes successfully')

	def restart (self):
		"""kill the BGP session and restart it"""
		self.logger.reactor('performing restart of exabgp %s' % version)
		self.configuration.reload()

		for key in self.peers.keys():
			if key not in self.configuration.neighbors.keys():
				neighbor = self.configuration.neighbors[key]
				self.logger.reactor('removing Peer %s' % neighbor.name())
				self.peers[key].stop()
			else:
				self.peers[key].reestablish()
		self.processes.terminate()
		self.processes.start()

	def unschedule (self, peer):
		key = peer.neighbor.name()
		if key in self.peers:
			del self.peers[key]

	def answer (self, service, string):
		self.processes.write(service,string)
		self.logger.reactor('responding to %s : %s' % (service,string.replace('\n','\\n')))

	def api_shutdown (self):
		self._shutdown = True
		self._pending = deque()
		self._running = None

	def api_reload (self):
		self._reload = True
		self._pending = deque()
		self._running = None

	def api_restart (self):
		self._restart = True
		self._pending = deque()
		self._running = None

	@staticmethod
	def match_neighbor (description, name):
		for string in description:
			if re.search(r'(^|[\s])%s($|[\s,])' % re.escape(string), name) is None:
				return False
		return True

	def match_neighbors (self, descriptions):
		"""return the sublist of peers matching the description passed, or None if no description is given"""
		if not descriptions:
			return self.peers.keys()

		returned = []
		for key in self.peers:
			for description in descriptions:
				if Reactor.match_neighbor(description,key):
					if key not in returned:
						returned.append(key)
		return returned

	def nexthops (self, peers):
		return dict((peer,self.peers[peer].neighbor.local_address) for peer in peers)

	def plan (self, callback,name):
		self._pending.append((callback,name))
Example #10
0
File: api.py Project: Shmuma/exabgp
class API (object):
	callback = {
		'text': {},
		'json': {},
	}

	# need to sort and reverse, in order for the shorter command to not used by error
	# "show neighbor" should not match "show neighbors"
	functions = sorted([
		'withdraw watchdog',
		'withdraw vpls',
		'withdraw route',
		'withdraw flow',
		'withdraw attribute',
		'version',
		'teardown',
		'shutdown',
		'show routes extensive',
		'show routes',
		'show neighbors',
		'show neighbor',
		'restart',
		'reload',
		'flush route',
		'announce watchdog',
		'announce vpls',
		'announce route-refresh',
		'announce route',
		'announce flow',
		'announce eor',
		'announce attribute',
		'announce operational',
	],reverse=True)

	def __init__ (self,reactor):
		self.reactor = reactor
		self.logger = Logger()
		self.parser = Parser.Text()

		try:
			for name in self.functions:
				self.callback['text'][name] = Command.Text.callback[name]
		except KeyError:
			raise RuntimeError('The code does not have an implementation for "%s", please code it !' % name)

	def text (self, reactor, service, command):
		for registered in self.functions:
			if registered in command:
				self.logger.reactor("callback | handling '%s' with %s" % (command,self.callback['text'][registered].func_name),'warning')
				# XXX: should we not test the return value ?
				self.callback['text'][registered](self,reactor,service,command)
				return True
		self.logger.reactor("Command from process not understood : %s" % command,'warning')
		return False

	def change_to_peers (self, change, peers):
		neighbors = self.reactor.configuration.neighbor.neighbors
		result = True
		for neighbor in neighbors:
			if neighbor in peers:
				if change.nlri.family() in neighbors[neighbor].families():
					neighbors[neighbor].rib.outgoing.insert_announced(change)
				else:
					self.logger.configuration('the route family is not configured on neighbor','error')
					result = False
		return result

	def eor_to_peers (self, family, peers):
		neighbors = self.reactor.configuration.neighbor.neighbors
		result = False
		for neighbor in neighbors:
			if neighbor in peers:
				result = True
				neighbors[neighbor].eor.append(family)
		return result

	def operational_to_peers (self, operational, peers):
		neighbors = self.reactor.configuration.neighbor.neighbors
		result = True
		for neighbor in neighbors:
			if neighbor in peers:
				if operational.family() in neighbors[neighbor].families():
					if operational.name == 'ASM':
						neighbors[neighbor].asm[operational.family()] = operational
					neighbors[neighbor].messages.append(operational)
				else:
					self.logger.configuration('the route family is not configured on neighbor','error')
					result = False
		return result

	def refresh_to_peers (self, refresh, peers):
		neighbors = self.reactor.configuration.neighbor.neighbors
		result = True
		for neighbor in neighbors:
			if neighbor in peers:
				family = (refresh.afi,refresh.safi)
				if family in neighbors[neighbor].families():
					neighbors[neighbor].refresh.append(refresh.__class__(refresh.afi,refresh.safi))
				else:
					result = False
		return result

	def shutdown (self):
		self.reactor.api_shutdown()
		return True

	def reload (self):
		self.reactor.api_reload()
		return True

	def restart (self):
		self.reactor.api_restart()
		return True
Example #11
0
class Reactor(object):
    # [hex(ord(c)) for c in os.popen('clear').read()]
    clear = concat_bytes_i(
        character(int(c, 16))
        for c in ['0x1b', '0x5b', '0x48', '0x1b', '0x5b', '0x32', '0x4a'])

    def __init__(self, configurations):
        self.ips = environment.settings().tcp.bind
        self.port = environment.settings().tcp.port
        self.ack = environment.settings().api.ack

        self.max_loop_time = environment.settings().reactor.speed
        self.early_drop = environment.settings().daemon.drop

        self.logger = Logger()
        self.daemon = Daemon(self)
        self.processes = None
        self.listener = None
        self.configuration = Configuration(configurations)
        self.api = API(self)

        self.peers = {}
        self.route_update = False

        self._stopping = environment.settings().tcp.once
        self._shutdown = False
        self._reload = False
        self._reload_processes = False
        self._restart = False
        self._saved_pid = False
        self._running = None
        self._pending = deque()
        self._async = deque()

        self._signal = {}

        signal.signal(signal.SIGTERM, self.sigterm)
        signal.signal(signal.SIGHUP, self.sighup)
        signal.signal(signal.SIGALRM, self.sigalrm)
        signal.signal(signal.SIGUSR1, self.sigusr1)
        signal.signal(signal.SIGUSR2, self.sigusr2)

    def _termination(self, reason):
        while True:
            try:
                self._shutdown = True
                self.logger.reactor(reason, 'warning')
                break
            except KeyboardInterrupt:
                pass

    def sigterm(self, signum, frame):
        self.logger.reactor('SIG TERM received - shutdown')
        self._shutdown = True
        for key in self.peers:
            if self.peers[key].neighbor.api['signal']:
                self._signal[key] = signum

    def sighup(self, signum, frame):
        self.logger.reactor('SIG HUP received - shutdown')
        self._shutdown = True
        for key in self.peers:
            if self.peers[key].neighbor.api['signal']:
                self._signal[key] = signum

    def sigalrm(self, signum, frame):
        self.logger.reactor('SIG ALRM received - restart')
        self._restart = True
        for key in self.peers:
            if self.peers[key].neighbor.api['signal']:
                self._signal[key] = signum

    def sigusr1(self, signum, frame):
        self.logger.reactor('SIG USR1 received - reload configuration')
        self._reload = True
        for key in self.peers:
            if self.peers[key].neighbor.api['signal']:
                self._signal[key] = signum

    def sigusr2(self, signum, frame):
        self.logger.reactor(
            'SIG USR2 received - reload configuration and processes')
        self._reload = True
        self._reload_processes = True
        for key in self.peers:
            if self.peers[key].neighbor.api['signal']:
                self._signal[key] = signum

    def _api_ready(self, sockets):
        sleeptime = self.max_loop_time / 20
        fds = self.processes.fds()
        ios = fds + sockets
        try:
            read, _, _ = select.select(ios, [], [], sleeptime)
            for fd in fds:
                if fd in read:
                    read.remove(fd)
            return read
        except select.error as exc:
            errno, message = exc.args  # pylint: disable=W0633
            if errno not in error.block:
                raise exc
            return []
        except socket.error as exc:
            if exc.errno in error.fatal:
                raise exc
            return []
        except KeyboardInterrupt:
            self._termination('^C received')
            return []

    def _setup_listener(self, local_addr, remote_addr, port, md5_password,
                        md5_base64, ttl_in):
        try:
            if not self.listener:
                self.listener = Listener()
            if not remote_addr:
                remote_addr = IP.create(
                    '0.0.0.0') if local_addr.ipv4() else IP.create('::')
            self.listener.listen(local_addr, remote_addr, port, md5_password,
                                 md5_base64, ttl_in)
            self.logger.reactor(
                'Listening for BGP session(s) on %s:%d%s' %
                (local_addr, port, ' with MD5' if md5_password else ''))
            return True
        except NetworkError as exc:
            if os.geteuid() != 0 and port <= 1024:
                self.logger.reactor(
                    'Can not bind to %s:%d, you may need to run ExaBGP as root'
                    % (local_addr, port), 'critical')
            else:
                self.logger.reactor(
                    'Can not bind to %s:%d (%s)' %
                    (local_addr, port, str(exc)), 'critical')
            self.logger.reactor(
                'unset exabgp.tcp.bind if you do not want listen for incoming connections',
                'critical')
            self.logger.reactor(
                'and check that no other daemon is already binding to port %d'
                % port, 'critical')
            return False

    def _handle_listener(self):
        if not self.listener:
            return

        ranged_neighbor = []

        for connection in self.listener.connected():
            for key in self.peers:
                peer = self.peers[key]
                neighbor = peer.neighbor

                connection_local = IP.create(connection.local).address()
                neighbor_peer_start = neighbor.peer_address.address()
                neighbor_peer_next = neighbor_peer_start + neighbor.range_size

                if not neighbor_peer_start <= connection_local < neighbor_peer_next:
                    continue

                connection_peer = IP.create(connection.peer).address()
                neighbor_local = neighbor.local_address.address()

                if connection_peer != neighbor_local:
                    if not neighbor.auto_discovery:
                        continue

                # we found a range matching for this connection
                # but the peer may already have connected, so
                # we need to iterate all individual peers before
                # handling "range" peers
                if neighbor.range_size > 1:
                    ranged_neighbor.append(peer.neighbor)
                    continue

                denied = peer.handle_connection(connection)
                if denied:
                    self.logger.reactor(
                        'refused connection from %s due to the state machine' %
                        connection.name())
                    self._async.append(denied)
                    break
                self.logger.reactor('accepted connection from %s' %
                                    connection.name())
                break
            else:
                # we did not break (and nothign was found/done or we have group match)
                matched = len(ranged_neighbor)
                if matched > 1:
                    self.logger.reactor(
                        'could not accept connection from %s (more than one neighbor match)'
                        % connection.name())
                    self._async.append(
                        connection.notification(
                            6, 5,
                            b'could not accept the connection (more than one neighbor match)'
                        ))
                    return
                if not matched:
                    self.logger.reactor('no session configured for %s' %
                                        connection.name())
                    self._async.append(
                        connection.notification(
                            6, 3, b'no session configured for the peer'))
                    return

                new_neighbor = copy.copy(ranged_neighbor[0])
                new_neighbor.range_size = 1
                new_neighbor.generated = True
                new_neighbor.local_address = IP.create(connection.peer)
                new_neighbor.peer_address = IP.create(connection.local)

                new_peer = Peer(new_neighbor, self)
                denied = new_peer.handle_connection(connection)
                if denied:
                    self.logger.reactor(
                        'refused connection from %s due to the state machine' %
                        connection.name())
                    self._async.append(denied)
                    return

                self.peers[new_neighbor.name()] = new_peer
                return

    def run(self, validate):
        self.daemon.daemonise()

        # Make sure we create processes once we have closed file descriptor
        # unfortunately, this must be done before reading the configuration file
        # so we can not do it with dropped privileges
        self.processes = Processes(self)

        # we have to read the configuration possibly with root privileges
        # as we need the MD5 information when we bind, and root is needed
        # to bind to a port < 1024

        # this is undesirable as :
        # - handling user generated data as root should be avoided
        # - we may not be able to reload the configuration once the privileges are dropped

        # but I can not see any way to avoid it
        for ip in self.ips:
            if not self._setup_listener(ip, None, self.port, None, False,
                                        None):
                return False

        if not self.load():
            return False

        if validate:  # only validate configuration
            self.logger.configuration('')
            self.logger.configuration('Parsed Neighbors, un-templated')
            self.logger.configuration('------------------------------')
            self.logger.configuration('')
            for key in self.peers:
                self.logger.configuration(str(self.peers[key].neighbor))
                self.logger.configuration('')
            return True

        for neighbor in self.configuration.neighbors.values():
            if neighbor.listen:
                if not self._setup_listener(
                        neighbor.md5_ip, neighbor.peer_address,
                        neighbor.listen, neighbor.md5_password,
                        neighbor.md5_base64, neighbor.ttl_in):
                    return False

        if not self.early_drop:
            self.processes.start()

        if not self.daemon.drop_privileges():
            self.logger.reactor(
                'Could not drop privileges to \'%s\' refusing to run as root' %
                self.daemon.user, 'critical')
            self.logger.reactor(
                'Set the environmemnt value exabgp.daemon.user to change the unprivileged user',
                'critical')
            return

        if self.early_drop:
            self.processes.start()

        # This is required to make sure we can write in the log location as we now have dropped root privileges
        if not self.logger.restart():
            self.logger.reactor('Could not setup the logger, aborting',
                                'critical')
            return

        if not self.daemon.savepid():
            return

        # did we complete the run of updates caused by the last SIGUSR1/SIGUSR2 ?
        reload_completed = True

        wait = environment.settings().tcp.delay
        if wait:
            sleeptime = (wait * 60) - int(time.time()) % (wait * 60)
            self.logger.reactor('waiting for %d seconds before connecting' %
                                sleeptime)
            time.sleep(float(sleeptime))

        workers = {}
        peers = set()
        busy = False

        while True:
            try:
                start = time.time()
                end = start + self.max_loop_time

                if self._shutdown:
                    self._shutdown = False
                    self.shutdown()
                    break

                if self._reload and reload_completed:
                    self._reload = False
                    self.load()
                    self.processes.start(self._reload_processes)
                    self._reload_processes = False
                elif self._restart:
                    self._restart = False
                    self.restart()

                # We got some API routes to announce
                if self.route_update:
                    self.route_update = False
                    self.route_send()

                for key, peer in self.peers.items():
                    if not peer.neighbor.passive or peer.proto:
                        peers.add(key)
                    if key in self._signal:
                        self.peers[key].reactor.processes.signal(
                            self.peers[key].neighbor, self._signal[key])
                self._signal = {}

                # check all incoming connection
                self._handle_listener()

                # give a turn to all the peers
                while start < time.time() < end:
                    for key in list(peers):
                        peer = self.peers[key]
                        action = peer.run()

                        # .run() returns an ACTION enum:
                        # * immediate if it wants to be called again
                        # * later if it should be called again but has no work atm
                        # * close if it is finished and is closing down, or restarting
                        if action == ACTION.CLOSE:
                            self._unschedule(key)
                            peers.discard(key)
                        # we are loosing this peer, not point to schedule more process work
                        elif action == ACTION.LATER:
                            for io in peer.sockets():
                                workers[io] = key
                            # no need to come back to it before a a full cycle
                            peers.discard(key)

                    # handle API calls
                    busy = self._scheduled_api()
                    # handle new connections
                    busy |= self._scheduled_listener()

                    if not peers and not busy:
                        break

                if not peers:
                    reload_completed = True

                for io in self._api_ready(list(workers)):
                    peers.add(workers[io])
                    del workers[io]

                if self._stopping and not self.peers.keys():
                    break

            except KeyboardInterrupt:
                self._termination('^C received')
            # socket.error is a subclass of IOError (so catch it first)
            except socket.error:
                self._termination('socket error received')
            except IOError:
                self._termination(
                    'I/O Error received, most likely ^C during IO')
            except SystemExit:
                self._termination('exiting')
            except ProcessError:
                self._termination(
                    'Problem when sending message(s) to helper program, stopping'
                )
            except select.error:
                self._termination('problem using select, stopping')

    def shutdown(self):
        """terminate all the current BGP connections"""
        self.logger.reactor('performing shutdown')
        if self.listener:
            self.listener.stop()
            self.listener = None
        for key in self.peers.keys():
            self.peers[key].stop()
        self.processes.terminate()
        self.daemon.removepid()
        self._stopping = True

    def load(self):
        """reload the configuration and send to the peer the route which changed"""
        self.logger.reactor('performing reload of exabgp %s' % version)

        reloaded = self.configuration.reload()

        if not reloaded:
            #
            # Careful the string below is used but the QA code to check for sucess of failure
            self.logger.configuration(
                'problem with the configuration file, no change done', 'error')
            # Careful the string above is used but the QA code to check for sucess of failure
            #
            self.logger.configuration(str(self.configuration.error), 'error')
            return False

        for key, peer in self.peers.items():
            if key not in self.configuration.neighbors:
                self.logger.reactor('removing peer: %s' % peer.neighbor.name())
                peer.stop()

        for key, neighbor in self.configuration.neighbors.items():
            # new peer
            if key not in self.peers:
                self.logger.reactor('new peer: %s' % neighbor.name())
                peer = Peer(neighbor, self)
                self.peers[key] = peer
            # modified peer
            elif self.peers[key].neighbor != neighbor:
                self.logger.reactor(
                    'peer definition change, establishing a new connection for %s'
                    % str(key))
                self.peers[key].reestablish(neighbor)
            # same peer but perhaps not the routes
            else:
                # finding what route changed and sending the delta is not obvious
                self.logger.reactor(
                    'peer definition identical, updating peer routes if required for %s'
                    % str(key))
                self.peers[key].reconfigure(neighbor)
            for ip in self.ips:
                if ip.afi == neighbor.peer_address.afi:
                    self._setup_listener(ip, neighbor.peer_address, self.port,
                                         neighbor.md5_password,
                                         neighbor.md5_base64, None)
        self.logger.configuration('loaded new configuration successfully',
                                  'info')

        return True

    def _scheduled_listener(self, flipflop=[]):
        try:
            for generator in self._async:
                try:
                    six.next(generator)
                    six.next(generator)
                    flipflop.append(generator)
                except StopIteration:
                    pass
            self._async, flipflop = flipflop, self._async
            return len(self._async) > 0
        except KeyboardInterrupt:
            self._termination('^C received')
            return False

    def _scheduled_api(self):
        try:
            # read at least on message per process if there is some and parse it
            for service, command in self.processes.received():
                self.api.text(self, service, command)

            # if we have nothing to do, return or save the work
            if not self._running:
                if not self._pending:
                    return False
                self._running, name = self._pending.popleft()
                self.logger.reactor('callback | installing %s' % name)

            if self._running:
                # run it
                try:
                    self.logger.reactor('callback | running')
                    six.next(self._running)  # run
                    # should raise StopIteration in most case
                    # and prevent us to have to run twice to run one command
                    six.next(self._running)  # run
                except StopIteration:
                    self._running = None
                    self.logger.reactor('callback | removing')
                return True
            return False

        except KeyboardInterrupt:
            self._termination('^C received')
            return False

    def route_send(self):
        """the process ran and we need to figure what routes to changes"""
        self.logger.reactor('performing dynamic route update')
        for key in self.configuration.neighbors.keys():
            self.peers[key].send_new()
        self.logger.reactor('updated peers dynamic routes successfully')

    def restart(self):
        """kill the BGP session and restart it"""
        self.logger.reactor('performing restart of exabgp %s' % version)
        self.configuration.reload()

        for key in self.peers.keys():
            if key not in self.configuration.neighbors.keys():
                neighbor = self.configuration.neighbors[key]
                self.logger.reactor('removing Peer %s' % neighbor.name())
                self.peers[key].stop()
            else:
                self.peers[key].reestablish()
        self.processes.terminate()
        self.processes.start()

    def _unschedule(self, peer):
        if peer in self.peers:
            del self.peers[peer]

    def answer(self, service, string):
        if self.ack:
            self.always_answer(service, string)

    def always_answer(self, service, string):
        self.processes.write(service, string)
        self.logger.reactor('responding to %s : %s' %
                            (service, string.replace('\n', '\\n')))

    def api_shutdown(self):
        self._shutdown = True
        self._pending = deque()
        self._running = None

    def api_reload(self):
        self._reload = True
        self._pending = deque()
        self._running = None

    def api_restart(self):
        self._restart = True
        self._pending = deque()
        self._running = None

    @staticmethod
    def match_neighbor(description, name):
        for string in description:
            if re.search(r'(^|[\s])%s($|[\s,])' % re.escape(string),
                         name) is None:
                return False
        return True

    def match_neighbors(self, descriptions):
        """return the sublist of peers matching the description passed, or None if no description is given"""
        if not descriptions:
            return self.peers.keys()

        returned = []
        for key in self.peers:
            for description in descriptions:
                if Reactor.match_neighbor(description, key):
                    if key not in returned:
                        returned.append(key)
        return returned

    def nexthops(self, peers):
        return dict(
            (peer, self.peers[peer].neighbor.local_address) for peer in peers)

    def plan(self, callback, name):
        self._pending.append((callback, name))
Example #12
0
def run(env, comment, configurations, validate, pid=0):
    logger = Logger()

    logger.error('', source='ExaBGP')
    logger.error('%s' % version, source='version')
    logger.error('%s' % sys.version.replace('\n', ' '), source='interpreter')
    logger.error('%s' % ' '.join(platform.uname()[:5]), source='os')
    logger.error('', source='ExaBGP')

    if comment:
        logger.configuration(comment)

    warning = warn()
    if warning:
        logger.configuration(warning)

    if not env.profile.enable:
        ok = Reactor(configurations).run(validate)
        __exit(env.debug.memory, 0 if ok else 1)

    try:
        import cProfile as profile
    except ImportError:
        import profile

    if not env.profile.file or env.profile.file == 'stdout':
        ok = profile.run('Reactor(configurations).run(validate)')
        __exit(env.debug.memory, 0 if ok else 1)

    if pid:
        profile_name = "%s-pid-%d" % (env.profile.file, pid)
    else:
        profile_name = env.profile.file

    notice = ''
    if os.path.isdir(profile_name):
        notice = 'profile can not use this filename as output, it is not a directory (%s)' % profile_name
    if os.path.exists(profile_name):
        notice = 'profile can not use this filename as output, it already exists (%s)' % profile_name

    if not notice:
        logger.reactor('profiling ....')
        profiler = profile.Profile()
        profiler.enable()
        try:
            ok = Reactor(configurations).run()
        except Exception:
            raise
        finally:
            profiler.disable()
            kprofile = lsprofcalltree.KCacheGrind(profiler)

            with open(profile_name, 'w+') as write:
                kprofile.output(write)

            __exit(env.debug.memory, 0 if ok else 1)
    else:
        logger.reactor("-" * len(notice))
        logger.reactor(notice)
        logger.reactor("-" * len(notice))
        Reactor(configurations).run()
        __exit(env.debug.memory, 1)
Example #13
0
class Reactor(object):
    # [hex(ord(c)) for c in os.popen('clear').read()]
    clear = concat_bytes_i(
        character(int(c, 16))
        for c in ['0x1b', '0x5b', '0x48', '0x1b', '0x5b', '0x32', '0x4a'])

    def __init__(self, configurations):
        self._ips = environment.settings().tcp.bind
        self._port = environment.settings().tcp.port
        self._stopping = environment.settings().tcp.once

        self.max_loop_time = environment.settings().reactor.speed
        self.early_drop = environment.settings().daemon.drop

        self.processes = None

        self.configuration = Configuration(configurations)
        self.logger = Logger()
        self. async = ASYNC()
        self.signal = Signal()
        self.daemon = Daemon(self)
        self.listener = Listener(self)
        self.api = API(self)

        self.peers = {}

        self._reload_processes = False
        self._saved_pid = False

    def _termination(self, reason):
        self.signal.received = Signal.SHUTDOWN
        self.logger.reactor(reason, 'warning')

    def _api_ready(self, sockets):
        sleeptime = self.max_loop_time / 100
        fds = self.processes.fds()
        ios = fds + sockets
        try:
            read, _, _ = select.select(ios, [], [], sleeptime)
            for fd in fds:
                if fd in read:
                    read.remove(fd)
            return read
        except select.error as exc:
            err_no, message = exc.args  # pylint: disable=W0633
            if err_no not in error.block:
                raise exc
            return []
        except socket.error as exc:
            # python 3 does not raise on closed FD, but python2 does
            # we have lost a peer and it is causing the select
            # to complain, the code will self-heal, ignore the issue
            # (EBADF from python2 must be ignored if when checkign error.fatal)
            # otherwise sending  notification causes TCP to drop and cause
            # this code to kill ExaBGP
            return []
        except ValueError as exc:
            # The peer closing the TCP connection lead to a negative file descritor
            return []
        except KeyboardInterrupt:
            self._termination('^C received')
            return []

    def schedule_rib_check(self):
        self.logger.reactor('performing dynamic route update')
        for key in self.configuration.neighbors.keys():
            self.peers[key].schedule_rib_check()

    def _active_peers(self):
        peers = set()
        for key, peer in self.peers.items():
            if not peer.neighbor.passive or peer.proto:
                peers.add(key)
        return peers

    def run(self, validate, root):
        self.daemon.daemonise()

        # Make sure we create processes once we have closed file descriptor
        # unfortunately, this must be done before reading the configuration file
        # so we can not do it with dropped privileges
        self.processes = Processes()

        # we have to read the configuration possibly with root privileges
        # as we need the MD5 information when we bind, and root is needed
        # to bind to a port < 1024

        # this is undesirable as :
        # - handling user generated data as root should be avoided
        # - we may not be able to reload the configuration once the privileges are dropped

        # but I can not see any way to avoid it
        for ip in self._ips:
            if not self.listener.listen_on(ip, None, self._port, None, False,
                                           None):
                return False

        if not self.load():
            return False

        if validate:  # only validate configuration
            self.logger.configuration('')
            self.logger.configuration('Parsed Neighbors, un-templated')
            self.logger.configuration('------------------------------')
            self.logger.configuration('')
            for key in self.peers:
                self.logger.configuration(str(self.peers[key].neighbor))
                self.logger.configuration('')
            return True

        for neighbor in self.configuration.neighbors.values():
            if neighbor.listen:
                if not self.listener.listen_on(
                        neighbor.md5_ip, neighbor.peer_address,
                        neighbor.listen, neighbor.md5_password,
                        neighbor.md5_base64, neighbor.ttl_in):
                    return False

        if not self.early_drop:
            self.processes.start(self.configuration.processes)

        if not self.daemon.drop_privileges():
            self.logger.reactor(
                'Could not drop privileges to \'%s\' refusing to run as root' %
                self.daemon.user, 'critical')
            self.logger.reactor(
                'Set the environmemnt value exabgp.daemon.user to change the unprivileged user',
                'critical')
            return

        if self.early_drop:
            self.processes.start(self.configuration.processes)

        # This is required to make sure we can write in the log location as we now have dropped root privileges
        if not self.logger.restart():
            self.logger.reactor('Could not setup the logger, aborting',
                                'critical')
            return

        if not self.daemon.savepid():
            return

        # did we complete the run of updates caused by the last SIGUSR1/SIGUSR2 ?
        reload_completed = False

        wait = environment.settings().tcp.delay
        if wait:
            sleeptime = (wait * 60) - int(time.time()) % (wait * 60)
            self.logger.reactor('waiting for %d seconds before connecting' %
                                sleeptime)
            time.sleep(float(sleeptime))

        workers = {}
        peers = set()

        while True:
            try:
                if self.signal.received:
                    for key in self.peers:
                        if self.peers[key].neighbor.api['signal']:
                            self.peers[key].reactor.processes.signal(
                                self.peers[key].neighbor, self.signal.number)

                    signaled = self.signal.received
                    self.signal.rearm()

                    if signaled == Signal.SHUTDOWN:
                        self.shutdown()
                        break

                    if signaled == Signal.RESTART:
                        self.restart()
                        continue

                    if not reload_completed:
                        continue

                    if signaled == Signal.RELOAD:
                        self._reload_processes = True

                    if signaled in (Signal.RELOAD, Signal.FULL_RELOAD):
                        self.load()
                        self.processes.start(self.configuration.processes,
                                             self._reload_processes)
                        self._reload_processes = False
                        continue

                if self.listener.incoming():
                    # check all incoming connection
                    self. async .schedule(str(uuid.uuid1()),
                                          'check new connection',
                                          self.listener.new_connections())

                peers = self._active_peers()
                if not peers:
                    reload_completed = True

                # give a turn to all the peers
                for key in list(peers):
                    peer = self.peers[key]
                    action = peer.run()

                    # .run() returns an ACTION enum:
                    # * immediate if it wants to be called again
                    # * later if it should be called again but has no work atm
                    # * close if it is finished and is closing down, or restarting
                    if action == ACTION.CLOSE:
                        if key in self.peers:
                            del self.peers[key]
                        peers.discard(key)
                    # we are loosing this peer, not point to schedule more process work
                    elif action == ACTION.LATER:
                        for io in peer.sockets():
                            workers[io] = key
                        # no need to come back to it before a a full cycle
                        peers.discard(key)

                    if not peers:
                        break

                # read at least on message per process if there is some and parse it
                for service, command in self.processes.received():
                    self.api.text(self, service, command)

                self. async .run()

                for io in self._api_ready(list(workers)):
                    peers.add(workers[io])
                    del workers[io]

                if self._stopping and not self.peers.keys():
                    break

            except KeyboardInterrupt:
                self._termination('^C received')
            # socket.error is a subclass of IOError (so catch it first)
            except socket.error:
                self._termination('socket error received')
            except IOError:
                self._termination(
                    'I/O Error received, most likely ^C during IO')
            except SystemExit:
                self._termination('exiting')
            except ProcessError:
                self._termination(
                    'Problem when sending message(s) to helper program, stopping'
                )
            except select.error:
                self._termination('problem using select, stopping')

    def shutdown(self):
        """Terminate all the current BGP connections"""
        self.logger.reactor('performing shutdown')
        if self.listener:
            self.listener.stop()
            self.listener = None
        for key in self.peers.keys():
            self.peers[key].stop()
        self. async .clear()
        self.processes.terminate()
        self.daemon.removepid()
        self._stopping = True

    def load(self):
        """Reload the configuration and send to the peer the route which changed"""
        self.logger.reactor('performing reload of exabgp %s' % version)

        reloaded = self.configuration.reload()

        if not reloaded:
            #
            # Careful the string below is used but the QA code to check for sucess of failure
            self.logger.configuration(
                'problem with the configuration file, no change done', 'error')
            # Careful the string above is used but the QA code to check for sucess of failure
            #
            self.logger.configuration(str(self.configuration.error), 'error')
            return False

        for key, peer in self.peers.items():
            if key not in self.configuration.neighbors:
                self.logger.reactor('removing peer: %s' % peer.neighbor.name())
                peer.stop()

        for key, neighbor in self.configuration.neighbors.items():
            # new peer
            if key not in self.peers:
                self.logger.reactor('new peer: %s' % neighbor.name())
                peer = Peer(neighbor, self)
                self.peers[key] = peer
            # modified peer
            elif self.peers[key].neighbor != neighbor:
                self.logger.reactor(
                    'peer definition change, establishing a new connection for %s'
                    % str(key))
                self.peers[key].reestablish(neighbor)
            # same peer but perhaps not the routes
            else:
                # finding what route changed and sending the delta is not obvious
                self.logger.reactor(
                    'peer definition identical, updating peer routes if required for %s'
                    % str(key))
                self.peers[key].reconfigure(neighbor)
            for ip in self._ips:
                if ip.afi == neighbor.peer_address.afi:
                    self.listener.listen_on(ip, neighbor.peer_address,
                                            self._port, neighbor.md5_password,
                                            neighbor.md5_base64, None)
        self.logger.configuration('loaded new configuration successfully',
                                  'info')

        return True

    def restart(self):
        """Kill the BGP session and restart it"""
        self.logger.reactor('performing restart of exabgp %s' % version)
        self.configuration.reload()

        for key in self.peers.keys():
            if key not in self.configuration.neighbors.keys():
                neighbor = self.configuration.neighbors[key]
                self.logger.reactor('removing Peer %s' % neighbor.name())
                self.peers[key].stop()
            else:
                self.peers[key].reestablish()
        self.processes.start(self.configuration.processes, True)
Example #14
0
def main ():
	options = docopt.docopt(usage, help=False)

	major = int(sys.version[0])
	minor = int(sys.version[2])

	if major != 2 or minor < 5:
		sys.exit('This program can not work (is not tested) with your python version (< 2.5 or >= 3.0)')

	if options["--version"]:
		print('ExaBGP : %s' % version)
		print('Python : %s' % sys.version.replace('\n',' '))
		print('Uname  : %s' % ' '.join(platform.uname()[:5]))
		sys.exit(0)

	if options["--folder"]:
		folder = os.path.realpath(os.path.normpath(options["--folder"]))
	elif sys.argv[0].endswith('/bin/exabgp'):
		folder = sys.argv[0][:-len('/bin/exabgp')] + '/etc/exabgp'
	elif sys.argv[0].endswith('/sbin/exabgp'):
		folder = sys.argv[0][:-len('/sbin/exabgp')] + '/etc/exabgp'
	else:
		folder = '/etc/exabgp'

	os.environ['EXABGP_ETC'] = folder  # This is not most pretty

	if options["--run"]:
		sys.argv = sys.argv[sys.argv.index('--run')+1:]
		if sys.argv[0] == 'healthcheck':
			from exabgp.application import run_healthcheck
			run_healthcheck()
		elif sys.argv[0] == 'cli':
			from exabgp.application import run_cli
			run_cli()
		else:
			print(usage)
			sys.exit(0)
		return

	envfile = 'exabgp.env' if not options["--env"] else options["--env"]
	if not envfile.startswith('/'):
		envfile = '%s/%s' % (folder, envfile)

	from exabgp.configuration.setup import environment

	try:
		env = environment.setup(envfile)
	except environment.Error as exc:
		print(usage)
		print('\nconfiguration issue,', str(exc))
		sys.exit(1)

	# Must be done before setting the logger as it modify its behaviour

	if options["--debug"]:
		env.log.all = True
		env.log.level = syslog.LOG_DEBUG

	logger = Logger()

	named_pipe = os.environ.get('NAMED_PIPE','')
	if named_pipe:
		from exabgp.application.control import main as control
		control(named_pipe)
		sys.exit(0)

	if options["--decode"]:
		decode = ''.join(options["--decode"]).replace(':','').replace(' ','')
		if not is_bgp(decode):
			print(usage)
			print('Environment values are:\n' + '\n'.join(' - %s' % _ for _ in environment.default()))
			print("")
			print("The BGP message must be an hexadecimal string.")
			print("")
			print("All colons or spaces are ignored, for example:")
			print("")
			print("  --decode 001E0200000007900F0003000101")
			print("  --decode 001E:02:0000:0007:900F:0003:0001:01")
			print("  --decode FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF001E0200000007900F0003000101")
			print("  --decode FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:001E:02:0000:0007:900F:0003:0001:01")
			print("  --decode 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF 001E02 00000007900F0003000101'")
			sys.exit(1)
	else:
		decode = ''

	# Make sure our child has a named pipe name
	if env.api.file:
		os.environ['NAMED_PIPE'] = env.api.file

	duration = options["--signal"]
	if duration and duration.isdigit():
		pid = os.fork()
		if pid:
			import time
			import signal
			try:
				time.sleep(int(duration))
				os.kill(pid,signal.SIGUSR1)
			except KeyboardInterrupt:
				pass
			try:
				pid,code = os.wait()
				sys.exit(code)
			except KeyboardInterrupt:
				try:
					pid,code = os.wait()
					sys.exit(code)
				except Exception:
					sys.exit(0)

	if options["--help"]:
		print(usage)
		print('Environment values are:\n' + '\n'.join(' - %s' % _ for _ in environment.default()))
		sys.exit(0)

	if options["--decode"]:
		env.log.parser = True
		env.debug.route = decode
		env.tcp.bind = ''

	if options["--profile"]:
		env.profile.enable = True
		if options["--profile"].lower() in ['1','true']:
			env.profile.file = True
		elif options["--profile"].lower() in ['0','false']:
			env.profile.file = False
		else:
			env.profile.file = options["--profile"]

	if envfile and not os.path.isfile(envfile):
		comment = 'environment file missing\ngenerate it using "exabgp --fi > %s"' % envfile
	else:
		comment = ''

	if options["--full-ini"] or options["--fi"]:
		for line in environment.iter_ini():
			print(line)
		sys.exit(0)

	if options["--full-env"] or options["--fe"]:
		print()
		for line in environment.iter_env():
			print(line)
		sys.exit(0)

	if options["--diff-ini"] or options["--di"]:
		for line in environment.iter_ini(True):
			print(line)
		sys.exit(0)

	if options["--diff-env"] or options["--de"]:
		for line in environment.iter_env(True):
			print(line)
		sys.exit(0)

	if options["--once"]:
		env.tcp.once = True

	if options["--pdb"]:
		# The following may fail on old version of python (but is required for debug.py)
		os.environ['PDB'] = 'true'
		env.debug.pdb = True

	if options["--test"]:
		env.debug.selfcheck = True
		env.log.parser = True

	if options["--memory"]:
		env.debug.memory = True

	configurations = []
	# check the file only once that we have parsed all the command line options and allowed them to run
	if options["<configuration>"]:
		for f in options["<configuration>"]:
			normalised = os.path.realpath(os.path.normpath(f))
			if os.path.isfile(normalised):
				configurations.append(normalised)
				continue
			if f.startswith('etc/exabgp'):
				normalised = os.path.join(folder,f[11:])
				if os.path.isfile(normalised):
					configurations.append(normalised)
					continue

			logger.configuration('one of the arguments passed as configuration is not a file (%s)' % f,'error')
			sys.exit(1)

	else:
		print(usage)
		print('Environment values are:\n' + '\n'.join(' - %s' % _ for _ in environment.default()))
		print('\nno configuration file provided')
		sys.exit(1)

	from exabgp.bgp.message.update.attribute import Attribute
	Attribute.caching = env.cache.attributes

	if env.debug.rotate or len(configurations) == 1:
		run(env,comment,configurations)

	if not (env.log.destination in ('syslog','stdout','stderr') or env.log.destination.startswith('host:')):
		logger.configuration('can not log to files when running multiple configuration (as we fork)','error')
		sys.exit(1)

	try:
		# run each configuration in its own process
		pids = []
		for configuration in configurations:
			pid = os.fork()
			if pid == 0:
				run(env,comment,[configuration],os.getpid())
			else:
				pids.append(pid)

		# If we get a ^C / SIGTERM, ignore just continue waiting for our child process
		import signal
		signal.signal(signal.SIGINT, signal.SIG_IGN)

		# wait for the forked processes
		for pid in pids:
			os.waitpid(pid,0)
	except OSError as exc:
		logger.reactor('Can not fork, errno %d : %s' % (exc.errno,exc.strerror),'critical')
		sys.exit(1)
Example #15
0
class Decoder (object):
	_dispatch = {}
	_order = {}

	def __init__ (self):
		self.logger = Logger()
		self.format = Text()

	# callaback code

	def register_command (command,storage,order):
		def closure (f):
			def wrap (*args):
				f(*args)
			storage[command] = wrap
			order = sorted(storage.keys(),key=len)
			return wrap
		return closure

	def parse_command (self,reactor,service,command):
		for registered in sorted(self._dispatch, reverse=True):
			if registered in command:
				return self._dispatch[registered](self,reactor,service,command)
		self.logger.reactor("Command from process not understood : %s" % command,'warning')
		return False

	#

	@staticmethod
	def extract_neighbors (command):
		"""return a list of neighbor definition : the neighbor definition is a list of string which are in the neighbor indexing string"""
		# This function returns a list and a string
		# The first list contains parsed neighbor to match against our defined peers
		# The string is the command to be run for those peers
		# The parsed neighbor is a list of the element making the neighbor string so each part can be checked against the neighbor name

		returned = []
		neighbor,remaining = command.split(' ',1)
		if neighbor != 'neighbor':
			return [],command

		ip,command = remaining.split(' ',1)
		definition = ['neighbor %s' % (ip)]

		while True:
			try:
				key,value,remaining = command.split(' ',2)
			except ValueError:
				key,value = command.split(' ',1)
			if key == ',':
				returned.append(definition)
				_,command = command.split(' ',1)
				definition = []
				continue
			if key not in ['neighbor','local-ip','local-as','peer-as','router-id','family-allowed']:
				if definition:
					returned.append(definition)
				break
			definition.append('%s %s' % (key,value))
			command = remaining

		return returned,command


	#

	@register_command('shutdown',_dispatch,_order)
	def _shutdown (self,reactor,service,command):
		reactor.api_shutdown()
		reactor.answer(service,'shutdown in progress')
		return True

	@register_command('reload',_dispatch,_order)
	def _reload (self,reactor,service,command):
		reactor.api_reload()
		reactor.answer(service,'reload in progress')
		return True

	@register_command('reload',_dispatch,_order)
	def _restart (self,reactor,service,command):
		reactor.api_restart()
		reactor.answer(service,'restart in progress')
		return True

	@register_command('version',_dispatch,_order)
	def _version (self,reactor,service,command):
		reactor.answer(service,'exabgp %s' % version)
		return True

	# teardown

	@register_command('teardown',_dispatch,_order)
	def _t (self,reactor,service,command):
		try:
			descriptions,command = Decoder.extract_neighbors(command)
			_,code = command.split(' ',1)
			for key in reactor.peers:
				for description in descriptions:
					if reactor.match_neighbor(description,key):
						reactor.peers[key].teardown(int(code))
						self.logger.reactor('teardown scheduled for %s' % ' '.join(description))
			return True
		except ValueError:
			return False
		except IndexError:
			return False

	# show neighbor(s)

	@register_command('show neighbor',_dispatch,_order)
	def _show_neighbor (self,reactor,service,command):
		def _callback ():
			for key in reactor.configuration.neighbor.keys():
				neighbor = reactor.configuration.neighbor[key]
				for line in str(neighbor).split('\n'):
					reactor.answer(service,line)
					yield True

		reactor.plan(_callback())
		return True

	@register_command('show neighbors',_dispatch,_order)
	def _show_neighbors (self,reactor,service,command):
		def _callback ():
			for key in reactor.configuration.neighbor.keys():
				neighbor = reactor.configuration.neighbor[key]
				for line in str(neighbor).split('\n'):
					reactor.answer(service,line)
					yield True

		reactor.plan(_callback())
		return True

	# show route(s)

	@register_command('show routes',_dispatch,_order)
	def _show_routes (self,reactor,service,command):
		def _callback ():
			for key in reactor.configuration.neighbor.keys():
				neighbor = reactor.configuration.neighbor[key]
				for change in list(neighbor.rib.outgoing.sent_changes()):
					reactor.answer(service,'neighbor %s %s' % (neighbor.local_address,str(change.nlri)))
					yield True

		reactor.plan(_callback())
		return True

	@register_command('show routes extensive',_dispatch,_order)
	def _show_routes_extensive (self,reactor,service,command):
		def _callback ():
			for key in reactor.configuration.neighbor.keys():
				neighbor = reactor.configuration.neighbor[key]
				for change in list(neighbor.rib.outgoing.sent_changes()):
					reactor.answer(service,'neighbor %s %s' % (neighbor.name(),change.extensive()))
					yield True

		reactor.plan(_callback())
		return True

	# watchdogs

	@register_command('announce watchdog',_dispatch,_order)
	def _announce_watchdog (self,reactor,service,command):
		def _callback (name):
			for neighbor in reactor.configuration.neighbor:
				reactor.configuration.neighbor[neighbor].rib.outgoing.announce_watchdog(name)
				yield False
			reactor.route_update = True

		try:
			name = command.split(' ')[2]
		except IndexError:
			name = service
		reactor.plan(_callback(name))
		return True


	@register_command('withdraw watchdog',_dispatch,_order)
	def _withdraw_watchdog (self,reactor,service,command):
		def _callback (name):
			for neighbor in reactor.configuration.neighbor:
				reactor.configuration.neighbor[neighbor].rib.outgoing.withdraw_watchdog(name)
				yield False
			reactor.route_update = True
		try:
			name = command.split(' ')[2]
		except IndexError:
			name = service
		reactor.plan(_callback(name))
		return True

	# flush routes

	@register_command('flush route',_dispatch,_order)
	def _flush_route (self,reactor,service,command):
		def _callback (self,peers):
			self.logger.reactor("Flushing routes for %s" % ', '.join(peers if peers else []) if peers is not None else 'all peers')
			yield True
			reactor.route_update = True

		try:
			descriptions,command = Decoder.extract_neighbors(command)
			peers = reactor.match_neighbors(descriptions)
			if not peers:
				self.logger.reactor('no neighbor matching the command : %s' % command,'warning')
				return False
			reactor.plan(_callback(self,peers))
			return True
		except ValueError:
			return False
		except IndexError:
			return False

	# route

	@register_command('announce route',_dispatch,_order)
	def _announce_route (self,reactor,service,command):
		def _callback (self,command,nexthops):
			changes = self.format.parse_api_route(command,nexthops,'announce')
			if not changes:
				self.logger.reactor("Command could not parse route in : %s" % command,'warning')
				yield True
			else:
				peers = []
				for (peer,change) in changes:
					peers.append(peer)
					reactor.configuration.change_to_peers(change,[peer,])
					yield False
				self.logger.reactor("Route added to %s : %s" % (', '.join(peers if peers else []) if peers is not None else 'all peers',change.extensive()))
				reactor.route_update = True

		try:
			descriptions,command = Decoder.extract_neighbors(command)
			peers = reactor.match_neighbors(descriptions)
			if not peers:
				self.logger.reactor('no neighbor matching the command : %s' % command,'warning')
				return False
			reactor.plan(_callback(self,command,reactor.nexthops(peers)))
			return True
		except ValueError:
			return False
		except IndexError:
			return False

	@register_command('withdraw route',_dispatch,_order)
	def _withdraw_route (self,reactor,service,command):
		def _callback (self,command,nexthops):
			changes = self.format.parse_api_route(command,nexthops,'withdraw')
			if not changes:
				self.logger.reactor("Command could not parse route in : %s" % command,'warning')
				yield True
			else:
				for (peer,change) in changes:
					if reactor.configuration.change_to_peers(change,[peer,]):
						self.logger.reactor("Route removed : %s" % change.extensive())
						yield False
					else:
						self.logger.reactor("Could not find therefore remove route : %s" % change.extensive(),'warning')
						yield False
				reactor.route_update = True

		try:
			descriptions,command = Decoder.extract_neighbors(command)
			peers = reactor.match_neighbors(descriptions)
			if not peers:
				self.logger.reactor('no neighbor matching the command : %s' % command,'warning')
				return False
			reactor.plan(_callback(self,command,reactor.nexthops(peers)))
			return True
		except ValueError:
			return False
		except IndexError:
			return False

	# vpls

	@register_command('announce vpls',_dispatch,_order)
	def _announce_vpls (self,reactor,service,command):
		def _callback (self,command,nexthops):
			changes = self.format.parse_api_vpls(command,nexthops,'announce')
			if not changes:
				self.logger.reactor("Command could not parse vpls in : %s" % command,'warning')
				yield True
			else:
				peers = []
				for (peer,change) in changes:
					peers.append(peer)
					reactor.configuration.change_to_peers(change,[peer,])
					yield False
				self.logger.reactor("vpls added to %s : %s" % (', '.join(peers if peers else []) if peers is not None else 'all peers',change.extensive()))
				reactor.route_update = True

		try:
			descriptions,command = Decoder.extract_neighbors(command)
			peers = reactor.match_neighbors(descriptions)
			if not peers:
				self.logger.reactor('no neighbor matching the command : %s' % command,'warning')
				return False
			reactor.plan(_callback(self,command,reactor.nexthops(peers)))
			return True
		except ValueError:
			return False
		except IndexError:
			return False

	@register_command('withdraw vpls',_dispatch,_order)
	def _withdraw_change (self,reactor,service,command):
		def _callback (self,command,nexthops):
			changes = self.format.parse_api_vpls(command,nexthops,'withdraw')
			if not changes:
				self.logger.reactor("Command could not parse vpls in : %s" % command,'warning')
				yield True
			else:
				for (peer,change) in changes:
					if reactor.configuration.change_to_peers(change,[peer,]):
						self.logger.reactor("vpls removed : %s" % change.extensive())
						yield False
					else:
						self.logger.reactor("Could not find therefore remove vpls : %s" % change.extensive(),'warning')
						yield False
				reactor.route_update = True

		try:
			descriptions,command = Decoder.extract_neighbors(command)
			peers = reactor.match_neighbors(descriptions)
			if not peers:
				self.logger.reactor('no neighbor matching the command : %s' % command,'warning')
				return False
			reactor.plan(_callback(self,command,reactor.nexthops(peers)))
			return True
		except ValueError:
			return False
		except IndexError:
			return False

	# attribute

	@register_command('announce attribute',_dispatch,_order)
	def _announce_attribute (self,reactor,service,command):
		def _callback (self,command,nexthops):
			changes = self.format.parse_api_attribute(command,nexthops,'announce')
			if not changes:
				self.logger.reactor("Command could not parse attribute in : %s" % command,'warning')
				yield True
			else:
				for (peers,change) in changes:
					reactor.configuration.change_to_peers(change,peers)
					self.logger.reactor("Route added to %s : %s" % (', '.join(peers if peers else []) if peers is not None else 'all peers',change.extensive()))
				yield False
				reactor.route_update = True

		try:
			descriptions,command = Decoder.extract_neighbors(command)
			peers = reactor.match_neighbors(descriptions)
			if not peers:
				self.logger.reactor('no neighbor matching the command : %s' % command,'warning')
				return False
			reactor.plan(_callback(self,command,reactor.nexthops(peers)))
			return True
		except ValueError:
			return False
		except IndexError:
			return False

	@register_command('withdraw attribute',_dispatch,_order)
	def _withdraw_attribute (self,reactor,service,command):
		def _callback (self,command,nexthops):
			changes = self.format.parse_api_attribute(command,nexthops,'withdraw')
			if not changes:
				self.logger.reactor("Command could not parse attribute in : %s" % command,'warning')
				yield True
			else:
				for (peers,change) in changes:
					if reactor.configuration.change_to_peers(change,peers):
						self.logger.reactor("Route removed : %s" % change.extensive())
						yield False
					else:
						self.logger.reactor("Could not find therefore remove route : %s" % change.extensive(),'warning')
						yield False
				reactor.route_update = True

		try:
			descriptions,command = Decoder.extract_neighbors(command)
			peers = reactor.match_neighbors(descriptions)
			if not peers:
				self.logger.reactor('no neighbor matching the command : %s' % command,'warning')
				return False
			reactor.plan(_callback(self,command,reactor.nexthops(peers)))
			return True
		except ValueError:
			return False
		except IndexError:
			return False

	# flow

	@register_command('announce flow',_dispatch,_order)
	def _announce_flow (self,reactor,service,command):
		def _callback (self,command,peers):
			changes = self.format.parse_api_flow(command,'announce')
			if not changes:
				self.logger.reactor("Command could not parse flow in : %s" % command)
				yield True
			else:
				for change in changes:
					reactor.configuration.change_to_peers(change,peers)
					self.logger.reactor("Flow added to %s : %s" % (', '.join(peers if peers else []) if peers is not None else 'all peers',change.extensive()))
					yield False
				reactor.route_update = True

		try:
			descriptions,command = Decoder.extract_neighbors(command)
			peers = reactor.match_neighbors(descriptions)
			if not peers:
				self.logger.reactor('no neighbor matching the command : %s' % command,'warning')
				return False
			reactor.plan(_callback(self,command,peers))
			return True
		except ValueError:
			return False
		except IndexError:
			return False


	@register_command('withdraw flow',_dispatch,_order)
	def _withdraw_flow (self,reactor,service,command):
		def _callback (self,command,peers):
			changes = self.format.parse_api_flow(command,'withdraw')
			if not changes:
				self.logger.reactor("Command could not parse flow in : %s" % command)
				yield True
			else:
				for change in changes:
					if reactor.configuration.change_to_peers(change,peers):
						self.logger.reactor("Flow found and removed : %s" % change.extensive())
						yield False
					else:
						self.logger.reactor("Could not find therefore remove flow : %s" % change.extensive(),'warning')
						yield False
				reactor.route_update = True

		try:
			descriptions,command = Decoder.extract_neighbors(command)
			peers = reactor.match_neighbors(descriptions)
			if not peers:
				self.logger.reactor('no neighbor matching the command : %s' % command,'warning')
				return False
			reactor.plan(_callback(self,command,peers))
			return True
		except ValueError:
			return False
		except IndexError:
			return False

	# eor

	@register_command('announce eor',_dispatch,_order)
	def _announce_eor (self,reactor,service,command):
		def _callback (self,command,peers):
			family = self.format.parse_api_eor(command)
			if not family:
				self.logger.reactor("Command could not parse eor : %s" % command)
				yield True
			else:
				reactor.configuration.eor_to_peers(family,peers)
				self.logger.reactor("Sent to %s : %s" % (', '.join(peers if peers else []) if peers is not None else 'all peers',family.extensive()))
				yield False
				reactor.route_update = True

		try:
			descriptions,command = Decoder.extract_neighbors(command)
			peers = reactor.match_neighbors(descriptions)
			if not peers:
				self.logger.reactor('no neighbor matching the command : %s' % command,'warning')
				return False
			reactor.plan(_callback(self,command,peers))
			return True
		except ValueError:
			return False
		except IndexError:
			return False

	# route-refresh

	@register_command('announce route-refresh',_dispatch,_order)
	def _announce_refresh (self,reactor,service,command):
		def _callback (self,command,peers):
			rr = self.format.parse_api_refresh(command)
			if not rr:
				self.logger.reactor("Command could not parse flow in : %s" % command)
				yield True
			else:
				reactor.configuration.refresh_to_peers(rr,peers)
				self.logger.reactor("Sent to %s : %s" % (', '.join(peers if peers else []) if peers is not None else 'all peers',rr.extensive()))
				yield False
				reactor.route_update = True

		try:
			descriptions,command = Decoder.extract_neighbors(command)
			peers = reactor.match_neighbors(descriptions)
			if not peers:
				self.logger.reactor('no neighbor matching the command : %s' % command,'warning')
				return False
			reactor.plan(_callback(self,command,peers))
			return True
		except ValueError:
			return False
		except IndexError:
			return False

	# operational

	@register_command('operational',_dispatch,_order)
	def _announce_operational (self,reactor,service,command):
		def _callback (self,command,peers):
			operational = self.format.parse_api_operational(command)
			if not operational:
				self.logger.reactor("Command could not parse operational command : %s" % command)
				yield True
			else:
				reactor.configuration.operational_to_peers(operational,peers)
				self.logger.reactor("operational message sent to %s : %s" % (', '.join(peers if peers else []) if peers is not None else 'all peers',operational.extensive()))
				yield False
				reactor.route_update = True

		if (command.split() + ['safe'])[1].lower() not in ('asm','adm','rpcq','rpcp','apcq','apcp','lpcq','lpcp'):
			return False

		try:
			descriptions,command = Decoder.extract_neighbors(command)
			peers = reactor.match_neighbors(descriptions)
			if not peers:
				self.logger.reactor('no neighbor matching the command : %s' % command,'warning')
				return False
			reactor.plan(_callback(self,command,peers))
			return True
		except ValueError:
			return False
		except IndexError:
			return False
Example #16
0
class Reactor (object):
	# [hex(ord(c)) for c in os.popen('clear').read()]
	clear = ''.join([chr(int(c,16)) for c in ['0x1b', '0x5b', '0x48', '0x1b', '0x5b', '0x32', '0x4a']])

	def __init__ (self,configuration):
		self.ip = environment.settings().tcp.bind
		self.port = environment.settings().tcp.port

		self.max_loop_time = environment.settings().reactor.speed
		self.half_loop_time = self.max_loop_time / 2

		self.logger = Logger()
		self.daemon = Daemon(self)
		self.processes = None
		self.listener = None
		self.configuration = Configuration(configuration)

		self._peers = {}
		self._shutdown = False
		self._reload = False
		self._reload_processes = False
		self._restart = False
		self._route_update = False
		self._saved_pid = False
		self._commands = []
		self._pending = []

		signal.signal(signal.SIGTERM, self.sigterm)
		signal.signal(signal.SIGHUP, self.sighup)
		signal.signal(signal.SIGALRM, self.sigalrm)
		signal.signal(signal.SIGUSR1, self.sigusr1)
		signal.signal(signal.SIGUSR2, self.sigusr2)

	def sigterm (self,signum, frame):
		self.logger.reactor("SIG TERM received - shutdown")
		self._shutdown = True

	def sighup (self,signum, frame):
		self.logger.reactor("SIG HUP received - shutdown")
		self._shutdown = True

	def sigalrm (self,signum, frame):
		self.logger.reactor("SIG ALRM received - restart")
		self._restart = True

	def sigusr1 (self,signum, frame):
		self.logger.reactor("SIG USR1 received - reload configuration")
		self._reload = True

	def sigusr2 (self,signum, frame):
		self.logger.reactor("SIG USR2 received - reload configuration and processes")
		self._reload = True
		self._reload_processes = True

	def run (self):
		if self.ip:
			try:
				self.listener = Listener([self.ip,],self.port)
				self.listener.start()
			except NetworkError,e:
				self.listener = None
				if os.geteuid() != 0 and self.port <= 1024:
					self.logger.reactor("Can not bind to %s:%d, you may need to run ExaBGP as root" % (self.ip,self.port),'critical')
				else:
					self.logger.reactor("Can not bind to %s:%d (%s)" % (self.ip,self.port,str(e)),'critical')
				self.logger.reactor("unset exabgp.tcp.bind if you do not want listen for incoming connections",'critical')
				self.logger.reactor("and check that no other daemon is already binding to port %d" % self.port,'critical')
				sys.exit(1)
			self.logger.reactor("Listening for BGP session(s) on %s:%d" % (self.ip,self.port))

		if self.daemon.drop_privileges():
			self.logger.reactor("Could not drop privileges to '%s' refusing to run as root" % self.daemon.user,'critical')
			self.logger.reactor("Set the environmemnt value exabgp.daemon.user to change the unprivileged user",'critical')
			return

		self.daemon.daemonise()

		if not self.daemon.savepid():
			self.logger.reactor('could not update PID, not starting','error')

		# Make sure we create processes one we have dropped privileges and closed file descriptor
		self.processes = Processes(self)
		self.reload()

		# did we complete the run of updates caused by the last SIGUSR1/SIGUSR2 ?
		reload_completed = True

		wait = environment.settings().tcp.delay
		if wait:
			sleeptime = (wait * 60) - int(time.time()) % (wait * 60)
			self.logger.reactor("waiting for %d seconds before connecting" % sleeptime)
			time.sleep(float(sleeptime))

		while True:
			try:
				while self._peers:
					start = time.time()

					if self._shutdown:
						self._shutdown = False
						self.shutdown()
					elif self._reload and reload_completed:
						self._reload = False
						self.reload(self._reload_processes)
						self._reload_processes = False
					elif self._restart:
						self._restart = False
						self.restart()
					elif self._route_update:
						self._route_update = False
						self.route_update()

					while self.schedule(self.processes.received()) or self._pending:
						self._pending = list(self.run_pending(self._pending))

						duration = time.time() - start
						if duration >= self.half_loop_time:
							break

					# Handle all connection
					peers = self._peers.keys()
					ios = []
					while peers:
						for key in peers[:]:
							peer = self._peers[key]
							action = peer.run()
							# .run() returns:
							# * True if it wants to be called again
							# * None if it should be called again but has no work atm
							# * False if it is finished and is closing down, or restarting
							if action == ACTION.close:
								self.unschedule(peer)
								peers.remove(key)
							elif action == ACTION.later:
								ios.extend(peer.sockets())
								# no need to come back to it before a a full cycle
								peers.remove(key)

						duration = time.time() - start
						if duration >= self.max_loop_time:
							ios=[]
							break

					if not peers:
						reload_completed = True

					# append here after reading as if read fails due to a dead process
					# we may respawn the process which changes the FD
					ios.extend(self.processes.fds())

					# RFC state that we MUST not send more than one KEEPALIVE / sec
					# And doing less could cause the session to drop

					while self.schedule(self.processes.received()) or self._pending:
						self._pending = list(self.run_pending(self._pending))

						duration = time.time() - start
						if duration >= self.max_loop_time:
							break

					if self.listener:
						for connection in self.listener.connected():
							# found
							# * False, not peer found for this TCP connection
							# * True, peer found
							# * None, conflict found for this TCP connections
							found = False
							for key in self._peers:
								peer = self._peers[key]
								neighbor = peer.neighbor
								# XXX: FIXME: Inet can only be compared to Inet
								if connection.local == str(neighbor.peer_address) and connection.peer == str(neighbor.local_address):
									if peer.incoming(connection):
										found = True
										break
									found = None
									break

							if found:
								self.logger.reactor("accepted connection from  %s - %s" % (connection.local,connection.peer))
							elif found is False:
								self.logger.reactor("no session configured for  %s - %s" % (connection.local,connection.peer))
								connection.notification(6,3,'no session configured for the peer')
								connection.close()
							elif found is None:
								self.logger.reactor("connection refused (already connected to the peer) %s - %s" % (connection.local,connection.peer))
								connection.notification(6,5,'could not accept the connection')
								connection.close()

					if ios:
						delay = max(start+self.max_loop_time-time.time(),0.0)
						try:
							read,_,_ = select.select(ios,[],[],delay)
						except select.error,e:
							errno,message = e.args
							if not errno in error.block:
								raise e

					delay = max(start+self.max_loop_time-time.time(),0.0)
					if delay:
						time.sleep(delay)

				self.processes.terminate()
				self.daemon.removepid()
				break
			except KeyboardInterrupt:
				while True:
					try:
						self._shutdown = True
						self.logger.reactor("^C received")
						break
					except KeyboardInterrupt:
						pass
Example #17
0
class Signal(object):
    NONE = 0
    SHUTDOWN = 1
    RESTART = 2
    RELOAD = 4
    FULL_RELOAD = 8

    def __init__(self):
        self.logger = Logger()
        self.received = self.NONE
        self.number = 0
        self.rearm()

    def rearm(self):
        self.received = Signal.NONE
        self.number = 0

        signal.signal(signal.SIGTERM, self.sigterm)
        signal.signal(signal.SIGHUP, self.sighup)
        signal.signal(signal.SIGALRM, self.sigalrm)
        signal.signal(signal.SIGUSR1, self.sigusr1)
        signal.signal(signal.SIGUSR2, self.sigusr2)

    def sigterm(self, signum, frame):
        self.logger.reactor('SIG TERM received')
        if self.received:
            self.logger.reactor('ignoring - still handling previous signal')
            return
        self.logger.reactor('scheduling shutdown')
        self.received = self.SHUTDOWN
        self.number = signum

    def sighup(self, signum, frame):
        self.logger.reactor('SIG HUP received')
        if self.received:
            self.logger.reactor('ignoring - still handling previous signal')
            return
        self.logger.reactor('scheduling shutdown')
        self.received = self.SHUTDOWN
        self.number = signum

    def sigalrm(self, signum, frame):
        self.logger.reactor('SIG ALRM received')
        if self.received:
            self.logger.reactor('ignoring - still handling previous signal')
            return
        self.logger.reactor('scheduling restart')
        self.received = self.RESTART
        self.number = signum

    def sigusr1(self, signum, frame):
        self.logger.reactor('SIG USR1 received')
        if self.received:
            self.logger.reactor('ignoring - still handling previous signal')
            return
        self.logger.reactor('scheduling reload of configuration')
        self.received = self.RELOAD
        self.number = signum

    def sigusr2(self, signum, frame):
        self.logger.reactor('SIG USR1 received')
        if self.received:
            self.logger.reactor('ignoring - still handling previous signal')
            return
        self.logger.reactor('scheduling reload of configuration and processes')
        self.received = self.FULL_RELOAD
        self.number = signum