class UnitWorkflowTestBase(WorkflowTestBase): @inlineCallbacks def setUp(self): yield super(UnitWorkflowTestBase, self).setUp() yield self.setup_default_test_relation() self.lifecycle = UnitLifecycle( self.client, self.states["unit"], self.states["service"], self.unit_directory, self.state_directory, self.executor) self.workflow = UnitWorkflowState( self.client, self.states["unit"], self.lifecycle, self.state_directory) self.write_exit_hook("install") self.write_exit_hook("start") self.write_exit_hook("stop") self.write_exit_hook("config-changed") self.write_exit_hook("upgrade-charm") @inlineCallbacks def assert_transition(self, transition, success=True): with (yield self.workflow.lock()): result = yield self.workflow.fire_transition(transition) self.assertEquals(result, success) @inlineCallbacks def assert_transition_alias(self, transition, success=True): with (yield self.workflow.lock()): result = yield self.workflow.fire_transition_alias(transition) self.assertEquals(result, success) @inlineCallbacks def assert_state(self, expected): actual = yield self.workflow.get_state() self.assertEquals(actual, expected) def assert_hooks(self, *hooks): with open(self.output) as f: lines = tuple(l.strip() for l in f) self.assertEquals(lines, hooks)
class UnitAgent(BaseAgent): """An juju Unit Agent. Provides for the management of a charm, via hook execution in response to external events in the coordination space (zookeeper). """ name = "juju-unit-agent" @classmethod def setup_options(cls, parser): super(UnitAgent, cls).setup_options(parser) unit_name = os.environ.get("JUJU_UNIT_NAME", "") parser.add_argument("--unit-name", default=unit_name) @property def unit_name(self): return self.config["unit_name"] def get_agent_name(self): return "unit:%s" % self.unit_name def configure(self, options): """Configure the unit agent.""" super(UnitAgent, self).configure(options) if not options.get("unit_name"): msg = ("--unit-name must be provided in the command line, " "or $JUJU_UNIT_NAME in the environment") raise JujuError(msg) self.executor = HookExecutor() self.api_factory = UnitSettingsFactory( self.executor.get_hook_context, logging.getLogger("unit.hook.api")) self.api_socket = None self.workflow = None @inlineCallbacks def start(self): """Start the unit agent process.""" self.service_state_manager = ServiceStateManager(self.client) self.executor.start() # Retrieve our unit and configure working directories. service_name = self.unit_name.split("/")[0] service_state = yield self.service_state_manager.get_service_state( service_name) self.unit_state = yield service_state.get_unit_state( self.unit_name) self.unit_directory = os.path.join( self.config["juju_directory"], "units", self.unit_state.unit_name.replace("/", "-")) self.state_directory = os.path.join( self.config["juju_directory"], "state") # Setup the server portion of the cli api exposed to hooks. from twisted.internet import reactor self.api_socket = reactor.listenUNIX( os.path.join(self.unit_directory, HOOK_SOCKET_FILE), self.api_factory) # Setup the unit state's address address = yield get_unit_address(self.client) yield self.unit_state.set_public_address( (yield address.get_public_address())) yield self.unit_state.set_private_address( (yield address.get_private_address())) # Inform the system, we're alive. yield self.unit_state.connect_agent() self.lifecycle = UnitLifecycle( self.client, self.unit_state, service_state, self.unit_directory, self.executor) self.workflow = UnitWorkflowState( self.client, self.unit_state, self.lifecycle, self.state_directory) if self.get_watch_enabled(): yield self.unit_state.watch_resolved(self.cb_watch_resolved) yield self.unit_state.watch_hook_debug(self.cb_watch_hook_debug) yield service_state.watch_config_state( self.cb_watch_config_changed) # Fire initial transitions, only if successful if (yield self.workflow.transition_state("installed")): yield self.workflow.transition_state("started") # Upgrade can only be processed if we're in a running state so # for case of a newly started unit, do it after the unit is started. if self.get_watch_enabled(): yield self.unit_state.watch_upgrade_flag( self.cb_watch_upgrade_flag) @inlineCallbacks def stop(self): """Stop the unit agent process.""" if self.workflow: yield self.workflow.transition_state("stopped") if self.api_socket: yield self.api_socket.stopListening() yield self.api_factory.stopFactory() @inlineCallbacks def cb_watch_resolved(self, change): """Update the unit's state, when its resolved. Resolved operations form the basis of error recovery for unit workflows. A resolved operation can optionally specify hook execution. The unit agent runs the error recovery transition if the unit is not in a running state. """ # Would be nice if we could fold this into an atomic # get and delete primitive. # Check resolved setting resolved = yield self.unit_state.get_resolved() if resolved is None: returnValue(None) # Clear out the setting yield self.unit_state.clear_resolved() # Verify its not already running if (yield self.workflow.get_state()) == "started": returnValue(None) log.info("Resolved detected, firing retry transition") # Fire a resolved transition try: if resolved["retry"] == RETRY_HOOKS: yield self.workflow.fire_transition_alias("retry_hook") else: yield self.workflow.fire_transition_alias("retry") except Exception: log.exception("Unknown error while transitioning for resolved") @inlineCallbacks def cb_watch_hook_debug(self, change): """Update the hooks to be debugged when the settings change. """ debug = yield self.unit_state.get_hook_debug() debug_hooks = debug and debug.get("debug_hooks") or None self.executor.set_debug(debug_hooks) @inlineCallbacks def cb_watch_upgrade_flag(self, change): """Update the unit's charm when requested. """ upgrade_flag = yield self.unit_state.get_upgrade_flag() if upgrade_flag: log.info("Upgrade detected, starting upgrade") upgrade = CharmUpgradeOperation(self) try: yield upgrade.run() except Exception: log.exception("Error while upgrading") @inlineCallbacks def cb_watch_config_changed(self, change): """Trigger hook on configuration change""" # Verify it is running current_state = yield self.workflow.get_state() log.debug("Configuration Changed") if current_state != "started": log.debug( "Configuration updated on service in a non-started state") returnValue(None) yield self.workflow.fire_transition("reconfigure")
class UnitWorkflowTest(WorkflowTestBase): @inlineCallbacks def setUp(self): yield super(UnitWorkflowTest, self).setUp() yield self.setup_default_test_relation() self.lifecycle = UnitLifecycle(self.client, self.states["unit"], self.states["service"], self.unit_directory, self.executor) self.juju_directory = self.makeDir() self.state_directory = self.makeDir( path=os.path.join(self.juju_directory, "state")) self.workflow = UnitWorkflowState(self.client, self.states["unit"], self.lifecycle, self.state_directory) @inlineCallbacks def test_install(self): file_path = self.makeFile() self.write_hook("install", "#!/bin/bash\necho installed >> %s\n" % file_path) result = yield self.workflow.fire_transition("install") self.assertTrue(result) current_state = yield self.workflow.get_state() self.assertEqual(current_state, "installed") f_state, history, zk_state = yield self.read_persistent_state() self.assertEqual(f_state, zk_state) self.assertEqual(f_state, { "state": "installed", "state_variables": {} }) self.assertEqual(history, [{ "state": "installed", "state_variables": {} }]) @inlineCallbacks def test_install_with_error_and_retry(self): """If the install hook fails, the workflow is transition to the install_error state. If the install is retried, a success transition will take us to the started state. """ self.write_hook("install", "#!/bin/bash\nexit 1") result = yield self.workflow.fire_transition("install") self.assertFalse(result) current_state = yield self.workflow.get_state() yield self.assertEqual(current_state, "install_error") result = yield self.workflow.fire_transition("retry_install") yield self.assertState(self.workflow, "started") @inlineCallbacks def test_install_error_with_retry_hook(self): """If the install hook fails, the workflow is transition to the install_error state. """ self.write_hook("install", "#!/bin/bash\nexit 1") result = yield self.workflow.fire_transition("install") self.assertFalse(result) current_state = yield self.workflow.get_state() yield self.assertEqual(current_state, "install_error") result = yield self.workflow.fire_transition("retry_install_hook") yield self.assertState(self.workflow, "install_error") self.write_hook("install", "#!/bin/bash\necho hello\n") hook_deferred = self.wait_on_hook("install") result = yield self.workflow.fire_transition_alias("retry_hook") yield hook_deferred yield self.assertState(self.workflow, "started") @inlineCallbacks def test_start(self): file_path = self.makeFile() self.write_hook("install", "#!/bin/bash\necho installed >> %s\n" % file_path) self.write_hook("start", "#!/bin/bash\necho start >> %s\n" % file_path) self.write_hook("stop", "#!/bin/bash\necho stop >> %s\n" % file_path) result = yield self.workflow.fire_transition("install") self.assertTrue(result) result = yield self.workflow.fire_transition("start") self.assertTrue(result) current_state = yield self.workflow.get_state() self.assertEqual(current_state, "started") f_state, history, zk_state = yield self.read_persistent_state() self.assertEqual(f_state, zk_state) self.assertEqual(f_state, {"state": "started", "state_variables": {}}) self.assertEqual(history, [{ "state": "installed", "state_variables": {} }, { "state": "started", "state_variables": {} }]) @inlineCallbacks def test_start_with_error(self): """Executing the start transition with a hook error, results in the workflow going to the start_error state. The start can be retried. """ self.write_hook("install", "#!/bin/bash\necho hello\n") result = yield self.workflow.fire_transition("install") self.assertTrue(result) self.write_hook("start", "#!/bin/bash\nexit 1") result = yield self.workflow.fire_transition("start") self.assertFalse(result) current_state = yield self.workflow.get_state() self.assertEqual(current_state, "start_error") result = yield self.workflow.fire_transition("retry_start") yield self.assertState(self.workflow, "started") @inlineCallbacks def test_start_error_with_retry_hook(self): """Executing the start transition with a hook error, results in the workflow going to the start_error state. The start can be retried. """ self.write_hook("install", "#!/bin/bash\necho hello\n") result = yield self.workflow.fire_transition("install") self.assertTrue(result) self.write_hook("start", "#!/bin/bash\nexit 1") result = yield self.workflow.fire_transition("start") self.assertFalse(result) current_state = yield self.workflow.get_state() self.assertEqual(current_state, "start_error") hook_deferred = self.wait_on_hook("start") result = yield self.workflow.fire_transition("retry_start_hook") yield hook_deferred yield self.assertState(self.workflow, "start_error") self.write_hook("start", "#!/bin/bash\nexit 0") hook_deferred = self.wait_on_hook("start") result = yield self.workflow.fire_transition_alias("retry_hook") yield hook_deferred yield self.assertState(self.workflow, "started") @inlineCallbacks def test_is_unit_running(self): running, state = yield is_unit_running(self.client, self.states["unit"]) self.assertIdentical(running, False) self.assertIdentical(state, None) yield self.workflow.fire_transition("install") yield self.workflow.fire_transition("start") running, state = yield is_unit_running(self.client, self.states["unit"]) self.assertIdentical(running, True) self.assertEqual(state, "started") @inlineCallbacks def test_configure(self): """Configuring a unit results in the config-changed hook being run. """ yield self.workflow.fire_transition("install") result = yield self.workflow.fire_transition("start") self.assertTrue(result) self.assertState(self.workflow, "started") hook_deferred = self.wait_on_hook("config-changed") file_path = self.makeFile() self.write_hook("config-changed", "#!/bin/bash\necho hello >> %s" % file_path) result = yield self.workflow.fire_transition("reconfigure") self.assertTrue(result) yield hook_deferred yield self.assertState(self.workflow, "started") self.assertEqual(open(file_path).read().strip(), "hello") @inlineCallbacks def test_configure_error_and_retry(self): """An error while configuring, transitions the unit and stops the lifecycle.""" yield self.workflow.fire_transition("install") result = yield self.workflow.fire_transition("start") self.assertTrue(result) self.assertState(self.workflow, "started") # Verify transition to error state hook_deferred = self.wait_on_hook("config-changed") self.write_hook("config-changed", "#!/bin/bash\nexit 1") result = yield self.workflow.fire_transition("reconfigure") yield hook_deferred self.assertFalse(result) yield self.assertState(self.workflow, "configure_error") # Verify recovery from error state result = yield self.workflow.fire_transition_alias("retry") self.assertTrue(result) yield self.assertState(self.workflow, "started") @inlineCallbacks def test_configure_error_and_retry_hook(self): """An error while configuring, transitions the unit and stops the lifecycle.""" #self.capture_output() yield self.workflow.fire_transition("install") result = yield self.workflow.fire_transition("start") self.assertTrue(result) self.assertState(self.workflow, "started") # Verify transition to error state hook_deferred = self.wait_on_hook("config-changed") self.write_hook("config-changed", "#!/bin/bash\nexit 1") result = yield self.workflow.fire_transition("reconfigure") yield hook_deferred self.assertFalse(result) yield self.assertState(self.workflow, "configure_error") # Verify retry hook with hook error stays in error state hook_deferred = self.wait_on_hook("config-changed") result = yield self.workflow.fire_transition("retry_configure_hook") self.assertFalse(result) yield hook_deferred yield self.assertState(self.workflow, "configure_error") hook_deferred = self.wait_on_hook("config-changed") self.write_hook("config-changed", "#!/bin/bash\nexit 0") result = yield self.workflow.fire_transition_alias("retry_hook") yield hook_deferred yield self.assertState(self.workflow, "started") @inlineCallbacks def test_upgrade(self): """Upgrading a workflow results in the upgrade hook being executed. """ self.makeFile() yield self.workflow.fire_transition("install") yield self.workflow.fire_transition("start") current_state = yield self.workflow.get_state() self.assertEqual(current_state, "started") file_path = self.makeFile() self.write_hook("upgrade-charm", ("#!/bin/bash\n" "echo upgraded >> %s") % file_path) self.executor.stop() yield self.workflow.fire_transition("upgrade_charm") current_state = yield self.workflow.get_state() self.assertEqual(current_state, "started") @inlineCallbacks def test_upgrade_without_stopping_hooks_errors(self): """Attempting to execute an upgrade without stopping the executor is an error. """ yield self.workflow.fire_transition("install") yield self.workflow.fire_transition("start") current_state = yield self.workflow.get_state() self.assertEqual(current_state, "started") yield self.assertFailure( self.workflow.fire_transition("upgrade_charm"), AssertionError) @inlineCallbacks def test_upgrade_error_retry(self): """A hook error during an upgrade transitions to upgrade_error. """ self.write_hook("upgrade-charm", "#!/bin/bash\nexit 1") yield self.workflow.fire_transition("install") yield self.workflow.fire_transition("start") current_state = yield self.workflow.get_state() self.assertEqual(current_state, "started") self.executor.stop() yield self.workflow.fire_transition("upgrade_charm") current_state = yield self.workflow.get_state() self.assertEqual(current_state, "charm_upgrade_error") file_path = self.makeFile() self.write_hook("upgrade-charm", ("#!/bin/bash\n" "echo upgraded >> %s") % file_path) # The upgrade error hook should ensure that the executor is stoppped. self.assertFalse(self.executor.running) yield self.workflow.fire_transition("retry_upgrade_charm") current_state = yield self.workflow.get_state() self.assertEqual(current_state, "started") @inlineCallbacks def test_upgrade_error_retry_hook(self): """A hook error during an upgrade transitions to upgrade_error, and can be re-tried with hook execution. """ yield self.workflow.fire_transition("install") yield self.workflow.fire_transition("start") current_state = yield self.workflow.get_state() self.assertEqual(current_state, "started") # Agent prepares this. self.executor.stop() self.write_hook("upgrade-charm", "#!/bin/bash\nexit 1") hook_deferred = self.wait_on_hook("upgrade-charm") yield self.workflow.fire_transition("upgrade_charm") yield hook_deferred current_state = yield self.workflow.get_state() self.assertEqual(current_state, "charm_upgrade_error") hook_deferred = self.wait_on_hook("upgrade-charm") self.write_hook("upgrade-charm", "#!/bin/bash\nexit 0") # The upgrade error hook should ensure that the executor is stoppped. self.assertFalse(self.executor.running) yield self.workflow.fire_transition_alias("retry_hook") yield hook_deferred current_state = yield self.workflow.get_state() self.assertEqual(current_state, "started") self.assertTrue(self.executor.running) @inlineCallbacks def test_stop(self): """Executing the stop transition, results in the workflow going to the down state. """ file_path = self.makeFile() self.write_hook("install", "#!/bin/bash\necho installed >> %s\n" % file_path) self.write_hook("start", "#!/bin/bash\necho start >> %s\n" % file_path) self.write_hook("stop", "#!/bin/bash\necho stop >> %s\n" % file_path) result = yield self.workflow.fire_transition("install") result = yield self.workflow.fire_transition("start") result = yield self.workflow.fire_transition("stop") self.assertTrue(result) current_state = yield self.workflow.get_state() self.assertEqual(current_state, "stopped") f_state, history, zk_state = yield self.read_persistent_state() self.assertEqual(f_state, zk_state) self.assertEqual(f_state, {"state": "stopped", "state_variables": {}}) workflow_client = WorkflowStateClient(self.client, self.states["unit"]) value = yield workflow_client.get_state() self.assertEqual(value, "stopped") self.assertEqual(history, [{ "state": "installed", "state_variables": {} }, { "state": "started", "state_variables": {} }, { "state": "stopped", "state_variables": {} }]) @inlineCallbacks def test_stop_with_error(self): self.write_hook("install", "#!/bin/bash\necho hello\n") self.write_hook("start", "#!/bin/bash\necho hello\n") result = yield self.workflow.fire_transition("install") self.assertTrue(result) result = yield self.workflow.fire_transition("start") self.assertTrue(result) self.write_hook("stop", "#!/bin/bash\nexit 1") result = yield self.workflow.fire_transition("stop") self.assertFalse(result) yield self.assertState(self.workflow, "stop_error") self.write_hook("stop", "#!/bin/bash\necho hello\n") result = yield self.workflow.fire_transition("retry_stop") yield self.assertState(self.workflow, "stopped") @inlineCallbacks def test_stop_error_with_retry_hook(self): self.write_hook("install", "#!/bin/bash\necho hello\n") self.write_hook("start", "#!/bin/bash\necho hello\n") result = yield self.workflow.fire_transition("install") self.assertTrue(result) result = yield self.workflow.fire_transition("start") self.assertTrue(result) self.write_hook("stop", "#!/bin/bash\nexit 1") result = yield self.workflow.fire_transition("stop") self.assertFalse(result) yield self.assertState(self.workflow, "stop_error") result = yield self.workflow.fire_transition_alias("retry_hook") yield self.assertState(self.workflow, "stop_error") self.write_hook("stop", "#!/bin/bash\nexit 0") result = yield self.workflow.fire_transition_alias("retry_hook") yield self.assertState(self.workflow, "stopped") @inlineCallbacks def test_client_with_no_state(self): workflow_client = WorkflowStateClient(self.client, self.states["unit"]) state = yield workflow_client.get_state() self.assertEqual(state, None) @inlineCallbacks def test_client_with_state(self): yield self.workflow.fire_transition("install") workflow_client = WorkflowStateClient(self.client, self.states["unit"]) self.assertEqual((yield workflow_client.get_state()), "installed") @inlineCallbacks def test_client_readonly(self): yield self.workflow.fire_transition("install") workflow_client = WorkflowStateClient(self.client, self.states["unit"]) self.assertEqual((yield workflow_client.get_state()), "installed") yield self.assertFailure(workflow_client.set_state("started"), NotImplementedError) self.assertEqual((yield workflow_client.get_state()), "installed")
class UnitWorkflowTest(WorkflowTestBase): @inlineCallbacks def setUp(self): yield super(UnitWorkflowTest, self).setUp() yield self.setup_default_test_relation() self.lifecycle = UnitLifecycle( self.client, self.states["unit"], self.states["service"], self.unit_directory, self.executor) self.juju_directory = self.makeDir() self.state_directory = self.makeDir( path=os.path.join(self.juju_directory, "state")) self.workflow = UnitWorkflowState( self.client, self.states["unit"], self.lifecycle, self.state_directory) @inlineCallbacks def test_install(self): file_path = self.makeFile() self.write_hook( "install", "#!/bin/bash\necho installed >> %s\n" % file_path) result = yield self.workflow.fire_transition("install") self.assertTrue(result) current_state = yield self.workflow.get_state() self.assertEqual(current_state, "installed") f_state, history, zk_state = yield self.read_persistent_state() self.assertEqual(f_state, zk_state) self.assertEqual(f_state, {"state": "installed", "state_variables": {}}) self.assertEqual(history, [{"state": "installed", "state_variables": {}}]) @inlineCallbacks def test_install_with_error_and_retry(self): """If the install hook fails, the workflow is transition to the install_error state. If the install is retried, a success transition will take us to the started state. """ self.write_hook("install", "#!/bin/bash\nexit 1") result = yield self.workflow.fire_transition("install") self.assertFalse(result) current_state = yield self.workflow.get_state() yield self.assertEqual(current_state, "install_error") result = yield self.workflow.fire_transition("retry_install") yield self.assertState(self.workflow, "started") @inlineCallbacks def test_install_error_with_retry_hook(self): """If the install hook fails, the workflow is transition to the install_error state. """ self.write_hook("install", "#!/bin/bash\nexit 1") result = yield self.workflow.fire_transition("install") self.assertFalse(result) current_state = yield self.workflow.get_state() yield self.assertEqual(current_state, "install_error") result = yield self.workflow.fire_transition("retry_install_hook") yield self.assertState(self.workflow, "install_error") self.write_hook("install", "#!/bin/bash\necho hello\n") hook_deferred = self.wait_on_hook("install") result = yield self.workflow.fire_transition_alias("retry_hook") yield hook_deferred yield self.assertState(self.workflow, "started") @inlineCallbacks def test_start(self): file_path = self.makeFile() self.write_hook( "install", "#!/bin/bash\necho installed >> %s\n" % file_path) self.write_hook( "start", "#!/bin/bash\necho start >> %s\n" % file_path) self.write_hook( "stop", "#!/bin/bash\necho stop >> %s\n" % file_path) result = yield self.workflow.fire_transition("install") self.assertTrue(result) result = yield self.workflow.fire_transition("start") self.assertTrue(result) current_state = yield self.workflow.get_state() self.assertEqual(current_state, "started") f_state, history, zk_state = yield self.read_persistent_state() self.assertEqual(f_state, zk_state) self.assertEqual(f_state, {"state": "started", "state_variables": {}}) self.assertEqual(history, [{"state": "installed", "state_variables": {}}, {"state": "started", "state_variables": {}}]) @inlineCallbacks def test_start_with_error(self): """Executing the start transition with a hook error, results in the workflow going to the start_error state. The start can be retried. """ self.write_hook("install", "#!/bin/bash\necho hello\n") result = yield self.workflow.fire_transition("install") self.assertTrue(result) self.write_hook("start", "#!/bin/bash\nexit 1") result = yield self.workflow.fire_transition("start") self.assertFalse(result) current_state = yield self.workflow.get_state() self.assertEqual(current_state, "start_error") result = yield self.workflow.fire_transition("retry_start") yield self.assertState(self.workflow, "started") @inlineCallbacks def test_start_error_with_retry_hook(self): """Executing the start transition with a hook error, results in the workflow going to the start_error state. The start can be retried. """ self.write_hook("install", "#!/bin/bash\necho hello\n") result = yield self.workflow.fire_transition("install") self.assertTrue(result) self.write_hook("start", "#!/bin/bash\nexit 1") result = yield self.workflow.fire_transition("start") self.assertFalse(result) current_state = yield self.workflow.get_state() self.assertEqual(current_state, "start_error") hook_deferred = self.wait_on_hook("start") result = yield self.workflow.fire_transition("retry_start_hook") yield hook_deferred yield self.assertState(self.workflow, "start_error") self.write_hook("start", "#!/bin/bash\nexit 0") hook_deferred = self.wait_on_hook("start") result = yield self.workflow.fire_transition_alias("retry_hook") yield hook_deferred yield self.assertState(self.workflow, "started") @inlineCallbacks def test_is_unit_running(self): running, state = yield is_unit_running( self.client, self.states["unit"]) self.assertIdentical(running, False) self.assertIdentical(state, None) yield self.workflow.fire_transition("install") yield self.workflow.fire_transition("start") running, state = yield is_unit_running( self.client, self.states["unit"]) self.assertIdentical(running, True) self.assertEqual(state, "started") @inlineCallbacks def test_configure(self): """Configuring a unit results in the config-changed hook being run. """ yield self.workflow.fire_transition("install") result = yield self.workflow.fire_transition("start") self.assertTrue(result) self.assertState(self.workflow, "started") hook_deferred = self.wait_on_hook("config-changed") file_path = self.makeFile() self.write_hook("config-changed", "#!/bin/bash\necho hello >> %s" % file_path) result = yield self.workflow.fire_transition("reconfigure") self.assertTrue(result) yield hook_deferred yield self.assertState(self.workflow, "started") self.assertEqual(open(file_path).read().strip(), "hello") @inlineCallbacks def test_configure_error_and_retry(self): """An error while configuring, transitions the unit and stops the lifecycle.""" yield self.workflow.fire_transition("install") result = yield self.workflow.fire_transition("start") self.assertTrue(result) self.assertState(self.workflow, "started") # Verify transition to error state hook_deferred = self.wait_on_hook("config-changed") self.write_hook("config-changed", "#!/bin/bash\nexit 1") result = yield self.workflow.fire_transition("reconfigure") yield hook_deferred self.assertFalse(result) yield self.assertState(self.workflow, "configure_error") # Verify recovery from error state result = yield self.workflow.fire_transition_alias("retry") self.assertTrue(result) yield self.assertState(self.workflow, "started") @inlineCallbacks def test_configure_error_and_retry_hook(self): """An error while configuring, transitions the unit and stops the lifecycle.""" #self.capture_output() yield self.workflow.fire_transition("install") result = yield self.workflow.fire_transition("start") self.assertTrue(result) self.assertState(self.workflow, "started") # Verify transition to error state hook_deferred = self.wait_on_hook("config-changed") self.write_hook("config-changed", "#!/bin/bash\nexit 1") result = yield self.workflow.fire_transition("reconfigure") yield hook_deferred self.assertFalse(result) yield self.assertState(self.workflow, "configure_error") # Verify retry hook with hook error stays in error state hook_deferred = self.wait_on_hook("config-changed") result = yield self.workflow.fire_transition("retry_configure_hook") self.assertFalse(result) yield hook_deferred yield self.assertState(self.workflow, "configure_error") hook_deferred = self.wait_on_hook("config-changed") self.write_hook("config-changed", "#!/bin/bash\nexit 0") result = yield self.workflow.fire_transition_alias("retry_hook") yield hook_deferred yield self.assertState(self.workflow, "started") @inlineCallbacks def test_upgrade(self): """Upgrading a workflow results in the upgrade hook being executed. """ self.makeFile() yield self.workflow.fire_transition("install") yield self.workflow.fire_transition("start") current_state = yield self.workflow.get_state() self.assertEqual(current_state, "started") file_path = self.makeFile() self.write_hook("upgrade-charm", ("#!/bin/bash\n" "echo upgraded >> %s") % file_path) self.executor.stop() yield self.workflow.fire_transition("upgrade_charm") current_state = yield self.workflow.get_state() self.assertEqual(current_state, "started") @inlineCallbacks def test_upgrade_without_stopping_hooks_errors(self): """Attempting to execute an upgrade without stopping the executor is an error. """ yield self.workflow.fire_transition("install") yield self.workflow.fire_transition("start") current_state = yield self.workflow.get_state() self.assertEqual(current_state, "started") yield self.assertFailure( self.workflow.fire_transition("upgrade_charm"), AssertionError) @inlineCallbacks def test_upgrade_error_retry(self): """A hook error during an upgrade transitions to upgrade_error. """ self.write_hook("upgrade-charm", "#!/bin/bash\nexit 1") yield self.workflow.fire_transition("install") yield self.workflow.fire_transition("start") current_state = yield self.workflow.get_state() self.assertEqual(current_state, "started") self.executor.stop() yield self.workflow.fire_transition("upgrade_charm") current_state = yield self.workflow.get_state() self.assertEqual(current_state, "charm_upgrade_error") file_path = self.makeFile() self.write_hook("upgrade-charm", ("#!/bin/bash\n" "echo upgraded >> %s") % file_path) # The upgrade error hook should ensure that the executor is stoppped. self.assertFalse(self.executor.running) yield self.workflow.fire_transition("retry_upgrade_charm") current_state = yield self.workflow.get_state() self.assertEqual(current_state, "started") @inlineCallbacks def test_upgrade_error_retry_hook(self): """A hook error during an upgrade transitions to upgrade_error, and can be re-tried with hook execution. """ yield self.workflow.fire_transition("install") yield self.workflow.fire_transition("start") current_state = yield self.workflow.get_state() self.assertEqual(current_state, "started") # Agent prepares this. self.executor.stop() self.write_hook("upgrade-charm", "#!/bin/bash\nexit 1") hook_deferred = self.wait_on_hook("upgrade-charm") yield self.workflow.fire_transition("upgrade_charm") yield hook_deferred current_state = yield self.workflow.get_state() self.assertEqual(current_state, "charm_upgrade_error") hook_deferred = self.wait_on_hook("upgrade-charm") self.write_hook("upgrade-charm", "#!/bin/bash\nexit 0") # The upgrade error hook should ensure that the executor is stoppped. self.assertFalse(self.executor.running) yield self.workflow.fire_transition_alias("retry_hook") yield hook_deferred current_state = yield self.workflow.get_state() self.assertEqual(current_state, "started") self.assertTrue(self.executor.running) @inlineCallbacks def test_stop(self): """Executing the stop transition, results in the workflow going to the down state. """ file_path = self.makeFile() self.write_hook( "install", "#!/bin/bash\necho installed >> %s\n" % file_path) self.write_hook( "start", "#!/bin/bash\necho start >> %s\n" % file_path) self.write_hook( "stop", "#!/bin/bash\necho stop >> %s\n" % file_path) result = yield self.workflow.fire_transition("install") result = yield self.workflow.fire_transition("start") result = yield self.workflow.fire_transition("stop") self.assertTrue(result) current_state = yield self.workflow.get_state() self.assertEqual(current_state, "stopped") f_state, history, zk_state = yield self.read_persistent_state() self.assertEqual(f_state, zk_state) self.assertEqual(f_state, {"state": "stopped", "state_variables": {}}) workflow_client = WorkflowStateClient(self.client, self.states["unit"]) value = yield workflow_client.get_state() self.assertEqual(value, "stopped") self.assertEqual(history, [{"state": "installed", "state_variables": {}}, {"state": "started", "state_variables": {}}, {"state": "stopped", "state_variables": {}}]) @inlineCallbacks def test_stop_with_error(self): self.write_hook("install", "#!/bin/bash\necho hello\n") self.write_hook("start", "#!/bin/bash\necho hello\n") result = yield self.workflow.fire_transition("install") self.assertTrue(result) result = yield self.workflow.fire_transition("start") self.assertTrue(result) self.write_hook("stop", "#!/bin/bash\nexit 1") result = yield self.workflow.fire_transition("stop") self.assertFalse(result) yield self.assertState(self.workflow, "stop_error") self.write_hook("stop", "#!/bin/bash\necho hello\n") result = yield self.workflow.fire_transition("retry_stop") yield self.assertState(self.workflow, "stopped") @inlineCallbacks def test_stop_error_with_retry_hook(self): self.write_hook("install", "#!/bin/bash\necho hello\n") self.write_hook("start", "#!/bin/bash\necho hello\n") result = yield self.workflow.fire_transition("install") self.assertTrue(result) result = yield self.workflow.fire_transition("start") self.assertTrue(result) self.write_hook("stop", "#!/bin/bash\nexit 1") result = yield self.workflow.fire_transition("stop") self.assertFalse(result) yield self.assertState(self.workflow, "stop_error") result = yield self.workflow.fire_transition_alias("retry_hook") yield self.assertState(self.workflow, "stop_error") self.write_hook("stop", "#!/bin/bash\nexit 0") result = yield self.workflow.fire_transition_alias("retry_hook") yield self.assertState(self.workflow, "stopped") @inlineCallbacks def test_client_with_no_state(self): workflow_client = WorkflowStateClient(self.client, self.states["unit"]) state = yield workflow_client.get_state() self.assertEqual(state, None) @inlineCallbacks def test_client_with_state(self): yield self.workflow.fire_transition("install") workflow_client = WorkflowStateClient(self.client, self.states["unit"]) self.assertEqual( (yield workflow_client.get_state()), "installed") @inlineCallbacks def test_client_readonly(self): yield self.workflow.fire_transition("install") workflow_client = WorkflowStateClient( self.client, self.states["unit"]) self.assertEqual( (yield workflow_client.get_state()), "installed") yield self.assertFailure( workflow_client.set_state("started"), NotImplementedError) self.assertEqual( (yield workflow_client.get_state()), "installed")
class UnitAgent(BaseAgent): """An juju Unit Agent. Provides for the management of a charm, via hook execution in response to external events in the coordination space (zookeeper). """ name = "juju-unit-agent" @classmethod def setup_options(cls, parser): super(UnitAgent, cls).setup_options(parser) unit_name = os.environ.get("JUJU_UNIT_NAME", "") parser.add_argument("--unit-name", default=unit_name) @property def unit_name(self): return self.config["unit_name"] def get_agent_name(self): return "unit:%s" % self.unit_name def configure(self, options): """Configure the unit agent.""" super(UnitAgent, self).configure(options) if not options.get("unit_name"): msg = ("--unit-name must be provided in the command line, " "or $JUJU_UNIT_NAME in the environment") raise JujuError(msg) self.executor = HookExecutor() self.api_factory = UnitSettingsFactory( self.executor.get_hook_context, logging.getLogger("unit.hook.api")) self.api_socket = None self.workflow = None @inlineCallbacks def start(self): """Start the unit agent process.""" self.service_state_manager = ServiceStateManager(self.client) self.executor.start() # Retrieve our unit and configure working directories. service_name = self.unit_name.split("/")[0] service_state = yield self.service_state_manager.get_service_state( service_name) self.unit_state = yield service_state.get_unit_state(self.unit_name) self.unit_directory = os.path.join( self.config["juju_directory"], "units", self.unit_state.unit_name.replace("/", "-")) self.state_directory = os.path.join(self.config["juju_directory"], "state") # Setup the server portion of the cli api exposed to hooks. from twisted.internet import reactor self.api_socket = reactor.listenUNIX( os.path.join(self.unit_directory, HOOK_SOCKET_FILE), self.api_factory) # Setup the unit state's address address = yield get_unit_address(self.client) yield self.unit_state.set_public_address( (yield address.get_public_address())) yield self.unit_state.set_private_address( (yield address.get_private_address())) # Inform the system, we're alive. yield self.unit_state.connect_agent() self.lifecycle = UnitLifecycle(self.client, self.unit_state, service_state, self.unit_directory, self.executor) self.workflow = UnitWorkflowState(self.client, self.unit_state, self.lifecycle, self.state_directory) if self.get_watch_enabled(): yield self.unit_state.watch_resolved(self.cb_watch_resolved) yield self.unit_state.watch_hook_debug(self.cb_watch_hook_debug) yield service_state.watch_config_state( self.cb_watch_config_changed) # Fire initial transitions, only if successful if (yield self.workflow.transition_state("installed")): yield self.workflow.transition_state("started") # Upgrade can only be processed if we're in a running state so # for case of a newly started unit, do it after the unit is started. if self.get_watch_enabled(): yield self.unit_state.watch_upgrade_flag( self.cb_watch_upgrade_flag) @inlineCallbacks def stop(self): """Stop the unit agent process.""" if self.workflow: yield self.workflow.transition_state("stopped") if self.api_socket: yield self.api_socket.stopListening() yield self.api_factory.stopFactory() @inlineCallbacks def cb_watch_resolved(self, change): """Update the unit's state, when its resolved. Resolved operations form the basis of error recovery for unit workflows. A resolved operation can optionally specify hook execution. The unit agent runs the error recovery transition if the unit is not in a running state. """ # Would be nice if we could fold this into an atomic # get and delete primitive. # Check resolved setting resolved = yield self.unit_state.get_resolved() if resolved is None: returnValue(None) # Clear out the setting yield self.unit_state.clear_resolved() # Verify its not already running if (yield self.workflow.get_state()) == "started": returnValue(None) log.info("Resolved detected, firing retry transition") # Fire a resolved transition try: if resolved["retry"] == RETRY_HOOKS: yield self.workflow.fire_transition_alias("retry_hook") else: yield self.workflow.fire_transition_alias("retry") except Exception: log.exception("Unknown error while transitioning for resolved") @inlineCallbacks def cb_watch_hook_debug(self, change): """Update the hooks to be debugged when the settings change. """ debug = yield self.unit_state.get_hook_debug() debug_hooks = debug and debug.get("debug_hooks") or None self.executor.set_debug(debug_hooks) @inlineCallbacks def cb_watch_upgrade_flag(self, change): """Update the unit's charm when requested. """ upgrade_flag = yield self.unit_state.get_upgrade_flag() if upgrade_flag: log.info("Upgrade detected, starting upgrade") upgrade = CharmUpgradeOperation(self) try: yield upgrade.run() except Exception: log.exception("Error while upgrading") @inlineCallbacks def cb_watch_config_changed(self, change): """Trigger hook on configuration change""" # Verify it is running current_state = yield self.workflow.get_state() log.debug("Configuration Changed") if current_state != "started": log.debug( "Configuration updated on service in a non-started state") returnValue(None) yield self.workflow.fire_transition("reconfigure")
class UnitAgent(BaseAgent): """An juju Unit Agent. Provides for the management of a charm, via hook execution in response to external events in the coordination space (zookeeper). """ name = "juju-unit-agent" @classmethod def setup_options(cls, parser): super(UnitAgent, cls).setup_options(parser) unit_name = os.environ.get("JUJU_UNIT_NAME", "") parser.add_argument("--unit-name", default=unit_name) @property def unit_name(self): return self.config["unit_name"] def get_agent_name(self): return "unit:%s" % self.unit_name def configure(self, options): """Configure the unit agent.""" super(UnitAgent, self).configure(options) if not options.get("unit_name"): msg = ("--unit-name must be provided in the command line, " "or $JUJU_UNIT_NAME in the environment") raise JujuError(msg) self.executor = HookExecutor() self.api_factory = UnitSettingsFactory( self.executor.get_hook_context, self.executor.get_invoker, logging.getLogger("unit.hook.api")) self.api_socket = None self.workflow = None @inlineCallbacks def start(self): """Start the unit agent process.""" service_state_manager = ServiceStateManager(self.client) # Retrieve our unit and configure working directories. service_name = self.unit_name.split("/")[0] self.service_state = yield service_state_manager.get_service_state( service_name) self.unit_state = yield self.service_state.get_unit_state( self.unit_name) self.unit_directory = os.path.join( self.config["juju_directory"], "units", self.unit_state.unit_name.replace("/", "-")) self.state_directory = os.path.join( self.config["juju_directory"], "state") # Setup the server portion of the cli api exposed to hooks. socket_path = os.path.join(self.unit_directory, HOOK_SOCKET_FILE) if os.path.exists(socket_path): os.unlink(socket_path) from twisted.internet import reactor self.api_socket = reactor.listenUNIX(socket_path, self.api_factory) # Setup the unit state's address address = yield get_unit_address(self.client) yield self.unit_state.set_public_address( (yield address.get_public_address())) yield self.unit_state.set_private_address( (yield address.get_private_address())) if self.get_watch_enabled(): yield self.unit_state.watch_hook_debug(self.cb_watch_hook_debug) # Inform the system, we're alive. yield self.unit_state.connect_agent() # Start paying attention to the debug-log setting if self.get_watch_enabled(): yield self.unit_state.watch_hook_debug(self.cb_watch_hook_debug) self.lifecycle = UnitLifecycle( self.client, self.unit_state, self.service_state, self.unit_directory, self.state_directory, self.executor) self.workflow = UnitWorkflowState( self.client, self.unit_state, self.lifecycle, self.state_directory) # Set up correct lifecycle and executor state given the persistent # unit workflow state, and fire any starting transitions if necessary. with (yield self.workflow.lock()): yield self.workflow.synchronize(self.executor) if self.get_watch_enabled(): yield self.unit_state.watch_resolved(self.cb_watch_resolved) yield self.service_state.watch_config_state( self.cb_watch_config_changed) yield self.unit_state.watch_upgrade_flag( self.cb_watch_upgrade_flag) @inlineCallbacks def stop(self): """Stop the unit agent process.""" if self.lifecycle.running: yield self.lifecycle.stop(fire_hooks=False, stop_relations=False) yield self.executor.stop() if self.api_socket: yield self.api_socket.stopListening() yield self.api_factory.stopFactory() @inlineCallbacks def cb_watch_resolved(self, change): """Update the unit's state, when its resolved. Resolved operations form the basis of error recovery for unit workflows. A resolved operation can optionally specify hook execution. The unit agent runs the error recovery transition if the unit is not in a running state. """ # Would be nice if we could fold this into an atomic # get and delete primitive. # Check resolved setting resolved = yield self.unit_state.get_resolved() if resolved is None: returnValue(None) # Clear out the setting yield self.unit_state.clear_resolved() with (yield self.workflow.lock()): if (yield self.workflow.get_state()) == "started": returnValue(None) try: log.info("Resolved detected, firing retry transition") if resolved["retry"] == RETRY_HOOKS: yield self.workflow.fire_transition_alias("retry_hook") else: yield self.workflow.fire_transition_alias("retry") except Exception: log.exception("Unknown error while transitioning for resolved") @inlineCallbacks def cb_watch_hook_debug(self, change): """Update the hooks to be debugged when the settings change. """ debug = yield self.unit_state.get_hook_debug() debug_hooks = debug and debug.get("debug_hooks") or None self.executor.set_debug(debug_hooks) @inlineCallbacks def cb_watch_upgrade_flag(self, change): """Update the unit's charm when requested. """ upgrade_flag = yield self.unit_state.get_upgrade_flag() if not upgrade_flag: log.info("No upgrade flag set.") return log.info("Upgrade detected") # Clear the flag immediately; this means that upgrade requests will # be *ignored* by units which are not "started", and will need to be # reissued when the units are in acceptable states. yield self.unit_state.clear_upgrade_flag() new_id = yield self.service_state.get_charm_id() old_id = yield self.unit_state.get_charm_id() if new_id == old_id: log.info("Upgrade ignored: already running latest charm") return with (yield self.workflow.lock()): state = yield self.workflow.get_state() if state != "started": if upgrade_flag["force"]: yield self.lifecycle.upgrade_charm( fire_hooks=False, force=True) log.info("Forced upgrade complete") return log.warning( "Cannot upgrade: unit is in non-started state %s. Reissue " "upgrade command to try again.", state) return log.info("Starting upgrade") if (yield self.workflow.fire_transition("upgrade_charm")): log.info("Upgrade complete") else: log.info("Upgrade failed") @inlineCallbacks def cb_watch_config_changed(self, change): """Trigger hook on configuration change""" # Verify it is running with (yield self.workflow.lock()): current_state = yield self.workflow.get_state() log.debug("Configuration Changed") if current_state != "started": log.debug( "Configuration updated on service in a non-started state") returnValue(None) yield self.workflow.fire_transition("configure")