Example #1
0
class Receiver(service.Service):
    """
        The receiver connects to the broadcaster and receives events
        for the targets that it subscribed to.
    """
    def subscribeTarget(self, targetname):
        self.configuration.reload_targets()
        if targetname in self.configuration['allowed_targets']:
            log.msg('subscribing to target "%s".' % targetname)
            self.broadcaster.client.subscribe(self.onEvent,
                                              unicode(targetname))
        else:
            log.msg(
                "Can't subscribe to target %s. Target not in allowed targets."
                % targetname)

    def unsubscribeTarget(self, targetname):
        log.msg('unsubscribing from target "%s".' % targetname)
        self.broadcaster.client.unsubscribe(targetname)

    def get_target_directory(self, target):
        """
            Appends the given target name to the targets_directory.

            @raise ReceiverException: if the target directory does not exist.
        """
        hostname = self.configuration['hostname']
        targets_directory = self.configuration['targets_directory']

        target_directory = os.path.join(targets_directory, target)

        if not os.path.exists(target_directory):
            raise ReceiverException(
                '(%s) target[%s] request failed: target directory "%s" does not exist.'
                % (hostname, target, target_directory))

        return target_directory

    def handle_request(self, event):
        tracking_id = _determine_tracking_id(event.arguments)
        vote = str(random_uuid())

        def broadcast_vote(_):
            log.msg('Voting %r for request with tracking-id %r' %
                    (vote, tracking_id))
            self.broadcaster._sendEvent('vote',
                                        data=vote,
                                        tracking_id=tracking_id,
                                        target=event.target)

        def cleanup_fsm(_):
            del self.states[tracking_id]
            log.msg('Cleaned up fsm for %s, %d left in memory' %
                    (event.target, len(self.states)))

        def fold(_):
            METRICS['voting_folds'] += 1

        self.states[tracking_id] = create_voting_fsm(
            tracking_id, vote, broadcast_vote,
            functools.partial(self.perform_request, event), fold, cleanup_fsm)

        reactor.callLater(10, self.states[tracking_id].showdown)

    def perform_request(self, event, _):
        """
            Handles a request for the given target by executing the given
            command (using the python_command and script_to_execute from
            the configuration).
        """
        log.msg('I have won the vote for %r, starting it now..' %
                (event.target))
        METRICS['voting_wins'] += 1
        try:
            hostname = str(self.configuration['hostname'])
            python_command = str(self.configuration['python_command'])
            script_to_execute = str(self.configuration['script_to_execute'])
            command_and_arguments_list = [python_command, script_to_execute
                                          ] + event.arguments
            command_with_arguments = ' '.join(command_and_arguments_list)

            event.tracking_id = _determine_tracking_id(
                command_and_arguments_list)
            self.publish_start(event)

            if event.tracking_id in self.states:
                self.states[event.tracking_id].spawned()
            else:
                log.err(
                    'Tracking ID %r not registered with my FSM, but handling it anyway.'
                    % event.tracking_id)

            process_protocol = ProcessProtocol(hostname,
                                               self.broadcaster,
                                               event.target,
                                               command_with_arguments,
                                               tracking_id=event.tracking_id)

            target_dir = self.get_target_directory(event.target)
            #  we pulled the arguments out of the event, so they are unicode, not string yet
            command_and_arguments_list = map(
                lambda possible_unicode: str(possible_unicode),
                command_and_arguments_list)

            reactor.spawnProcess(process_protocol,
                                 python_command,
                                 command_and_arguments_list,
                                 env={},
                                 path=target_dir)
        except Exception as e:
            self.publish_failed(event, "%s : %s" % (type(e), e.message))

    def publish_failed(self, event, message):
        """
            Publishes a event to signal that the command on the target failed.
        """
        log.err(_stuff=Exception(message), _why=message)
        METRICS['commands_failed.%s' % (event.target)] += 1
        self.broadcaster.publish_cmd_for_target(event.target,
                                                event.command,
                                                events.FAILED,
                                                message,
                                                tracking_id=event.tracking_id)

    def publish_start(self, event):
        """
            Publishes a event to signal that the command on the target started.
        """
        hostname = self.configuration['hostname']
        message = '(%s) target[%s] request: command="%s", arguments=%s' % (
            hostname, event.target, event.command, event.arguments)
        log.msg(message)
        METRICS['commands_started.%s' % (event.target)] += 1
        self.broadcaster.publish_cmd_for_target(event.target,
                                                event.command,
                                                events.STARTED,
                                                message,
                                                tracking_id=event.tracking_id)

    def onConnect(self):
        """
            Subscribes to the targets from the configuration. The receiver
            is useless when no targets are configured, therefore it will exit
            with error code 1 when no targets are configured.
        """
        self.states = {}

        self.broadcaster.client.connectionLost = self.onConnectionLost

        host = self.configuration['broadcaster_host']
        port = self.configuration['broadcaster_port']

        log.msg('Successfully connected to broadcaster on %s:%s' %
                (host, port))

        self.configuration.reload_targets()
        targets = sorted(self.configuration['allowed_targets'])

        if not targets:
            log.err('No targets configured or no targets in allowed targets.')
            exit(1)

        for targetname in targets:
            log.msg('subscribing to target "%s".' % targetname)
            self.broadcaster.client.subscribe(self.onEvent,
                                              unicode(targetname))

    def _should_refresh_connection(self):
        if not hasattr(self, 'broadcaster') or not self.broadcaster.client:
            log.msg('Not connected, cannot refresh connection')
            return False  # no connection, cannot refresh

        current_hour = datetime.now().hour
        if not current_hour == 2:  # only refresh at 2:xx a.m.
            log.msg("It's %d:xx, not 2:xx a.m., no connection-refresh now." %
                    current_hour)
            return False

        return True

    def _refresh_connection(self, delay=60 * 60, first_call=False):
        """
            When connected, closes connection to force a clean reconnect,
            except on first_call
        """
        reactor.callLater(delay, self._refresh_connection)
        log.msg('Might want to refresh connection now.')
        if not first_call and self._should_refresh_connection():
            log.msg(
                'Closing connection to broadcaster. This should force a connection-refresh.'
            )
            self.broadcaster.client.sendClose()

    def onConnectionLost(self, reason):
        """
            Allows for clean reconnect behaviour, because it 'none'ifies
            the client explicitly
        """
        log.err('connection lost: %s' % reason)
        self.broadcaster.client = None

    def onEvent(self, *args):
        """
            Will be called when receiving an event from the broadcaster.
            See onConnect which subscribes to the targets.
        """

        try:
            # Wamp v1: onEvent is callbacked with topic and event
            target, event_data = args
        except ValueError:
            # Wamp v2: onEvent is callbacked with event
            event_data, = args
            target = None

        event = events.Event(target, event_data)

        if event.is_a_vote:
            voting_fsm = self.states.get(event.tracking_id)
            if not voting_fsm:
                log.msg('Ignoring vote %r because I have already lost' %
                        event.vote)
                return
            own_vote = voting_fsm.vote
            is_a_fold = (own_vote < event.vote)

            if is_a_fold:
                log.msg(
                    'Folding due to vote %r being higher than own vote %r' %
                    (event.vote, own_vote))
                voting_fsm.fold()
            else:
                log.msg('Calling due to vote %r being lower than own vote %r' %
                        (event.vote, own_vote))
                voting_fsm.call()

        elif event.is_a_request:
            try:
                self.handle_request(event)
            except Exception as e:
                log.err(e.message)

                for line in traceback.format_exc().splitlines():
                    log.err(line)

                self.publish_failed(event, e.message)

        else:
            log.msg(str(event))

    def set_configuration(self, configuration):
        """
            Assigns a configuration to this receiver instance.
        """
        self.configuration = configuration

    def initialize_twisted_logging(self):
        twenty_megabytes = 20000000
        log_file = LogFile.fromFullPath(self.configuration['log_filename'],
                                        maxRotatedFiles=10,
                                        rotateLength=twenty_megabytes)
        log.startLogging(log_file)

    def startService(self):
        """
            Initializes logging and establishes connection to broadcaster.
        """
        self.initialize_twisted_logging()
        log.msg('yadtreceiver version %s' % __version__)
        self._connect_broadcaster()
        self._refresh_connection(first_call=True)
        self.schedule_write_metrics(first_call=True)
        self.reset_metrics_at_midnight(first_call=True)

    def stopService(self):
        """
            Writes 'shutting down service' to the log.
        """
        log.msg('shutting down service')

    def _connect_broadcaster(self):
        """
            Establishes a connection to the broadcaster as found in the
            configuration.
        """
        host = self.configuration['broadcaster_host']
        port = self.configuration['broadcaster_port']

        log.msg('Connecting to broadcaster on %s:%s' % (host, port))

        self.broadcaster = WampBroadcaster(host, port, 'yadtreceiver')
        self.broadcaster.addOnSessionOpenHandler(self.onConnect)

    def write_metrics_to_file(self):

        metrics_directory = self.configuration['metrics_directory']
        if not metrics_directory:
            return

        if not os.path.isdir(metrics_directory):
            try:
                os.makedirs(metrics_directory)
            except Exception as e:
                log.err("Cannot create metrics directory : {0}".format(e))
                return

        metrics_file_name = self.configuration['metrics_file']
        with open(metrics_file_name, 'w') as metrics_file:
            _write_metrics(METRICS, metrics_file)

    def schedule_write_metrics(self, delay=30, first_call=False):
        reactor.callLater(delay, self.schedule_write_metrics)
        if not first_call:
            start = time()
            self.write_metrics_to_file()
            write_duration = time() - start
            log.msg(
                "Wrote metrics to file in {0} seconds".format(write_duration))
            METRICS["last_write_duration"] = write_duration

    def reset_metrics_at_midnight(cls, first_call=False):
        reactor.callLater(seconds_to_midnight(), cls.reset_metrics_at_midnight)
        if not first_call:
            log.msg("Resetting metrics")
            _reset_metrics(METRICS)
    reactor.callLater(5, reactor.stop)


def ensure_test_data_is_present():
    tc = unittest.TestCase('__init__')
    logger.info("Checking full target API..")
    target_status = requests.get("http://%s:8080/api/v1/targets/%s/full" % (host, test_id))
    tc.assertEqual(target_status.status_code, 200)
    logger.info("Status ok, checking contents..")
    actual_stored_data = json.loads(target_status.text)
    expected_stored_data = [{u'services': [{u'state': u'down', u'name': u'frontend'}, {u'state': u'down', u'name': u'iptables'}, {u'state': u'up', u'name': u'middleservice1'}, {u'state': u'up', u'name': u'middleservice2'}, {u'state': u'up', u'name': u'backservice'}, {u'state': u'up', u'name': u'docker'}], u'host': u'machine', u'artefacts': [{u'version': u'0.9.1-42.el9.x86_64', u'name': u'ConsoleKit-libs'}, {u'version': u'1.3.10-33.el9.x86_64', u'name': u'zsh'}]}, {u'services': [{u'state': u'up', u'name': u'frontend'}, {u'state': u'down', u'name': u'iptables'}, {u'state': u'up', u'name': u'middleservice1'}, {u'state': u'up', u'name': u'middleservice2'}, {u'state': u'up', u'name': u'backservice'}, {u'state': u'up', u'name': u'docker'}], u'host': u'host2', u'artefacts': [{u'version': u'0.9.1-42.el9.x86_64', u'name': u'ConsoleKit-libs'}, {u'version': u'1.3.10-33.el9.x86_64', u'name': u'zsh'}]}]
    tc.assertEqual(expected_stored_data, actual_stored_data)
    reactor.exit_code = 0


def timeout(nr_seconds):
    print("Trying timeout")
    if not getattr(reactor, "connected", default=False):
        logger.error("Timed out after %d seconds waiting for connection" % nr_seconds)
        reactor.exit_code = 1
        reactor.stop()


w.addOnSessionOpenHandler(connected)
send_full_update_and_service_change()
reactor.callLater(10, timeout, 10)
reactor.run()

ensure_test_data_is_present()
sys.exit(reactor.exit_code)
Example #3
0
class Receiver(service.Service):

    """
        The receiver connects to the broadcaster and receives events
        for the targets that it subscribed to.
    """

    def subscribeTarget(self, targetname):
        self.configuration.reload_targets()
        if targetname in self.configuration['allowed_targets']:
            log.msg('subscribing to target "%s".' % targetname)
            self.broadcaster.client.subscribe(self.onEvent, unicode(targetname))
        else:
            log.msg(
                "Can't subscribe to target %s. Target not in allowed targets." %
                targetname)

    def unsubscribeTarget(self, targetname):
        log.msg('unsubscribing from target "%s".' % targetname)
        self.broadcaster.client.unsubscribe(targetname)

    def get_target_directory(self, target):
        """
            Appends the given target name to the targets_directory.

            @raise ReceiverException: if the target directory does not exist.
        """
        hostname = self.configuration['hostname']
        targets_directory = self.configuration['targets_directory']

        target_directory = os.path.join(targets_directory, target)

        if not os.path.exists(target_directory):
            raise ReceiverException('(%s) target[%s] request failed: target directory "%s" does not exist.'
                                    % (hostname, target, target_directory))

        return target_directory

    def handle_request(self, event):
        tracking_id = _determine_tracking_id(event.arguments)
        vote = str(random_uuid())

        def broadcast_vote(_):
            log.msg('Voting %r for request with tracking-id %r' %
                    (vote, tracking_id))
            self.broadcaster._sendEvent('vote',
                                        data=vote,
                                        tracking_id=tracking_id,
                                        target=event.target)

        def cleanup_fsm(_):
            del self.states[tracking_id]
            log.msg('Cleaned up fsm for %s, %d left in memory' % (event.target, len(self.states)))

        def fold(_):
            METRICS['voting_folds'] += 1

        self.states[tracking_id] = create_voting_fsm(tracking_id,
                                                     vote,
                                                     broadcast_vote,
                                                     functools.partial(
                                                         self.perform_request, event),
                                                     fold,
                                                     cleanup_fsm)

        reactor.callLater(10, self.states[tracking_id].showdown)

    def perform_request(self, event, _):
        """
            Handles a request for the given target by executing the given
            command (using the python_command and script_to_execute from
            the configuration).
        """
        log.msg('I have won the vote for %r, starting it now..' %
                (event.target))
        METRICS['voting_wins'] += 1
        try:
            hostname = str(self.configuration['hostname'])
            python_command = str(self.configuration['python_command'])
            script_to_execute = str(self.configuration['script_to_execute'])
            command_and_arguments_list = [
                python_command, script_to_execute] + event.arguments
            command_with_arguments = ' '.join(command_and_arguments_list)

            event.tracking_id = _determine_tracking_id(command_and_arguments_list)
            self.publish_start(event)

            if event.tracking_id in self.states:
                self.states[event.tracking_id].spawned()
            else:
                log.err('Tracking ID %r not registered with my FSM, but handling it anyway.' % event.tracking_id)

            process_protocol = ProcessProtocol(
                hostname, self.broadcaster, event.target, command_with_arguments, tracking_id=event.tracking_id)

            target_dir = self.get_target_directory(event.target)
            #  we pulled the arguments out of the event, so they are unicode, not string yet
            command_and_arguments_list = map(lambda possible_unicode: str(possible_unicode), command_and_arguments_list)

            reactor.spawnProcess(process_protocol, python_command,
                                 command_and_arguments_list, env={}, path=target_dir)
        except Exception as e:
            self.publish_failed(event, "%s : %s" % (type(e), e.message))

    def publish_failed(self, event, message):
        """
            Publishes a event to signal that the command on the target failed.
        """
        log.err(_stuff=Exception(message), _why=message)
        METRICS['commands_failed.%s' % (event.target)] += 1
        self.broadcaster.publish_cmd_for_target(
            event.target,
            event.command,
            events.FAILED,
            message,
            tracking_id=event.tracking_id)

    def publish_start(self, event):
        """
            Publishes a event to signal that the command on the target started.
        """
        hostname = self.configuration['hostname']
        message = '(%s) target[%s] request: command="%s", arguments=%s' % (
            hostname, event.target, event.command, event.arguments)
        log.msg(message)
        METRICS['commands_started.%s' % (event.target)] += 1
        self.broadcaster.publish_cmd_for_target(
            event.target,
            event.command,
            events.STARTED,
            message,
            tracking_id=event.tracking_id)

    def onConnect(self):
        """
            Subscribes to the targets from the configuration. The receiver
            is useless when no targets are configured, therefore it will exit
            with error code 1 when no targets are configured.
        """
        self.states = {}

        self.broadcaster.client.connectionLost = self.onConnectionLost

        host = self.configuration['broadcaster_host']
        port = self.configuration['broadcaster_port']

        log.msg('Successfully connected to broadcaster on %s:%s' %
                (host, port))

        self.configuration.reload_targets()
        targets = sorted(self.configuration['allowed_targets'])

        if not targets:
            log.err('No targets configured or no targets in allowed targets.')
            exit(1)

        for targetname in targets:
            log.msg('subscribing to target "%s".' % targetname)
            self.broadcaster.client.subscribe(self.onEvent, unicode(targetname))

    def _should_refresh_connection(self):
        if not hasattr(self, 'broadcaster') or not self.broadcaster.client:
            log.msg('Not connected, cannot refresh connection')
            return False  # no connection, cannot refresh

        current_hour = datetime.now().hour
        if not current_hour == 2:  # only refresh at 2:xx a.m.
            log.msg("It's %d:xx, not 2:xx a.m., no connection-refresh now." % current_hour)
            return False

        return True

    def _refresh_connection(self, delay=60 * 60, first_call=False):
        """
            When connected, closes connection to force a clean reconnect,
            except on first_call
        """
        reactor.callLater(delay, self._refresh_connection)
        log.msg('Might want to refresh connection now.')
        if not first_call and self._should_refresh_connection():
            log.msg(
                'Closing connection to broadcaster. This should force a connection-refresh.')
            self.broadcaster.client.sendClose()

    def onConnectionLost(self, reason):
        """
            Allows for clean reconnect behaviour, because it 'none'ifies
            the client explicitly
        """
        log.err('connection lost: %s' % reason)
        self.broadcaster.client = None

    def onEvent(self, *args):
        """
            Will be called when receiving an event from the broadcaster.
            See onConnect which subscribes to the targets.
        """

        try:
            # Wamp v1: onEvent is callbacked with topic and event
            target, event_data = args
        except ValueError:
            # Wamp v2: onEvent is callbacked with event
            event_data, = args
            target = None

        event = events.Event(target, event_data)

        if event.is_a_vote:
            voting_fsm = self.states.get(event.tracking_id)
            if not voting_fsm:
                log.msg(
                    'Ignoring vote %r because I have already lost' % event.vote)
                return
            own_vote = voting_fsm.vote
            is_a_fold = (own_vote < event.vote)

            if is_a_fold:
                log.msg(
                    'Folding due to vote %r being higher than own vote %r' %
                    (event.vote, own_vote))
                voting_fsm.fold()
            else:
                log.msg(
                    'Calling due to vote %r being lower than own vote %r' %
                    (event.vote, own_vote))
                voting_fsm.call()

        elif event.is_a_request:
            try:
                self.handle_request(event)
            except Exception as e:
                log.err(e.message)

                for line in traceback.format_exc().splitlines():
                    log.err(line)

                self.publish_failed(event, e.message)

        else:
            log.msg(str(event))

    def set_configuration(self, configuration):
        """
            Assigns a configuration to this receiver instance.
        """
        self.configuration = configuration

    def initialize_twisted_logging(self):
        twenty_megabytes = 20000000
        log_file = LogFile.fromFullPath(self.configuration['log_filename'],
                                        maxRotatedFiles=10,
                                        rotateLength=twenty_megabytes)
        log.startLogging(log_file)

    def startService(self):
        """
            Initializes logging and establishes connection to broadcaster.
        """
        self.initialize_twisted_logging()
        log.msg('yadtreceiver version %s' % __version__)
        self._connect_broadcaster()
        self._refresh_connection(first_call=True)
        self.schedule_write_metrics(first_call=True)
        self.reset_metrics_at_midnight(first_call=True)

    def stopService(self):
        """
            Writes 'shutting down service' to the log.
        """
        log.msg('shutting down service')

    def _connect_broadcaster(self):
        """
            Establishes a connection to the broadcaster as found in the
            configuration.
        """
        host = self.configuration['broadcaster_host']
        port = self.configuration['broadcaster_port']

        log.msg('Connecting to broadcaster on %s:%s' % (host, port))

        self.broadcaster = WampBroadcaster(host, port, 'yadtreceiver')
        self.broadcaster.addOnSessionOpenHandler(self.onConnect)

    def write_metrics_to_file(self):

        metrics_directory = self.configuration['metrics_directory']
        if not metrics_directory:
            return

        if not os.path.isdir(metrics_directory):
            try:
                os.makedirs(metrics_directory)
            except Exception as e:
                log.err("Cannot create metrics directory : {0}".format(e))
                return

        metrics_file_name = self.configuration['metrics_file']
        with open(metrics_file_name, 'w') as metrics_file:
            _write_metrics(METRICS, metrics_file)

    def schedule_write_metrics(self, delay=30, first_call=False):
        reactor.callLater(delay, self.schedule_write_metrics)
        if not first_call:
            start = time()
            self.write_metrics_to_file()
            write_duration = time() - start
            log.msg("Wrote metrics to file in {0} seconds".format(write_duration))
            METRICS["last_write_duration"] = write_duration

    def reset_metrics_at_midnight(cls, first_call=False):
        reactor.callLater(seconds_to_midnight(), cls.reset_metrics_at_midnight)
        if not first_call:
            log.msg("Resetting metrics")
            _reset_metrics(METRICS)