Example #1
0
 def setup_unit_relations(self, service_relation, *units):
     """
     Given a service relation and set of unit tuples in the form
     unit_state, unit_relation_workflow_state, will add unit relations
     for these units and update their workflow state to the desired/given
     state.
     """
     for unit, state in units:
         unit_relation = yield service_relation.add_unit_state(unit)
         lifecycle = UnitRelationLifecycle(self.client,
                                           unit.unit_name, unit_relation,
                                           service_relation.relation_name,
                                           self.makeDir(), self.executor)
         workflow_state = RelationWorkflowState(
             self.client, unit_relation, lifecycle, self.makeDir())
         yield workflow_state.set_state(state)
Example #2
0
 def setup_unit_relations(self, service_relation, *units):
     """
     Given a service relation and set of unit tuples in the form
     unit_state, unit_relation_workflow_state, will add unit relations
     for these units and update their workflow state to the desired/given
     state.
     """
     for unit, state in units:
         unit_relation = yield service_relation.add_unit_state(unit)
         lifecycle = UnitRelationLifecycle(self.client, unit.unit_name,
                                           unit_relation,
                                           service_relation.relation_name,
                                           self.makeDir(), self.executor)
         workflow_state = RelationWorkflowState(self.client, unit_relation,
                                                lifecycle, self.makeDir())
         yield workflow_state.set_state(state)
Example #3
0
    def get_unit_relation_workflow(self, states):
        state_dir = os.path.join(self.juju_directory, "state")
        lifecycle = UnitRelationLifecycle(
            self.client, states["unit_relation"],
            states["service_relation"].relation_name, self.unit_directory,
            self.executor)

        workflow = RelationWorkflowState(self.client, states["unit_relation"],
                                         lifecycle, state_dir)

        return (workflow, lifecycle)
Example #4
0
    def setUp(self):
        yield super(UnitRelationWorkflowTest, self).setUp()
        yield self.setup_default_test_relation()
        self.relation_name = self.states["service_relation"].relation_name
        self.juju_directory = self.makeDir()
        self.log_stream = self.capture_logging("unit.relation.lifecycle",
                                               logging.DEBUG)

        self.lifecycle = UnitRelationLifecycle(self.client,
                                               self.states["unit"].unit_name,
                                               self.states["unit_relation"],
                                               self.relation_name,
                                               self.unit_directory,
                                               self.executor)

        self.state_directory = self.makeDir(
            path=os.path.join(self.juju_directory, "state"))

        self.workflow = RelationWorkflowState(self.client,
                                              self.states["unit_relation"],
                                              self.lifecycle,
                                              self.state_directory)
Example #5
0
    def _add_relation(self, service_relation):
        try:
            unit_relation = yield service_relation.get_unit_state(
                self._unit)
        except UnitRelationStateNotFound:
            # This unit has not yet been assigned a unit relation state,
            # Go ahead and add one.
            unit_relation = yield service_relation.add_unit_state(
                self._unit)

        lifecycle = UnitRelationLifecycle(
            self._client, self._unit.unit_name, unit_relation,
            service_relation.relation_ident,
            self._unit_dir, self._state_dir, self._executor)

        workflow = RelationWorkflowState(
            self._client, unit_relation, service_relation.relation_name,
            lifecycle, self._state_dir)

        self._relations[service_relation.internal_relation_id] = workflow

        with (yield workflow.lock()):
            yield workflow.synchronize()
Example #6
0
    def _get_unit_relation_workflow(self, unit_relation, service_relation):

        lifecycle = UnitRelationLifecycle(self._client, self._unit.unit_name,
                                          unit_relation,
                                          service_relation.relation_name,
                                          self._get_unit_path(),
                                          self._executor)

        state_directory = os.path.abspath(
            os.path.join(self._unit_path, "../../state"))

        workflow = RelationWorkflowState(self._client, unit_relation,
                                         lifecycle, state_directory)

        return workflow
Example #7
0
    def setUp(self):
        yield super(UnitRelationWorkflowTest, self).setUp()
        yield self.setup_default_test_relation()
        self.relation_name = self.states["service_relation"].relation_name
        self.relation_ident = self.states["service_relation"].relation_ident
        self.log_stream = self.capture_logging(
            "unit.relation.lifecycle", logging.DEBUG)

        self.lifecycle = UnitRelationLifecycle(
            self.client,
            self.states["unit"].unit_name,
            self.states["unit_relation"],
            self.relation_ident,
            self.unit_directory,
            self.state_directory,
            self.executor)

        self.workflow = RelationWorkflowState(
            self.client, self.states["unit_relation"],
            self.states["unit"].unit_name, self.lifecycle,
            self.state_directory)
Example #8
0
class UnitRelationWorkflowTest(WorkflowTestBase):

    @inlineCallbacks
    def setUp(self):
        yield super(UnitRelationWorkflowTest, self).setUp()
        yield self.setup_default_test_relation()
        self.relation_name = self.states["service_relation"].relation_name
        self.relation_ident = self.states["service_relation"].relation_ident
        self.log_stream = self.capture_logging(
            "unit.relation.lifecycle", logging.DEBUG)

        self.lifecycle = UnitRelationLifecycle(
            self.client,
            self.states["unit"].unit_name,
            self.states["unit_relation"],
            self.relation_ident,
            self.unit_directory,
            self.state_directory,
            self.executor)

        self.workflow = RelationWorkflowState(
            self.client, self.states["unit_relation"],
            self.states["unit"].unit_name, self.lifecycle,
            self.state_directory)

    @inlineCallbacks
    def tearDown(self):
        yield self.lifecycle.stop()
        yield super(UnitRelationWorkflowTest, self).tearDown()

    @inlineCallbacks
    def test_is_relation_running(self):
        """The unit relation's workflow state can be categorized as a
        boolean.
        """
        with (yield self.workflow.lock()):
            running, state = yield is_relation_running(
                self.client, self.states["unit_relation"])
            self.assertIdentical(running, False)
            self.assertIdentical(state, None)
            relation_state = self.workflow.get_relation_info()
            self.assertEquals(relation_state,
                              {"relation-0000000000":
                               {"relation_name": "mysql/0",
                                "relation_scope": "global"}})

            yield self.workflow.fire_transition("start")
            running, state = yield is_relation_running(
                self.client, self.states["unit_relation"])
            self.assertIdentical(running, True)
            self.assertEqual(state, "up")

            relation_state = self.workflow.get_relation_info()
            self.assertEquals(relation_state,
                              {"relation-0000000000":
                               {"relation_name": "mysql/0",
                                "relation_scope": "global"}})

            yield self.workflow.fire_transition("stop")
            running, state = yield is_relation_running(
                self.client, self.states["unit_relation"])
            self.assertIdentical(running, False)
            self.assertEqual(state, "down")
            relation_state = self.workflow.get_relation_info()
            self.assertEquals(relation_state,
                              {"relation-0000000000":
                               {"relation_name": "mysql/0",
                                "relation_scope": "global"}})

    @inlineCallbacks
    def test_up_down_cycle(self):
        """The workflow can be transition from up to down, and back.
        """
        self.write_hook("%s-relation-changed" % self.relation_name,
                        "#!/bin/bash\nexit 0\n")

        with (yield self.workflow.lock()):
            yield self.workflow.fire_transition("start")
        yield self.assertState(self.workflow, "up")

        hook_executed = self.wait_on_hook("app-relation-changed")

        # Add a new unit, while we're stopped.
        with (yield self.workflow.lock()):
            yield self.workflow.fire_transition("stop")
        yield self.add_opposite_service_unit(self.states)
        yield self.assertState(self.workflow, "down")
        self.assertFalse(hook_executed.called)

        # Come back up; check unit add detected.
        with (yield self.workflow.lock()):
            yield self.workflow.fire_transition("restart")
        yield self.assertState(self.workflow, "up")
        yield hook_executed

        self.assert_history_concise(
            ("start", "up"), ("stop", "down"), ("restart", "up"),
            history_id=self.workflow.zk_state_id)

    @inlineCallbacks
    def test_join_hook_with_error(self):
        """A join hook error stops execution of synthetic change hooks.
        """
        self.capture_logging("unit.relation.lifecycle", logging.DEBUG)
        self.write_hook("%s-relation-joined" % self.relation_name,
                        "#!/bin/bash\nexit 1\n")
        self.write_hook("%s-relation-changed" % self.relation_name,
                        "#!/bin/bash\nexit 1\n")

        with (yield self.workflow.lock()):
            yield self.workflow.fire_transition("start")
        yield self.assertState(self.workflow, "up")

        # Add a new unit, and wait for the broken hook to result in
        # the transition to the down state.
        yield self.add_opposite_service_unit(self.states)
        yield self.wait_on_state(self.workflow, "error")

        f_state, history, zk_state = yield self.read_persistent_state(
            history_id=self.workflow.zk_state_id)

        self.assertEqual(f_state, zk_state)
        error = "Error processing '%s': exit code 1." % (
            os.path.join(self.unit_directory,
                         "charm", "hooks", "app-relation-joined"))

        self.assertEqual(f_state,
                         {"state": "error",
                          "state_variables": {
                              "change_type": "joined",
                              "error_message": error}})

    @inlineCallbacks
    def test_change_hook_with_error(self):
        """An error while processing a change hook, results
        in the workflow transitioning to the down state.
        """
        self.capture_logging("unit.relation.lifecycle", logging.DEBUG)
        self.write_hook("%s-relation-joined" % self.relation_name,
                        "#!/bin/bash\nexit 0\n")
        self.write_hook("%s-relation-changed" % self.relation_name,
                        "#!/bin/bash\nexit 1\n")

        with (yield self.workflow.lock()):
            yield self.workflow.fire_transition("start")
        yield self.assertState(self.workflow, "up")

        # Add a new unit, and wait for the broken hook to result in
        # the transition to the down state.
        yield self.add_opposite_service_unit(self.states)
        yield self.wait_on_state(self.workflow, "error")

        f_state, history, zk_state = yield self.read_persistent_state(
            history_id=self.workflow.zk_state_id)

        self.assertEqual(f_state, zk_state)
        error = "Error processing '%s': exit code 1." % (
            os.path.join(self.unit_directory,
                         "charm", "hooks", "app-relation-changed"))

        self.assertEqual(f_state,
                         {"state": "error",
                          "state_variables": {
                              "change_type": "modified",
                              "error_message": error}})

    @inlineCallbacks
    def test_depart(self):
        """When the workflow is transition to the down state, a relation
        broken hook is executed, and the unit stops responding to relation
        changes.
        """
        with (yield self.workflow.lock()):
            yield self.workflow.fire_transition("start")
        yield self.assertState(self.workflow, "up")

        wait_on_hook = self.wait_on_hook("app-relation-changed")
        states = yield self.add_opposite_service_unit(self.states)
        yield wait_on_hook

        wait_on_hook = self.wait_on_hook("app-relation-broken")
        wait_on_state = self.wait_on_state(self.workflow, "departed")
        with (yield self.workflow.lock()):
            yield self.workflow.fire_transition("depart")
        yield wait_on_hook
        yield wait_on_state

        # verify further changes to the related unit don't result in
        # hook executions.
        results = []

        def collect_executions(*args):
            results.append(args)

        self.executor.set_observer(collect_executions)
        yield states["unit_relation"].set_data(dict(a=1))

        # Sleep to give errors a chance.
        yield self.sleep(0.1)
        self.assertFalse(results)

    def test_lifecycle_attribute(self):
        """The workflow lifecycle is accessible from the workflow."""
        self.assertIdentical(self.workflow.lifecycle, self.lifecycle)

    @inlineCallbacks
    def test_client_read_none(self):
        workflow = WorkflowStateClient(
            self.client, self.states["unit_relation"])
        self.assertEqual(None, (yield workflow.get_state()))

    @inlineCallbacks
    def test_client_read_state(self):
        """The relation workflow client can read the state of a unit
        relation."""
        with (yield self.workflow.lock()):
            yield self.workflow.fire_transition("start")
        yield self.assertState(self.workflow, "up")

        self.write_hook("%s-relation-changed" % self.relation_name,
                        "#!/bin/bash\necho hello\n")
        wait_on_hook = self.wait_on_hook("app-relation-changed")
        yield self.add_opposite_service_unit(self.states)
        yield wait_on_hook

        workflow = WorkflowStateClient(
            self.client, self.states["unit_relation"])
        self.assertEqual("up", (yield workflow.get_state()))

    @inlineCallbacks
    def test_client_read_only(self):
        workflow_client = WorkflowStateClient(
            self.client, self.states["unit_relation"])
        with (yield workflow_client.lock()):
            yield self.assertFailure(
                workflow_client.set_state("up"),
                NotImplementedError)

    @inlineCallbacks
    def assert_synchronize(self, start_state, state, watches, scheduler,
                           sync_watches=None, sync_scheduler=None,
                           start_inflight=None):
        # Handle cases where we expect to be in a different state pre-sync
        # to the final state post-sync.
        if sync_watches is None:
            sync_watches = watches
        if sync_scheduler is None:
            sync_scheduler = scheduler
        super_sync = WorkflowState.synchronize

        @inlineCallbacks
        def check_sync(obj):
            self.assertEquals(
                self.workflow.lifecycle.watching, sync_watches)
            self.assertEquals(
                self.workflow.lifecycle.executing, sync_scheduler)
            yield super_sync(obj)

        start_states = itertools.product((True, False), (True, False))
        for (initial_watches, initial_scheduler) in start_states:
            yield self.workflow.lifecycle.stop()
            yield self.workflow.lifecycle.start(
                start_watches=initial_watches,
                start_scheduler=initial_scheduler)
            self.assertEquals(
                self.workflow.lifecycle.watching, initial_watches)
            self.assertEquals(
                self.workflow.lifecycle.executing, initial_scheduler)
            with (yield self.workflow.lock()):
                yield self.workflow.set_state(start_state)
                yield self.workflow.set_inflight(start_inflight)

                # self.patch is not suitable because we can't unpatch until
                # the end of the test, and we don't really want 13 distinct
                # one-line test_synchronize_foo methods.
                WorkflowState.synchronize = check_sync
                try:
                    yield self.workflow.synchronize()
                finally:
                    WorkflowState.synchronize = super_sync

            new_inflight = yield self.workflow.get_inflight()
            self.assertEquals(new_inflight, None)
            new_state = yield self.workflow.get_state()
            self.assertEquals(new_state, state)
            self.assertEquals(self.workflow.lifecycle.watching, watches)
            self.assertEquals(self.workflow.lifecycle.executing, scheduler)

    @inlineCallbacks
    def test_synchronize(self):
        # No transition in flight
        yield self.assert_synchronize(None, "up", True, True, False, False)
        yield self.assert_synchronize("down", "down", False, False)
        yield self.assert_synchronize("departed", "departed", False, False)
        yield self.assert_synchronize("error", "error", True, False)
        yield self.assert_synchronize("up", "up", True, True)

        # With transition inflight
        yield self.assert_synchronize(
            None, "up", True, True, False, False, "start")
        yield self.assert_synchronize(
            "up", "down", False, False, True, True, "stop")
        yield self.assert_synchronize(
            "down", "up", True, True, False, False, "restart")
        yield self.assert_synchronize(
            "up", "error", True, False, True, True, "error")
        yield self.assert_synchronize(
            "error", "up", True, True, True, False, "reset")
        yield self.assert_synchronize(
            "up", "departed", False, False, True, True, "depart")
        yield self.assert_synchronize(
            "down", "departed", False, False, False, False, "down_depart")
        yield self.assert_synchronize(
            "error", "departed", False, False, True, False, "error_depart")

    @inlineCallbacks
    def test_depart_hook_error(self):
        """A depart hook error, still results in a transition to the
        departed state with a state variable noting the error."""

        self.write_hook("%s-relation-broken" % self.relation_name,
                        "#!/bin/bash\nexit 1\n")
        error_output = self.capture_logging("unit.relation.workflow")

        with (yield self.workflow.lock()):
            yield self.workflow.fire_transition("start")
        yield self.assertState(self.workflow, "up")

        wait_on_hook = self.wait_on_hook("app-relation-changed")
        states = yield self.add_opposite_service_unit(self.states)
        yield wait_on_hook

        wait_on_hook = self.wait_on_hook("app-relation-broken")
        wait_on_state = self.wait_on_state(self.workflow, "departed")
        with (yield self.workflow.lock()):
            yield self.workflow.fire_transition("depart")
        yield wait_on_hook
        yield wait_on_state

        # verify further changes to the related unit don't result in
        # hook executions.
        results = []

        def collect_executions(*args):
            results.append(args)

        self.executor.set_observer(collect_executions)
        yield states["unit_relation"].set_data(dict(a=1))

        # Sleep to give errors a chance.
        yield self.sleep(0.1)
        self.assertFalse(results)

        # Verify final state and log output.
        msg = "Depart hook error, ignoring: "
        error_msg = "Error processing "
        error_msg += repr(os.path.join(
            self.unit_directory, "charm", "hooks",
            "app-relation-broken"))
        error_msg += ": exit code 1."

        self.assertEqual(
            error_output.getvalue(), (msg + error_msg + "\n"))
        current_state = yield self.workflow.get_state()
        self.assertEqual(current_state, "departed")

        f_state, history, zk_state = yield self.read_persistent_state(
            history_id=self.workflow.zk_state_id)

        self.assertEqual(f_state, zk_state)
        self.assertEqual(f_state,
                         {"state": "departed",
                          "state_variables": {
                              "change_type": "depart",
                              "error_message": error_msg}})

    def test_depart_down(self):
        """When the workflow is transitioned from down to departed, a relation
        broken hook is executed, and the unit stops responding to relation
        changes.
        """
        with (yield self.workflow.lock()):
            yield self.workflow.fire_transition("start")
            yield self.assertState(self.workflow, "up")
            yield self.workflow.fire_transition("stop")
            yield self.assertState(self.workflow, "down")

        states = yield self.add_opposite_service_unit(self.states)
        wait_on_hook = self.wait_on_hook("app-relation-broken")
        wait_on_state = self.wait_on_state(self.workflow, "departed")
        with (yield self.workflow.lock()):
            yield self.workflow.fire_transition("depart")
        yield wait_on_hook
        yield wait_on_state

        # Verify further changes to the related unit don't result in
        # hook executions.
        results = []

        def collect_executions(*args):
            results.append(args)

        self.executor.set_observer(collect_executions)
        yield states["unit_relation"].set_data(dict(a=1))

        # Sleep to give errors a chance.
        yield self.sleep(0.1)
        self.assertFalse(results)

    def test_depart_error(self):
        with (yield self.workflow.lock()):
            yield self.workflow.fire_transition("start")
            yield self.assertState(self.workflow, "up")
            yield self.workflow.fire_transition("error")
            yield self.assertState(self.workflow, "error")

        states = yield self.add_opposite_service_unit(self.states)
        wait_on_hook = self.wait_on_hook("app-relation-broken")
        wait_on_state = self.wait_on_state(self.workflow, "departed")
        with (yield self.workflow.lock()):
            yield self.workflow.fire_transition("depart")
        yield wait_on_hook
        yield wait_on_state

        # Verify further changes to the related unit don't result in
        # hook executions.
        results = []

        def collect_executions(*args):
            results.append(args)

        self.executor.set_observer(collect_executions)
        yield states["unit_relation"].set_data(dict(a=1))

        # Sleep to give errors a chance.
        yield self.sleep(0.1)
        self.assertFalse(results)
Example #9
0
class UnitRelationWorkflowTest(WorkflowTestBase):
    @inlineCallbacks
    def setUp(self):
        yield super(UnitRelationWorkflowTest, self).setUp()
        yield self.setup_default_test_relation()
        self.relation_name = self.states["service_relation"].relation_name
        self.juju_directory = self.makeDir()
        self.log_stream = self.capture_logging("unit.relation.lifecycle",
                                               logging.DEBUG)

        self.lifecycle = UnitRelationLifecycle(self.client,
                                               self.states["unit"].unit_name,
                                               self.states["unit_relation"],
                                               self.relation_name,
                                               self.unit_directory,
                                               self.executor)

        self.state_directory = self.makeDir(
            path=os.path.join(self.juju_directory, "state"))

        self.workflow = RelationWorkflowState(self.client,
                                              self.states["unit_relation"],
                                              self.lifecycle,
                                              self.state_directory)

    @inlineCallbacks
    def test_is_relation_running(self):
        """The unit relation's workflow state can be categorized as a
        boolean.
        """
        running, state = yield is_relation_running(
            self.client, self.states["unit_relation"])
        self.assertIdentical(running, False)
        self.assertIdentical(state, None)
        yield self.workflow.fire_transition("start")
        running, state = yield is_relation_running(
            self.client, self.states["unit_relation"])
        self.assertIdentical(running, True)
        self.assertEqual(state, "up")
        yield self.workflow.fire_transition("stop")
        running, state = yield is_relation_running(
            self.client, self.states["unit_relation"])
        self.assertIdentical(running, False)
        self.assertEqual(state, "down")

    @inlineCallbacks
    def test_up_down_cycle(self):
        """The workflow can be transition from up to down, and back.
        """
        self.write_hook("%s-relation-changed" % self.relation_name,
                        "#!/bin/bash\nexit 0\n")

        yield self.workflow.fire_transition("start")
        yield self.assertState(self.workflow, "up")

        hook_executed = self.wait_on_hook("app-relation-changed")

        # Add a new unit, and this will be scheduled by the time
        # we finish stopping.
        yield self.add_opposite_service_unit(self.states)
        yield self.workflow.fire_transition("stop")
        yield self.assertState(self.workflow, "down")
        self.assertFalse(hook_executed.called)

        # Currently if we restart, we will only see the previously
        # queued event, as the last watch active when a lifecycle is
        # stopped, may already be in flight and will be scheduled, and
        # will be executed when the lifecycle is started. However any
        # events that may have occured after the lifecycle is stopped
        # are currently ignored and un-notified.
        yield self.workflow.fire_transition("restart")
        yield self.assertState(self.workflow, "up")
        yield hook_executed

        f_state, history, zk_state = yield self.read_persistent_state(
            history_id=self.workflow.zk_state_id)

        self.assertEqual(f_state, zk_state)
        self.assertEqual(f_state, {"state": "up", "state_variables": {}})

        self.assertEqual(history, [{
            "state": "up",
            "state_variables": {}
        }, {
            "state": "down",
            "state_variables": {}
        }, {
            "state": "up",
            "state_variables": {}
        }])

    @inlineCallbacks
    def test_change_hook_with_error(self):
        """An error while processing a change hook, results
        in the workflow transitioning to the down state.
        """
        self.capture_logging("unit.relation.lifecycle", logging.DEBUG)
        self.write_hook("%s-relation-joined" % self.relation_name,
                        "#!/bin/bash\nexit 0\n")
        self.write_hook("%s-relation-changed" % self.relation_name,
                        "#!/bin/bash\nexit 1\n")

        current_state = yield self.workflow.get_state()
        self.assertEqual(current_state, None)
        yield self.workflow.fire_transition("start")
        yield self.assertState(self.workflow, "up")
        current_state = yield self.workflow.get_state()

        # Add a new unit, and wait for the broken hook to result in
        # the transition to the down state.
        yield self.add_opposite_service_unit(self.states)
        yield self.wait_on_state(self.workflow, "error")

        f_state, history, zk_state = yield self.read_persistent_state(
            history_id=self.workflow.zk_state_id)

        self.assertEqual(f_state, zk_state)
        error = "Error processing '%s': exit code 1." % (os.path.join(
            self.unit_directory, "charm", "hooks", "app-relation-changed"))

        self.assertEqual(
            f_state, {
                "state": "error",
                "state_variables": {
                    "change_type": "joined",
                    "error_message": error
                }
            })

    @inlineCallbacks
    def test_depart(self):
        """When the workflow is transition to the down state, a relation
        broken hook is executed, and the unit stops responding to relation
        changes.
        """
        self.write_hook("%s-relation-joined" % self.relation_name,
                        "#!/bin/bash\necho hello\n")
        self.write_hook("%s-relation-changed" % self.relation_name,
                        "#!/bin/bash\necho hello\n")
        self.write_hook("%s-relation-broken" % self.relation_name,
                        "#!/bin/bash\necho hello\n")

        results = []

        def collect_executions(*args):
            results.append(args)

        yield self.workflow.fire_transition("start")
        yield self.assertState(self.workflow, "up")

        wait_on_hook = self.wait_on_hook("app-relation-changed")
        states = yield self.add_opposite_service_unit(self.states)
        yield wait_on_hook

        wait_on_hook = self.wait_on_hook("app-relation-broken")
        wait_on_state = self.wait_on_state(self.workflow, "departed")
        yield self.workflow.fire_transition("depart")
        yield wait_on_hook
        yield wait_on_state

        # verify further changes to the related unit, don't result in
        # hook executions.
        self.executor.set_observer(collect_executions)
        yield states["unit_relation"].set_data(dict(a=1))
        # Sleep to give errors a chance.
        yield self.sleep(0.1)
        self.assertFalse(results)

    def test_lifecycle_attribute(self):
        """The workflow lifecycle is accessible from the workflow."""
        self.assertIdentical(self.workflow.lifecycle, self.lifecycle)

    @inlineCallbacks
    def test_client_read_none(self):
        workflow = WorkflowStateClient(self.client,
                                       self.states["unit_relation"])
        self.assertEqual(None, (yield workflow.get_state()))

    @inlineCallbacks
    def test_client_read_state(self):
        """The relation workflow client can read the state of a unit
        relation."""
        yield self.workflow.fire_transition("start")
        yield self.assertState(self.workflow, "up")

        self.write_hook("%s-relation-changed" % self.relation_name,
                        "#!/bin/bash\necho hello\n")
        wait_on_hook = self.wait_on_hook("app-relation-changed")
        yield self.add_opposite_service_unit(self.states)
        yield wait_on_hook

        workflow = WorkflowStateClient(self.client,
                                       self.states["unit_relation"])
        self.assertEqual("up", (yield workflow.get_state()))

    @inlineCallbacks
    def test_client_read_only(self):
        workflow_client = WorkflowStateClient(self.client,
                                              self.states["unit_relation"])
        yield self.assertFailure(workflow_client.set_state("up"),
                                 NotImplementedError)

    @inlineCallbacks
    def test_depart_hook_error(self):
        """A depart hook error, still results in a transition to the
        departed state with a state variable noting the error."""

        self.write_hook("%s-relation-changed" % self.relation_name,
                        "#!/bin/bash\necho hello\n")
        self.write_hook("%s-relation-broken" % self.relation_name,
                        "#!/bin/bash\nexit 1\n")

        error_output = self.capture_logging("unit.relation.workflow")

        results = []

        def collect_executions(*args):
            results.append(args)

        yield self.workflow.fire_transition("start")
        yield self.assertState(self.workflow, "up")

        wait_on_hook = self.wait_on_hook("app-relation-changed")
        states = yield self.add_opposite_service_unit(self.states)
        yield wait_on_hook

        wait_on_hook = self.wait_on_hook("app-relation-broken")
        wait_on_state = self.wait_on_state(self.workflow, "departed")
        yield self.workflow.fire_transition("depart")
        yield wait_on_hook
        yield wait_on_state

        # verify further changes to the related unit, don't result in
        # hook executions.
        self.executor.set_observer(collect_executions)
        yield states["unit_relation"].set_data(dict(a=1))
        # Sleep to give errors a chance.
        yield self.sleep(0.1)
        self.assertFalse(results)

        # Verify final state and log output.
        msg = "Depart hook error, ignoring: "
        error_msg = "Error processing "
        error_msg += repr(
            os.path.join(self.unit_directory, "charm", "hooks",
                         "app-relation-broken"))
        error_msg += ": exit code 1."

        self.assertEqual(error_output.getvalue(), (msg + error_msg + "\n"))
        current_state = yield self.workflow.get_state()
        self.assertEqual(current_state, "departed")

        f_state, history, zk_state = yield self.read_persistent_state(
            history_id=self.workflow.zk_state_id)

        self.assertEqual(f_state, zk_state)
        self.assertEqual(
            f_state, {
                "state": "departed",
                "state_variables": {
                    "change_type": "depart",
                    "error_message": error_msg
                }
            })

    def test_depart_down(self):
        """When the workflow is transition to the down state, a relation
        broken hook is executed, and the unit stops responding to relation
        changes.
        """
        self.write_hook("%s-relation-changed" % self.relation_name,
                        "#!/bin/bash\necho hello\n")
        self.write_hook("%s-relation-broken" % self.relation_name,
                        "#!/bin/bash\necho hello\n")

        results = []

        def collect_executions(*args):
            results.append(args)

        yield self.workflow.fire_transition("start")
        yield self.assertState(self.workflow, "up")

        yield self.workflow.fire_transition("stop")
        yield self.assertState(self.workflow, "down")

        states = yield self.add_opposite_service_unit(self.states)

        wait_on_hook = self.wait_on_hook("app-relation-broken")
        wait_on_state = self.wait_on_state(self.workflow, "departed")

        yield self.workflow.fire_transition("depart")
        yield wait_on_hook
        yield wait_on_state

        # Verify further changes to the related unit, don't result in
        # hook executions.
        self.executor.set_observer(collect_executions)
        yield states["unit_relation"].set_data(dict(a=1))
        # Sleep to give errors a chance.
        yield self.sleep(0.1)
        self.assertFalse(results)
Example #10
0
class UnitRelationWorkflowTest(WorkflowTestBase):

    @inlineCallbacks
    def setUp(self):
        yield super(UnitRelationWorkflowTest, self).setUp()
        yield self.setup_default_test_relation()
        self.relation_name = self.states["service_relation"].relation_name
        self.juju_directory = self.makeDir()
        self.log_stream = self.capture_logging(
            "unit.relation.lifecycle", logging.DEBUG)

        self.lifecycle = UnitRelationLifecycle(
            self.client,
            self.states["unit"].unit_name,
            self.states["unit_relation"],
            self.relation_name,
            self.unit_directory,
            self.executor)

        self.state_directory = self.makeDir(
            path=os.path.join(self.juju_directory, "state"))

        self.workflow = RelationWorkflowState(
            self.client, self.states["unit_relation"], self.lifecycle,
            self.state_directory)

    @inlineCallbacks
    def test_is_relation_running(self):
        """The unit relation's workflow state can be categorized as a
        boolean.
        """
        running, state = yield is_relation_running(
            self.client, self.states["unit_relation"])
        self.assertIdentical(running, False)
        self.assertIdentical(state, None)
        yield self.workflow.fire_transition("start")
        running, state = yield is_relation_running(
            self.client, self.states["unit_relation"])
        self.assertIdentical(running, True)
        self.assertEqual(state, "up")
        yield self.workflow.fire_transition("stop")
        running, state = yield is_relation_running(
            self.client, self.states["unit_relation"])
        self.assertIdentical(running, False)
        self.assertEqual(state, "down")

    @inlineCallbacks
    def test_up_down_cycle(self):
        """The workflow can be transition from up to down, and back.
        """
        self.write_hook("%s-relation-changed" % self.relation_name,
                        "#!/bin/bash\nexit 0\n")

        yield self.workflow.fire_transition("start")
        yield self.assertState(self.workflow, "up")

        hook_executed = self.wait_on_hook("app-relation-changed")

        # Add a new unit, and this will be scheduled by the time
        # we finish stopping.
        yield self.add_opposite_service_unit(self.states)
        yield self.workflow.fire_transition("stop")
        yield self.assertState(self.workflow, "down")
        self.assertFalse(hook_executed.called)

        # Currently if we restart, we will only see the previously
        # queued event, as the last watch active when a lifecycle is
        # stopped, may already be in flight and will be scheduled, and
        # will be executed when the lifecycle is started. However any
        # events that may have occured after the lifecycle is stopped
        # are currently ignored and un-notified.
        yield self.workflow.fire_transition("restart")
        yield self.assertState(self.workflow, "up")
        yield hook_executed

        f_state, history, zk_state = yield self.read_persistent_state(
            history_id=self.workflow.zk_state_id)

        self.assertEqual(f_state, zk_state)
        self.assertEqual(f_state,
                         {"state": "up", "state_variables": {}})

        self.assertEqual(history,
                         [{"state": "up", "state_variables": {}},
                          {"state": "down", "state_variables": {}},
                          {"state": "up", "state_variables": {}}])

    @inlineCallbacks
    def test_change_hook_with_error(self):
        """An error while processing a change hook, results
        in the workflow transitioning to the down state.
        """
        self.capture_logging("unit.relation.lifecycle", logging.DEBUG)
        self.write_hook("%s-relation-joined" % self.relation_name,
                        "#!/bin/bash\nexit 0\n")
        self.write_hook("%s-relation-changed" % self.relation_name,
                        "#!/bin/bash\nexit 1\n")

        current_state = yield self.workflow.get_state()
        self.assertEqual(current_state, None)
        yield self.workflow.fire_transition("start")
        yield self.assertState(self.workflow, "up")
        current_state = yield self.workflow.get_state()

        # Add a new unit, and wait for the broken hook to result in
        # the transition to the down state.
        yield self.add_opposite_service_unit(self.states)
        yield self.wait_on_state(self.workflow, "error")

        f_state, history, zk_state = yield self.read_persistent_state(
            history_id=self.workflow.zk_state_id)

        self.assertEqual(f_state, zk_state)
        error = "Error processing '%s': exit code 1." % (
            os.path.join(self.unit_directory,
                         "charm", "hooks", "app-relation-changed"))

        self.assertEqual(f_state,
                         {"state": "error",
                          "state_variables": {
                              "change_type": "joined",
                              "error_message": error}})

    @inlineCallbacks
    def test_depart(self):
        """When the workflow is transition to the down state, a relation
        broken hook is executed, and the unit stops responding to relation
        changes.
        """
        self.write_hook("%s-relation-joined" % self.relation_name,
                        "#!/bin/bash\necho hello\n")
        self.write_hook("%s-relation-changed" % self.relation_name,
                        "#!/bin/bash\necho hello\n")
        self.write_hook("%s-relation-broken" % self.relation_name,
                        "#!/bin/bash\necho hello\n")

        results = []

        def collect_executions(*args):
            results.append(args)

        yield self.workflow.fire_transition("start")
        yield self.assertState(self.workflow, "up")

        wait_on_hook = self.wait_on_hook("app-relation-changed")
        states = yield self.add_opposite_service_unit(self.states)
        yield wait_on_hook

        wait_on_hook = self.wait_on_hook("app-relation-broken")
        wait_on_state = self.wait_on_state(self.workflow, "departed")
        yield self.workflow.fire_transition("depart")
        yield wait_on_hook
        yield wait_on_state

        # verify further changes to the related unit, don't result in
        # hook executions.
        self.executor.set_observer(collect_executions)
        yield states["unit_relation"].set_data(dict(a=1))
        # Sleep to give errors a chance.
        yield self.sleep(0.1)
        self.assertFalse(results)

    def test_lifecycle_attribute(self):
        """The workflow lifecycle is accessible from the workflow."""
        self.assertIdentical(self.workflow.lifecycle, self.lifecycle)

    @inlineCallbacks
    def test_client_read_none(self):
        workflow = WorkflowStateClient(
            self.client, self.states["unit_relation"])
        self.assertEqual(None, (yield workflow.get_state()))

    @inlineCallbacks
    def test_client_read_state(self):
        """The relation workflow client can read the state of a unit
        relation."""
        yield self.workflow.fire_transition("start")
        yield self.assertState(self.workflow, "up")

        self.write_hook("%s-relation-changed" % self.relation_name,
                        "#!/bin/bash\necho hello\n")
        wait_on_hook = self.wait_on_hook("app-relation-changed")
        yield self.add_opposite_service_unit(self.states)
        yield wait_on_hook

        workflow = WorkflowStateClient(
            self.client, self.states["unit_relation"])
        self.assertEqual("up", (yield workflow.get_state()))

    @inlineCallbacks
    def test_client_read_only(self):
        workflow_client = WorkflowStateClient(
            self.client, self.states["unit_relation"])
        yield self.assertFailure(
            workflow_client.set_state("up"),
            NotImplementedError)

    @inlineCallbacks
    def test_depart_hook_error(self):
        """A depart hook error, still results in a transition to the
        departed state with a state variable noting the error."""

        self.write_hook("%s-relation-changed" % self.relation_name,
                        "#!/bin/bash\necho hello\n")
        self.write_hook("%s-relation-broken" % self.relation_name,
                        "#!/bin/bash\nexit 1\n")

        error_output = self.capture_logging("unit.relation.workflow")

        results = []

        def collect_executions(*args):
            results.append(args)

        yield self.workflow.fire_transition("start")
        yield self.assertState(self.workflow, "up")

        wait_on_hook = self.wait_on_hook("app-relation-changed")
        states = yield self.add_opposite_service_unit(self.states)
        yield wait_on_hook

        wait_on_hook = self.wait_on_hook("app-relation-broken")
        wait_on_state = self.wait_on_state(self.workflow, "departed")
        yield self.workflow.fire_transition("depart")
        yield wait_on_hook
        yield wait_on_state

        # verify further changes to the related unit, don't result in
        # hook executions.
        self.executor.set_observer(collect_executions)
        yield states["unit_relation"].set_data(dict(a=1))
        # Sleep to give errors a chance.
        yield self.sleep(0.1)
        self.assertFalse(results)

        # Verify final state and log output.
        msg = "Depart hook error, ignoring: "
        error_msg = "Error processing "
        error_msg += repr(os.path.join(
            self.unit_directory, "charm", "hooks",
            "app-relation-broken"))
        error_msg += ": exit code 1."

        self.assertEqual(
            error_output.getvalue(), (msg + error_msg + "\n"))
        current_state = yield self.workflow.get_state()
        self.assertEqual(current_state, "departed")

        f_state, history, zk_state = yield self.read_persistent_state(
            history_id=self.workflow.zk_state_id)

        self.assertEqual(f_state, zk_state)
        self.assertEqual(f_state,
                         {"state": "departed",
                          "state_variables": {
                              "change_type": "depart",
                              "error_message": error_msg}})

    def test_depart_down(self):
        """When the workflow is transition to the down state, a relation
        broken hook is executed, and the unit stops responding to relation
        changes.
        """
        self.write_hook("%s-relation-changed" % self.relation_name,
                        "#!/bin/bash\necho hello\n")
        self.write_hook("%s-relation-broken" % self.relation_name,
                        "#!/bin/bash\necho hello\n")

        results = []

        def collect_executions(*args):
            results.append(args)

        yield self.workflow.fire_transition("start")
        yield self.assertState(self.workflow, "up")

        yield self.workflow.fire_transition("stop")
        yield self.assertState(self.workflow, "down")

        states = yield self.add_opposite_service_unit(self.states)

        wait_on_hook = self.wait_on_hook("app-relation-broken")
        wait_on_state = self.wait_on_state(self.workflow, "departed")

        yield self.workflow.fire_transition("depart")
        yield wait_on_hook
        yield wait_on_state

        # Verify further changes to the related unit, don't result in
        # hook executions.
        self.executor.set_observer(collect_executions)
        yield states["unit_relation"].set_data(dict(a=1))
        # Sleep to give errors a chance.
        yield self.sleep(0.1)
        self.assertFalse(results)