Example #1
0
    def test_separate_eliot_log(self, base_test_case):
        """
        Flocker base test cases attach the eliot log as a detail separate from
        the Twisted log.
        """
        message_type = MessageType(u'foo', fields(name=str), u'test message')

        class SomeTest(base_test_case):
            def test_something(self):
                from twisted.python import log
                log.msg('foo')
                message_type(name='qux').write()

        test = SomeTest('test_something')
        test.run()
        self.assertThat(
            test.getDetails(),
            MatchesDict({
                'twisted-log':
                match_text_content(
                    MatchesRegex(
                        r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[+-]\d{4} \[-\] foo'
                    )),
                _SplitEliotLogs._ELIOT_LOG_DETAIL_NAME:
                match_text_content(
                    Contains("  message_type: 'foo'\n"
                             "  name: 'qux'\n")),
            }))
Example #2
0
    def test_separate_eliot_log(self, base_test_case):
        """
        Flocker base test cases attach the eliot log as a detail separate from
        the Twisted log.
        """
        message_type = MessageType(u"foo", fields(name=str), u"test message")

        class SomeTest(base_test_case):
            def test_something(self):
                from twisted.python import log

                log.msg("foo")
                message_type(name="qux").write()

        test = SomeTest("test_something")
        test.run()
        self.assertThat(
            test.getDetails(),
            MatchesDict(
                {
                    "twisted-log": match_text_content(
                        MatchesRegex(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[+-]\d{4} \[-\] foo")
                    ),
                    _SplitEliotLogs._ELIOT_LOG_DETAIL_NAME: match_text_content(
                        Contains("  message_type: 'foo'\n" "  name: 'qux'\n")
                    ),
                }
            ),
        )
Example #3
0
    def test_separate_eliot_log(self, base_test_case):
        """
        Flocker base test cases attach the eliot log as a detail separate from
        the Twisted log.
        """
        # XXX: If debugging is enabled (either by setting this to True or by
        # removing this line and running --debug-stacktraces, then the log
        # fixtures in this test are empty. However, if we run a failing test
        # manually, the logs appear in the details. Not sure what's going on,
        # so disabling debugging for now.
        self.useFixture(DebugTwisted(False))
        message_type = MessageType(u'foo', fields(name=str), u'test message')

        class SomeTest(base_test_case):
            def test_something(self):
                from twisted.python import log
                log.msg('foo')
                message_type(name='qux').write()

        test = SomeTest('test_something')
        result = TestResult()
        test.run(result)
        self.expectThat(result, has_results(tests_run=Equals(1)))
        self.assertThat(
            test.getDetails(),
            MatchesDict({
                'twisted-log': match_text_content(MatchesRegex(
                    r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[+-]\d{4} \[-\] foo'
                )),
                _SplitEliotLogs._ELIOT_LOG_DETAIL_NAME: match_text_content(
                    Contains("  message_type: 'foo'\n"
                             "  name: 'qux'\n")
                ),
            }))
Example #4
0
    def test_logs_after_timeout(self):
        """
        We include logs for tests, even if they time out.
        """
        message_type = MessageType(u'foo', fields(name=str), u'test message')

        class SomeTest(AsyncTestCase):

            # Set the timeout super low, because we're not doing anything.
            run_tests_with = async_runner(timeout=timedelta(seconds=0.00005))

            def test_something(self):
                from twisted.python import log
                log.msg('foo')
                message_type(name='qux').write()
                # Return a Deferred that never fires to guarantee a timeout.
                return Deferred()

        test = SomeTest('test_something')
        result = TestResult()
        test.run(result)
        self.assertThat(
            result,
            has_results(
                tests_run=Equals(1),
                errors=MatchesListwise([
                    MatchesListwise([
                        Equals(test),
                        MatchesAll(
                            Contains('[-] foo\n'),
                            Contains("message_type: 'foo'"),
                        ),
                    ])
                ]),
            ))
Example #5
0
    def test_logs_after_timeout(self):
        """
        We include logs for tests, even if they time out.
        """
        message_type = MessageType(u'foo', fields(name=str), u'test message')

        class SomeTest(AsyncTestCase):

            # Set the timeout super low, because we're not doing anything.
            run_tests_with = async_runner(timeout=timedelta(seconds=0.00005))

            def test_something(self):
                from twisted.python import log
                log.msg('foo')
                message_type(name='qux').write()
                # Return a Deferred that never fires to guarantee a timeout.
                return Deferred()

        test = SomeTest('test_something')
        result = TestResult()
        test.run(result)
        self.assertThat(
            result,
            has_results(
                tests_run=Equals(1),
                errors=MatchesListwise([MatchesListwise([
                    Equals(test),
                    MatchesAll(
                        Contains('[-] foo\n'),
                        Contains("message_type: 'foo'"),
                    ),
                ])]),
            )
        )
Example #6
0
    def test_separate_eliot_log(self, base_test_case):
        """
        Flocker base test cases attach the eliot log as a detail separate from
        the Twisted log.
        """
        # XXX: If debugging is enabled (either by setting this to True or by
        # removing this line and running --debug-stacktraces, then the log
        # fixtures in this test are empty. However, if we run a failing test
        # manually, the logs appear in the details. Not sure what's going on,
        # so disabling debugging for now.
        self.useFixture(DebugTwisted(False))
        message_type = MessageType(u'foo', fields(name=str), u'test message')

        class SomeTest(base_test_case):
            def test_something(self):
                from twisted.python import log
                log.msg('foo')
                message_type(name='qux').write()

        test = SomeTest('test_something')
        result = TestResult()
        test.run(result)
        self.expectThat(result, has_results(tests_run=Equals(1)))
        self.assertThat(
            test.getDetails(),
            MatchesDict({
                'twisted-log':
                match_text_content(
                    MatchesRegex(
                        r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[+-]\d{4} \[-\] foo'
                    )),
                _SplitEliotLogs._ELIOT_LOG_DETAIL_NAME:
                match_text_content(
                    Contains("  message_type: 'foo'\n"
                             "  name: 'qux'\n")),
            }))
Example #7
0
        self.assertEqual(self.clock.getDelayedCalls(), [])

    def test_timeout_cleaned_up_on_failure(self):
        """
        If the deferred is failed before the timeout, the timeout is not still
        pending on the reactor.
        """
        timeout(self.clock, self.deferred, self.timeout)
        self.deferred.errback(CustomException(self.id()))
        # We need to handle the errback so that Trial and/or testtools don't
        # fail with unhandled errors.
        self.addCleanup(self.deferred.addErrback, lambda _: None)
        self.assertEqual(self.clock.getDelayedCalls(), [])


ITERATION_MESSAGE = MessageType("iteration_message", fields(iteration=int))


class RetryFailureTests(SynchronousTestCase):
    """
    Tests for :py:func:`retry_failure`.
    """
    def test_immediate_success(self):
        """
        If the function returns a successful value immediately, then
        ``retry_failure`` returns a deferred that has already fired with that
        value.
        """
        result = object()

        def function():
Example #8
0
import eliot


HANDOFF = eliot.ActionType(
    u'wip:handoff',
    eliot.fields(
        path=str),
    eliot.fields(
        family=int, type=int, proto=int),
    u'A listening socket is being handed off.')

SCGI_ACCEPTED = eliot.MessageType(
    u'wip:scgi_accepted',
    [],
    u'A listening SCGI socket has accepted a connection.')

SCGI_REQUEST = eliot.ActionType(
    u'wip:scgi_request',
    [],
    [],
    u'An SCGI request is being handled.')

SCGI_PARSE = eliot.ActionType(
    u'wip:scgi_parse',
    [],
    [],
    u'A new SCGI request is being parsed.')

WSGI_REQUEST = eliot.ActionType(
    u'wip:wsgi_request',
    eliot.fields(
Example #9
0
    """
    cluster_state = compute_cluster_state(node_state, additional_node_states,
                                          nonmanifest_datasets)
    cluster_configuration = Deployment(
        nodes={node_config} | additional_node_config,
        leases=leases,
    )
    changes = deployer.calculate_changes(
        cluster_configuration, cluster_state, local_state
    )
    case.assertEqual(expected_changes, changes)


ADDRESS_IN_USE = MessageType(
    u"flocker:test:address_in_use",
    fields(ip=unicode, port=int, name=bytes),
)


def _find_process_name(port_number):
    """
    Get the name of the process using the given port number.
    """
    for connection in psutil.net_connections():
        if connection.laddr[1] == port_number:
            return psutil.Process(connection.pid).name()
    return None


def _retry_on_port_collision(reason, add, cleanup):
    """
Example #10
0
        :param dict options: A dictionary of configuration options.
        :return: A ``Deferred`` which fires when the script has completed.
        """


def eliot_logging_service(destination, reactor, capture_stdout):
    service = MultiService()
    ThreadedWriter(destination, reactor).setServiceParent(service)
    EliotObserver(capture_stdout=capture_stdout).setServiceParent(service)
    return service


# This should probably be built-in functionality in Eliot;
# see https://github.com/ClusterHQ/eliot/issues/143
TWISTED_LOG_MESSAGE = MessageType("twisted:log",
                                  fields(error=bool, message=unicode),
                                  u"A log message from Twisted.")


class EliotObserver(Service):
    """
    A Twisted log observer that logs to Eliot.
    """
    def __init__(self, publisher=twisted_log, capture_stdout=True):
        """
        :param publisher: A ``LogPublisher`` to capture logs from, or if no
            argument is given the default Twisted log system.
        :param bool capture_stdout: Wether to capture standard output and
            standard error to eliot.
        """
        self.logger = Logger()
Example #11
0
        :param dict options: A dictionary of configuration options.
        :return: A ``Deferred`` which fires when the script has completed.
        """


def eliot_logging_service(destination, reactor, capture_stdout):
    service = MultiService()
    ThreadedWriter(destination, reactor).setServiceParent(service)
    EliotObserver(capture_stdout=capture_stdout).setServiceParent(service)
    return service


# This should probably be built-in functionality in Eliot;
# see https://github.com/ClusterHQ/eliot/issues/143
TWISTED_LOG_MESSAGE = MessageType("twisted:log",
                                  fields(error=bool, message=unicode),
                                  u"A log message from Twisted.")


def eliot_to_stdout(message_formats, action_formats, stdout=sys.stdout):
    """
    Write pretty versions of eliot log messages to stdout.
    """
    def eliot_output(message):
        message_type = message.get('message_type')
        action_type = message.get('action_type')
        action_status = message.get('action_status')

        message_format = ''
        if message_type is not None:
            if message_type == 'twisted:log' and message.get('error'):
Example #12
0
"""
Eliot message and action definitions.
"""
from operator import methodcaller

from eliot import ActionType, Field, fields
from twisted.python.compat import unicode

NONCE = Field(u'nonce', lambda nonce: nonce.encode('hex').decode('ascii'),
              u'A nonce value')

LOG_JWS_SIGN = ActionType(u'txacme:jws:sign',
                          fields(NONCE, key_type=unicode, alg=unicode),
                          fields(), u'Signing a message with JWS')

LOG_JWS_HEAD = ActionType(u'txacme:jws:http:head', fields(), fields(),
                          u'A JWSClient HEAD request')

LOG_JWS_GET = ActionType(u'txacme:jws:http:get', fields(), fields(),
                         u'A JWSClient GET request')

LOG_JWS_POST = ActionType(u'txacme:jws:http:post', fields(), fields(),
                          u'A JWSClient POST request')

LOG_JWS_REQUEST = ActionType(
    u'txacme:jws:http:request', fields(url=unicode),
    fields(Field.for_types(u'content_type', [unicode, None],
                           u'Content-Type header field'),
           code=int), u'A JWSClient request')

LOG_JWS_CHECK_RESPONSE = ActionType(
Example #13
0
import eliot

HANDOFF = eliot.ActionType(u'wip:handoff', eliot.fields(path=str),
                           eliot.fields(family=int, type=int, proto=int),
                           u'A listening socket is being handed off.')

SCGI_ACCEPTED = eliot.MessageType(
    u'wip:scgi_accepted', [],
    u'A listening SCGI socket has accepted a connection.')

SCGI_REQUEST = eliot.ActionType(u'wip:scgi_request', [], [],
                                u'An SCGI request is being handled.')

SCGI_PARSE = eliot.ActionType(u'wip:scgi_parse', [], [],
                              u'A new SCGI request is being parsed.')

WSGI_REQUEST = eliot.ActionType(u'wip:wsgi_request', eliot.fields(path=str),
                                [], u'A WSGI application is being called.')

RESPONSE_STARTED = eliot.MessageType(
    u'wip:response_started', eliot.fields(status=str),
    u'A WSGI application has called start_response.')
Example #14
0
# TWISTED_LOG_MESSAGE and EliotObserver licensed under APL 2.0 from
# ClusterHQ/flocker
# https://github.com/ClusterHQ/flocker/blob/master/flocker/common/script.py#L81-L106
import json
import os
import pkg_resources
import socket
import sys

import raven
from eliot import add_destination, fields, Logger, MessageType
from twisted.python.log import textFromEventDict, startLoggingWithObserver

HOSTNAME = socket.getfqdn()
LOGGER = None
TWISTED_LOG_MESSAGE = MessageType("twisted:log", fields(error=bool, message=unicode), u"A log message from Twisted.")
HUMAN = False


class EliotObserver(object):
    """A Twisted log observer that logs to Eliot"""

    def __init__(self):
        """Create the Eliot Observer"""
        if os.environ.get("SENTRY_DSN"):
            self.raven_client = raven.Client(release=raven.fetch_package_version("autopush"))
        else:
            self.raven_client = None
        self.logger = Logger()

    def raven_log(self, event):
Example #15
0

LOG_START_SERVICE = ActionType(
    u'fusion_index:service:start',
    [_SERVICE_DESCRIPTION],
    [],
    u'Indexing service is starting')


LOG_STOP_SERVICE = ActionType(
    u'fusion_index:service:stop', [], [], u'Indexing service is stopping')


LOG_LOOKUP_GET = ActionType(
    u'fusion_index:lookup:get',
    fields(environment=unicode, indexType=unicode, key=unicode),
    [Field.for_types('value', [bytes, None], u'Value in the index, if any')],
    u'Retrieving a value from the lookup index')


LOG_LOOKUP_PUT = ActionType(
    u'fusion_index:lookup:put',
    fields(environment=unicode, indexType=unicode, key=unicode),
    fields(value=bytes),
    u'Storing a value in the lookup index')


_SEARCH_TYPE = Field.for_types(
    'searchType', [unicode, None], u'The search type')
LOG_SEARCH_GET = ActionType(
    u'fusion_index:search:get',
Example #16
0
"""
Eliot message and action definitions.
"""
from operator import methodcaller

from eliot import ActionType, Field, fields
from twisted.python.compat import unicode

NONCE = Field(u'nonce', lambda nonce: nonce.encode('hex').decode('ascii'),
              u'A nonce value')

LOG_JWS_SIGN = ActionType(
    u'txacme:jws:sign',
    fields(NONCE, key_type=unicode, alg=unicode, kid=unicode), fields(),
    u'Signing a message with JWS')

LOG_JWS_HEAD = ActionType(u'txacme:jws:http:head', fields(), fields(),
                          u'A JWSClient HEAD request')

LOG_JWS_GET = ActionType(u'txacme:jws:http:get', fields(), fields(),
                         u'A JWSClient GET request')

LOG_JWS_POST = ActionType(u'txacme:jws:http:post', fields(), fields(),
                          u'A JWSClient POST request')

LOG_JWS_REQUEST = ActionType(
    u'txacme:jws:http:request', fields(url=unicode),
    fields(Field.for_types(u'content_type', [unicode, None],
                           u'Content-Type header field'),
           code=int), u'A JWSClient request')
        self.assertEqual(self.clock.getDelayedCalls(), [])

    def test_timeout_cleaned_up_on_failure(self):
        """
        If the deferred is failed before the timeout, the timeout is not still
        pending on the reactor.
        """
        timeout(self.clock, self.deferred, self.timeout)
        self.deferred.errback(CustomException(self.id()))
        # We need to handle the errback so that Trial and/or testtools don't
        # fail with unhandled errors.
        self.addCleanup(self.deferred.addErrback, lambda _: None)
        self.assertEqual(self.clock.getDelayedCalls(), [])


ITERATION_MESSAGE = MessageType("iteration_message", fields(iteration=int))


class RetryFailureTests(TestCase):
    """
    Tests for :py:func:`retry_failure`.
    """

    def test_immediate_success(self):
        """
        If the function returns a successful value immediately, then
        ``retry_failure`` returns a deferred that has already fired with that
        value.
        """
        result = object()
Example #18
0
            """
            result = run_process([executable] + [b"--help"])
            self.assertIn(executable, result.output)

    return ScriptTests


class CustomException(Exception):
    """
    An exception that will never be raised by real code, useful for
    testing.
    """


TWISTED_CHILD_PROCESS_ACTION = ActionType(
    u"flocker:testtools:logged_run_process", fields(command=list), [], u"A child process is spawned using Twisted"
)

STDOUT_RECEIVED = MessageType(
    u"flocker:testtools:logged_run_process:stdout",
    fields(output=str),
    u"A chunk of stdout received from a child process",
)

STDERR_RECEIVED = MessageType(
    u"flocker:testtools:logged_run_process:stdout",
    fields(output=str),
    u"A chunk of stderr received from a child process",
)

PROCESS_ENDED = MessageType(
Example #19
0
            """
            result = run_process([executable] + [b"--help"])
            self.assertIn(executable, result.output)
    return ScriptTests


class CustomException(Exception):
    """
    An exception that will never be raised by real code, useful for
    testing.
    """


TWISTED_CHILD_PROCESS_ACTION = ActionType(
    u'flocker:testtools:logged_run_process',
    fields(command=list),
    [],
    u'A child process is spawned using Twisted',
)

STDOUT_RECEIVED = MessageType(
    u'flocker:testtools:logged_run_process:stdout',
    fields(output=str),
    u'A chunk of stdout received from a child process',
)

STDERR_RECEIVED = MessageType(
    u'flocker:testtools:logged_run_process:stdout',
    fields(output=str),
    u'A chunk of stderr received from a child process',
)
Example #20
0
from eliot import ActionType, Field, fields

_SEARCH_CLASS = Field(u'searchClass', lambda c: c.value, u'The search class')

LOG_LOOKUP_GET = ActionType(
    u'fusion_index:lookup:get',
    fields(environment=unicode, indexType=unicode, key=unicode),
    [Field.for_types('value', [bytes, None], u'Value in the index, if any')],
    u'Retrieving a value from the lookup index')

LOG_LOOKUP_PUT = ActionType(
    u'fusion_index:lookup:put',
    fields(environment=unicode, indexType=unicode, key=unicode),
    fields(value=bytes), u'Storing a value in the lookup index')

_SEARCH_TYPE = Field.for_types('searchType', [unicode, None],
                               u'The search type')
LOG_SEARCH_GET = ActionType(
    u'fusion_index:search:get',
    fields(_SEARCH_CLASS,
           _SEARCH_TYPE,
           environment=unicode,
           indexType=unicode,
           searchValue=unicode), fields(results=list),
    u'Searching the search index')

LOG_SEARCH_PUT = ActionType(
    u'fusion_index:search:put',
    fields(_SEARCH_CLASS,
           environment=unicode,
           indexType=unicode,
Example #21
0
            """
            result = run_process([executable] + [b"--help"])
            self.assertIn(executable, result.output)
    return ScriptTests


class CustomException(Exception):
    """
    An exception that will never be raised by real code, useful for
    testing.
    """


TWISTED_CHILD_PROCESS_ACTION = ActionType(
    u'flocker:testtools:logged_run_process',
    fields(command=list),
    [],
    u'A child process is spawned using Twisted',
)

STDOUT_RECEIVED = MessageType(
    u'flocker:testtools:logged_run_process:stdout',
    fields(output=str),
    u'A chunk of stdout received from a child process',
)

STDERR_RECEIVED = MessageType(
    u'flocker:testtools:logged_run_process:stdout',
    fields(output=str),
    u'A chunk of stderr received from a child process',
)
Example #22
0
    This starts a Docker-based container convergence agent.
    """
    def deployer_factory(cluster_uuid, **kwargs):
        return ApplicationNodeDeployer(**kwargs)
    service_factory = AgentServiceFactory(
        deployer_factory=deployer_factory
    ).get_service
    agent_script = AgentScript(service_factory=service_factory)
    return FlockerScriptRunner(
        script=agent_script,
        options=ContainerAgentOptions()
    ).main()


LOG_GET_EXTERNAL_IP = ActionType(u"flocker:node:script:get_external_ip",
                                 fields(host=unicode, port=int),
                                 fields(local_ip=unicode),
                                 "An attempt to discover the local IP.")


def _get_external_ip(host, port):
    """
    Get an external IP address for this node that can in theory connect to
    the given host and port.

    Failures are retried until a successful connect.

    See https://clusterhq.atlassian.net/browse/FLOC-1751 for a possibly
    better solution.

    :param host: A host to connect to.
Example #23
0
            result = run_process([executable] + [b"--help"])
            self.assertIn(executable, result.output)

    return ScriptTests


class CustomException(Exception):
    """
    An exception that will never be raised by real code, useful for
    testing.
    """


TWISTED_CHILD_PROCESS_ACTION = ActionType(
    u'flocker:testtools:logged_run_process',
    fields(command=list),
    [],
    u'A child process is spawned using Twisted',
)

STDOUT_RECEIVED = MessageType(
    u'flocker:testtools:logged_run_process:stdout',
    fields(output=str),
    u'A chunk of stdout received from a child process',
)

STDERR_RECEIVED = MessageType(
    u'flocker:testtools:logged_run_process:stdout',
    fields(output=str),
    u'A chunk of stderr received from a child process',
)
Example #24
0
"""
Eliot message and action definitions.
"""
from operator import methodcaller

from eliot import ActionType, Field, fields
from twisted.python.compat import unicode

NONCE = Field(
    u'nonce',
    lambda nonce: nonce.encode('hex').decode('ascii'),
    u'A nonce value')

LOG_JWS_SIGN = ActionType(
    u'txacme:jws:sign',
    fields(NONCE, key_type=unicode, alg=unicode),
    fields(),
    u'Signing a message with JWS')

LOG_JWS_HEAD = ActionType(
    u'txacme:jws:http:head',
    fields(),
    fields(),
    u'A JWSClient HEAD request')

LOG_JWS_GET = ActionType(
    u'txacme:jws:http:get',
    fields(),
    fields(),
    u'A JWSClient GET request')
Example #25
0
            for dataset in nonmanifest_datasets
        },
    )
    cluster_configuration = Deployment(
        nodes={node_config} | additional_node_config,
        leases=leases,
    )
    changes = deployer.calculate_changes(
        cluster_configuration, cluster_state,
    )
    case.assertEqual(expected_changes, changes)


ADDRESS_IN_USE = MessageType(
    u"flocker:test:address_in_use",
    fields(ip=unicode, port=int, name=bytes),
)


def _find_process_name(port_number):
    """
    Get the name of the process using the given port number.
    """
    for connection in psutil.net_connections():
        if connection.laddr[1] == port_number:
            return psutil.Process(connection.pid).name()
    return None


def _retry_on_port_collision(reason, add, cleanup):
    """
Example #26
0
 def test_bad_message(self):
     # This message does not validate because "Hello" is not an
     # int.
     MSG = MessageType("test:eliotutil", fields(foo=int))
     MSG(foo="Hello").write()
Example #27
0
    service_factory = AgentServiceFactory(
        deployer_factory=deployer_factory).get_service
    agent_script = AgentScript(service_factory=service_factory)

    # Use CPU time instead of wallclock time.
    pr = cProfile.Profile(clock)

    signal.signal(signal.SIGUSR1, partial(enable_profiling, pr))
    signal.signal(signal.SIGUSR2, partial(disable_profiling, pr, 'container'))

    return FlockerScriptRunner(script=agent_script,
                               options=ContainerAgentOptions()).main()


LOG_GET_EXTERNAL_IP = ActionType(u"flocker:node:script:get_external_ip",
                                 fields(host=unicode, port=int),
                                 fields(local_ip=unicode),
                                 "An attempt to discover the local IP.")


def _get_external_ip(host, port):
    """
    Get an external IP address for this node that can in theory connect to
    the given host and port.

    Failures are retried until a successful connect.

    See https://clusterhq.atlassian.net/browse/FLOC-1751 for a possibly
    better solution.

    :param host: A host to connect to.
Example #28
0
        of the cluster state without manifestations on any node.
    :param set additional_node_states: A set of ``NodeState`` for other nodes.
    :param set additional_node_config: A set of ``Node`` for other nodes.
    :param expected_changes: The ``IStateChange`` expected to be returned.
    :param ILocalState local_state: The ``local_state`` to pass into
        ``calculate_changes``. Must be the correct type for the type of
        ``IDeployer`` being tested.
    :param Leases leases: Currently configured leases. By default none exist.
    """
    cluster_state = compute_cluster_state(node_state, additional_node_states, nonmanifest_datasets)
    cluster_configuration = Deployment(nodes={node_config} | additional_node_config, leases=leases)
    changes = deployer.calculate_changes(cluster_configuration, cluster_state, local_state)
    case.assertEqual(expected_changes, changes)


ADDRESS_IN_USE = MessageType(u"flocker:test:address_in_use", fields(ip=unicode, port=int, name=bytes))


def _find_process_name(port_number):
    """
    Get the name of the process using the given port number.
    """
    for connection in psutil.net_connections():
        if connection.laddr[1] == port_number:
            return psutil.Process(connection.pid).name()
    return None


def _retry_on_port_collision(reason, add, cleanup):
    """
    Cleanup and re-add a container if it failed to start because of a port