<commit_before># Copyright Hybrid Logic Ltd.  See LICENSE file for details.

"""
Tests for ``flocker.node._deploy``.
"""

from uuid import uuid4

from eliot.testing import validate_logging

from ipaddr import IPAddress

from pyrsistent import pset, pvector

from twisted.internet.defer import fail, FirstError, succeed, Deferred
from twisted.trial.unittest import SynchronousTestCase, TestCase
from twisted.python.filepath import FilePath

from .. import (
    ApplicationNodeDeployer, P2PManifestationDeployer,
)
from ..testtools import (
    ControllableAction, ControllableDeployer, ideployer_tests_factory, EMPTY,
    EMPTY_STATE
)
from ...control import (
    Application, DockerImage, Deployment, Node, Port, Link,
    NodeState, DeploymentState, RestartAlways)

from .. import sequentially, in_parallel

from .._deploy import (
    StartApplication, StopApplication,
    CreateDataset, HandoffDataset, SetProxies, PushDataset,
    ResizeDataset, _link_environment, _to_volume_name,
    DeleteDataset, OpenPorts
)
from ...testtools import CustomException
from .. import _deploy
from ...control._model import AttachedVolume, Dataset, Manifestation
from .._docker import (
    FakeDockerClient, AlreadyExists, Unit, PortMap, Environment,
    DockerClient, Volume as DockerVolume)
from ...route import Proxy, OpenPort, make_memory_network
from ...route._iptables import HostNetwork
from ...volume.service import VolumeName
from ...volume._model import VolumeSize
from ...volume.testtools import create_volume_service
from ...volume._ipc import RemoteVolumeManager, standard_node

from .istatechange import make_istatechange_tests


# This models an application that has a volume.
APPLICATION_WITH_VOLUME_NAME = u"psql-clusterhq"
DATASET_ID = unicode(uuid4())
DATASET = Dataset(dataset_id=DATASET_ID)
APPLICATION_WITH_VOLUME_MOUNTPOINT = FilePath(b"/var/lib/postgresql")
APPLICATION_WITH_VOLUME_IMAGE = u"clusterhq/postgresql:9.1"
APPLICATION_WITH_VOLUME = Application(
    name=APPLICATION_WITH_VOLUME_NAME,
    image=DockerImage.from_string(APPLICATION_WITH_VOLUME_IMAGE),
    volume=AttachedVolume(
        manifestation=Manifestation(dataset=DATASET, primary=True),
        mountpoint=APPLICATION_WITH_VOLUME_MOUNTPOINT,
    ),
    links=frozenset(),
)
MANIFESTATION = APPLICATION_WITH_VOLUME.volume.manifestation

DATASET_WITH_SIZE = Dataset(dataset_id=DATASET_ID,
                            metadata=DATASET.metadata,
                            maximum_size=1024 * 1024 * 100)

APPLICATION_WITH_VOLUME_SIZE = Application(
    name=APPLICATION_WITH_VOLUME_NAME,
    image=DockerImage.from_string(APPLICATION_WITH_VOLUME_IMAGE),
    volume=AttachedVolume(
        manifestation=Manifestation(dataset=DATASET_WITH_SIZE,
                                    primary=True),
        mountpoint=APPLICATION_WITH_VOLUME_MOUNTPOINT,
    ),
    links=frozenset(),
)

MANIFESTATION_WITH_SIZE = APPLICATION_WITH_VOLUME_SIZE.volume.manifestation

# Placeholder in case at some point discovered application is different
# than requested application:
DISCOVERED_APPLICATION_WITH_VOLUME = APPLICATION_WITH_VOLUME


class ApplicationNodeDeployerAttributesTests(SynchronousTestCase):
    """
    Tests for attributes and initialiser arguments of
    `ApplicationNodeDeployer`.
    """
    def test_docker_client_default(self):
        """
        ``ApplicationNodeDeployer.docker_client`` is a ``DockerClient`` by
        default.
        """
        self.assertIsInstance(
            ApplicationNodeDeployer(u"example.com", None).docker_client,
            DockerClient
        )

    def test_docker_override(self):
        """
        ``ApplicationNodeDeployer.docker_client`` can be overridden in the
        constructor.
        """
        dummy_docker_client = object()
        self.assertIs(
            dummy_docker_client,
            ApplicationNodeDeployer(
                u'example.com',
                docker_client=dummy_docker_client).docker_client
        )

    def test_network_default(self):
        """
        ``ApplicationNodeDeployer._network`` is a ``HostNetwork`` by default.
        """
        self.assertIsInstance(
            ApplicationNodeDeployer(u'example.com', None).network,
            HostNetwork)

    def test_network_override(self):
        """
        ``ApplicationNodeDeployer._network`` can be overridden in the
        constructor.
        """
        dummy_network = object()
        self.assertIs(
            dummy_network,
            ApplicationNodeDeployer(u'example.com',
                                    network=dummy_network).network
        )


_DATASET_A = Dataset(dataset_id=unicode(uuid4()))
_DATASET_B = Dataset(dataset_id=unicode(uuid4()))


StartApplicationIStateChangeTests = make_istatechange_tests(
    StartApplication,
    dict(
        application=APPLICATION_WITH_VOLUME,
        node_state=NodeState(hostname="node1.example.com")
    ),
    dict(
        application=APPLICATION_WITH_VOLUME.set(name=u"throwaway-app"),
        node_state=NodeState(hostname="node2.example.com")
    )
)
StopApplicationIStageChangeTests = make_istatechange_tests(
    StopApplication,
    dict(application=APPLICATION_WITH_VOLUME),
    dict(application=APPLICATION_WITH_VOLUME.set(name=u"throwaway-app")),
)
SetProxiesIStateChangeTests = make_istatechange_tests(
    SetProxies,
    dict(ports=[Proxy(ip=IPAddress("10.0.0.1"), port=1000)]),
    dict(ports=[Proxy(ip=IPAddress("10.0.0.2"), port=2000)]),
)
CreateDatasetIStateChangeTests = make_istatechange_tests(
    CreateDataset,
    dict(dataset=_DATASET_A),
    dict(dataset=_DATASET_B),
)
HandoffVolumeIStateChangeTests = make_istatechange_tests(
    HandoffDataset,
    dict(dataset=_DATASET_A, hostname=b"123"),
    dict(dataset=_DATASET_B, hostname=b"123")
)
PushVolumeIStateChangeTests = make_istatechange_tests(
    PushDataset,
    dict(dataset=_DATASET_A, hostname=b"123"),
    dict(dataset=_DATASET_B, hostname=b"123")
)
DeleteDatasetTests = make_istatechange_tests(
    DeleteDataset,
    dict(dataset=_DATASET_A),
    dict(dataset=_DATASET_B),
)


class ControllableActionIStateChangeTests(
        make_istatechange_tests(
            ControllableAction,
            kwargs1=dict(result=1),
            kwargs2=dict(result=2),
        )
):
    """
    Tests for ``ControllableAction``.
    """


class StartApplicationTests(SynchronousTestCase):
    """
    Tests for ``StartApplication``.
    """
    def test_start(self):
        """
        ``StartApplication`` accepts an application object and when ``run()``
        is called returns a ``Deferred`` which fires when the docker container
        has been added and started.
        """
        fake_docker = FakeDockerClient()
        api = ApplicationNodeDeployer(u'example.com',
                                      docker_client=fake_docker)
        docker_image = DockerImage(repository=u'clusterhq/flocker',
                                   tag=u'release-14.0')
        ports = frozenset([Port(internal_port=80, external_port=8080)])
        application = Application(
            name=u'site-example.com',
            image=docker_image,
            ports=ports,
            links=frozenset(),
        )
        start_result = StartApplication(application=application,
                                        node_state=EMPTY_NODESTATE).run(api)
        exists_result = fake_docker.exists(unit_name=application.name)

        port_maps = pset(
            [PortMap(internal_port=80, external_port=8080)]
        )
        self.assertEqual(
            (None, True, docker_image.full_name, port_maps),
            (self.successResultOf(start_result),
             self.successResultOf(exists_result),
             fake_docker._units[application.name].container_image,
             fake_docker._units[application.name].ports)
        )

    def test_already_exists(self):
        """
        ``StartApplication.run`` returns a `Deferred` which errbacks with
        an ``AlreadyExists`` error if there is already a unit with the supplied
        application name.
        """
        api = ApplicationNodeDeployer(u'example.com',
                                      docker_client=FakeDockerClient())
        application = Application(
            name=b'site-example.com',
            image=DockerImage(repository=u'clusterhq/flocker',
                              tag=u'release-14.0'),
            links=frozenset(),
        )

        result1 = StartApplication(application=application,
                                   node_state=EMPTY_NODESTATE).run(api)
        self.successResultOf(result1)

        result2 = StartApplication(application=application,
                                   node_state=EMPTY_NODESTATE).run(api)
        self.failureResultOf(result2, AlreadyExists)

    def test_environment_supplied_to_docker(self):
        """
        ``StartApplication.run()`` passes the environment dictionary of the
        application to ``DockerClient.add`` as an ``Environment`` instance.
        """
        fake_docker = FakeDockerClient()
        deployer = ApplicationNodeDeployer(u'example.com', fake_docker)

        application_name = u'site-example.com'
        variables = frozenset({u'foo': u"bar", u"baz": u"qux"}.iteritems())
        application = Application(
            name=application_name,
            image=DockerImage(repository=u'clusterhq/postgresql',
                              tag=u'9.3.5'),
            environment=variables.copy(),
            links=frozenset(),
            ports=(),
        )

        StartApplication(application=application,
                         node_state=EMPTY_NODESTATE).run(deployer)

        expected_environment = Environment(variables=variables.copy())

        self.assertEqual(
            expected_environment,
            fake_docker._units[application_name].environment
        )

    def test_environment_not_supplied(self):
        """
        ``StartApplication.run()`` only passes an ``Environment`` instance
        if the application defines an environment.
        """
        fake_docker = FakeDockerClient()
        deployer = ApplicationNodeDeployer(u'example.com', fake_docker)

        application_name = u'site-example.com'
        application = Application(
            name=application_name,
            image=DockerImage(repository=u'clusterhq/postgresql',
                              tag=u'9.3.5'),
            environment=None,
            links=frozenset(),
        )

        StartApplication(application=application,
                         node_state=EMPTY_NODESTATE).run(deployer)

        self.assertEqual(
            None,
            fake_docker._units[application_name].environment
        )

    def test_links(self):
        """
        ``StartApplication.run()`` passes environment variables to connect to
        the remote application to ``DockerClient.add``.
        """
        fake_docker = FakeDockerClient()
        deployer = ApplicationNodeDeployer(u'example.com', fake_docker)

        application_name = u'site-example.com'
        application = Application(
            name=application_name,
            image=DockerImage(repository=u'clusterhq/postgresql',
                              tag=u'9.3.5'),
            links=frozenset([Link(alias="alias", local_port=80,
                                  remote_port=8080)]))

        StartApplication(application=application,
                         node_state=EMPTY_NODESTATE).run(deployer)

        variables = frozenset({
            'ALIAS_PORT_80_TCP': 'tcp://example.com:8080',
            'ALIAS_PORT_80_TCP_ADDR': 'example.com',
            'ALIAS_PORT_80_TCP_PORT': '8080',
            'ALIAS_PORT_80_TCP_PROTO': 'tcp',
        }.iteritems())
        expected_environment = Environment(variables=variables.copy())

        self.assertEqual(
            expected_environment,
            fake_docker._units[application_name].environment
        )

    def test_volumes(self):
        """
        ``StartApplication.run()`` passes the appropriate volume arguments to
        ``DockerClient.add`` based on the application's volume.
        """
        DATASET_ID = unicode(uuid4())
        fake_docker = FakeDockerClient()
        deployer = ApplicationNodeDeployer(u'example.com', fake_docker)
        node_path = FilePath(b"/flocker/" + DATASET_ID.encode("ascii"))

        mountpoint = FilePath(b"/mymount")
        application_name = u'site-example.com'
        application = Application(
            name=application_name,
            image=DockerImage(repository=u'clusterhq/postgresql',
                              tag=u'9.3.5'),
            links=frozenset(),
            volume=AttachedVolume(
                manifestation=Manifestation(
                    dataset=Dataset(dataset_id=DATASET_ID),
                    primary=True),
                mountpoint=mountpoint))

        StartApplication(
            application=application,
            node_state=EMPTY_NODESTATE.set(
                "paths", {DATASET_ID: node_path})).run(deployer)

        self.assertEqual(
            pset([DockerVolume(node_path=node_path,
                               container_path=mountpoint)]),
            fake_docker._units[application_name].volumes
        )

    def test_memory_limit(self):
        """
        ``StartApplication.run()`` passes an ``Application``'s mem_limit to
        ``DockerClient.add`` which is used when creating a Unit.
        """
        EXPECTED_MEMORY_LIMIT = 100000000
        fake_docker = FakeDockerClient()
        deployer = ApplicationNodeDeployer(u'example.com', fake_docker)

        application_name = u'site-example.com'
        application = Application(
            name=application_name,
            image=DockerImage(repository=u'clusterhq/postgresql',
                              tag=u'9.3.5'),
            environment=None,
            links=frozenset(),
            memory_limit=EXPECTED_MEMORY_LIMIT
        )

        StartApplication(application=application,
                         node_state=EMPTY_NODESTATE).run(deployer)

        self.assertEqual(
            EXPECTED_MEMORY_LIMIT,
            fake_docker._units[application_name].mem_limit
        )

    def test_cpu_shares(self):
        """
        ``StartApplication.run()`` passes an ``Application``'s cpu_shares to
        ``DockerClient.add`` which is used when creating a Unit.
        """
        EXPECTED_CPU_SHARES = 512
        fake_docker = FakeDockerClient()
        deployer = ApplicationNodeDeployer(u'example.com', fake_docker)

        application_name = u'site-example.com'
        application = Application(
            name=application_name,
            image=DockerImage(repository=u'clusterhq/postgresql',
                              tag=u'9.3.5'),
            environment=None,
            links=frozenset(),
            cpu_shares=EXPECTED_CPU_SHARES
        )

        StartApplication(application=application,
                         node_state=EMPTY_NODESTATE).run(deployer)

        self.assertEqual(
            EXPECTED_CPU_SHARES,
            fake_docker._units[application_name].cpu_shares
        )

    def test_restart_policy(self):
        """
        ``StartApplication.run()`` passes an ``Application``'s restart_policy
        to ``DockerClient.add`` which is used when creating a Unit.
        """
        policy = object()
        fake_docker = FakeDockerClient()
        deployer = ApplicationNodeDeployer(u'example.com', fake_docker)

        application_name = u'site-example.com'
        application = Application(
            name=application_name,
            image=DockerImage(repository=u'clusterhq/postgresql',
                              tag=u'9.3.5'),
            restart_policy=policy,
        )

        StartApplication(application=application,
                         node_state=EMPTY_NODESTATE).run(deployer)

        self.assertIs(
            policy,
            fake_docker._units[application_name].restart_policy,
        )

    def test_command_line(self):
        """
        ``StartApplication.run()`` passes an ``Application``'s
        ``command_line`` to ``DockerClient.add``.
        """
        command_line = [u"hello", u"there"]
        fake_docker = FakeDockerClient()
        deployer = ApplicationNodeDeployer(u'example.com', fake_docker)

        application_name = u'site-example.com'
        application = Application(
            name=application_name,
            image=DockerImage.from_string(u"postgresql"),
            command_line=command_line)

        StartApplication(application=application,
                         node_state=EMPTY_NODESTATE).run(deployer)

        self.assertEqual(
            fake_docker._units[application_name].command_line,
            pvector(command_line),
        )


class LinkEnviromentTests(SynchronousTestCase):
    """
    Tests for ``_link_environment``.
    """

    def test_link_environment(self):
        """
        ``_link_environment(link)`` returns a dictonary
        with keys used by docker to represent links. Specifically
        ``<alias>_PORT_<local_port>_<protocol>`` and the broken out variants
        ``_ADDR``, ``_PORT`` and ``_PROTO``.
        """

        environment = _link_environment(
            protocol="udp",
            alias="dash-alias",
            local_port=80,
            hostname=u"the-host",
            remote_port=8080)
        self.assertEqual(
            environment,
            {
                u'DASH_ALIAS_PORT_80_UDP': u'udp://the-host:8080',
                u'DASH_ALIAS_PORT_80_UDP_PROTO': u'udp',
                u'DASH_ALIAS_PORT_80_UDP_ADDR': u'the-host',
                u'DASH_ALIAS_PORT_80_UDP_PORT': u'8080',
            })


class StopApplicationTests(SynchronousTestCase):
    """
    Tests for ``StopApplication``.
    """
    def test_stop(self):
        """
        ``StopApplication`` accepts an application object and when ``run()``
        is called returns a ``Deferred`` which fires when the container
        has been removed.
        """
        fake_docker = FakeDockerClient()
        api = ApplicationNodeDeployer(u'example.com',
                                      docker_client=fake_docker)
        application = Application(
            name=b'site-example.com',
            image=DockerImage(repository=u'clusterhq/flocker',
                              tag=u'release-14.0'),
            links=frozenset(),
        )

        StartApplication(application=application,
                         node_state=EMPTY_NODESTATE).run(api)
        existed = fake_docker.exists(application.name)
        stop_result = StopApplication(application=application).run(api)
        exists_result = fake_docker.exists(unit_name=application.name)

        self.assertEqual(
            (None, True, False),
            (self.successResultOf(stop_result),
             self.successResultOf(existed),
             self.successResultOf(exists_result))
        )

    def test_does_not_exist(self):
        """
        ``StopApplication.run()`` does not errback if the application does
        not exist.
        """
        api = ApplicationNodeDeployer(u'example.com',
                                      docker_client=FakeDockerClient())
        application = Application(
            name=b'site-example.com',
            image=DockerImage(repository=u'clusterhq/flocker',
                              tag=u'release-14.0'),
            links=frozenset(),
        )
        result = StopApplication(application=application).run(api)
        result = self.successResultOf(result)

        self.assertIs(None, result)


# This models an application that has a volume.

APPLICATION_WITH_VOLUME_NAME = b"psql-clusterhq"
DATASET_ID = unicode(uuid4())
DATASET = Dataset(dataset_id=DATASET_ID)
APPLICATION_WITH_VOLUME_MOUNTPOINT = FilePath(b"/var/lib/postgresql")
APPLICATION_WITH_VOLUME_IMAGE = u"clusterhq/postgresql:9.1"
APPLICATION_WITH_VOLUME = Application(
    name=APPLICATION_WITH_VOLUME_NAME,
    image=DockerImage.from_string(APPLICATION_WITH_VOLUME_IMAGE),
    volume=AttachedVolume(
        manifestation=Manifestation(dataset=DATASET, primary=True),
        mountpoint=APPLICATION_WITH_VOLUME_MOUNTPOINT,
    ),
    links=frozenset(),
)
MANIFESTATION = APPLICATION_WITH_VOLUME.volume.manifestation

DATASET_WITH_SIZE = Dataset(dataset_id=DATASET_ID,
                            metadata=DATASET.metadata,
                            maximum_size=1024 * 1024 * 100)

APPLICATION_WITH_VOLUME_SIZE = Application(
    name=APPLICATION_WITH_VOLUME_NAME,
    image=DockerImage.from_string(APPLICATION_WITH_VOLUME_IMAGE),
    volume=AttachedVolume(
        manifestation=Manifestation(dataset=DATASET_WITH_SIZE,
                                    primary=True),
        mountpoint=APPLICATION_WITH_VOLUME_MOUNTPOINT,
    ),
    links=frozenset(),
)

MANIFESTATION_WITH_SIZE = APPLICATION_WITH_VOLUME_SIZE.volume.manifestation

# Placeholder in case at some point discovered application is different
# than requested application:
DISCOVERED_APPLICATION_WITH_VOLUME = APPLICATION_WITH_VOLUME

APP_NAME = u"site-example.com"
UNIT_FOR_APP = Unit(name=APP_NAME,
                    container_name=APP_NAME,
                    container_image=u"flocker/wordpress:latest",
                    activation_state=u'active')
APP = Application(
    name=APP_NAME,
    image=DockerImage.from_string(UNIT_FOR_APP.container_image)
)
APP_NAME2 = u"site-example.net"
UNIT_FOR_APP2 = Unit(name=APP_NAME2,
                     container_name=APP_NAME2,
                     container_image=u"flocker/wordpress:latest",
                     activation_state=u'active')
APP2 = Application(
    name=APP_NAME2,
    image=DockerImage.from_string(UNIT_FOR_APP2.container_image)
)
EMPTY_NODESTATE = NodeState(hostname=u"example.com",
                            manifestations={}, devices={}, paths={},
                            applications=[], used_ports=[])


class ApplicationNodeDeployerDiscoverNodeConfigurationTests(
        SynchronousTestCase):
    """
    Tests for ``ApplicationNodeDeployer.discover_local_state``.
    """
    def setUp(self):
        self.network = make_memory_network()
        self.node_uuid = uuid4()
        self.EMPTY_NODESTATE = NodeState(
            hostname=u"example.com",
            uuid=self.node_uuid,
            manifestations={}, devices={}, paths={},
            applications=[], used_ports=[])

    def test_discover_none(self):
        """
        ``ApplicationNodeDeployer.discover_state`` returns an empty
        ``NodeState`` if there are no Docker containers on the host.
        """
        fake_docker = FakeDockerClient(units={})
        api = ApplicationNodeDeployer(
            u'example.com',
            node_uuid=self.node_uuid,
            docker_client=fake_docker,
            network=self.network
        )
        d = api.discover_state(self.EMPTY_NODESTATE)

        self.assertEqual([NodeState(uuid=api.node_uuid, hostname=api.hostname,
                                    applications=[], used_ports=[])],
                         self.successResultOf(d))

    def test_discover_one(self):
        """
        ``ApplicationNodeDeployer.discover_state`` returns ``NodeState``
        with a a list of running ``Application``\ s; one for each active
        container.
        """
        fake_docker = FakeDockerClient(units={APP_NAME: UNIT_FOR_APP})
        api = ApplicationNodeDeployer(
            u'example.com',
            node_uuid=self.node_uuid,
            docker_client=fake_docker,
            network=self.network
        )
        d = api.discover_state(self.EMPTY_NODESTATE)

        self.assertEqual([NodeState(uuid=api.node_uuid, hostname=api.hostname,
                                    applications=[APP], used_ports=[])],
                         self.successResultOf(d))

    def test_discover_multiple(self):
        """
        ``ApplicationNodeDeployer.discover_state`` returns a
        ``NodeState`` with a running ``Application`` for every active
        container on the host.
        """
        units = {APP_NAME: UNIT_FOR_APP, APP_NAME2: UNIT_FOR_APP2}

        fake_docker = FakeDockerClient(units=units)
        applications = [APP, APP2]
        api = ApplicationNodeDeployer(
            u'example.com',
            node_uuid=self.node_uuid,
            docker_client=fake_docker,
            network=self.network
        )
        d = api.discover_state(self.EMPTY_NODESTATE)

        self.assertItemsEqual(pset(applications),
                              self.successResultOf(d)[0].applications)

    def test_discover_application_with_environment(self):
        """
        An ``Application`` with ``Environment`` objects is discovered from a
        ``Unit`` with ``Environment`` objects.
        """
        environment_variables = (
            (b'CUSTOM_ENV_A', b'a value'),
            (b'CUSTOM_ENV_B', b'something else'),
        )
        environment = Environment(variables=environment_variables)
        unit1 = UNIT_FOR_APP.set("environment", environment)
        units = {unit1.name: unit1}

        fake_docker = FakeDockerClient(units=units)
        applications = [APP.set("environment", dict(environment_variables))]
        api = ApplicationNodeDeployer(
            u'example.com',
            node_uuid=self.node_uuid,
            docker_client=fake_docker,
            network=self.network
        )
        d = api.discover_state(self.EMPTY_NODESTATE)

        self.assertItemsEqual(pset(applications),
                              self.successResultOf(d)[0].applications)

    def test_discover_application_with_environment_and_links(self):
        """
        An ``Application`` with ``Environment`` and ``Link`` objects is
        discovered from a ``Unit`` with both custom environment variables and
        environment variables representing container links. The environment
        variables taking the format <ALIAS>_PORT_<PORT>_TCP are separated in
        to ``Link`` representations in the ``Application``.
        """
        environment_variables = (
            (b'CUSTOM_ENV_A', b'a value'),
            (b'CUSTOM_ENV_B', b'something else'),
        )
        link_environment_variables = (
            (b'APACHE_PORT_80_TCP', b'tcp://example.com:8080'),
            (b'APACHE_PORT_80_TCP_PROTO', b'tcp'),
            (b'APACHE_PORT_80_TCP_ADDR', b'example.com'),
            (b'APACHE_PORT_80_TCP_PORT', b'8080'),
        )
        unit_environment = environment_variables + link_environment_variables
        environment = Environment(variables=frozenset(unit_environment))
        unit1 = UNIT_FOR_APP.set("environment", environment)
        units = {unit1.name: unit1}

        fake_docker = FakeDockerClient(units=units)
        links = [
            Link(local_port=80, remote_port=8080, alias=u"APACHE")
        ]
        applications = [APP.set("links", links).set(
            "environment", dict(environment_variables))]

        api = ApplicationNodeDeployer(
            u'example.com',
            node_uuid=self.node_uuid,
            docker_client=fake_docker,
            network=self.network
        )
        d = api.discover_state(self.EMPTY_NODESTATE)

        self.assertItemsEqual(pset(applications),
                              self.successResultOf(d)[0].applications)

    def test_discover_application_with_links(self):
        """
        An ``Application`` with ``Link`` objects is discovered from a ``Unit``
        with environment variables that correspond to an exposed link.
        """
        fake_docker = FakeDockerClient()
        applications = [APP.set("links", [
            Link(local_port=80, remote_port=8080, alias=u'APACHE')
        ])]
        api = ApplicationNodeDeployer(
            u'example.com',
            node_uuid=self.node_uuid,
            docker_client=fake_docker,
            network=self.network
        )
        for app in applications:
            StartApplication(
                node_state=NodeState(uuid=api.node_uuid,
                                     hostname=api.hostname),
                application=app
            ).run(api)
        d = api.discover_state(self.EMPTY_NODESTATE)

        self.assertItemsEqual(applications,
                              self.successResultOf(d)[0].applications)

    def test_discover_application_with_ports(self):
        """
        An ``Application`` with ``Port`` objects is discovered from a ``Unit``
        with exposed ``Portmap`` objects.
        """
        ports = [PortMap(internal_port=80, external_port=8080)]
        unit1 = UNIT_FOR_APP.set("ports", ports)
        units = {unit1.name: unit1}

        fake_docker = FakeDockerClient(units=units)
        applications = [APP.set("ports",
                                [Port(internal_port=80, external_port=8080)])]
        api = ApplicationNodeDeployer(
            u'example.com',
            node_uuid=self.node_uuid,
            docker_client=fake_docker,
            network=self.network
        )
        d = api.discover_state(self.EMPTY_NODESTATE)

        self.assertEqual(sorted(applications),
                         sorted(self.successResultOf(d)[0].applications))

    def test_discover_attached_volume(self):
        """
        Datasets that are mounted at a path that matches the container's
        volume are added to ``Application`` with same name as an
        ``AttachedVolume``.
        """
        DATASET_ID = unicode(uuid4())
        DATASET_ID2 = unicode(uuid4())

        path1 = FilePath(b"/flocker").child(DATASET_ID.encode("ascii"))
        path2 = FilePath(b"/flocker").child(DATASET_ID2.encode("ascii"))
        manifestations = {dataset_id:
                          Manifestation(
                              dataset=Dataset(dataset_id=dataset_id),
                              primary=True,
                          )
                          for dataset_id in (DATASET_ID, DATASET_ID2)}
        current_known_state = NodeState(uuid=self.node_uuid,
                                        hostname=u'example.com',
                                        manifestations=manifestations,
                                        devices={},
                                        paths={DATASET_ID: path1,
                                               DATASET_ID2: path2})

        unit1 = UNIT_FOR_APP.set("volumes", [
            DockerVolume(
                node_path=path1,
                container_path=FilePath(b'/var/lib/data')
            )]
        )

        unit2 = UNIT_FOR_APP2.set("volumes", [
            DockerVolume(
                node_path=path2,
                container_path=FilePath(b'/var/lib/data')
            )]
        )
        units = {unit1.name: unit1, unit2.name: unit2}

        fake_docker = FakeDockerClient(units=units)
        applications = [app.set("volume", AttachedVolume(
            manifestation=manifestations[respective_id],
            mountpoint=FilePath(b'/var/lib/data')
        )) for (app, respective_id) in [(APP, DATASET_ID),
                                        (APP2, DATASET_ID2)]]
        api = ApplicationNodeDeployer(
            u'example.com',
            node_uuid=self.node_uuid,
            docker_client=fake_docker,
            network=self.network
        )
        d = api.discover_state(current_known_state)

        self.assertItemsEqual(pset(applications),
                              self.successResultOf(d)[0].applications)

    def test_ignore_unknown_volumes(self):
        """
        Docker volumes that cannot be matched to a dataset are ignored.
        """
        unit = UNIT_FOR_APP.set("volumes", [
            DockerVolume(
                node_path=FilePath(b"/some/random/path"),
                container_path=FilePath(b'/var/lib/data')
            )],
        )
        units = {unit.name: unit}

        fake_docker = FakeDockerClient(units=units)

        applications = [APP]
        api = ApplicationNodeDeployer(
            u'example.com',
            node_uuid=self.node_uuid,
            docker_client=fake_docker,
            network=self.network
        )
        d = api.discover_state(self.EMPTY_NODESTATE)

        self.assertEqual(sorted(applications),
                         sorted(self.successResultOf(d)[0].applications))

    def test_not_running_units(self):
        """
        Units that are not active are considered to be not running by
        ``discover_state()``.
        """
        unit1 = UNIT_FOR_APP.set("activation_state", u"inactive")
        unit2 = UNIT_FOR_APP2.set("activation_state", u'madeup')
        units = {unit1.name: unit1, unit2.name: unit2}

        fake_docker = FakeDockerClient(units=units)
        applications = [APP.set("running", False), APP2.set("running", False)]
        api = ApplicationNodeDeployer(
            u'example.com',
            node_uuid=self.node_uuid,
            docker_client=fake_docker,
            network=self.network
        )
        d = api.discover_state(EMPTY_NODESTATE)
        result = self.successResultOf(d)

        self.assertEqual([NodeState(uuid=api.node_uuid, hostname=api.hostname,
                                    applications=applications, used_ports=[])],
                         result)

    def test_discover_used_ports(self):
        """
        Any ports in use, as reported by the deployer's ``INetwork`` provider,
        are reported in the ``used_ports`` attribute of the ``NodeState``
        returned by ``discover_state``.
        """
        used_ports = frozenset([1, 3, 5, 1000])
        api = ApplicationNodeDeployer(
            u'example.com',
            node_uuid=self.node_uuid,
            docker_client=FakeDockerClient(),
            network=make_memory_network(used_ports=used_ports)
        )

        discovering = api.discover_state(self.EMPTY_NODESTATE)
        states = self.successResultOf(discovering)

        self.assertEqual(
            [NodeState(uuid=api.node_uuid, hostname=api.hostname,
                       used_ports=used_ports, applications=[])],
            states
        )

    def test_discover_application_restart_policy(self):
        """
        An ``Application`` with the appropriate ``IRestartPolicy`` is
        discovered from the corresponding restart policy of the ``Unit``.
        """
        policy = RestartAlways()
        unit1 = UNIT_FOR_APP.set("restart_policy", policy)
        units = {unit1.name: unit1}

        fake_docker = FakeDockerClient(units=units)
        applications = [APP.set("restart_policy", policy)]
        api = ApplicationNodeDeployer(
            u'example.com',
            node_uuid=self.node_uuid,
            docker_client=fake_docker,
            network=self.network
        )
        d = api.discover_state(self.EMPTY_NODESTATE)

        self.assertEqual(applications,
                         list(self.successResultOf(d)[0].applications))

    def test_unknown_manifestations(self):
        """
        If the given ``NodeState`` indicates ignorance of manifestations, the
        ``ApplicationNodeDeployer`` doesn't bother doing any discovery and
        just indicates ignorance of applications.
        """
        fake_docker = FakeDockerClient(units={APP_NAME: UNIT_FOR_APP})
        api = ApplicationNodeDeployer(
            u'example.com',
            node_uuid=self.node_uuid,
            docker_client=fake_docker,
            network=self.network
        )
        # Apparently we know nothing about manifestations one way or the
        # other:
        d = api.discover_state(NodeState(
            uuid=api.node_uuid,
            hostname=api.hostname,
            manifestations=None, paths=None))

        self.assertEqual([NodeState(hostname=api.hostname,
                                    uuid=api.node_uuid,
                                    # Can't do app discovery if don't know
                                    # about manifestations:
                                    applications=None,
                                    used_ports=None,
                                    manifestations=None,
                                    paths=None)],
                         self.successResultOf(d))


class P2PManifestationDeployerDiscoveryTests(SynchronousTestCase):
    """
    Tests for ``P2PManifestationDeployer`` discovery.
    """
    def setUp(self):
        self.volume_service = create_volume_service(self)
        self.node_uuid = uuid4()
        self.EMPTY_NODESTATE = NodeState(hostname=u"example.com",
                                         uuid=self.node_uuid)

    DATASET_ID = unicode(uuid4())
    DATASET_ID2 = unicode(uuid4())

    def test_unknown_applications_and_ports(self):
        """
        Applications and ports are left as ``None`` in discovery results.
        """
        deployer = P2PManifestationDeployer(
            u'example.com', self.volume_service, node_uuid=self.node_uuid)
        self.assertEqual(
            self.successResultOf(deployer.discover_state(
                self.EMPTY_NODESTATE)),
            [NodeState(hostname=deployer.hostname,<commit_msg>Link to an issue.<commit_after># Copyright Hybrid Logic Ltd.  See LICENSE file for details.

"""
Tests for ``flocker.node._deploy``.
"""

from uuid import uuid4

from eliot.testing import validate_logging

from ipaddr import IPAddress

from pyrsistent import pset, pvector

from twisted.internet.defer import fail, FirstError, succeed, Deferred
from twisted.trial.unittest import SynchronousTestCase, TestCase
from twisted.python.filepath import FilePath

from .. import (
    ApplicationNodeDeployer, P2PManifestationDeployer,
)
from ..testtools import (
    ControllableAction, ControllableDeployer, ideployer_tests_factory, EMPTY,
    EMPTY_STATE
)
from ...control import (
    Application, DockerImage, Deployment, Node, Port, Link,
    NodeState, DeploymentState, RestartAlways)

from .. import sequentially, in_parallel

from .._deploy import (
    StartApplication, StopApplication,
    CreateDataset, HandoffDataset, SetProxies, PushDataset,
    ResizeDataset, _link_environment, _to_volume_name,
    DeleteDataset, OpenPorts
)
from ...testtools import CustomException
from .. import _deploy
from ...control._model import AttachedVolume, Dataset, Manifestation
from .._docker import (
    FakeDockerClient, AlreadyExists, Unit, PortMap, Environment,
    DockerClient, Volume as DockerVolume)
from ...route import Proxy, OpenPort, make_memory_network
from ...route._iptables import HostNetwork
from ...volume.service import VolumeName
from ...volume._model import VolumeSize
from ...volume.testtools import create_volume_service
from ...volume._ipc import RemoteVolumeManager, standard_node

from .istatechange import make_istatechange_tests


# This models an application that has a volume.
APPLICATION_WITH_VOLUME_NAME = u"psql-clusterhq"
DATASET_ID = unicode(uuid4())
DATASET = Dataset(dataset_id=DATASET_ID)
APPLICATION_WITH_VOLUME_MOUNTPOINT = FilePath(b"/var/lib/postgresql")
APPLICATION_WITH_VOLUME_IMAGE = u"clusterhq/postgresql:9.1"
APPLICATION_WITH_VOLUME = Application(
    name=APPLICATION_WITH_VOLUME_NAME,
    image=DockerImage.from_string(APPLICATION_WITH_VOLUME_IMAGE),
    volume=AttachedVolume(
        manifestation=Manifestation(dataset=DATASET, primary=True),
        mountpoint=APPLICATION_WITH_VOLUME_MOUNTPOINT,
    ),
    links=frozenset(),
)
MANIFESTATION = APPLICATION_WITH_VOLUME.volume.manifestation

DATASET_WITH_SIZE = Dataset(dataset_id=DATASET_ID,
                            metadata=DATASET.metadata,
                            maximum_size=1024 * 1024 * 100)

APPLICATION_WITH_VOLUME_SIZE = Application(
    name=APPLICATION_WITH_VOLUME_NAME,
    image=DockerImage.from_string(APPLICATION_WITH_VOLUME_IMAGE),
    volume=AttachedVolume(
        manifestation=Manifestation(dataset=DATASET_WITH_SIZE,
                                    primary=True),
        mountpoint=APPLICATION_WITH_VOLUME_MOUNTPOINT,
    ),
    links=frozenset(),
)

MANIFESTATION_WITH_SIZE = APPLICATION_WITH_VOLUME_SIZE.volume.manifestation

# Placeholder in case at some point discovered application is different
# than requested application:
DISCOVERED_APPLICATION_WITH_VOLUME = APPLICATION_WITH_VOLUME


class ApplicationNodeDeployerAttributesTests(SynchronousTestCase):
    """
    Tests for attributes and initialiser arguments of
    `ApplicationNodeDeployer`.
    """
    def test_docker_client_default(self):
        """
        ``ApplicationNodeDeployer.docker_client`` is a ``DockerClient`` by
        default.
        """
        self.assertIsInstance(
            ApplicationNodeDeployer(u"example.com", None).docker_client,
            DockerClient
        )

    def test_docker_override(self):
        """
        ``ApplicationNodeDeployer.docker_client`` can be overridden in the
        constructor.
        """
        dummy_docker_client = object()
        self.assertIs(
            dummy_docker_client,
            ApplicationNodeDeployer(
                u'example.com',
                docker_client=dummy_docker_client).docker_client
        )

    def test_network_default(self):
        """
        ``ApplicationNodeDeployer._network`` is a ``HostNetwork`` by default.
        """
        self.assertIsInstance(
            ApplicationNodeDeployer(u'example.com', None).network,
            HostNetwork)

    def test_network_override(self):
        """
        ``ApplicationNodeDeployer._network`` can be overridden in the
        constructor.
        """
        dummy_network = object()
        self.assertIs(
            dummy_network,
            ApplicationNodeDeployer(u'example.com',
                                    network=dummy_network).network
        )


_DATASET_A = Dataset(dataset_id=unicode(uuid4()))
_DATASET_B = Dataset(dataset_id=unicode(uuid4()))


StartApplicationIStateChangeTests = make_istatechange_tests(
    StartApplication,
    dict(
        application=APPLICATION_WITH_VOLUME,
        node_state=NodeState(hostname="node1.example.com")
    ),
    dict(
        application=APPLICATION_WITH_VOLUME.set(name=u"throwaway-app"),
        node_state=NodeState(hostname="node2.example.com")
    )
)
StopApplicationIStageChangeTests = make_istatechange_tests(
    StopApplication,
    dict(application=APPLICATION_WITH_VOLUME),
    dict(application=APPLICATION_WITH_VOLUME.set(name=u"throwaway-app")),
)
SetProxiesIStateChangeTests = make_istatechange_tests(
    SetProxies,
    dict(ports=[Proxy(ip=IPAddress("10.0.0.1"), port=1000)]),
    dict(ports=[Proxy(ip=IPAddress("10.0.0.2"), port=2000)]),
)
CreateDatasetIStateChangeTests = make_istatechange_tests(
    CreateDataset,
    dict(dataset=_DATASET_A),
    dict(dataset=_DATASET_B),
)
HandoffVolumeIStateChangeTests = make_istatechange_tests(
    HandoffDataset,
    dict(dataset=_DATASET_A, hostname=b"123"),
    dict(dataset=_DATASET_B, hostname=b"123")
)
PushVolumeIStateChangeTests = make_istatechange_tests(
    PushDataset,
    dict(dataset=_DATASET_A, hostname=b"123"),
    dict(dataset=_DATASET_B, hostname=b"123")
)
DeleteDatasetTests = make_istatechange_tests(
    DeleteDataset,
    dict(dataset=_DATASET_A),
    dict(dataset=_DATASET_B),
)


class ControllableActionIStateChangeTests(
        make_istatechange_tests(
            ControllableAction,
            kwargs1=dict(result=1),
            kwargs2=dict(result=2),
        )
):
    """
    Tests for ``ControllableAction``.
    """


class StartApplicationTests(SynchronousTestCase):
    """
    Tests for ``StartApplication``.
    """
    def test_start(self):
        """
        ``StartApplication`` accepts an application object and when ``run()``
        is called returns a ``Deferred`` which fires when the docker container
        has been added and started.
        """
        fake_docker = FakeDockerClient()
        api = ApplicationNodeDeployer(u'example.com',
                                      docker_client=fake_docker)
        docker_image = DockerImage(repository=u'clusterhq/flocker',
                                   tag=u'release-14.0')
        ports = frozenset([Port(internal_port=80, external_port=8080)])
        application = Application(
            name=u'site-example.com',
            image=docker_image,
            ports=ports,
            links=frozenset(),
        )
        start_result = StartApplication(application=application,
                                        node_state=EMPTY_NODESTATE).run(api)
        exists_result = fake_docker.exists(unit_name=application.name)

        port_maps = pset(
            [PortMap(internal_port=80, external_port=8080)]
        )
        self.assertEqual(
            (None, True, docker_image.full_name, port_maps),
            (self.successResultOf(start_result),
             self.successResultOf(exists_result),
             fake_docker._units[application.name].container_image,
             fake_docker._units[application.name].ports)
        )

    def test_already_exists(self):
        """
        ``StartApplication.run`` returns a `Deferred` which errbacks with
        an ``AlreadyExists`` error if there is already a unit with the supplied
        application name.
        """
        api = ApplicationNodeDeployer(u'example.com',
                                      docker_client=FakeDockerClient())
        application = Application(
            name=b'site-example.com',
            image=DockerImage(repository=u'clusterhq/flocker',
                              tag=u'release-14.0'),
            links=frozenset(),
        )

        result1 = StartApplication(application=application,
                                   node_state=EMPTY_NODESTATE).run(api)
        self.successResultOf(result1)

        result2 = StartApplication(application=application,
                                   node_state=EMPTY_NODESTATE).run(api)
        self.failureResultOf(result2, AlreadyExists)

    def test_environment_supplied_to_docker(self):
        """
        ``StartApplication.run()`` passes the environment dictionary of the
        application to ``DockerClient.add`` as an ``Environment`` instance.
        """
        fake_docker = FakeDockerClient()
        deployer = ApplicationNodeDeployer(u'example.com', fake_docker)

        application_name = u'site-example.com'
        variables = frozenset({u'foo': u"bar", u"baz": u"qux"}.iteritems())
        application = Application(
            name=application_name,
            image=DockerImage(repository=u'clusterhq/postgresql',
                              tag=u'9.3.5'),
            environment=variables.copy(),
            links=frozenset(),
            ports=(),
        )

        StartApplication(application=application,
                         node_state=EMPTY_NODESTATE).run(deployer)

        expected_environment = Environment(variables=variables.copy())

        self.assertEqual(
            expected_environment,
            fake_docker._units[application_name].environment
        )

    def test_environment_not_supplied(self):
        """
        ``StartApplication.run()`` only passes an ``Environment`` instance
        if the application defines an environment.
        """
        fake_docker = FakeDockerClient()
        deployer = ApplicationNodeDeployer(u'example.com', fake_docker)

        application_name = u'site-example.com'
        application = Application(
            name=application_name,
            image=DockerImage(repository=u'clusterhq/postgresql',
                              tag=u'9.3.5'),
            environment=None,
            links=frozenset(),
        )

        StartApplication(application=application,
                         node_state=EMPTY_NODESTATE).run(deployer)

        self.assertEqual(
            None,
            fake_docker._units[application_name].environment
        )

    def test_links(self):
        """
        ``StartApplication.run()`` passes environment variables to connect to
        the remote application to ``DockerClient.add``.
        """
        fake_docker = FakeDockerClient()
        deployer = ApplicationNodeDeployer(u'example.com', fake_docker)

        application_name = u'site-example.com'
        application = Application(
            name=application_name,
            image=DockerImage(repository=u'clusterhq/postgresql',
                              tag=u'9.3.5'),
            links=frozenset([Link(alias="alias", local_port=80,
                                  remote_port=8080)]))

        StartApplication(application=application,
                         node_state=EMPTY_NODESTATE).run(deployer)

        variables = frozenset({
            'ALIAS_PORT_80_TCP': 'tcp://example.com:8080',
            'ALIAS_PORT_80_TCP_ADDR': 'example.com',
            'ALIAS_PORT_80_TCP_PORT': '8080',
            'ALIAS_PORT_80_TCP_PROTO': 'tcp',
        }.iteritems())
        expected_environment = Environment(variables=variables.copy())

        self.assertEqual(
            expected_environment,
            fake_docker._units[application_name].environment
        )

    def test_volumes(self):
        """
        ``StartApplication.run()`` passes the appropriate volume arguments to
        ``DockerClient.add`` based on the application's volume.
        """
        DATASET_ID = unicode(uuid4())
        fake_docker = FakeDockerClient()
        deployer = ApplicationNodeDeployer(u'example.com', fake_docker)
        node_path = FilePath(b"/flocker/" + DATASET_ID.encode("ascii"))

        mountpoint = FilePath(b"/mymount")
        application_name = u'site-example.com'
        application = Application(
            name=application_name,
            image=DockerImage(repository=u'clusterhq/postgresql',
                              tag=u'9.3.5'),
            links=frozenset(),
            volume=AttachedVolume(
                manifestation=Manifestation(
                    dataset=Dataset(dataset_id=DATASET_ID),
                    primary=True),
                mountpoint=mountpoint))

        StartApplication(
            application=application,
            node_state=EMPTY_NODESTATE.set(
                "paths", {DATASET_ID: node_path})).run(deployer)

        self.assertEqual(
            pset([DockerVolume(node_path=node_path,
                               container_path=mountpoint)]),
            fake_docker._units[application_name].volumes
        )

    def test_memory_limit(self):
        """
        ``StartApplication.run()`` passes an ``Application``'s mem_limit to
        ``DockerClient.add`` which is used when creating a Unit.
        """
        EXPECTED_MEMORY_LIMIT = 100000000
        fake_docker = FakeDockerClient()
        deployer = ApplicationNodeDeployer(u'example.com', fake_docker)

        application_name = u'site-example.com'
        application = Application(
            name=application_name,
            image=DockerImage(repository=u'clusterhq/postgresql',
                              tag=u'9.3.5'),
            environment=None,
            links=frozenset(),
            memory_limit=EXPECTED_MEMORY_LIMIT
        )

        StartApplication(application=application,
                         node_state=EMPTY_NODESTATE).run(deployer)

        self.assertEqual(
            EXPECTED_MEMORY_LIMIT,
            fake_docker._units[application_name].mem_limit
        )

    def test_cpu_shares(self):
        """
        ``StartApplication.run()`` passes an ``Application``'s cpu_shares to
        ``DockerClient.add`` which is used when creating a Unit.
        """
        EXPECTED_CPU_SHARES = 512
        fake_docker = FakeDockerClient()
        deployer = ApplicationNodeDeployer(u'example.com', fake_docker)

        application_name = u'site-example.com'
        application = Application(
            name=application_name,
            image=DockerImage(repository=u'clusterhq/postgresql',
                              tag=u'9.3.5'),
            environment=None,
            links=frozenset(),
            cpu_shares=EXPECTED_CPU_SHARES
        )

        StartApplication(application=application,
                         node_state=EMPTY_NODESTATE).run(deployer)

        self.assertEqual(
            EXPECTED_CPU_SHARES,
            fake_docker._units[application_name].cpu_shares
        )

    def test_restart_policy(self):
        """
        ``StartApplication.run()`` passes an ``Application``'s restart_policy
        to ``DockerClient.add`` which is used when creating a Unit.
        """
        policy = object()
        fake_docker = FakeDockerClient()
        deployer = ApplicationNodeDeployer(u'example.com', fake_docker)

        application_name = u'site-example.com'
        application = Application(
            name=application_name,
            image=DockerImage(repository=u'clusterhq/postgresql',
                              tag=u'9.3.5'),
            restart_policy=policy,
        )

        StartApplication(application=application,
                         node_state=EMPTY_NODESTATE).run(deployer)

        self.assertIs(
            policy,
            fake_docker._units[application_name].restart_policy,
        )

    def test_command_line(self):
        """
        ``StartApplication.run()`` passes an ``Application``'s
        ``command_line`` to ``DockerClient.add``.
        """
        command_line = [u"hello", u"there"]
        fake_docker = FakeDockerClient()
        deployer = ApplicationNodeDeployer(u'example.com', fake_docker)

        application_name = u'site-example.com'
        application = Application(
            name=application_name,
            image=DockerImage.from_string(u"postgresql"),
            command_line=command_line)

        StartApplication(application=application,
                         node_state=EMPTY_NODESTATE).run(deployer)

        self.assertEqual(
            fake_docker._units[application_name].command_line,
            pvector(command_line),
        )


class LinkEnviromentTests(SynchronousTestCase):
    """
    Tests for ``_link_environment``.
    """

    def test_link_environment(self):
        """
        ``_link_environment(link)`` returns a dictonary
        with keys used by docker to represent links. Specifically
        ``<alias>_PORT_<local_port>_<protocol>`` and the broken out variants
        ``_ADDR``, ``_PORT`` and ``_PROTO``.
        """

        environment = _link_environment(
            protocol="udp",
            alias="dash-alias",
            local_port=80,
            hostname=u"the-host",
            remote_port=8080)
        self.assertEqual(
            environment,
            {
                u'DASH_ALIAS_PORT_80_UDP': u'udp://the-host:8080',
                u'DASH_ALIAS_PORT_80_UDP_PROTO': u'udp',
                u'DASH_ALIAS_PORT_80_UDP_ADDR': u'the-host',
                u'DASH_ALIAS_PORT_80_UDP_PORT': u'8080',
            })


class StopApplicationTests(SynchronousTestCase):
    """
    Tests for ``StopApplication``.
    """
    def test_stop(self):
        """
        ``StopApplication`` accepts an application object and when ``run()``
        is called returns a ``Deferred`` which fires when the container
        has been removed.
        """
        fake_docker = FakeDockerClient()
        api = ApplicationNodeDeployer(u'example.com',
                                      docker_client=fake_docker)
        application = Application(
            name=b'site-example.com',
            image=DockerImage(repository=u'clusterhq/flocker',
                              tag=u'release-14.0'),
            links=frozenset(),
        )

        StartApplication(application=application,
                         node_state=EMPTY_NODESTATE).run(api)
        existed = fake_docker.exists(application.name)
        stop_result = StopApplication(application=application).run(api)
        exists_result = fake_docker.exists(unit_name=application.name)

        self.assertEqual(
            (None, True, False),
            (self.successResultOf(stop_result),
             self.successResultOf(existed),
             self.successResultOf(exists_result))
        )

    def test_does_not_exist(self):
        """
        ``StopApplication.run()`` does not errback if the application does
        not exist.
        """
        api = ApplicationNodeDeployer(u'example.com',
                                      docker_client=FakeDockerClient())
        application = Application(
            name=b'site-example.com',
            image=DockerImage(repository=u'clusterhq/flocker',
                              tag=u'release-14.0'),
            links=frozenset(),
        )
        result = StopApplication(application=application).run(api)
        result = self.successResultOf(result)

        self.assertIs(None, result)


# This models an application that has a volume.

APPLICATION_WITH_VOLUME_NAME = b"psql-clusterhq"
DATASET_ID = unicode(uuid4())
DATASET = Dataset(dataset_id=DATASET_ID)
APPLICATION_WITH_VOLUME_MOUNTPOINT = FilePath(b"/var/lib/postgresql")
APPLICATION_WITH_VOLUME_IMAGE = u"clusterhq/postgresql:9.1"
APPLICATION_WITH_VOLUME = Application(
    name=APPLICATION_WITH_VOLUME_NAME,
    image=DockerImage.from_string(APPLICATION_WITH_VOLUME_IMAGE),
    volume=AttachedVolume(
        manifestation=Manifestation(dataset=DATASET, primary=True),
        mountpoint=APPLICATION_WITH_VOLUME_MOUNTPOINT,
    ),
    links=frozenset(),
)
MANIFESTATION = APPLICATION_WITH_VOLUME.volume.manifestation

DATASET_WITH_SIZE = Dataset(dataset_id=DATASET_ID,
                            metadata=DATASET.metadata,
                            maximum_size=1024 * 1024 * 100)

APPLICATION_WITH_VOLUME_SIZE = Application(
    name=APPLICATION_WITH_VOLUME_NAME,
    image=DockerImage.from_string(APPLICATION_WITH_VOLUME_IMAGE),
    volume=AttachedVolume(
        manifestation=Manifestation(dataset=DATASET_WITH_SIZE,
                                    primary=True),
        mountpoint=APPLICATION_WITH_VOLUME_MOUNTPOINT,
    ),
    links=frozenset(),
)

MANIFESTATION_WITH_SIZE = APPLICATION_WITH_VOLUME_SIZE.volume.manifestation

# Placeholder in case at some point discovered application is different
# than requested application:
DISCOVERED_APPLICATION_WITH_VOLUME = APPLICATION_WITH_VOLUME

APP_NAME = u"site-example.com"
UNIT_FOR_APP = Unit(name=APP_NAME,
                    container_name=APP_NAME,
                    container_image=u"flocker/wordpress:latest",
                    activation_state=u'active')
APP = Application(
    name=APP_NAME,
    image=DockerImage.from_string(UNIT_FOR_APP.container_image)
)
APP_NAME2 = u"site-example.net"
UNIT_FOR_APP2 = Unit(name=APP_NAME2,
                     container_name=APP_NAME2,
                     container_image=u"flocker/wordpress:latest",
                     activation_state=u'active')
APP2 = Application(
    name=APP_NAME2,
    image=DockerImage.from_string(UNIT_FOR_APP2.container_image)
)
# https://clusterhq.atlassian.net/browse/FLOC-1926
EMPTY_NODESTATE = NodeState(hostname=u"example.com",
                            manifestations={}, devices={}, paths={},
                            applications=[], used_ports=[])


class ApplicationNodeDeployerDiscoverNodeConfigurationTests(
        SynchronousTestCase):
    """
    Tests for ``ApplicationNodeDeployer.discover_local_state``.
    """
    def setUp(self):
        self.network = make_memory_network()
        self.node_uuid = uuid4()
        # https://clusterhq.atlassian.net/browse/FLOC-1926
        self.EMPTY_NODESTATE = NodeState(
            hostname=u"example.com",
            uuid=self.node_uuid,
            manifestations={}, devices={}, paths={},
            applications=[], used_ports=[])

    def test_discover_none(self):
        """
        ``ApplicationNodeDeployer.discover_state`` returns an empty
        ``NodeState`` if there are no Docker containers on the host.
        """
        fake_docker = FakeDockerClient(units={})
        api = ApplicationNodeDeployer(
            u'example.com',
            node_uuid=self.node_uuid,
            docker_client=fake_docker,
            network=self.network
        )
        d = api.discover_state(self.EMPTY_NODESTATE)

        self.assertEqual([NodeState(uuid=api.node_uuid, hostname=api.hostname,
                                    applications=[], used_ports=[])],
                         self.successResultOf(d))

    def test_discover_one(self):
        """
        ``ApplicationNodeDeployer.discover_state`` returns ``NodeState``
        with a a list of running ``Application``\ s; one for each active
        container.
        """
        fake_docker = FakeDockerClient(units={APP_NAME: UNIT_FOR_APP})
        api = ApplicationNodeDeployer(
            u'example.com',
            node_uuid=self.node_uuid,
            docker_client=fake_docker,
            network=self.network
        )
        d = api.discover_state(self.EMPTY_NODESTATE)

        self.assertEqual([NodeState(uuid=api.node_uuid, hostname=api.hostname,
                                    applications=[APP], used_ports=[])],
                         self.successResultOf(d))

    def test_discover_multiple(self):
        """
        ``ApplicationNodeDeployer.discover_state`` returns a
        ``NodeState`` with a running ``Application`` for every active
        container on the host.
        """
        units = {APP_NAME: UNIT_FOR_APP, APP_NAME2: UNIT_FOR_APP2}

        fake_docker = FakeDockerClient(units=units)
        applications = [APP, APP2]
        api = ApplicationNodeDeployer(
            u'example.com',
            node_uuid=self.node_uuid,
            docker_client=fake_docker,
            network=self.network
        )
        d = api.discover_state(self.EMPTY_NODESTATE)

        self.assertItemsEqual(pset(applications),
                              self.successResultOf(d)[0].applications)

    def test_discover_application_with_environment(self):
        """
        An ``Application`` with ``Environment`` objects is discovered from a
        ``Unit`` with ``Environment`` objects.
        """
        environment_variables = (
            (b'CUSTOM_ENV_A', b'a value'),
            (b'CUSTOM_ENV_B', b'something else'),
        )
        environment = Environment(variables=environment_variables)
        unit1 = UNIT_FOR_APP.set("environment", environment)
        units = {unit1.name: unit1}

        fake_docker = FakeDockerClient(units=units)
        applications = [APP.set("environment", dict(environment_variables))]
        api = ApplicationNodeDeployer(
            u'example.com',
            node_uuid=self.node_uuid,
            docker_client=fake_docker,
            network=self.network
        )
        d = api.discover_state(self.EMPTY_NODESTATE)

        self.assertItemsEqual(pset(applications),
                              self.successResultOf(d)[0].applications)

    def test_discover_application_with_environment_and_links(self):
        """
        An ``Application`` with ``Environment`` and ``Link`` objects is
        discovered from a ``Unit`` with both custom environment variables and
        environment variables representing container links. The environment
        variables taking the format <ALIAS>_PORT_<PORT>_TCP are separated in
        to ``Link`` representations in the ``Application``.
        """
        environment_variables = (
            (b'CUSTOM_ENV_A', b'a value'),
            (b'CUSTOM_ENV_B', b'something else'),
        )
        link_environment_variables = (
            (b'APACHE_PORT_80_TCP', b'tcp://example.com:8080'),
            (b'APACHE_PORT_80_TCP_PROTO', b'tcp'),
            (b'APACHE_PORT_80_TCP_ADDR', b'example.com'),
            (b'APACHE_PORT_80_TCP_PORT', b'8080'),
        )
        unit_environment = environment_variables + link_environment_variables
        environment = Environment(variables=frozenset(unit_environment))
        unit1 = UNIT_FOR_APP.set("environment", environment)
        units = {unit1.name: unit1}

        fake_docker = FakeDockerClient(units=units)
        links = [
            Link(local_port=80, remote_port=8080, alias=u"APACHE")
        ]
        applications = [APP.set("links", links).set(
            "environment", dict(environment_variables))]

        api = ApplicationNodeDeployer(
            u'example.com',
            node_uuid=self.node_uuid,
            docker_client=fake_docker,
            network=self.network
        )
        d = api.discover_state(self.EMPTY_NODESTATE)

        self.assertItemsEqual(pset(applications),
                              self.successResultOf(d)[0].applications)

    def test_discover_application_with_links(self):
        """
        An ``Application`` with ``Link`` objects is discovered from a ``Unit``
        with environment variables that correspond to an exposed link.
        """
        fake_docker = FakeDockerClient()
        applications = [APP.set("links", [
            Link(local_port=80, remote_port=8080, alias=u'APACHE')
        ])]
        api = ApplicationNodeDeployer(
            u'example.com',
            node_uuid=self.node_uuid,
            docker_client=fake_docker,
            network=self.network
        )
        for app in applications:
            StartApplication(
                node_state=NodeState(uuid=api.node_uuid,
                                     hostname=api.hostname),
                application=app
            ).run(api)
        d = api.discover_state(self.EMPTY_NODESTATE)

        self.assertItemsEqual(applications,
                              self.successResultOf(d)[0].applications)

    def test_discover_application_with_ports(self):
        """
        An ``Application`` with ``Port`` objects is discovered from a ``Unit``
        with exposed ``Portmap`` objects.
        """
        ports = [PortMap(internal_port=80, external_port=8080)]
        unit1 = UNIT_FOR_APP.set("ports", ports)
        units = {unit1.name: unit1}

        fake_docker = FakeDockerClient(units=units)
        applications = [APP.set("ports",
                                [Port(internal_port=80, external_port=8080)])]
        api = ApplicationNodeDeployer(
            u'example.com',
            node_uuid=self.node_uuid,
            docker_client=fake_docker,
            network=self.network
        )
        d = api.discover_state(self.EMPTY_NODESTATE)

        self.assertEqual(sorted(applications),
                         sorted(self.successResultOf(d)[0].applications))

    def test_discover_attached_volume(self):
        """
        Datasets that are mounted at a path that matches the container's
        volume are added to ``Application`` with same name as an
        ``AttachedVolume``.
        """
        DATASET_ID = unicode(uuid4())
        DATASET_ID2 = unicode(uuid4())

        path1 = FilePath(b"/flocker").child(DATASET_ID.encode("ascii"))
        path2 = FilePath(b"/flocker").child(DATASET_ID2.encode("ascii"))
        manifestations = {dataset_id:
                          Manifestation(
                              dataset=Dataset(dataset_id=dataset_id),
                              primary=True,
                          )
                          for dataset_id in (DATASET_ID, DATASET_ID2)}
        current_known_state = NodeState(uuid=self.node_uuid,
                                        hostname=u'example.com',
                                        manifestations=manifestations,
                                        devices={},
                                        paths={DATASET_ID: path1,
                                               DATASET_ID2: path2})

        unit1 = UNIT_FOR_APP.set("volumes", [
            DockerVolume(
                node_path=path1,
                container_path=FilePath(b'/var/lib/data')
            )]
        )

        unit2 = UNIT_FOR_APP2.set("volumes", [
            DockerVolume(
                node_path=path2,
                container_path=FilePath(b'/var/lib/data')
            )]
        )
        units = {unit1.name: unit1, unit2.name: unit2}

        fake_docker = FakeDockerClient(units=units)
        applications = [app.set("volume", AttachedVolume(
            manifestation=manifestations[respective_id],
            mountpoint=FilePath(b'/var/lib/data')
        )) for (app, respective_id) in [(APP, DATASET_ID),
                                        (APP2, DATASET_ID2)]]
        api = ApplicationNodeDeployer(
            u'example.com',
            node_uuid=self.node_uuid,
            docker_client=fake_docker,
            network=self.network
        )
        d = api.discover_state(current_known_state)

        self.assertItemsEqual(pset(applications),
                              self.successResultOf(d)[0].applications)

    def test_ignore_unknown_volumes(self):
        """
        Docker volumes that cannot be matched to a dataset are ignored.
        """
        unit = UNIT_FOR_APP.set("volumes", [
            DockerVolume(
                node_path=FilePath(b"/some/random/path"),
                container_path=FilePath(b'/var/lib/data')
            )],
        )
        units = {unit.name: unit}

        fake_docker = FakeDockerClient(units=units)

        applications = [APP]
        api = ApplicationNodeDeployer(
            u'example.com',
            node_uuid=self.node_uuid,
            docker_client=fake_docker,
            network=self.network
        )
        d = api.discover_state(self.EMPTY_NODESTATE)

        self.assertEqual(sorted(applications),
                         sorted(self.successResultOf(d)[0].applications))

    def test_not_running_units(self):
        """
        Units that are not active are considered to be not running by
        ``discover_state()``.
        """
        unit1 = UNIT_FOR_APP.set("activation_state", u"inactive")
        unit2 = UNIT_FOR_APP2.set("activation_state", u'madeup')
        units = {unit1.name: unit1, unit2.name: unit2}

        fake_docker = FakeDockerClient(units=units)
        applications = [APP.set("running", False), APP2.set("running", False)]
        api = ApplicationNodeDeployer(
            u'example.com',
            node_uuid=self.node_uuid,
            docker_client=fake_docker,
            network=self.network
        )
        d = api.discover_state(EMPTY_NODESTATE)
        result = self.successResultOf(d)

        self.assertEqual([NodeState(uuid=api.node_uuid, hostname=api.hostname,
                                    applications=applications, used_ports=[])],
                         result)

    def test_discover_used_ports(self):
        """
        Any ports in use, as reported by the deployer's ``INetwork`` provider,
        are reported in the ``used_ports`` attribute of the ``NodeState``
        returned by ``discover_state``.
        """
        used_ports = frozenset([1, 3, 5, 1000])
        api = ApplicationNodeDeployer(
            u'example.com',
            node_uuid=self.node_uuid,
            docker_client=FakeDockerClient(),
            network=make_memory_network(used_ports=used_ports)
        )

        discovering = api.discover_state(self.EMPTY_NODESTATE)
        states = self.successResultOf(discovering)

        self.assertEqual(
            [NodeState(uuid=api.node_uuid, hostname=api.hostname,
                       used_ports=used_ports, applications=[])],
            states
        )

    def test_discover_application_restart_policy(self):
        """
        An ``Application`` with the appropriate ``IRestartPolicy`` is
        discovered from the corresponding restart policy of the ``Unit``.
        """
        policy = RestartAlways()
        unit1 = UNIT_FOR_APP.set("restart_policy", policy)
        units = {unit1.name: unit1}

        fake_docker = FakeDockerClient(units=units)
        applications = [APP.set("restart_policy", policy)]
        api = ApplicationNodeDeployer(
            u'example.com',
            node_uuid=self.node_uuid,
            docker_client=fake_docker,
            network=self.network
        )
        d = api.discover_state(self.EMPTY_NODESTATE)

        self.assertEqual(applications,
                         list(self.successResultOf(d)[0].applications))

    def test_unknown_manifestations(self):
        """
        If the given ``NodeState`` indicates ignorance of manifestations, the
        ``ApplicationNodeDeployer`` doesn't bother doing any discovery and
        just indicates ignorance of applications.
        """
        fake_docker = FakeDockerClient(units={APP_NAME: UNIT_FOR_APP})
        api = ApplicationNodeDeployer(
            u'example.com',
            node_uuid=self.node_uuid,
            docker_client=fake_docker,
            network=self.network
        )
        # Apparently we know nothing about manifestations one way or the
        # other:
        d = api.discover_state(NodeState(
            uuid=api.node_uuid,
            hostname=api.hostname,
            manifestations=None, paths=None))

        self.assertEqual([NodeState(hostname=api.hostname,
                                    uuid=api.node_uuid,
                                    # Can't do app discovery if don't know
                                    # about manifestations:
                                    applications=None,
                                    used_ports=None,
                                    manifestations=None,
                                    paths=None)],
                         self.successResultOf(d))


class P2PManifestationDeployerDiscoveryTests(SynchronousTestCase):
    """
    Tests for ``P2PManifestationDeployer`` discovery.
    """
    def setUp(self):
        self.volume_service = create_volume_service(self)
        self.node_uuid = uuid4()
        # https://clusterhq.atlassian.net/browse/FLOC-1926
        self.EMPTY_NODESTATE = NodeState(hostname=u"example.com",
                                         uuid=self.node_uuid)

    DATASET_ID = unicode(uuid4())
    DATASET_ID2 = unicode(uuid4())

    def test_unknown_applications_and_ports(self):
        """
        Applications and ports are left as ``None`` in discovery results.
        """
        deployer = P2PManifestationDeployer(
            u'example.com', self.volume_service, node_uuid=self.node_uuid)
        self.assertEqual(
            self.successResultOf(deployer.discover_state(
                self.EMPTY_NODESTATE)),
            [NodeState(hostname=deployer.hostname,
{'fraction_non_alphanumeric': 0.09636282840252622, 'fraction_numerical': 0.00791801887935794, 'mean_word_length': 2.272895548699868, 'pattern_counts': {'":': 2, '<': 13, '<?xml version=': 0, '>': 13, 'https://': 3, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 4, 'substrings_counts': 2, 'word_list_counts': {'cursed_substrings.json': 64, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '341717', 'n_tokens_mistral': 21205, 'n_tokens_neox': 20475, 'n_words': 3363}