Esempio n. 1
0
 def setUp(self):
     yield super(UnitRelationLifecycleTest, self).setUp()
     yield self.setup_default_test_relation()
     self.relation_name = self.states["service_relation"].relation_name
     self.lifecycle = UnitRelationLifecycle(
         self.client, self.states["unit"].unit_name,
         self.states["unit_relation"],
         self.states["service_relation"].relation_name, self.unit_directory,
         self.executor)
     self.log_stream = self.capture_logging("unit.relation.lifecycle",
                                            logging.DEBUG)
Esempio n. 2
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)
Esempio n. 3
0
 def setUp(self):
     yield super(UnitRelationLifecycleTest, self).setUp()
     yield self.setup_default_test_relation()
     self.relation_name = self.states["service_relation"].relation_name
     self.lifecycle = UnitRelationLifecycle(
         self.client,
         self.states["unit"].unit_name,
         self.states["unit_relation"],
         self.states["service_relation"].relation_name,
         self.unit_directory,
         self.executor)
     self.log_stream = self.capture_logging("unit.relation.lifecycle",
                                            logging.DEBUG)
Esempio n. 4
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)
Esempio n. 5
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)
Esempio n. 6
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)
Esempio n. 7
0
class UnitRelationLifecycleTest(LifecycleTestBase):

    hook_template = (
            "#!/bin/bash\n"
            "echo %(change_type)s >> %(file_path)s\n"
            "echo JUJU_RELATION=$JUJU_RELATION >> %(file_path)s\n"
            "echo JUJU_REMOTE_UNIT=$JUJU_REMOTE_UNIT >> %(file_path)s")

    @inlineCallbacks
    def setUp(self):
        yield super(UnitRelationLifecycleTest, self).setUp()
        yield self.setup_default_test_relation()
        self.relation_name = self.states["service_relation"].relation_name
        self.lifecycle = UnitRelationLifecycle(
            self.client,
            self.states["unit"].unit_name,
            self.states["unit_relation"],
            self.states["service_relation"].relation_name,
            self.unit_directory,
            self.executor)
        self.log_stream = self.capture_logging("unit.relation.lifecycle",
                                               logging.DEBUG)

    @inlineCallbacks
    def test_initial_start_lifecycle_no_related_no_exec(self):
        """
        If there are no related units on startup, the relation joined hook
        is not invoked.
        """
        file_path = self.makeFile()
        self.write_hook(
            "%s-relation-changed" % self.relation_name,
            ("/bin/bash\n" "echo executed >> %s\n" % file_path))
        yield self.lifecycle.start()
        self.assertFalse(os.path.exists(file_path))

    @inlineCallbacks
    def test_stop_can_continue_watching(self):
        """
        """
        file_path = self.makeFile()
        self.write_hook(
            "%s-relation-changed" % self.relation_name,
            ("#!/bin/bash\n" "echo executed >> %s\n" % file_path))
        rel_states = yield self.add_opposite_service_unit(self.states)
        yield self.lifecycle.start()
        yield self.wait_on_hook(
            sequence=["app-relation-joined", "app-relation-changed"])
        changed_executed = self.wait_on_hook("app-relation-changed")
        yield self.lifecycle.stop(watches=False)
        rel_states["unit_relation"].set_data(yaml.dump(dict(hello="world")))
        # Sleep to give an error a chance.
        yield self.sleep(0.1)
        self.assertFalse(changed_executed.called)
        yield self.lifecycle.start(watches=False)
        yield changed_executed

    @inlineCallbacks
    def test_initial_start_lifecycle_with_related(self):
        """
        If there are related units on startup, the relation changed hook
        is invoked.
        """
        yield self.add_opposite_service_unit(self.states)

        file_path = self.makeFile()
        self.write_hook("%s-relation-joined" % self.relation_name,
                        self.hook_template % dict(change_type="joined",
                                                  file_path=file_path))
        self.write_hook("%s-relation-changed" % self.relation_name,
                        self.hook_template % dict(change_type="changed",
                                                  file_path=file_path))

        yield self.lifecycle.start()
        yield self.wait_on_hook(
            sequence=["app-relation-joined", "app-relation-changed"])
        self.assertTrue(os.path.exists(file_path))

        contents = open(file_path).read()
        self.assertEqual(contents,
                         ("joined\n"
                          "JUJU_RELATION=app\n"
                          "JUJU_REMOTE_UNIT=wordpress/0\n"
                          "changed\n"
                          "JUJU_RELATION=app\n"
                          "JUJU_REMOTE_UNIT=wordpress/0\n"))

    @inlineCallbacks
    def test_hooks_executed_during_lifecycle_start_stop_start(self):
        """If the unit relation lifecycle is stopped, hooks will no longer
        be executed."""
        file_path = self.makeFile()
        self.write_hook("%s-relation-joined" % self.relation_name,
                        self.hook_template % dict(change_type="joined",
                                                  file_path=file_path))
        self.write_hook("%s-relation-changed" % self.relation_name,
                        self.hook_template % dict(change_type="changed",
                                                  file_path=file_path))

        # starting is async
        yield self.lifecycle.start()

        # stopping is sync.
        self.lifecycle.stop()

        # Add a related unit.
        yield self.add_opposite_service_unit(self.states)

        # Give a chance for things to go bad
        yield self.sleep(0.1)

        self.assertFalse(os.path.exists(file_path))

        # Now start again
        yield self.lifecycle.start()

        # Verify we get our join event.
        yield self.wait_on_hook("app-relation-changed")

        self.assertTrue(os.path.exists(file_path))

    @inlineCallbacks
    def test_hook_error_handler(self):
        # use an error handler that completes async.
        self.write_hook("app-relation-joined", "#!/bin/bash\nexit 0\n")
        self.write_hook("app-relation-changed", "#!/bin/bash\nexit 1\n")

        results = []
        finish_callback = Deferred()

        @inlineCallbacks
        def error_handler(change, e):
            yield self.client.create(
                "/errors", str(e),
                flags=zookeeper.EPHEMERAL | zookeeper.SEQUENCE)
            results.append((change.change_type, e))
            yield self.lifecycle.stop()
            finish_callback.callback(True)

        self.lifecycle.set_hook_error_handler(error_handler)

        # Add a related unit.
        yield self.add_opposite_service_unit(self.states)

        yield self.lifecycle.start()
        yield finish_callback
        self.assertEqual(len(results), 1)
        self.assertTrue(results[0][0], "joined")
        self.assertTrue(isinstance(results[0][1], CharmInvocationError))

        hook_relative_path = "charm/hooks/app-relation-changed"
        output = (
            "started relation:app lifecycle",
            "Executing hook app-relation-joined",
            "Executing hook app-relation-changed",
            "Error in app-relation-changed hook: %s '%s/%s': exit code 1." % (
                "Error processing",
                self.unit_directory, hook_relative_path),
            "Invoked error handler for app-relation-changed hook",
            "stopped relation:app lifecycle\n")
        self.assertEqual(self.log_stream.getvalue(), "\n".join(output))

    @inlineCallbacks
    def test_depart(self):
        """If a relation is departed, the depart hook is executed.
        """
        file_path = self.makeFile()
        self.write_hook("%s-relation-joined" % self.relation_name,
                        "#!/bin/bash\n echo joined")
        self.write_hook("%s-relation-changed" % self.relation_name,
                        "#!/bin/bash\n echo hello")
        self.write_hook("%s-relation-broken" % self.relation_name,
                        self.hook_template % dict(change_type="broken",
                                                  file_path=file_path))

        yield self.lifecycle.start()
        wordpress_states = yield self.add_opposite_service_unit(self.states)
        yield self.wait_on_hook(
            sequence=["app-relation-joined", "app-relation-changed"])
        yield self.lifecycle.stop()
        yield self.relation_manager.remove_relation_state(
            wordpress_states["relation"])
        hook_complete = self.wait_on_hook("app-relation-broken")
        yield self.lifecycle.depart()
        yield hook_complete
        self.assertTrue(os.path.exists(file_path))

    @inlineCallbacks
    def test_lock_start_stop(self):
        """
        The relation lifecycle, internally uses a lock when its interacting
        with zk, and acquires the lock to protct its internal data structures.
        """

        original_method = ClientServerUnitWatcher.start
        watcher = self.mocker.patch(ClientServerUnitWatcher)

        finish_callback = Deferred()

        @inlineCallbacks
        def long_op(*args):
            yield finish_callback
            yield original_method(*args)

        watcher.start()
        self.mocker.call(long_op, with_object=True)
        self.mocker.replay()

        start_complete = self.lifecycle.start()
        stop_complete = self.lifecycle.stop()

        # Sadly this sleeping is the easiest way to verify that
        # the stop hasn't procesed prior to the start.
        yield self.sleep(0.1)
        self.assertFalse(start_complete.called)
        self.assertFalse(stop_complete.called)
        finish_callback.callback(True)

        yield start_complete
        self.assertTrue(stop_complete.called)
Esempio n. 8
0
class UnitRelationLifecycleTest(LifecycleTestBase):

    hook_template = (
        "#!/bin/bash\n"
        "echo %(change_type)s >> %(file_path)s\n"
        "echo JUJU_RELATION=$JUJU_RELATION >> %(file_path)s\n"
        "echo JUJU_REMOTE_UNIT=$JUJU_REMOTE_UNIT >> %(file_path)s")

    @inlineCallbacks
    def setUp(self):
        yield super(UnitRelationLifecycleTest, self).setUp()
        yield self.setup_default_test_relation()
        self.relation_name = self.states["service_relation"].relation_name
        self.lifecycle = UnitRelationLifecycle(
            self.client, self.states["unit"].unit_name,
            self.states["unit_relation"],
            self.states["service_relation"].relation_name, self.unit_directory,
            self.executor)
        self.log_stream = self.capture_logging("unit.relation.lifecycle",
                                               logging.DEBUG)

    @inlineCallbacks
    def test_initial_start_lifecycle_no_related_no_exec(self):
        """
        If there are no related units on startup, the relation joined hook
        is not invoked.
        """
        file_path = self.makeFile()
        self.write_hook("%s-relation-changed" % self.relation_name,
                        ("/bin/bash\n"
                         "echo executed >> %s\n" % file_path))
        yield self.lifecycle.start()
        self.assertFalse(os.path.exists(file_path))

    @inlineCallbacks
    def test_stop_can_continue_watching(self):
        """
        """
        file_path = self.makeFile()
        self.write_hook("%s-relation-changed" % self.relation_name,
                        ("#!/bin/bash\n"
                         "echo executed >> %s\n" % file_path))
        rel_states = yield self.add_opposite_service_unit(self.states)
        yield self.lifecycle.start()
        yield self.wait_on_hook(
            sequence=["app-relation-joined", "app-relation-changed"])
        changed_executed = self.wait_on_hook("app-relation-changed")
        yield self.lifecycle.stop(watches=False)
        rel_states["unit_relation"].set_data(yaml.dump(dict(hello="world")))
        # Sleep to give an error a chance.
        yield self.sleep(0.1)
        self.assertFalse(changed_executed.called)
        yield self.lifecycle.start(watches=False)
        yield changed_executed

    @inlineCallbacks
    def test_initial_start_lifecycle_with_related(self):
        """
        If there are related units on startup, the relation changed hook
        is invoked.
        """
        yield self.add_opposite_service_unit(self.states)

        file_path = self.makeFile()
        self.write_hook(
            "%s-relation-joined" % self.relation_name, self.hook_template %
            dict(change_type="joined", file_path=file_path))
        self.write_hook(
            "%s-relation-changed" % self.relation_name, self.hook_template %
            dict(change_type="changed", file_path=file_path))

        yield self.lifecycle.start()
        yield self.wait_on_hook(
            sequence=["app-relation-joined", "app-relation-changed"])
        self.assertTrue(os.path.exists(file_path))

        contents = open(file_path).read()
        self.assertEqual(contents, ("joined\n"
                                    "JUJU_RELATION=app\n"
                                    "JUJU_REMOTE_UNIT=wordpress/0\n"
                                    "changed\n"
                                    "JUJU_RELATION=app\n"
                                    "JUJU_REMOTE_UNIT=wordpress/0\n"))

    @inlineCallbacks
    def test_hooks_executed_during_lifecycle_start_stop_start(self):
        """If the unit relation lifecycle is stopped, hooks will no longer
        be executed."""
        file_path = self.makeFile()
        self.write_hook(
            "%s-relation-joined" % self.relation_name, self.hook_template %
            dict(change_type="joined", file_path=file_path))
        self.write_hook(
            "%s-relation-changed" % self.relation_name, self.hook_template %
            dict(change_type="changed", file_path=file_path))

        # starting is async
        yield self.lifecycle.start()

        # stopping is sync.
        self.lifecycle.stop()

        # Add a related unit.
        yield self.add_opposite_service_unit(self.states)

        # Give a chance for things to go bad
        yield self.sleep(0.1)

        self.assertFalse(os.path.exists(file_path))

        # Now start again
        yield self.lifecycle.start()

        # Verify we get our join event.
        yield self.wait_on_hook("app-relation-changed")

        self.assertTrue(os.path.exists(file_path))

    @inlineCallbacks
    def test_hook_error_handler(self):
        # use an error handler that completes async.
        self.write_hook("app-relation-joined", "#!/bin/bash\nexit 0\n")
        self.write_hook("app-relation-changed", "#!/bin/bash\nexit 1\n")

        results = []
        finish_callback = Deferred()

        @inlineCallbacks
        def error_handler(change, e):
            yield self.client.create("/errors",
                                     str(e),
                                     flags=zookeeper.EPHEMERAL
                                     | zookeeper.SEQUENCE)
            results.append((change.change_type, e))
            yield self.lifecycle.stop()
            finish_callback.callback(True)

        self.lifecycle.set_hook_error_handler(error_handler)

        # Add a related unit.
        yield self.add_opposite_service_unit(self.states)

        yield self.lifecycle.start()
        yield finish_callback
        self.assertEqual(len(results), 1)
        self.assertTrue(results[0][0], "joined")
        self.assertTrue(isinstance(results[0][1], CharmInvocationError))

        hook_relative_path = "charm/hooks/app-relation-changed"
        output = (
            "started relation:app lifecycle",
            "Executing hook app-relation-joined",
            "Executing hook app-relation-changed",
            "Error in app-relation-changed hook: %s '%s/%s': exit code 1." %
            ("Error processing", self.unit_directory, hook_relative_path),
            "Invoked error handler for app-relation-changed hook",
            "stopped relation:app lifecycle\n")
        self.assertEqual(self.log_stream.getvalue(), "\n".join(output))

    @inlineCallbacks
    def test_depart(self):
        """If a relation is departed, the depart hook is executed.
        """
        file_path = self.makeFile()
        self.write_hook("%s-relation-joined" % self.relation_name,
                        "#!/bin/bash\n echo joined")
        self.write_hook("%s-relation-changed" % self.relation_name,
                        "#!/bin/bash\n echo hello")
        self.write_hook(
            "%s-relation-broken" % self.relation_name, self.hook_template %
            dict(change_type="broken", file_path=file_path))

        yield self.lifecycle.start()
        wordpress_states = yield self.add_opposite_service_unit(self.states)
        yield self.wait_on_hook(
            sequence=["app-relation-joined", "app-relation-changed"])
        yield self.lifecycle.stop()
        yield self.relation_manager.remove_relation_state(
            wordpress_states["relation"])
        hook_complete = self.wait_on_hook("app-relation-broken")
        yield self.lifecycle.depart()
        yield hook_complete
        self.assertTrue(os.path.exists(file_path))

    @inlineCallbacks
    def test_lock_start_stop(self):
        """
        The relation lifecycle, internally uses a lock when its interacting
        with zk, and acquires the lock to protct its internal data structures.
        """

        original_method = ClientServerUnitWatcher.start
        watcher = self.mocker.patch(ClientServerUnitWatcher)

        finish_callback = Deferred()

        @inlineCallbacks
        def long_op(*args):
            yield finish_callback
            yield original_method(*args)

        watcher.start()
        self.mocker.call(long_op, with_object=True)
        self.mocker.replay()

        start_complete = self.lifecycle.start()
        stop_complete = self.lifecycle.stop()

        # Sadly this sleeping is the easiest way to verify that
        # the stop hasn't procesed prior to the start.
        yield self.sleep(0.1)
        self.assertFalse(start_complete.called)
        self.assertFalse(stop_complete.called)
        finish_callback.callback(True)

        yield start_complete
        self.assertTrue(stop_complete.called)