def test_sendEvent_should_use_kwargs_as_event_items(self):
        mock_broadcaster = Mock(WampBroadcaster)
        mock_broadcaster.target = "broadcaster-target"
        mock_broadcaster.logger = Mock()
        mock_broadcaster.client = Mock()

        WampBroadcaster._sendEvent(
            mock_broadcaster,
            "event-id",
            "event-data",
            tracking_id="tracking-id",
            target="target",
            state="foobar",
            bar="baz",
        )

        actual_call = mock_broadcaster.client.publish.call_args
        self.assertEqual(
            call(
                "target",
                {
                    "payload": "event-data",
                    "type": "event",
                    "id": "event-id",
                    "tracking_id": "tracking-id",
                    "target": "target",
                    "state": "foobar",
                    "bar": "baz",
                },
            ),
            actual_call,
        )
    def test_sendFullUpdate_should_forward_tracking_id_to_sendEvent(self):
        mock_broadcaster = Mock(WampBroadcaster)

        WampBroadcaster.sendFullUpdate(
            mock_broadcaster, 'data', tracking_id='tracking-id')

        self.assertEqual(
            call('full-update', 'data', 'tracking-id'), mock_broadcaster._sendEvent.call_args)
    def test_sendServiceChange_should_forward_tracking_id_to_sendEvent(self):
        mock_broadcaster = Mock(WampBroadcaster)

        WampBroadcaster.sendServiceChange(
            mock_broadcaster, 'data', tracking_id='tracking-id')

        self.assertEqual(
            call('service-change', 'data', 'tracking-id'), mock_broadcaster._sendEvent.call_args)
    def test_should_queue_messages_when_not_connected(self):
        self.mock_broadcaster._check_connection.return_value = False

        WampBroadcaster._publish(self.mock_broadcaster, "any-topic", {"any-key": "any-value"})
        WampBroadcaster._publish(self.mock_broadcaster, "other-topic", {"other-key": "other-value"})

        self.assertEqual(self.mock_broadcaster.queue.append.call_args_list,
                         [call(('any-topic', {'any-key': 'any-value'})),
                          call(('other-topic', {'other-key': 'other-value'}))])
    def test_should_run_session_open_handlers_when_establishing_connection(self):
        handler_1 = Mock()
        handler_2 = Mock()
        self.mock_broadcaster.on_session_open_handlers = [handler_1, handler_2]
        self.mock_broadcaster.client = Mock()
        self.mock_broadcaster.queue = []

        WampBroadcaster.onSessionOpen(self.mock_broadcaster)

        handler_1.assert_called_with()
        handler_2.assert_called_with()
Example #6
0
    def test_should_flush_messages_after_connecting(self):
        self.mock_broadcaster.queue = [("topic1", "payload1"),
                                       ("topic2", "payload2")]
        self.mock_broadcaster.client = Mock()
        self.mock_broadcaster.on_session_open_handlers = []

        WampBroadcaster.onSessionOpen(self.mock_broadcaster)

        self.assertEqual(
            self.mock_broadcaster._publish.call_args_list,
            [call('topic1', 'payload1'),
             call('topic2', 'payload2')])
Example #7
0
    def test_should_run_session_open_handlers_when_establishing_connection(
            self):
        handler_1 = Mock()
        handler_2 = Mock()
        self.mock_broadcaster.on_session_open_handlers = [handler_1, handler_2]
        self.mock_broadcaster.client = Mock()
        self.mock_broadcaster.queue = []

        WampBroadcaster.onSessionOpen(self.mock_broadcaster)

        handler_1.assert_called_with()
        handler_2.assert_called_with()
Example #8
0
    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 test_should_flush_messages_after_connecting(self):
        self.mock_broadcaster.queue = [("topic1", "payload1"),
                                       ("topic2", "payload2")]
        self.mock_broadcaster.client = Mock()
        self.mock_broadcaster.on_session_open_handlers = []

        WampBroadcaster.onSessionOpen(self.mock_broadcaster)

        self.assertEqual(
            self.mock_broadcaster._publish.call_args_list,
            [
                call('topic1', 'payload1'),
                call('topic2', 'payload2')])
Example #10
0
    def test_should_queue_messages_when_not_connected(self):
        self.mock_broadcaster._check_connection.return_value = False

        WampBroadcaster._publish(self.mock_broadcaster, "any-topic",
                                 {"any-key": "any-value"})
        WampBroadcaster._publish(self.mock_broadcaster, "other-topic",
                                 {"other-key": "other-value"})

        self.assertEqual(self.mock_broadcaster.queue.append.call_args_list, [
            call(('any-topic', {
                'any-key': 'any-value'
            })),
            call(('other-topic', {
                'other-key': 'other-value'
            }))
        ])
    def test_sendEvent_should_publish_expected_event_on_default_target(self):
        mock_broadcaster = Mock(WampBroadcaster)
        mock_broadcaster.target = 'broadcaster-target'
        mock_broadcaster.logger = Mock()
        mock_broadcaster.client = Mock()

        WampBroadcaster._sendEvent(
            mock_broadcaster, 'event-id', 'event-data', tracking_id='tracking-id')

        actual_call = mock_broadcaster.client.publish.call_args
        self.assertEqual(call('broadcaster-target', {'payload': 'event-data',
                                                     'type': 'event',
                                                     'id': 'event-id',
                                                     'tracking_id': 'tracking-id',
                                                     'target': 'broadcaster-target'}
                              ), actual_call)
    def test_check_connection_should_return_true_when_link_is_up(self):
        mock_broadcaster = Mock(WampBroadcaster)
        mock_broadcaster.logger = Mock()
        mock_broadcaster.url = "ws://broadcaster"
        mock_broadcaster.client = Mock()

        self.assertEqual(WampBroadcaster._check_connection(mock_broadcaster), True)
        self.assertFalse(hasattr(mock_broadcaster, "not_connected_warning_sent"))
    def test_check_connection_should_return_true_when_link_is_up(self):
        mock_broadcaster = Mock(WampBroadcaster)
        mock_broadcaster.logger = Mock()
        mock_broadcaster.url = "ws://broadcaster"
        mock_broadcaster.client = Mock()

        self.assertEqual(
            WampBroadcaster._check_connection(mock_broadcaster), True)
        self.assertFalse(
            hasattr(mock_broadcaster, "not_connected_warning_sent"))
Example #14
0
    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 test_sendEvent_should_publish_expected_event_on_default_target(self):
        mock_broadcaster = Mock(WampBroadcaster)
        mock_broadcaster.target = "broadcaster-target"
        mock_broadcaster.logger = Mock()
        mock_broadcaster.client = Mock()

        WampBroadcaster._sendEvent(mock_broadcaster, "event-id", "event-data", tracking_id="tracking-id")

        actual_call = mock_broadcaster.client.publish.call_args
        self.assertEqual(
            call(
                "broadcaster-target",
                {
                    "payload": "event-data",
                    "type": "event",
                    "id": "event-id",
                    "tracking_id": "tracking-id",
                    "target": "broadcaster-target",
                },
            ),
            actual_call,
        )
    def test_sendEvent_should_use_kwargs_as_event_items(self):
        mock_broadcaster = Mock(WampBroadcaster)
        mock_broadcaster.target = 'broadcaster-target'
        mock_broadcaster.logger = Mock()
        mock_broadcaster.client = Mock()

        WampBroadcaster._sendEvent(mock_broadcaster,
                                   'event-id',
                                   'event-data',
                                   tracking_id='tracking-id',
                                   target='target',
                                   state='foobar',
                                   bar='baz')

        actual_call = mock_broadcaster.client.publish.call_args
        self.assertEqual(call('target', {'payload': 'event-data',
                                         'type': 'event',
                                         'id': 'event-id',
                                         'tracking_id': 'tracking-id',
                                         'target': 'target',
                                         'state': 'foobar',
                                         'bar': 'baz'}
                              ), actual_call)
    def test_sendEvent_should_not_drop_data_when_connection(self, check_connection):
        check_connection.return_value = True
        ybc = WampBroadcaster("host", 42)
        ybc.target = "broadcaster-target"
        ybc.logger = Mock()
        ybc.client = Mock()

        WampBroadcaster._sendEvent(ybc, "event-id", "event-data", tracking_id="tracking-id", target="target")

        self.assertTrue(ybc.client.publish.called)
    def test_sendEvent_should_not_drop_data_when_connection(self, check_connection):
        check_connection.return_value = True
        ybc = WampBroadcaster('host', 42)
        ybc.target = 'broadcaster-target'
        ybc.logger = Mock()
        ybc.client = Mock()

        WampBroadcaster._sendEvent(ybc,
                                   'event-id',
                                   'event-data',
                                   tracking_id='tracking-id',
                                   target='target')

        self.assertTrue(ybc.client.publish.called)
    def test_should_publish_cmd_for_default_target(self):
        ybc = WampBroadcaster('host', 42)
        ybc.target = 'broadcaster-target'
        ybc.logger = Mock()
        ybc.client = Mock()

        ybc.publish_cmd('status', 'failed', 'hello', 'nsa-tracking')

        ybc.client.publish.assert_called_with('broadcaster-target', {
            'cmd': 'status',
            'state': 'failed',
            'payload': None,
            'tracking_id': 'nsa-tracking',
            'message': 'hello',
            'type': 'event',
            'target': 'broadcaster-target',
            'id': 'cmd'})
    def test_should_publish_request_for_target(self):

        ybc = WampBroadcaster('host', 42)
        ybc.target = 'broadcaster-target'
        ybc.logger = Mock()
        ybc.client = Mock()

        ybc.publish_request_for_target('target', 'cmd', 'args', 'nsa-tracker')

        ybc.client.publish.assert_called_with('target',
                                              {
                                                  'args': 'args',
                                                  'cmd': 'cmd',
                                                  'type': 'event',
                                                  'id': 'request',
                                                  'payload': None,
                                                  'target': 'target',
                                                  'tracking_id': 'nsa-tracker'})
    def test_should_publish_request_for_target(self):

        ybc = WampBroadcaster("host", 42)
        ybc.target = "broadcaster-target"
        ybc.logger = Mock()
        ybc.client = Mock()

        ybc.publish_request_for_target("target", "cmd", "args", "nsa-tracker")

        ybc.client.publish.assert_called_with(
            "target",
            {
                "args": "args",
                "cmd": "cmd",
                "type": "event",
                "id": "request",
                "payload": None,
                "target": "target",
                "tracking_id": "nsa-tracker",
            },
        )
    def test_should_publish_cmd_for_target(self):
        ybc = WampBroadcaster("host", 42)
        ybc.target = "broadcaster-target"
        ybc.logger = Mock()
        ybc.client = Mock()

        ybc.publish_cmd_for_target("target", "status", "failed", "hello", "nsa-tracking")

        ybc.client.publish.assert_called_with(
            "target",
            {
                "cmd": "status",
                "state": "failed",
                "payload": None,
                "tracking_id": "nsa-tracking",
                "message": "hello",
                "type": "event",
                "target": "target",
                "id": "cmd",
            },
        )
Example #23
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)
from __future__ import print_function
import sys
from twisted.internet import reactor
import logging
logging.basicConfig()
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)

try:
    host = sys.argv[1]
    message = sys.argv[2]
except IndexError:
    print("Usage: {0} hostname message".format(sys.argv[0]))
    sys.exit(1)

sys.path.insert(0, "src/main/python")

from yadtbroadcastclient import WampBroadcaster


def flood(message):
    wamp._publish("topic", {"id": message})
    reactor.callLater(1, flood, message)


wamp = WampBroadcaster(host, "8080", "topic")

flood(message)
reactor.callLater(5, reactor.stop)
reactor.run()
    try:
        verify_if_is_expected_event(event)
    except Exception as e:
        logger.error(e)


def verify_if_is_expected_event(event):
    if expected_event == event:
        logger.info("Success: found target event %s" % expected_event)
        global exit_code
        exit_code = 0
        reactor.stop()
    else:
        logger.info("Expected message not yet found")


def timeout(timeout_in_seconds):
    logger.error("Timed out after %d seconds" % timeout_in_seconds)
    reactor.stop()


wamp = WampBroadcaster(host, "8080", "topic")
wamp.onEvent = onEvent

logger.info("Waiting %d seconds for target event %s" %
            (timeout_in_seconds, expected_event))

reactor.callLater(timeout_in_seconds, timeout, timeout_in_seconds)
reactor.run()
sys.exit(exit_code)
]]

try:
    host = sys.argv[1]
except IndexError:
    print("Usage: {0} hostname".format(sys.argv[0]))
    sys.exit(1)


sys.path.insert(0, "src/main/python")
from yadtbroadcastclient import WampBroadcaster
from time import time

test_id = str(int(time()))

w = WampBroadcaster(host, "8080", test_id)
w.onEvent = partial(print, "Got event ")


def connected():
    reactor.connected = True


def send_full_update_and_service_change():
    w.sendServiceChange([{'uri': "service://foo/bar", 'state': "UP"}])
    w.sendFullUpdate(dummy_data)
    reactor.callLater(5, reactor.stop)


def ensure_test_data_is_present():
    tc = unittest.TestCase('__init__')
    def test_sendServiceChange_should_forward_tracking_id_to_sendEvent(self):
        mock_broadcaster = Mock(WampBroadcaster)

        WampBroadcaster.sendServiceChange(mock_broadcaster, "data", tracking_id="tracking-id")

        self.assertEqual(call("service-change", "data", "tracking-id"), mock_broadcaster._sendEvent.call_args)
Example #28
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)
    def test_sendFullUpdate_should_forward_tracking_id_to_sendEvent(self):
        mock_broadcaster = Mock(WampBroadcaster)

        WampBroadcaster.sendFullUpdate(mock_broadcaster, "data", tracking_id="tracking-id")

        self.assertEqual(call("full-update", "data", "tracking-id"), mock_broadcaster._sendEvent.call_args)