Esempio n. 1
0
class Processes(object):
    # how many time can a process can respawn in the time interval
    respawn_timemask = 0xFFFFFF - 0b111111
    # '0b111111111111111111000000' (around a minute, 63 seconds)

    _dispatch = {}

    def __init__(self):
        self.logger = Logger()
        self.clean()
        self.silence = False
        self._buffer = {}
        self._configuration = {}

        self.respawn_number = 5 if environment.settings().api.respawn else 0
        self.terminate_on_error = environment.settings().api.terminate
        self.ack = environment.settings().api.ack

    def number(self):
        return len(self._process)

    def clean(self):
        self.fds = []
        self._process = {}
        self._encoder = {}
        self._broken = []
        self._respawning = {}

    def _handle_problem(self, process):
        if process not in self._process:
            return
        if self.respawn_number:
            self.logger.debug('issue with the process, restarting it',
                              'process')
            self._terminate(process)
            self._start(process)
        else:
            self.logger.debug('issue with the process, terminating it',
                              'process')
            self._terminate(process)

    def _terminate(self, process_name):
        self.logger.debug('terminating process %s' % process_name, 'process')
        process = self._process[process_name]
        del self._process[process_name]
        self._update_fds()
        thread = Thread(target=self._terminate_run, args=(process, ))
        thread.start()
        return thread

    def _terminate_run(self, process):
        try:
            process.terminate()
            process.wait()
        except (OSError, KeyError):
            # the process is most likely already dead
            pass

    def terminate(self):
        for process in list(self._process):
            if not self.silence:
                try:
                    self.write(process, self._encoder[process].shutdown())
                except ProcessError:
                    pass
        self.silence = True
        # waiting a little to make sure IO is flushed to the pipes
        # we are using unbuffered IO but still ..
        time.sleep(0.1)
        for process in list(self._process):
            try:
                t = self._terminate(process)
                t.join()
            except OSError:
                # we most likely received a SIGTERM signal and our child is already dead
                self.logger.debug(
                    'child process %s was already dead' % process, 'process')
        self.clean()

    def _start(self, process):
        try:
            if process in self._process:
                self.logger.debug('process already running', 'process')
                return
            if process not in self._configuration:
                self.logger.debug(
                    'can not start process, no configuration for it',
                    'process')
                return
            # Prevent some weird termcap data to be created at the start of the PIPE
            # \x1b[?1034h (no-eol) (esc)
            os.environ['TERM'] = 'dumb'

            configuration = self._configuration[process]

            run = configuration.get('run', '')
            if run:
                api = configuration.get('encoder', '')
                self._encoder[process] = Response.Text(
                    text_version) if api == 'text' else Response.JSON(
                        json_version)

                self._process[process] = subprocess.Popen(
                    run,
                    stdin=subprocess.PIPE,
                    stdout=subprocess.PIPE,
                    preexec_fn=preexec_helper
                    # This flags exists for python 2.7.3 in the documentation but on on my MAC
                    # creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
                )
                self._update_fds()
                fcntl.fcntl(self._process[process].stdout.fileno(),
                            fcntl.F_SETFL, os.O_NONBLOCK)

                self.logger.debug('forked process %s' % process, 'process')

                around_now = int(time.time()) & self.respawn_timemask
                if process in self._respawning:
                    if around_now in self._respawning[process]:
                        self._respawning[process][around_now] += 1
                        # we are respawning too fast
                        if self._respawning[process][
                                around_now] > self.respawn_number:
                            self.logger.critical(
                                'Too many death for %s (%d) terminating program'
                                % (process, self.respawn_number), 'process')
                            raise ProcessError()
                    else:
                        # reset long time since last respawn
                        self._respawning[process] = {around_now: 1}
                else:
                    # record respawing
                    self._respawning[process] = {around_now: 1}

        except (subprocess.CalledProcessError, OSError, ValueError) as exc:
            self._broken.append(process)
            self.logger.debug('could not start process %s' % process,
                              'process')
            self.logger.debug('reason: %s' % str(exc), 'process')

    def start(self, configuration, restart=False):
        for process in list(self._process):
            if process not in configuration:
                self._terminate(process)
        self._configuration = configuration
        for process in configuration:
            if restart and process in list(self._process):
                self._terminate(process)
            self._start(process)

    def broken(self, neighbor):
        if self._broken:
            for process in self._configuration:
                if process in self._broken:
                    return True
        return False

    def _update_fds(self):
        self.fds = [
            self._process[process].stdout.fileno() for process in self._process
        ]

    def received(self):
        consumed_data = False

        for process in list(self._process):
            try:
                proc = self._process[process]
                poll = proc.poll()
                # proc.poll returns None if the process is still fine
                # -[signal], like -15, if the process was terminated
                if poll is not None:
                    self._handle_problem(process)
                    return

                poller = select.poll()
                poller.register(
                    proc.stdout, select.POLLIN | select.POLLPRI
                    | select.POLLHUP | select.POLLNVAL | select.POLLERR)

                ready = False
                for _, event in poller.poll(0):
                    if event & select.POLLIN or event & select.POLLPRI:
                        ready = True
                    elif event & select.POLLHUP or event & select.POLLRDHUP or event & select.POLLERR or event & select.POLLNVAL:
                        self._handle_problem(process)

                if not ready:
                    continue

                try:
                    # Calling next() on Linux and OSX works perfectly well
                    # but not on OpenBSD where it always raise StopIteration
                    # and only readline() works
                    buf = str_ascii(proc.stdout.read(16384))
                    if buf == '' and poll is not None:
                        # if proc.poll() is None then
                        # process is fine, we received an empty line because
                        # we're doing .readline() on a non-blocking pipe and
                        # the process maybe has nothing to send yet
                        self._handle_problem(process)
                        continue

                    raw = self._buffer.get(process, '') + buf

                    while '\n' in raw:
                        line, raw = raw.split('\n', 1)
                        line = line.rstrip()
                        consumed_data = True
                        self.logger.debug(
                            'command from process %s : %s ' % (process, line),
                            'process')
                        yield (process, formated(line))

                    self._buffer[process] = raw

                except IOError as exc:
                    if not exc.errno or exc.errno in error.fatal:
                        # if the program exits we can get an IOError with errno code zero !
                        self._handle_problem(process)
                    elif exc.errno in error.block:
                        # we often see errno.EINTR: call interrupted and
                        # we most likely have data, we will try to read them a the next loop iteration
                        pass
                    else:
                        self.logger.debug(
                            'unexpected errno received from forked process (%s)'
                            % errstr(exc), 'process')
                except StopIteration:
                    if not consumed_data:
                        self._handle_problem(process)
            except KeyError:
                pass
            except (subprocess.CalledProcessError, OSError, ValueError):
                self._handle_problem(process)

    def write(self, process, string, neighbor=None):
        if string is None:
            return True

        # XXX: FIXME: This is potentially blocking
        while True:
            try:
                self._process[process].stdin.write(bytes_ascii('%s\n' %
                                                               string))
            except IOError as exc:
                self._broken.append(process)
                if exc.errno == errno.EPIPE:
                    self._broken.append(process)
                    self.logger.debug(
                        'issue while sending data to our helper program',
                        'process')
                    raise ProcessError()
                else:
                    # Could it have been caused by a signal ? What to do.
                    self.logger.debug(
                        'error received while sending data to helper program, retrying (%s)'
                        % errstr(exc), 'process')
                    continue
            break

        try:
            self._process[process].stdin.flush()
        except IOError as exc:
            # AFAIK, the buffer should be flushed at the next attempt.
            self.logger.debug(
                'error received while FLUSHING data to helper program, retrying (%s)'
                % errstr(exc), 'process')

        return True

    def _answer(self, service, string, force=False):
        if force or self.ack:
            self.logger.debug(
                'responding to %s : %s' %
                (service, string.replace('\n', '\\n')), 'process')
            self.write(service, string)

    def answer_done(self, service):
        self._answer(service, Answer.done)

    def answer_error(self, service):
        self._answer(service, Answer.error)

    def _notify(self, neighbor, event):
        for process in neighbor.api[event]:
            yield process

    # do not do anything if silenced
    # no-self-argument

    def silenced(function):
        def closure(self, *args):
            if self.silence:
                return
            return function(self, *args)

        return closure

    # invalid-name
    @silenced
    def up(self, neighbor):
        for process in self._notify(neighbor, 'neighbor-changes'):
            self.write(process, self._encoder[process].up(neighbor), neighbor)

    @silenced
    def connected(self, neighbor):
        for process in self._notify(neighbor, 'neighbor-changes'):
            self.write(process, self._encoder[process].connected(neighbor),
                       neighbor)

    @silenced
    def down(self, neighbor, reason):
        for process in self._notify(neighbor, 'neighbor-changes'):
            self.write(process, self._encoder[process].down(neighbor, reason),
                       neighbor)

    @silenced
    def negotiated(self, neighbor, negotiated):
        for process in self._notify(neighbor, 'negotiated'):
            self.write(process,
                       self._encoder[process].negotiated(neighbor,
                                                         negotiated), neighbor)

    @silenced
    def fsm(self, neighbor, fsm):
        for process in self._notify(neighbor, 'fsm'):
            self.write(process, self._encoder[process].fsm(neighbor, fsm),
                       neighbor)

    @silenced
    def signal(self, neighbor, signal):
        for process in self._notify(neighbor, 'signal'):
            self.write(process,
                       self._encoder[process].signal(neighbor,
                                                     signal), neighbor)

    @silenced
    def packets(self, neighbor, direction, category, header, body):
        for process in self._notify(neighbor, '%s-packets' % direction):
            self.write(
                process,
                self._encoder[process].packets(neighbor, direction, category,
                                               header, body), neighbor)

    @silenced
    def notification(self, neighbor, direction, code, subcode, data, header,
                     body):
        for process in self._notify(neighbor, 'neighbor-changes'):
            self.write(
                process,
                self._encoder[process].notification(neighbor, direction, code,
                                                    subcode, data, header,
                                                    body), neighbor)

    @silenced
    def message(self, message_id, neighbor, direction, message, negotiated,
                header, *body):
        self._dispatch[message_id](self, neighbor, direction, message,
                                   negotiated, header, *body)

    # registering message functions
    # no-self-argument

    def register_process(message_id, storage=_dispatch):
        def closure(function):
            def wrap(*args):
                function(*args)

            storage[message_id] = wrap
            return wrap

        return closure

    # notifications are handled in the loop as they use different arguments

    @register_process(Message.CODE.OPEN)
    def _open(self, peer, direction, message, negotiated, header, body):
        for process in self._notify(
                peer, '%s-%s' % (direction, Message.CODE.OPEN.SHORT)):
            self.write(
                process, self._encoder[process].open(peer, direction, message,
                                                     negotiated, header, body),
                peer)

    @register_process(Message.CODE.UPDATE)
    def _update(self, peer, direction, update, negotiated, header, body):
        for process in self._notify(
                peer, '%s-%s' % (direction, Message.CODE.UPDATE.SHORT)):
            self.write(
                process,
                self._encoder[process].update(peer, direction, update,
                                              negotiated, header, body), peer)

    @register_process(Message.CODE.NOTIFICATION)
    def _notification(self, peer, direction, message, negotiated, header,
                      body):
        for process in self._notify(
                peer, '%s-%s' % (direction, Message.CODE.NOTIFICATION.SHORT)):
            self.write(
                process,
                self._encoder[process].notification(peer, direction, message,
                                                    negotiated, header, body),
                peer)

    # unused-argument, must keep the API
    @register_process(Message.CODE.KEEPALIVE)
    def _keepalive(self, peer, direction, keepalive, negotiated, header, body):
        for process in self._notify(
                peer, '%s-%s' % (direction, Message.CODE.KEEPALIVE.SHORT)):
            self.write(
                process,
                self._encoder[process].keepalive(peer, direction, negotiated,
                                                 header, body), peer)

    @register_process(Message.CODE.ROUTE_REFRESH)
    def _refresh(self, peer, direction, refresh, negotiated, header, body):
        for process in self._notify(
                peer, '%s-%s' % (direction, Message.CODE.ROUTE_REFRESH.SHORT)):
            self.write(
                process,
                self._encoder[process].refresh(peer, direction, refresh,
                                               negotiated, header, body), peer)

    @register_process(Message.CODE.OPERATIONAL)
    def _operational(self, peer, direction, operational, negotiated, header,
                     body):
        for process in self._notify(
                peer, '%s-%s' % (direction, Message.CODE.OPERATIONAL.SHORT)):
            self.write(
                process,
                self._encoder[process].operational(peer, direction,
                                                   operational.category,
                                                   operational, negotiated,
                                                   header, body), peer)
Esempio n. 2
0
class Reactor(object):
    class Exit(object):
        normal = 0
        validate = 0
        listening = 1
        configuration = 1
        privileges = 1
        log = 1
        pid = 1
        socket = 1
        io_error = 1
        process = 1
        select = 1
        unknown = 1

    # [hex(ord(c)) for c in os.popen('clear').read()]
    clear = concat_bytes_i(
        character(int(c, 16))
        for c in ['0x1b', '0x5b', '0x48', '0x1b', '0x5b', '0x32', '0x4a'])

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

        self.max_loop_time = environment.settings().reactor.speed
        self._sleep_time = self.max_loop_time / 100
        self._busyspin = {}
        self._ratelimit = {}
        self.early_drop = environment.settings().daemon.drop

        self.processes = None

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

        self._peers = {}

        self._reload_processes = False
        self._saved_pid = False
        self._poller = select.poll()

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

    def _prevent_spin(self):
        second = int(time.time())
        if not second in self._busyspin:
            self._busyspin = {second: 0}
        self._busyspin[second] += 1
        if self._busyspin[second] > self.max_loop_time:
            time.sleep(self._sleep_time)
            return True
        return False

    def _rate_limited(self, peer, rate):
        if rate <= 0:
            return False
        second = int(time.time())
        ratelimit = self._ratelimit.get(peer, {})
        if not second in ratelimit:
            self._ratelimit[peer] = {second: rate - 1}
            return False
        if self._ratelimit[peer][second] > 0:
            self._ratelimit[peer][second] -= 1
            return False
        return True

    def _wait_for_io(self, sleeptime):
        spin_prevention = False
        try:
            for fd, event in self._poller.poll(sleeptime):
                if event & select.POLLIN or event & select.POLLPRI:
                    yield fd
                    continue
                elif event & select.POLLHUP or event & select.POLLERR or event & select.POLLNVAL:
                    spin_prevention = True
                    continue
            if spin_prevention:
                self._prevent_spin()
        except KeyboardInterrupt:
            self._termination('^C received', self.Exit.normal)
            return
        except Exception:
            self._prevent_spin()
            return

    # peer related functions

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

    def established_peers(self):
        peers = set()
        for key, peer in self._peers.items():
            if peer.fsm == FSM.ESTABLISHED:
                peers.add(key)
        return peers

    def peers(self):
        return list(self._peers)

    def handle_connection(self, peer_name, connection):
        peer = self._peers.get(peer_name, None)
        if not peer:
            self.logger.critical('could not find referenced peer', 'reactor')
            return
        peer.handle_connection(connection)

    def neighbor(self, peer_name):
        peer = self._peers.get(peer_name, None)
        if not peer:
            self.logger.critical('could not find referenced peer', 'reactor')
            return
        return peer.neighbor

    def neighbor_name(self, peer_name):
        peer = self._peers.get(peer_name, None)
        if not peer:
            self.logger.critical('could not find referenced peer', 'reactor')
            return ""
        return peer.neighbor.name()

    def neighbor_ip(self, peer_name):
        peer = self._peers.get(peer_name, None)
        if not peer:
            self.logger.critical('could not find referenced peer', 'reactor')
            return ""
        return str(peer.neighbor.peer_address)

    def neighbor_cli_data(self, peer_name):
        peer = self._peers.get(peer_name, None)
        if not peer:
            self.logger.critical('could not find referenced peer', 'reactor')
            return ""
        return peer.cli_data()

    def neighor_rib(self, peer_name, rib_name, advertised=False):
        peer = self._peers.get(peer_name, None)
        if not peer:
            self.logger.critical('could not find referenced peer', 'reactor')
            return []
        families = None
        if advertised:
            families = peer.proto.negotiated.families if peer.proto else []
        rib = peer.neighbor.rib.outgoing if rib_name == 'out' else peer.neighbor.rib.incoming
        return list(rib.cached_changes(families))

    def neighbor_rib_resend(self, peer_name):
        peer = self._peers.get(peer_name, None)
        if not peer:
            self.logger.critical('could not find referenced peer', 'reactor')
            return
        peer.neighbor.rib.outgoing.resend(None, peer.neighbor.route_refresh)

    def neighbor_rib_out_withdraw(self, peer_name):
        peer = self._peers.get(peer_name, None)
        if not peer:
            self.logger.critical('could not find referenced peer', 'reactor')
            return
        peer.neighbor.rib.outgoing.withdraw(None, peer.neighbor.route_refresh)

    def neighbor_rib_in_clear(self, peer_name):
        peer = self._peers.get(peer_name, None)
        if not peer:
            self.logger.critical('could not find referenced peer', 'reactor')
            return
        peer.neighbor.rib.incoming.clear()

    # ...

    def _completed(self, peers):
        for peer in peers:
            if self._peers[peer].neighbor.rib.outgoing.pending():
                return False
        return True

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

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

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

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

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

        if not self.load():
            return self.Exit.configuration

        if validate:  # only validate configuration
            self.logger.warning('', 'configuration')
            self.logger.warning('parsed Neighbors, un-templated',
                                'configuration')
            self.logger.warning('------------------------------',
                                'configuration')
            self.logger.warning('', 'configuration')
            for key in self._peers:
                self.logger.warning(str(self._peers[key].neighbor),
                                    'configuration')
                self.logger.warning('', 'configuration')
            return self.Exit.validate

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

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

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

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

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

        if not self.daemon.savepid():
            return self.Exit.pid

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

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

        workers = {}
        peers = set()
        api_fds = []
        ms_sleep = int(self._sleep_time * 1000)

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

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

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

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

                    if not reload_completed:
                        continue

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

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

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

                peers = self.active_peers()
                if self._completed(peers):
                    reload_completed = True

                sleep = ms_sleep

                # do not attempt to listen on closed sockets even if the peer is still here
                for io in list(workers.keys()):
                    if io == -1:
                        self._poller.unregister(io)
                        del workers[io]

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

                    # limit the number of message handling per second
                    if self._rate_limited(key, peer.neighbor.rate_limit):
                        peers.discard(key)
                        continue

                    # handle the peer
                    action = peer.run()

                    # .run() returns an ACTION enum:
                    # * immediate if it wants to be called again
                    # * later if it should be called again but has no work atm
                    # * close if it is finished and is closing down, or restarting
                    if action == ACTION.CLOSE:
                        if key in self._peers:
                            del self._peers[key]
                        peers.discard(key)
                    # we are loosing this peer, not point to schedule more process work
                    elif action == ACTION.LATER:
                        io = peer.socket()
                        if io != -1:
                            self._poller.register(
                                io,
                                select.POLLIN | select.POLLPRI | select.POLLHUP
                                | select.POLLNVAL | select.POLLERR)
                            workers[io] = key
                        # no need to come back to it before a a full cycle
                        peers.discard(key)
                    elif action == ACTION.NOW:
                        sleep = 0

                    if not peers:
                        break

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

                self.asynchronous.run()

                if api_fds != self.processes.fds:
                    for fd in api_fds:
                        if fd == -1:
                            continue
                        if fd not in self.processes.fds:
                            self._poller.unregister(fd)
                    for fd in self.processes.fds:
                        if fd == -1:
                            continue
                        if fd not in api_fds:
                            self._poller.register(
                                fd,
                                select.POLLIN | select.POLLPRI | select.POLLHUP
                                | select.POLLNVAL | select.POLLERR)
                    api_fds = self.processes.fds

                for io in self._wait_for_io(sleep):
                    if io not in api_fds:
                        peers.add(workers[io])

                if self._stopping and not self._peers.keys():
                    self._termination('exiting on peer termination',
                                      self.Exit.normal)

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

        return self.exit_code

    def register_peer(self, name, peer):
        self._peers[name] = peer

    def teardown_peer(self, name, code):
        self._peers[name].teardown(code)

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

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

        reloaded = self.configuration.reload()

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

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

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

        return True

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

        # XXX: FIXME: Could return False, in case there is interference with old config...
        reloaded = self.configuration.reload()

        for key in self._peers.keys():
            if key not in self.configuration.neighbors.keys():
                peer = self._peers[key]
                self.logger.debug('removing peer %s' % peer.neighbor.name(),
                                  'reactor')
                self._peers[key].remove()
            else:
                self._peers[key].reestablish()
        self.processes.start(self.configuration.processes, True)
Esempio n. 3
0
class Processes (object):
	# how many time can a process can respawn in the time interval
	respawn_timemask = 0xFFFFFF - 0b111111
	# '0b111111111111111111000000' (around a minute, 63 seconds)

	_dispatch = {}

	def __init__ (self):
		self.logger = Logger()
		self.clean()
		self.silence = False
		self._buffer = {}
		self._configuration = {}

		self.respawn_number = 5 if environment.settings().api.respawn else 0
		self.terminate_on_error = environment.settings().api.terminate
		self.ack = environment.settings().api.ack

	def number (self):
		return len(self._process)

	def clean (self):
		self._process = {}
		self._encoder = {}
		self._broken = []
		self._respawning = {}

	def _handle_problem (self, process):
		if self.respawn_number:
			self.logger.debug('issue with the process, restarting it','process')
			self._terminate(process)
			self._start(process)
		else:
			self.logger.debug('issue with the process, terminating it','process')
			self._terminate(process)

	def _terminate (self, process):
		self.logger.debug('terminating process %s' % process,'process')
		try:
			self._process[process].terminate()
		except OSError:
			# the process is most likely already dead
			pass
		self._process[process].wait()
		del self._process[process]

	def terminate (self):
		for process in list(self._process):
			if not self.silence:
				try:
					self._write(process,self._encoder[process].shutdown())
				except ProcessError:
					pass
		self.silence = True
		# waiting a little to make sure IO is flushed to the pipes
		# we are using unbuffered IO but still ..
		time.sleep(0.1)
		for process in list(self._process):
			try:
				self._terminate(process)
			except OSError:
				# we most likely received a SIGTERM signal and our child is already dead
				self.logger.debug('child process %s was already dead' % process,'process')
		self.clean()

	def _start (self,process):
		try:
			if process in self._process:
				self.logger.debug('process already running','process')
				return
			if process not in self._configuration:
				self.logger.debug('can not start process, no configuration for it','process')
				return
			# Prevent some weird termcap data to be created at the start of the PIPE
			# \x1b[?1034h (no-eol) (esc)
			os.environ['TERM'] = 'dumb'

			configuration = self._configuration[process]

			run = configuration.get('run','')
			if run:
				api = configuration.get('encoder','')
				self._encoder[process] = Response.Text(text_version) if api == 'text' else Response.JSON(json_version)

				self._process[process] = subprocess.Popen(
					run,
					stdin=subprocess.PIPE,
					stdout=subprocess.PIPE,
					preexec_fn=preexec_helper
					# This flags exists for python 2.7.3 in the documentation but on on my MAC
					# creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
				)
				fcntl.fcntl(self._process[process].stdout.fileno(), fcntl.F_SETFL, os.O_NONBLOCK)

				self.logger.debug('forked process %s' % process,'process')

				around_now = int(time.time()) & self.respawn_timemask
				if process in self._respawning:
					if around_now in self._respawning[process]:
						self._respawning[process][around_now] += 1
						# we are respawning too fast
						if self._respawning[process][around_now] > self.respawn_number:
							self.logger.critical(
								'Too many death for %s (%d) terminating program' % (process,self.respawn_number),
								'process'
							)
							raise ProcessError()
					else:
						# reset long time since last respawn
						self._respawning[process] = {around_now: 1}
				else:
					# record respawing
					self._respawning[process] = {around_now: 1}

		except (subprocess.CalledProcessError,OSError,ValueError) as exc:
			self._broken.append(process)
			self.logger.debug('could not start process %s' % process,'process')
			self.logger.debug('reason: %s' % str(exc),'process')

	def start (self, configuration, restart=False):
		for process in list(self._process):
			if process not in configuration:
				self._terminate(process)
		self._configuration = configuration
		for process in configuration:
			if restart and process in list(self._process):
				self._terminate(process)
			self._start(process)

	def broken (self, neighbor):
		if self._broken:
			for process in self._configuration:
				if process in self._broken:
					return True
		return False

	def fds (self):
		return [self._process[process].stdout for process in self._process]

	def received (self):
		consumed_data = False

		for process in list(self._process):
			try:
				proc = self._process[process]
				poll = proc.poll()
				# proc.poll returns None if the process is still fine
				# -[signal], like -15, if the process was terminated
				if poll is not None:
					self._handle_problem(process)
					return
				r,_,_ = select.select([proc.stdout,],[],[],0)
				if r:
					try:
						# Calling next() on Linux and OSX works perfectly well
						# but not on OpenBSD where it always raise StopIteration
						# and only readline() works
						buf = str_ascii(proc.stdout.read(16384))
						if buf == '' and poll is not None:
							# if proc.poll() is None then
							# process is fine, we received an empty line because
							# we're doing .readline() on a non-blocking pipe and
							# the process maybe has nothing to send yet
							self._handle_problem(process)
							continue

						raw = self._buffer.get(process,'') + buf

						while '\n' in raw:
							line,raw = raw.split('\n',1)
							line = line.rstrip()
							consumed_data = True
							self.logger.debug('command from process %s : %s ' % (process,line),'process')
							yield (process,formated(line))

						self._buffer[process] = raw

					except IOError as exc:
						if not exc.errno or exc.errno in error.fatal:
							# if the program exits we can get an IOError with errno code zero !
							self._handle_problem(process)
						elif exc.errno in error.block:
							# we often see errno.EINTR: call interrupted and
							# we most likely have data, we will try to read them a the next loop iteration
							pass
						else:
							self.logger.debug('unexpected errno received from forked process (%s)' % errstr(exc),'process')
					except StopIteration:
						if not consumed_data:
							self._handle_problem(process)
			except (subprocess.CalledProcessError,OSError,ValueError):
				self._handle_problem(process)

	def _write (self, process, string, neighbor=None):
		if string is None:
			return True

		# XXX: FIXME: This is potentially blocking
		while True:
			try:
				self._process[process].stdin.write(bytes_ascii('%s\n' % string))
			except IOError as exc:
				self._broken.append(process)
				if exc.errno == errno.EPIPE:
					self._broken.append(process)
					self.logger.debug('issue while sending data to our helper program','process')
					raise ProcessError()
				else:
					# Could it have been caused by a signal ? What to do.
					self.logger.debug('error received while sending data to helper program, retrying (%s)' % errstr(exc),'process')
					continue
			break

		try:
			self._process[process].stdin.flush()
		except IOError as exc:
			# AFAIK, the buffer should be flushed at the next attempt.
			self.logger.debug('error received while FLUSHING data to helper program, retrying (%s)' % errstr(exc),'process')

		return True

	def answer (self, service, string, force=False):
		if force or self.ack:
			self.logger.debug('responding to %s : %s' % (service,string.replace('\n','\\n')),'process')
			self._write(service,string)

	def answer_done (self, service):
		self.answer(service,'done')

	def answer_error (self, service):
		self.answer(service,'error')

	def _notify (self, neighbor, event):
		for process in neighbor.api[event]:
			yield process

	# do not do anything if silenced
	# no-self-argument

	def silenced (function):
		def closure (self, *args):
			if self.silence:
				return
			return function(self,*args)
		return closure

	# invalid-name
	@silenced
	def up (self, neighbor):
		for process in self._notify(neighbor,'neighbor-changes'):
			self._write(process,self._encoder[process].up(neighbor),neighbor)

	@silenced
	def connected (self, neighbor):
		for process in self._notify(neighbor,'neighbor-changes'):
			self._write(process,self._encoder[process].connected(neighbor),neighbor)

	@silenced
	def down (self, neighbor, reason):
		for process in self._notify(neighbor,'neighbor-changes'):
			self._write(process,self._encoder[process].down(neighbor,reason),neighbor)

	@silenced
	def negotiated (self, neighbor, negotiated):
		for process in self._notify(neighbor,'negotiated'):
			self._write(process,self._encoder[process].negotiated(neighbor,negotiated),neighbor)

	@silenced
	def fsm (self, neighbor, fsm):
		for process in self._notify(neighbor,'fsm'):
			self._write(process,self._encoder[process].fsm(neighbor,fsm),neighbor)

	@silenced
	def signal (self, neighbor, signal):
		for process in self._notify(neighbor,'signal'):
			self._write(process,self._encoder[process].signal(neighbor,signal),neighbor)

	@silenced
	def packets (self, neighbor, direction, category, header, body):
		for process in self._notify(neighbor,'%s-packets' % direction):
			self._write(process,self._encoder[process].packets(neighbor,direction,category,header,body),neighbor)

	@silenced
	def notification (self, neighbor, direction, code, subcode, data, header, body):
		for process in self._notify(neighbor,'neighbor-changes'):
			self._write(process,self._encoder[process].notification(neighbor,direction,code,subcode,data,header,body),neighbor)

	@silenced
	def message (self, message_id, neighbor, direction, message, negotiated, header, *body):
		self._dispatch[message_id](self,neighbor,direction,message,negotiated,header,*body)

	# registering message functions
	# no-self-argument

	def register_process (message_id, storage=_dispatch):
		def closure (function):
			def wrap (*args):
				function(*args)
			storage[message_id] = wrap
			return wrap
		return closure

	# notifications are handled in the loop as they use different arguments

	@register_process(Message.CODE.OPEN)
	def _open (self, peer, direction, message, negotiated, header, body):
		for process in self._notify(peer,'%s-%s' % (direction,Message.CODE.OPEN.SHORT)):
			self._write(process,self._encoder[process].open(peer,direction,message,negotiated,header,body),peer)

	@register_process(Message.CODE.UPDATE)
	def _update (self, peer, direction, update, negotiated, header, body):
		for process in self._notify(peer,'%s-%s' % (direction,Message.CODE.UPDATE.SHORT)):
			self._write(process,self._encoder[process].update(peer,direction,update,negotiated,header,body),peer)

	@register_process(Message.CODE.NOTIFICATION)
	def _notification (self, peer, direction, message, negotiated, header, body):
		for process in self._notify(peer,'%s-%s' % (direction,Message.CODE.NOTIFICATION.SHORT)):
			self._write(process,self._encoder[process].notification(peer,direction,message,negotiated,header,body),peer)

	# unused-argument, must keep the API
	@register_process(Message.CODE.KEEPALIVE)
	def _keepalive (self, peer, direction, keepalive, negotiated, header, body):
		for process in self._notify(peer,'%s-%s' % (direction,Message.CODE.KEEPALIVE.SHORT)):
			self._write(process,self._encoder[process].keepalive(peer,direction,negotiated,header,body),peer)

	@register_process(Message.CODE.ROUTE_REFRESH)
	def _refresh (self, peer, direction, refresh, negotiated, header, body):
		for process in self._notify(peer,'%s-%s' % (direction,Message.CODE.ROUTE_REFRESH.SHORT)):
			self._write(process,self._encoder[process].refresh(peer,direction,refresh,negotiated,header,body),peer)

	@register_process(Message.CODE.OPERATIONAL)
	def _operational (self, peer, direction, operational, negotiated, header, body):
		for process in self._notify(peer,'%s-%s' % (direction,Message.CODE.OPERATIONAL.SHORT)):
			self._write(process,self._encoder[process].operational(peer,direction,operational.category,operational,negotiated,header,body),peer)
Esempio n. 4
0
class Listener(object):
    _family_AFI_map = {
        socket.AF_INET: AFI.ipv4,
        socket.AF_INET6: AFI.ipv6,
    }

    def __init__(self, reactor, backlog=200):
        self.serving = False
        self.logger = Logger()

        self._reactor = reactor
        self._backlog = backlog
        self._sockets = {}
        self._accepted = {}
        self._pending = 0

    def _new_socket(self, ip):
        if ip.afi == AFI.ipv6:
            return socket.socket(socket.AF_INET6, socket.SOCK_STREAM,
                                 socket.IPPROTO_TCP)
        if ip.afi == AFI.ipv4:
            return socket.socket(socket.AF_INET, socket.SOCK_STREAM,
                                 socket.IPPROTO_TCP)
        raise NetworkError(
            'Can not create socket for listening, family of IP %s is unknown' %
            ip)

    def _listen(self, local_ip, peer_ip, local_port, md5, md5_base64, ttl_in):
        self.serving = True

        for sock, (local, port, peer, md) in self._sockets.items():
            if local_ip.top() != local:
                continue
            if local_port != port:
                continue
            MD5(sock, peer_ip.top(), 0, md5, md5_base64)
            if ttl_in:
                MIN_TTL(sock, peer_ip, ttl_in)
            return

        try:
            sock = self._new_socket(local_ip)
            # MD5 must match the peer side of the TCP, not the local one
            MD5(sock, peer_ip.top(), 0, md5, md5_base64)
            if ttl_in:
                MIN_TTL(sock, peer_ip, ttl_in)
            try:
                sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
                if local_ip.ipv6():
                    sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
            except (socket.error, AttributeError):
                pass
            sock.setblocking(0)
            # s.settimeout(0.0)
            sock.bind((local_ip.top(), local_port))
            sock.listen(self._backlog)
            self._sockets[sock] = (local_ip.top(), local_port, peer_ip.top(),
                                   md5)
        except socket.error as exc:
            if exc.args[0] == errno.EADDRINUSE:
                raise BindingError(
                    'could not listen on %s:%d, the port may already be in use by another application'
                    % (local_ip, local_port))
            elif exc.args[0] == errno.EADDRNOTAVAIL:
                raise BindingError(
                    'could not listen on %s:%d, this is an invalid address' %
                    (local_ip, local_port))
            raise NetworkError(str(exc))
        except NetworkError as exc:
            self.logger.critical(str(exc), 'network')
            raise exc

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

    def incoming(self):
        if not self.serving:
            return False

        for sock in self._sockets:
            if sock in self._accepted:
                continue
            try:
                io, _ = sock.accept()
                self._accepted[sock] = io
                self._pending += 1
            except socket.error as exc:
                if exc.errno in error.block:
                    continue
                self.logger.critical(str(exc), 'network')
        if self._pending:
            self._pending -= 1
            return True
        return False

    def _connected(self):
        try:
            for sock, io in list(self._accepted.items()):
                del self._accepted[sock]
                if sock.family == socket.AF_INET:
                    local_ip = io.getpeername()[0]  # local_ip,local_port
                    remote_ip = io.getsockname()[0]  # remote_ip,remote_port
                elif sock.family == socket.AF_INET6:
                    local_ip = io.getpeername()[
                        0]  # local_ip,local_port,local_flow,local_scope
                    remote_ip = io.getsockname()[
                        0]  # remote_ip,remote_port,remote_flow,remote_scope
                else:
                    raise AcceptError('unexpected address family (%d)' %
                                      sock.family)
                fam = self._family_AFI_map[sock.family]
                yield Incoming(fam, remote_ip, local_ip, io)
        except NetworkError as exc:
            self.logger.critical(str(exc), 'network')

    def new_connections(self):
        if not self.serving:
            return
        yield None

        reactor = self._reactor
        ranged_neighbor = []

        for connection in self._connected():
            for key in reactor.peers:
                peer = reactor.peers[key]
                neighbor = peer.neighbor

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

                if not neighbor_peer_start <= connection_local < neighbor_peer_next:
                    continue

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

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

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

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

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

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

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

    def stop(self):
        if not self.serving:
            return

        for sock, (ip, port, _, _) in self._sockets.items():
            sock.close()
            self.logger.info('stopped listening on %s:%d' % (ip, port),
                             'network')

        self._sockets = {}
        self.serving = False
Esempio n. 5
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.critical('SIGTERM received', 'reactor')
        if self.received:
            self.logger.critical('ignoring - still handling previous signal',
                                 'reactor')
            return
        self.logger.critical('scheduling shutdown', 'reactor')
        self.received = self.SHUTDOWN
        self.number = signum

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

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

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

    def sigusr2(self, signum, frame):
        self.logger.critical('SIGUSR2 received', 'reactor')
        if self.received:
            self.logger.critical('ignoring - still handling previous signal',
                                 'reactor')
            return
        self.logger.critical(
            'scheduling reload of configuration and processes', 'reactor')
        self.received = self.FULL_RELOAD
        self.number = signum
Esempio n. 6
0
class Reactor (object):
	class Exit (object):
		normal = 0
		validate = 0
		listening = 1
		configuration = 1
		privileges = 1
		log = 1
		pid = 1
		socket = 1
		io_error = 1
		process = 1
		select = 1
		unknown = 1

	# [hex(ord(c)) for c in os.popen('clear').read()]
	clear = concat_bytes_i(character(int(c,16)) for c in ['0x1b', '0x5b', '0x48', '0x1b', '0x5b', '0x32', '0x4a'])

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

		self.max_loop_time = environment.settings().reactor.speed
		self._sleep_time = self.max_loop_time / 100
		self._busyspin = {}
		self.early_drop = environment.settings().daemon.drop

		self.processes = None

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

		self.peers = {}

		self._reload_processes = False
		self._saved_pid = False

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

	def _prevent_spin(self):
		second = int(time.time())
		if not second in self._busyspin:
			self._busyspin = {second: 0}
		self._busyspin[second] += 1
		if self._busyspin[second] > self.max_loop_time:
			time.sleep(self._sleep_time)
			return True
		return False

	def _api_ready (self,sockets,sleeptime):
		fds = self.processes.fds()
		ios = fds + sockets

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

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

	def _completed (self,peers):
		for peer in peers:
			if self.peers[peer].neighbor.rib.outgoing.pending():
				return False
		return True

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

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

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

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

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

		if not self.load():
			return self.Exit.configuration

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

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

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

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

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

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

		if not self.daemon.savepid():
			return self.Exit.pid

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

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

		workers = {}
		peers = set()

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

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

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

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

					if not reload_completed:
						continue

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

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

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

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

				sleep = self._sleep_time

				# do not attempt to listen on closed sockets even if the peer is still here
				for io in list(workers.keys()):
					if io.fileno() == -1:
						del workers[io]

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

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

					if not peers:
						break

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

				self.asynchronous.run()

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

				if self._stopping and not self.peers.keys():
					self._termination('exiting on peer termination',self.Exit.normal)

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

		return self.exit_code

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

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

		reloaded = self.configuration.reload()

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

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

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

		return True

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

		for key in self.peers.keys():
			if key not in self.configuration.neighbors.keys():
				neighbor = self.configuration.neighbors[key]
				self.logger.debug('removing Peer %s' % neighbor.name(),'reactor')
				self.peers[key].remove()
			else:
				self.peers[key].reestablish()
		self.processes.start(self.configuration.processes,True)
Esempio n. 7
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.debug(
                        "PIDfile already exists and program still running %s" %
                        self.pid, 'daemon')
                    return False
                else:
                    # If pid is not running, reopen file without O_EXCL
                    fd = os.open(self.pid, flags ^ os.O_EXCL, mode)
            except (OSError, IOError, ValueError):
                self.logger.debug(
                    "issue accessing PID file %s (most likely permission or ownership)"
                    % self.pid, 'daemon')
                return False

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

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

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

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

        if uid and gid:
            return True

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

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

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

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

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

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

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

        except OSError:
            return False

        return True

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

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

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

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

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

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

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

        for fd in range(0, maxfd):
            try:
                os.close(fd)
            except OSError:
                pass
        os.open("/dev/null", os.O_RDWR)
        os.dup2(0, 1)
        os.dup2(0, 2)
Esempio n. 8
0
def main():
    major = int(sys.version[0])
    minor = int(sys.version[2])

    if major <= 2 and minor < 5:
        sys.stdout.write(
            'This program can not work (is not tested) with your python version (< 2.5)\n'
        )
        sys.stdout.flush()
        sys.exit(1)

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

    options = docopt.docopt(usage, help=False)

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

    root = root_folder(options, [
        '/bin/exabgp', '/sbin/exabgp', '/lib/exabgp/application/bgp.py',
        '/lib/exabgp/application/control.py'
    ])
    prefix = '' if root == '/usr' else root
    etc = prefix + '/etc/exabgp'

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

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

    envfile = get_envfile(options, etc)
    env = get_env(envfile)

    # Must be done before setting the logger as it modify its behaviour
    if options["--debug"]:
        env.log.all = True
        env.log.level = syslog.LOG_DEBUG

    logger = Logger()

    from exabgp.configuration.setup import environment

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

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

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

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

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

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

    if options["--full-ini"] or options["--fi"]:
        for line in environment.iter_ini():
            sys.stdout.write('%s\n' % line)
            sys.stdout.flush()
        sys.exit(0)

    if options["--full-env"] or options["--fe"]:
        print()
        for line in environment.iter_env():
            sys.stdout.write('%s\n' % line)
            sys.stdout.flush()
        sys.exit(0)

    if options["--diff-ini"] or options["--di"]:
        for line in environment.iter_ini(True):
            sys.stdout.write('%s\n' % line)
            sys.stdout.flush()
        sys.exit(0)

    if options["--diff-env"] or options["--de"]:
        for line in environment.iter_env(True):
            sys.stdout.write('%s\n' % line)
            sys.stdout.flush()
        sys.exit(0)

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

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

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

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

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

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

    else:
        sys.stdout.write(usage)
        sys.stdout.write('Environment values are:\n%s\n\n' %
                         '\n'.join(' - %s' % _ for _ in environment.default()))
        sys.stdout.write('no configuration file provided')
        sys.stdout.flush()
        sys.exit(1)

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

    if env.debug.rotate or len(configurations) == 1:
        run(env, comment, configurations, root, options["--validate"])

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

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

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

        # wait for the forked processes
        for pid in pids:
            os.waitpid(pid, 0)
    except OSError as exc:
        logger.critical(
            'can not fork, errno %d : %s' % (exc.errno, exc.strerror),
            'reactor')
        sys.exit(1)
Esempio n. 9
0
class Reactor(object):
    class Exit(object):
        normal = 0
        validate = 0
        listening = 1
        configuration = 1
        privileges = 1
        log = 1
        pid = 1
        socket = 1
        io_error = 1
        process = 1
        select = 1
        unknown = 1

    # [hex(ord(c)) for c in os.popen('clear').read()]
    clear = concat_bytes_i(
        character(int(c, 16))
        for c in ['0x1b', '0x5b', '0x48', '0x1b', '0x5b', '0x32', '0x4a'])

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

        self.max_loop_time = environment.settings().reactor.speed
        self._sleep_time = self.max_loop_time / 100
        self._busyspin = {}
        self.early_drop = environment.settings().daemon.drop

        self.processes = None

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

        self.peers = {}

        self._reload_processes = False
        self._saved_pid = False

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

    def _prevent_spin(self):
        second = int(time.time())
        if not second in self._busyspin:
            self._busyspin = {second: 0}
        self._busyspin[second] += 1
        if self._busyspin[second] > self.max_loop_time:
            time.sleep(self._sleep_time)
            return True
        return False

    def _api_ready(self, sockets, sleeptime):
        fds = self.processes.fds()
        ios = fds + sockets

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

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

    def _completed(self, peers):
        for peer in peers:
            if self.peers[peer].neighbor.rib.outgoing.pending():
                return False
        return True

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

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

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

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

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

        if not self.load():
            return self.Exit.configuration

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

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

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

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

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

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

        if not self.daemon.savepid():
            return self.Exit.pid

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

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

        workers = {}
        peers = set()

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

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

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

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

                    if not reload_completed:
                        continue

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

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

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

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

                sleep = self._sleep_time

                # do not attempt to listen on closed sockets even if the peer is still here
                for io in list(workers.keys()):
                    if io.fileno() == -1:
                        del workers[io]

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

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

                    if not peers:
                        break

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

                self.asynchronous.run()

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

                if self._stopping and not self.peers.keys():
                    self._termination('exiting on peer termination',
                                      self.Exit.normal)

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

        return self.exit_code

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

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

        reloaded = self.configuration.reload()

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

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

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

        return True

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

        for key in self.peers.keys():
            if key not in self.configuration.neighbors.keys():
                neighbor = self.configuration.neighbors[key]
                self.logger.debug('removing Peer %s' % neighbor.name(),
                                  'reactor')
                self.peers[key].remove()
            else:
                self.peers[key].reestablish()
        self.processes.start(self.configuration.processes, True)
Esempio n. 10
0
class Listener (object):
	_family_AFI_map = {
		socket.AF_INET: AFI.ipv4,
		socket.AF_INET6: AFI.ipv6,
	}

	def __init__ (self, reactor, backlog=200):
		self.serving = False
		self.logger = Logger()

		self._reactor = reactor
		self._backlog = backlog
		self._sockets = {}
		self._accepted = {}
		self._pending = 0

	def _new_socket (self, ip):
		if ip.afi == AFI.ipv6:
			return socket.socket(socket.AF_INET6, socket.SOCK_STREAM, socket.IPPROTO_TCP)
		if ip.afi == AFI.ipv4:
			return socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP)
		raise NetworkError('Can not create socket for listening, family of IP %s is unknown' % ip)

	def _listen (self, local_ip, peer_ip, local_port, md5, md5_base64, ttl_in):
		self.serving = True

		for sock,(local,port,peer,md) in self._sockets.items():
			if local_ip.top() != local:
				continue
			if local_port != port:
				continue
			MD5(sock,peer_ip.top(),0,md5,md5_base64)
			if ttl_in:
				MIN_TTL(sock,peer_ip,ttl_in)
			return

		try:
			sock = self._new_socket(local_ip)
			# MD5 must match the peer side of the TCP, not the local one
			MD5(sock,peer_ip.top(),0,md5,md5_base64)
			if ttl_in:
				MIN_TTL(sock,peer_ip,ttl_in)
			try:
				sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
				if local_ip.ipv6():
					sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
			except (socket.error,AttributeError):
				pass
			sock.setblocking(0)
			# s.settimeout(0.0)
			sock.bind((local_ip.top(),local_port))
			sock.listen(self._backlog)
			self._sockets[sock] = (local_ip.top(),local_port,peer_ip.top(),md5)
		except socket.error as exc:
			if exc.args[0] == errno.EADDRINUSE:
				raise BindingError('could not listen on %s:%d, the port may already be in use by another application' % (local_ip,local_port))
			elif exc.args[0] == errno.EADDRNOTAVAIL:
				raise BindingError('could not listen on %s:%d, this is an invalid address' % (local_ip,local_port))
			raise NetworkError(str(exc))
		except NetworkError as exc:
			self.logger.critical(str(exc),'network')
			raise exc

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

	def incoming (self):
		if not self.serving:
			return False

		for sock in self._sockets:
			if sock in self._accepted:
				continue
			try:
				io, _ = sock.accept()
				self._accepted[sock] = io
				self._pending += 1
			except socket.error as exc:
				if exc.errno in error.block:
					continue
				self.logger.critical(str(exc),'network')
		if self._pending:
			self._pending -= 1
			return True
		return False

	def _connected (self):
		try:
			for sock,io in list(self._accepted.items()):
				del self._accepted[sock]
				if sock.family == socket.AF_INET:
					local_ip  = io.getpeername()[0]  # local_ip,local_port
					remote_ip = io.getsockname()[0]  # remote_ip,remote_port
				elif sock.family == socket.AF_INET6:
					local_ip  = io.getpeername()[0]  # local_ip,local_port,local_flow,local_scope
					remote_ip = io.getsockname()[0]  # remote_ip,remote_port,remote_flow,remote_scope
				else:
					raise AcceptError('unexpected address family (%d)' % sock.family)
				fam = self._family_AFI_map[sock.family]
				yield Incoming(fam,remote_ip,local_ip,io)
		except NetworkError as exc:
			self.logger.critical(str(exc),'network')

	def new_connections (self):
		if not self.serving:
			return
		yield None

		reactor = self._reactor
		ranged_neighbor = []

		for connection in self._connected():
			self.logger.debug('new connection received %s' % connection.name(),'network')
			for key in reactor.peers:
				peer = reactor.peers[key]
				neighbor = peer.neighbor

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

				if not neighbor_peer_start <= connection_local < neighbor_peer_next:
					continue

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

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

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

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

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

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

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

	def stop (self):
		if not self.serving:
			return

		for sock,(ip,port,_,_) in self._sockets.items():
			sock.close()
			self.logger.info('stopped listening on %s:%d' % (ip,port),'network')

		self._sockets = {}
		self.serving = False
Esempio n. 11
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.debug("PIDfile already exists and program still running %s" % self.pid,'daemon')
					return False
				else:
					# If pid is not running, reopen file without O_EXCL
					fd = os.open(self.pid,flags ^ os.O_EXCL,mode)
			except (OSError,IOError,ValueError):
				self.logger.debug("issue accessing PID file %s (most likely permission or ownership)" % self.pid,'daemon')
				return False

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

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

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

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

		if uid and gid:
			return True

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

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

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

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

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

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

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

		except OSError:
			return False

		return True

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

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

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

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

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

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

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

		for fd in range(0, maxfd):
			try:
				os.close(fd)
			except OSError:
				pass
		os.open("/dev/null", os.O_RDWR)
		os.dup2(0, 1)
		os.dup2(0, 2)
Esempio n. 12
0
File: bgp.py Progetto: aabdnn/exabgp
def main ():
	major = int(sys.version[0])
	minor = int(sys.version[2])

	if major <= 2 and minor < 5:
		sys.stdout.write('This program can not work (is not tested) with your python version (< 2.5)\n')
		sys.stdout.flush()
		sys.exit(1)

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

	options = docopt.docopt(usage, help=False)

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

	root = root_folder(options,['/bin/exabgp','/sbin/exabgp','/lib/exabgp/application/bgp.py','/lib/exabgp/application/control.py'])
	etc = root + '/etc/exabgp'
	os.environ['EXABGP_ETC'] = etc  # This is not most pretty

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

	envfile = get_envfile(options,etc)
	env = get_env(envfile)

	# Must be done before setting the logger as it modify its behaviour
	if options["--debug"]:
		env.log.all = True
		env.log.level = syslog.LOG_DEBUG

	logger = Logger()

	from exabgp.configuration.setup import environment

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

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

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

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

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

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

	if options["--full-ini"] or options["--fi"]:
		for line in environment.iter_ini():
			sys.stdout.write('%s\n' % line)
			sys.stdout.flush()
		sys.exit(0)

	if options["--full-env"] or options["--fe"]:
		print()
		for line in environment.iter_env():
			sys.stdout.write('%s\n' % line)
			sys.stdout.flush()
		sys.exit(0)

	if options["--diff-ini"] or options["--di"]:
		for line in environment.iter_ini(True):
			sys.stdout.write('%s\n' % line)
			sys.stdout.flush()
		sys.exit(0)

	if options["--diff-env"] or options["--de"]:
		for line in environment.iter_env(True):
			sys.stdout.write('%s\n' % line)
			sys.stdout.flush()
		sys.exit(0)

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

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

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

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

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

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

	else:
		sys.stdout.write(usage)
		sys.stdout.write('Environment values are:\n%s\n\n' % '\n'.join(' - %s' % _ for _ in environment.default()))
		sys.stdout.write('no configuration file provided')
		sys.stdout.flush()
		sys.exit(1)

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

	if env.debug.rotate or len(configurations) == 1:
		run(env,comment,configurations,root,options["--validate"])

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

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

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

		# wait for the forked processes
		for pid in pids:
			os.waitpid(pid,0)
	except OSError as exc:
		logger.critical('can not fork, errno %d : %s' % (exc.errno,exc.strerror),'reactor')
		sys.exit(1)
Esempio n. 13
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.critical('SIGTERM received','reactor')
		if self.received:
			self.logger.critical('ignoring - still handling previous signal','reactor')
			return
		self.logger.critical('scheduling shutdown','reactor')
		self.received = self.SHUTDOWN
		self.number = signum

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

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

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

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