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)
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)
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)
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)
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()
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
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)
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)
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)
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)