Пример #1
0
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)
Пример #2
0
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")
Пример #3
0
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")
Пример #4
0
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")
Пример #5
0
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")
Пример #6
0
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")