コード例 #1
0
    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
コード例 #2
0
ファイル: test_lifecycle.py プロジェクト: mcclurmc/juju
    def setUp(self):
        yield super(LifecycleTestBase, self).setUp()

        if self.juju_directory is None:
            self.juju_directory = self.makeDir()

        self.hook_log = self.capture_logging("hook.output",
                                             level=logging.DEBUG)
        self.agent_log = self.capture_logging("unit-agent",
                                              level=logging.DEBUG)
        self.executor = HookExecutor()
        self.executor.start()
        self.change_environment(PATH=os.environ["PATH"],
                                JUJU_UNIT_NAME="service-unit/0")
コード例 #3
0
ファイル: test_resolved.py プロジェクト: mcclurmc/juju
    def setUp(self):
        yield super(ControlResolvedTest, self).setUp()
        config = {"environments": {"firstenv": {"type": "dummy"}}}

        self.write_config(dump(config))
        self.config.load()

        yield self.add_relation_state("wordpress", "mysql")
        yield self.add_relation_state("wordpress", "varnish")

        self.service1 = yield self.service_state_manager.get_service_state(
            "mysql")
        self.service_unit1 = yield self.service1.add_unit_state()
        self.service_unit2 = yield self.service1.add_unit_state()

        self.unit1_workflow = UnitWorkflowState(self.client,
                                                self.service_unit1, None,
                                                self.makeDir())
        yield self.unit1_workflow.set_state("started")

        self.environment = self.config.get_default()
        self.provider = self.environment.get_machine_provider()

        self.output = self.capture_logging()
        self.stderr = self.capture_stream("stderr")
        self.executor = HookExecutor()
コード例 #4
0
ファイル: unit.py プロジェクト: mcclurmc/juju
    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
コード例 #5
0
ファイル: test_lifecycle.py プロジェクト: mcclurmc/juju
    def setUp(self):
        yield super(LifecycleTestBase, self).setUp()

        if self.juju_directory is None:
            self.juju_directory = self.makeDir()

        self.hook_log = self.capture_logging("hook.output",
                                             level=logging.DEBUG)
        self.agent_log = self.capture_logging("unit-agent",
                                              level=logging.DEBUG)
        self.executor = HookExecutor()
        self.executor.start()
        self.change_environment(
            PATH=os.environ["PATH"],
            JUJU_UNIT_NAME="service-unit/0")
コード例 #6
0
ファイル: unit.py プロジェクト: mcclurmc/juju
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")
コード例 #7
0
ファイル: test_executor.py プロジェクト: anbangr/trusted-juju
 def setUp(self):
     self._executor = HookExecutor()
     self.output = self.capture_logging("hook.executor", logging.DEBUG)
コード例 #8
0
ファイル: test_executor.py プロジェクト: anbangr/trusted-juju
class HookExecutorTest(TestCase):

    def setUp(self):
        self._executor = HookExecutor()
        self.output = self.capture_logging("hook.executor", logging.DEBUG)

    @inlineCallbacks
    def test_observer(self):
        """An observer can be registered against the executor
        to recieve callbacks when hooks are executed."""
        results = []
        d = Deferred()

        def observer(hook_path):
            results.append(hook_path)
            if len(results) == 3:
                d.callback(True)

        self._executor.set_observer(observer)
        self._executor.start()

        class _Invoker(object):

            def get_context(self):
                return None

            def __call__(self, hook_path):
                results.append(hook_path)

        hook_path = self.makeFile("hook content")
        yield self._executor(_Invoker(), hook_path)
        # Also observes non existant hooks
        yield self._executor(_Invoker(), self.makeFile())
        self.assertEqual(len(results), 3)

    @inlineCallbacks
    def test_start_deferred_ends_on_stop(self):
        """The executor start method returns a deferred that
        fires when the executor has been stopped."""

        stopped = []

        def on_start_finish(result):
            self.assertTrue(stopped)

        d = self._executor.start()
        d.addCallback(on_start_finish)
        stopped.append(True)
        yield self._executor.stop()
        self._executor.debug = True
        yield d

    def test_start_start(self):
        """Attempting to start twice raises an exception."""
        self._executor.start()
        return self.assertFailure(self._executor.start(), AssertionError)

    def test_stop_stop(self):
        """Attempt to stop twice raises an exception."""
        self._executor.start()
        self._executor.stop()
        return self.assertFailure(self._executor.stop(), AssertionError)

    @inlineCallbacks
    def test_debug_hook(self):
        """A debug hook is executed if a debug hook name is found.
        """
        self.output = self.capture_logging(
            "hook.executor", level=logging.DEBUG)
        results = []

        class _Invoker(object):

            def get_context(self):
                return None

            def __call__(self, hook_path):
                results.append(hook_path)

        self._executor.set_debug(["*"])
        self._executor.start()

        yield self._executor(_Invoker(), "abc")
        self.assertNotEqual(results, ["abc"])
        self.assertIn("abc", self.output.getvalue())

    def test_get_debug_hook_path_executable(self):
        """The debug hook path return from the executor should be executable.
        """
        self.patch(
            juju.hooks.executor, "DEBUG_HOOK_TEMPLATE",
            "#!/bin/bash\n echo {hook_name}\n exit 0")
        self._executor.set_debug(["*"])

        debug_hook = self._executor.get_hook_path("something/good")
        stdout = open(self.makeFile(), "w+")
        p = subprocess.Popen(debug_hook, stdout=stdout.fileno())
        self.assertEqual(p.wait(), 0)
        stdout.seek(0)
        self.assertEqual(stdout.read(), "good\n")

    @inlineCallbacks
    def test_end_debug_with_exited_process(self):
        """Ending debug with a process that has already ended is a noop."""

        results = []

        class _Invoker(object):

            process_ended = Deferred()

            def get_context(self):
                return None

            def __call__(self, hook_path):
                results.append(hook_path)
                return self.process_ended

            def send_signal(self, signal_id):
                if results:
                    results.append(1)
                    raise ProcessExitedAlready()
                results.append(2)
                raise ValueError("No such process")

        self._executor.start()
        self._executor.set_debug(["abc"])
        hook_done = self._executor(_Invoker(), "abc")
        self._executor.set_debug(None)

        _Invoker.process_ended.callback(True)

        yield hook_done
        self.assertEqual(len(results), 2)
        self.assertNotEqual(results[0], "abc")
        self.assertEqual(results[1], 1)

    @inlineCallbacks
    def test_end_debug_with_hook_not_started(self):
        results = []

        class _Invoker(object):

            process_ended = Deferred()

            def get_context(self):
                return None

            def __call__(self, hook_path):
                results.append(hook_path)
                return self.process_ended

            def send_signal(self, signal_id):
                if len(results) == 1:
                    results.append(1)
                    raise ValueError()
                results.append(2)
                raise ProcessExitedAlready()

        self._executor.start()
        self._executor.set_debug(["abc"])
        hook_done = self._executor(_Invoker(), "abc")
        self._executor.set_debug(None)

        _Invoker.process_ended.callback(True)
        yield hook_done
        self.assertEqual(len(results), 2)
        self.assertNotEqual(results[0], "abc")
        self.assertEqual(results[1], 1)

    @inlineCallbacks
    def test_end_debug_with_debug_running(self):
        """If a debug hook is running, it is signaled if the debug is disabled.
        """
        self.patch(
            juju.hooks.executor, "DEBUG_HOOK_TEMPLATE",
            "\n".join(("#!/bin/bash",
             "exit_handler() {",
             "  echo clean exit",
             "  exit 0",
             "}",
             'trap "exit_handler" HUP',
             "sleep 0.2",
             "exit 1")))

        unit_dir = self.makeDir()

        charm_dir = os.path.join(unit_dir, "charm")
        self.makeDir(path=charm_dir)

        self._executor.set_debug(["*"])
        log = logging.getLogger("invoker")
        # Populate environment variables for default invoker.
        self.change_environment(
            JUJU_UNIT_NAME="dummy/1", PATH="/bin/:/usr/bin")
        output = self.capture_logging("invoker", level=logging.DEBUG)
        invoker = Invoker(
            None, None, "constant", self.makeFile(), unit_dir, log)

        self._executor.start()
        hook_done = self._executor(invoker, "abc")

        # Give a moment for execution to start.
        yield self.sleep(0.1)
        self._executor.set_debug(None)
        yield hook_done
        self.assertIn("clean exit", output.getvalue())

    def test_get_debug_hook_path(self):
        """A debug hook file path is returned if a debug hook name is found.
        """
        # Default is to return the file path.
        file_path = self.makeFile()
        hook_name = os.path.basename(file_path)
        self.assertEquals(self._executor.get_hook_path(file_path), file_path)

        # Hook names can be specified as globs.
        self._executor.set_debug(["*"])
        debug_hook_path = self._executor.get_hook_path(file_path)
        self.assertNotEquals(file_path, debug_hook_path)
        # The hook base name is suffixed onto the debug hook file
        self.assertIn(os.path.basename(file_path),
                      os.path.basename(debug_hook_path))

        # Verify the debug hook contents.
        debug_hook_file = open(debug_hook_path)
        debug_contents = debug_hook_file.read()
        debug_hook_file.close()

        self.assertIn("hook.sh", debug_contents)
        self.assertIn("-n %s" % hook_name, debug_contents)
        self.assertTrue(os.access(debug_hook_path, os.X_OK))

        # The hook debug can be set back to none.
        self._executor.set_debug(None)
        self.assertEquals(self._executor.get_hook_path(file_path), file_path)

        # the executor can debug only selected hooks.
        self._executor.set_debug(["abc"])
        self.assertEquals(self._executor.get_hook_path(file_path), file_path)

        # The debug hook file is removed on the next hook path access.
        self.assertFalse(os.path.exists(debug_hook_path))

    def test_hook_exception_propgates(self):
        """An error in a hook is propogated to the execution deferred."""

        class _Invoker:
            def get_context(self):
                return None

            def __call__(self, hook_path):
                raise AttributeError("Foo")

        hook_path = self.makeFile("never got here")
        self._executor.start()
        return self.assertFailure(
            self._executor(_Invoker(), hook_path), AttributeError)

    @inlineCallbacks
    def test_executor_running_property(self):
        self._executor.start()
        self.assertTrue(self._executor.running)
        yield self._executor.stop()
        self.assertFalse(self._executor.running)

    @inlineCallbacks
    def test_nonexistant_hook_skipped(self):
        """If a hook does not exist a warning is logged and the hook skipped.
        """

        class _Invoker:
            def get_context(self):
                return None

        self._executor.start()
        hook_path = self.makeFile()
        value = yield self._executor(_Invoker(), hook_path)
        self.assertEqual(value, False)
        self.assertIn("Hook does not exist, skipping %s" % hook_path,
                      self.output.getvalue())

    def test_start_stop_start(self):
        """The executor can be stopped and restarted."""
        results = []

        def invoke(hook_path):
            results.append(hook_path)

        self._executor(invoke, "1")
        start_complete = self._executor.start()
        self._executor.stop()
        yield start_complete

        self.assertEqual(len(results), 1)
        self._executor(invoke, "1")
        self._executor(invoke, "2")
        start_complete = self._executor.start()
        self._executor.stop()
        yield start_complete
        self.assertEqual(len(results), 3)

    @inlineCallbacks
    def test_run_priority_hook_while_already_running(self):
        """Attempting to run a priority hook while running is an error.
        """
        def invoke(hook_path):
            pass

        self._executor.start()
        error = yield self.assertFailure(
            self._executor.run_priority_hook(invoke, "foobar"),
            AssertionError)
        self.assertEquals(str(error), "Executor must not be running")

    @inlineCallbacks
    def test_prioritize_with_queued(self):
        """A prioritized hook will execute before queued hooks.
        """
        results = []
        execs = []
        hooks = [self.makeFile(str(i)) for i in range(5)]

        class _Invoker(object):

            def get_context(self):
                return None

            def __call__(self, hook_path):
                results.append(hook_path)

        invoke = _Invoker()
        for i in hooks:
            execs.append(self._executor(invoke, i))

        priority_hook = self.makeFile(str("me first"))
        yield self._executor.run_priority_hook(invoke, priority_hook)
        self._executor.start()
        yield gather_results(execs)
        hooks.insert(0, priority_hook)
        self.assertEqual(results, hooks)

    def test_serialized_execution(self):
        """Hook execution is serialized via the HookExecution api.
        """

        wait_callback = [Deferred() for i in range(5)]
        finish_callback = [Deferred() for i in range(5)]
        results = []

        @inlineCallbacks
        def invoker(hook_path):
            results.append(hook_path)
            yield finish_callback[len(results) - 1]
            wait_callback[len(results) - 1].callback(True)

        start_complete = self._executor.start()
        for i in range(5):
            self._executor(invoker, "hook-%s" % i)

        self.assertEqual(len(results), 1)
        finish_callback[1].callback(True)
        self.assertEqual(len(results), 1)

        # Verify stop behavior
        stop_complete = yield self._executor.stop()

        # Finish the running execution.
        finish_callback[0].callback(True)

        # Verify we've stopped executing.
        yield stop_complete
        self.assertTrue(start_complete.called)
        self.assertEqual(len(results), 1)

        # Start the executioner again.
        self._executor.start()

        for finish in finish_callback[2:]:
            finish.callback(True)

        self.assertEqual(len(results), 5)
コード例 #9
0
ファイル: test_lifecycle.py プロジェクト: mcclurmc/juju
class LifecycleTestBase(RelationTestBase):

    juju_directory = None

    @inlineCallbacks
    def setUp(self):
        yield super(LifecycleTestBase, self).setUp()

        if self.juju_directory is None:
            self.juju_directory = self.makeDir()

        self.hook_log = self.capture_logging("hook.output",
                                             level=logging.DEBUG)
        self.agent_log = self.capture_logging("unit-agent",
                                              level=logging.DEBUG)
        self.executor = HookExecutor()
        self.executor.start()
        self.change_environment(
            PATH=os.environ["PATH"],
            JUJU_UNIT_NAME="service-unit/0")

    @inlineCallbacks
    def setup_default_test_relation(self):
        mysql_ep = RelationEndpoint(
            "mysql", "client-server", "app", "server")
        wordpress_ep = RelationEndpoint(
            "wordpress", "client-server", "db", "client")
        self.states = yield self.add_relation_service_unit_from_endpoints(
            mysql_ep, wordpress_ep)
        self.unit_directory = os.path.join(self.juju_directory,
            "units",
            self.states["unit"].unit_name.replace("/", "-"))
        os.makedirs(os.path.join(self.unit_directory, "charm", "hooks"))
        os.makedirs(os.path.join(self.juju_directory, "state"))

    def write_hook(self, name, text, no_exec=False):
        hook_path = os.path.join(self.unit_directory, "charm", "hooks", name)
        hook_file = open(hook_path, "w")
        hook_file.write(text.strip())
        hook_file.flush()
        hook_file.close()
        if not no_exec:
            os.chmod(hook_path, stat.S_IRWXU)
        return hook_path

    def wait_on_hook(self, name=None, count=None, sequence=(), debug=False,
                     executor=None):
        """Wait on the given named hook to be executed.

        @param: name: if specified only one hook name can be waited on
        at a given time.

        @param: count: Multiples of the same name can be captured by specifying
        the count parameter.

        @param: sequence: A list of hook names executed in sequence to
        be waited on

        @param: debug: This parameter enables debug stdout loogging.

        @param: executor: A HookExecutor instance to use instead of the default
        """
        d = Deferred()
        results = []
        assert name is not None or sequence, "Hook match must be specified"

        def observer(hook_path):
            hook_name = os.path.basename(hook_path)
            results.append(hook_name)
            if debug:
                print "-> exec hook", hook_name
            if d.called:
                return
            if results == sequence:
                d.callback(True)
            if hook_name == name and count is None:
                d.callback(True)
            if hook_name == name and results.count(hook_name) == count:
                d.callback(True)

        executor = executor or self.executor
        executor.set_observer(observer)
        return d

    def wait_on_state(self, workflow, state, debug=False):
        state_changed = Deferred()

        def observer(workflow_state, state_variables):
            if debug:
                print " workflow state", state, workflow
            if workflow_state == state:
                state_changed.callback(True)

        workflow.set_observer(observer)
        return state_changed

    def capture_output(self, stdout=True):
        """Convience method to capture log output.

        Useful tool for observing interaction between components.
        """
        if stdout:
            output = sys.stdout
        else:
            output = StringIO.StringIO()
        for log_name, indent in (
            ("statemachine", 0),
            ("hook.executor", 2),
            ("hook.scheduler", 1),
            ("unit.lifecycle", 1),
            ("unit.relation.watch", 1),
            ("unit.relation.lifecycle", 1)):
            formatter = logging.Formatter(
                (" " * indent) + "%(name)s: %(message)s")
            self.capture_logging(
                log_name, level=logging.DEBUG,
                log_file=output, formatter=formatter)
        print
        return output
コード例 #10
0
ファイル: test_executor.py プロジェクト: mcclurmc/juju
 def setUp(self):
     self._executor = HookExecutor()
     self.output = self.capture_logging("hook.executor", logging.DEBUG)
コード例 #11
0
ファイル: test_executor.py プロジェクト: mcclurmc/juju
class HookExecutorTest(TestCase):
    def setUp(self):
        self._executor = HookExecutor()
        self.output = self.capture_logging("hook.executor", logging.DEBUG)

    @inlineCallbacks
    def test_observer(self):
        """An observer can be registered against the executor
        to recieve callbacks when hooks are executed."""
        results = []
        d = Deferred()

        def observer(hook_path):
            results.append(hook_path)
            if len(results) == 3:
                d.callback(True)

        self._executor.set_observer(observer)
        self._executor.start()

        class _Invoker(object):
            def get_context(self):
                return None

            def __call__(self, hook_path):
                results.append(hook_path)

        hook_path = self.makeFile("hook content")
        yield self._executor(_Invoker(), hook_path)
        # Also observes non existant hooks
        yield self._executor(_Invoker(), self.makeFile())
        self.assertEqual(len(results), 3)

    @inlineCallbacks
    def test_start_deferred_ends_on_stop(self):
        """The executor start method returns a deferred that
        fires when the executor has been stopped."""

        stopped = []

        def on_start_finish(result):
            self.assertTrue(stopped)

        d = self._executor.start()
        d.addCallback(on_start_finish)
        stopped.append(True)
        yield self._executor.stop()
        self._executor.debug = True
        yield d

    def test_start_start(self):
        """Attempting to start twice raises an exception."""
        self._executor.start()
        return self.assertFailure(self._executor.start(), AssertionError)

    def test_stop_stop(self):
        """Attempt to stop twice raises an exception."""
        self._executor.start()
        self._executor.stop()
        return self.assertFailure(self._executor.stop(), AssertionError)

    @inlineCallbacks
    def test_debug_hook(self):
        """A debug hook is executed if a debug hook name is found.
        """
        self.output = self.capture_logging("hook.executor",
                                           level=logging.DEBUG)
        results = []

        class _Invoker(object):
            def get_context(self):
                return None

            def __call__(self, hook_path):
                results.append(hook_path)

        self._executor.set_debug(["*"])
        self._executor.start()

        yield self._executor(_Invoker(), "abc")
        self.assertNotEqual(results, ["abc"])
        self.assertIn("abc", self.output.getvalue())

    def test_get_debug_hook_path_executable(self):
        """The debug hook path return from the executor should be executable.
        """
        self.patch(juju.hooks.executor, "DEBUG_HOOK_TEMPLATE",
                   "#!/bin/bash\n echo {hook_name}\n exit 0")
        self._executor.set_debug(["*"])

        debug_hook = self._executor.get_hook_path("something/good")
        stdout = open(self.makeFile(), "w+")
        p = subprocess.Popen(debug_hook, stdout=stdout.fileno())
        self.assertEqual(p.wait(), 0)
        stdout.seek(0)
        self.assertEqual(stdout.read(), "good\n")

    @inlineCallbacks
    def test_end_debug_with_exited_process(self):
        """Ending debug with a process that has already ended is a noop."""

        results = []

        class _Invoker(object):

            process_ended = Deferred()

            def get_context(self):
                return None

            def __call__(self, hook_path):
                results.append(hook_path)
                return self.process_ended

            def send_signal(self, signal_id):
                if results:
                    results.append(1)
                    raise ProcessExitedAlready()
                results.append(2)
                raise ValueError("No such process")

        self._executor.start()
        self._executor.set_debug(["abc"])
        hook_done = self._executor(_Invoker(), "abc")
        self._executor.set_debug(None)

        _Invoker.process_ended.callback(True)

        yield hook_done
        self.assertEqual(len(results), 2)
        self.assertNotEqual(results[0], "abc")
        self.assertEqual(results[1], 1)

    @inlineCallbacks
    def test_end_debug_with_hook_not_started(self):
        results = []

        class _Invoker(object):

            process_ended = Deferred()

            def get_context(self):
                return None

            def __call__(self, hook_path):
                results.append(hook_path)
                return self.process_ended

            def send_signal(self, signal_id):
                if len(results) == 1:
                    results.append(1)
                    raise ValueError()
                results.append(2)
                raise ProcessExitedAlready()

        self._executor.start()
        self._executor.set_debug(["abc"])
        hook_done = self._executor(_Invoker(), "abc")
        self._executor.set_debug(None)

        _Invoker.process_ended.callback(True)
        yield hook_done
        self.assertEqual(len(results), 2)
        self.assertNotEqual(results[0], "abc")
        self.assertEqual(results[1], 1)

    @inlineCallbacks
    def test_end_debug_with_debug_running(self):
        """If a debug hook is running, it is signaled if the debug is disabled.
        """
        self.patch(
            juju.hooks.executor, "DEBUG_HOOK_TEMPLATE", "\n".join(
                ("#!/bin/bash", "exit_handler() {", "  echo clean exit",
                 "  exit 0", "}", 'trap "exit_handler" HUP', "sleep 0.2",
                 "exit 1")))

        unit_dir = self.makeDir()

        charm_dir = os.path.join(unit_dir, "charm")
        self.makeDir(path=charm_dir)

        self._executor.set_debug(["*"])
        log = logging.getLogger("invoker")
        # Populate environment variables for default invoker.
        self.change_environment(JUJU_UNIT_NAME="dummy/1",
                                PATH="/bin/:/usr/bin")
        output = self.capture_logging("invoker", level=logging.DEBUG)
        invoker = Invoker(None, None, "constant", self.makeFile(), unit_dir,
                          log)

        self._executor.start()
        hook_done = self._executor(invoker, "abc")

        # Give a moment for execution to start.
        yield self.sleep(0.1)
        self._executor.set_debug(None)
        yield hook_done
        self.assertIn("clean exit", output.getvalue())

    def test_get_debug_hook_path(self):
        """A debug hook file path is returned if a debug hook name is found.
        """
        # Default is to return the file path.
        file_path = self.makeFile()
        hook_name = os.path.basename(file_path)
        self.assertEquals(self._executor.get_hook_path(file_path), file_path)

        # Hook names can be specified as globs.
        self._executor.set_debug(["*"])
        debug_hook_path = self._executor.get_hook_path(file_path)
        self.assertNotEquals(file_path, debug_hook_path)
        # The hook base name is suffixed onto the debug hook file
        self.assertIn(os.path.basename(file_path),
                      os.path.basename(debug_hook_path))

        # Verify the debug hook contents.
        debug_hook_file = open(debug_hook_path)
        debug_contents = debug_hook_file.read()
        debug_hook_file.close()

        self.assertIn("hook.sh", debug_contents)
        self.assertIn("-n %s" % hook_name, debug_contents)
        self.assertTrue(os.access(debug_hook_path, os.X_OK))

        # The hook debug can be set back to none.
        self._executor.set_debug(None)
        self.assertEquals(self._executor.get_hook_path(file_path), file_path)

        # the executor can debug only selected hooks.
        self._executor.set_debug(["abc"])
        self.assertEquals(self._executor.get_hook_path(file_path), file_path)

        # The debug hook file is removed on the next hook path access.
        self.assertFalse(os.path.exists(debug_hook_path))

    def test_hook_exception_propgates(self):
        """An error in a hook is propogated to the execution deferred."""
        class _Invoker:
            def get_context(self):
                return None

            def __call__(self, hook_path):
                raise AttributeError("Foo")

        hook_path = self.makeFile("never got here")
        self._executor.start()
        return self.assertFailure(self._executor(_Invoker(), hook_path),
                                  AttributeError)

    @inlineCallbacks
    def test_executor_running_property(self):
        self._executor.start()
        self.assertTrue(self._executor.running)
        yield self._executor.stop()
        self.assertFalse(self._executor.running)

    @inlineCallbacks
    def test_nonexistant_hook_skipped(self):
        """If a hook does not exist a warning is logged and the hook skipped.
        """
        class _Invoker:
            def get_context(self):
                return None

        self._executor.start()
        hook_path = self.makeFile()
        value = yield self._executor(_Invoker(), hook_path)
        self.assertEqual(value, False)
        self.assertIn("Hook does not exist, skipping %s" % hook_path,
                      self.output.getvalue())

    def test_start_stop_start(self):
        """The executor can be stopped and restarted."""
        results = []

        def invoke(hook_path):
            results.append(hook_path)

        self._executor(invoke, "1")
        start_complete = self._executor.start()
        self._executor.stop()
        yield start_complete

        self.assertEqual(len(results), 1)
        self._executor(invoke, "1")
        self._executor(invoke, "2")
        start_complete = self._executor.start()
        self._executor.stop()
        yield start_complete
        self.assertEqual(len(results), 3)

    @inlineCallbacks
    def test_run_priority_hook_while_already_running(self):
        """Attempting to run a priority hook while running is an error.
        """
        def invoke(hook_path):
            pass

        self._executor.start()
        error = yield self.assertFailure(
            self._executor.run_priority_hook(invoke, "foobar"), AssertionError)
        self.assertEquals(str(error), "Executor must not be running")

    @inlineCallbacks
    def test_prioritize_with_queued(self):
        """A prioritized hook will execute before queued hooks.
        """
        results = []
        execs = []
        hooks = [self.makeFile(str(i)) for i in range(5)]

        class _Invoker(object):
            def get_context(self):
                return None

            def __call__(self, hook_path):
                results.append(hook_path)

        invoke = _Invoker()
        for i in hooks:
            execs.append(self._executor(invoke, i))

        priority_hook = self.makeFile(str("me first"))
        yield self._executor.run_priority_hook(invoke, priority_hook)
        self._executor.start()
        yield gather_results(execs)
        hooks.insert(0, priority_hook)
        self.assertEqual(results, hooks)

    def test_serialized_execution(self):
        """Hook execution is serialized via the HookExecution api.
        """

        wait_callback = [Deferred() for i in range(5)]
        finish_callback = [Deferred() for i in range(5)]
        results = []

        @inlineCallbacks
        def invoker(hook_path):
            results.append(hook_path)
            yield finish_callback[len(results) - 1]
            wait_callback[len(results) - 1].callback(True)

        start_complete = self._executor.start()
        for i in range(5):
            self._executor(invoker, "hook-%s" % i)

        self.assertEqual(len(results), 1)
        finish_callback[1].callback(True)
        self.assertEqual(len(results), 1)

        # Verify stop behavior
        stop_complete = yield self._executor.stop()

        # Finish the running execution.
        finish_callback[0].callback(True)

        # Verify we've stopped executing.
        yield stop_complete
        self.assertTrue(start_complete.called)
        self.assertEqual(len(results), 1)

        # Start the executioner again.
        self._executor.start()

        for finish in finish_callback[2:]:
            finish.callback(True)

        self.assertEqual(len(results), 5)
コード例 #12
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")
コード例 #13
0
ファイル: unit.py プロジェクト: anbangr/trusted-juju
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")
コード例 #14
0
ファイル: test_lifecycle.py プロジェクト: mcclurmc/juju
class LifecycleTestBase(RelationTestBase):

    juju_directory = None

    @inlineCallbacks
    def setUp(self):
        yield super(LifecycleTestBase, self).setUp()

        if self.juju_directory is None:
            self.juju_directory = self.makeDir()

        self.hook_log = self.capture_logging("hook.output",
                                             level=logging.DEBUG)
        self.agent_log = self.capture_logging("unit-agent",
                                              level=logging.DEBUG)
        self.executor = HookExecutor()
        self.executor.start()
        self.change_environment(PATH=os.environ["PATH"],
                                JUJU_UNIT_NAME="service-unit/0")

    @inlineCallbacks
    def setup_default_test_relation(self):
        mysql_ep = RelationEndpoint("mysql", "client-server", "app", "server")
        wordpress_ep = RelationEndpoint("wordpress", "client-server", "db",
                                        "client")
        self.states = yield self.add_relation_service_unit_from_endpoints(
            mysql_ep, wordpress_ep)
        self.unit_directory = os.path.join(
            self.juju_directory, "units",
            self.states["unit"].unit_name.replace("/", "-"))
        os.makedirs(os.path.join(self.unit_directory, "charm", "hooks"))
        os.makedirs(os.path.join(self.juju_directory, "state"))

    def write_hook(self, name, text, no_exec=False):
        hook_path = os.path.join(self.unit_directory, "charm", "hooks", name)
        hook_file = open(hook_path, "w")
        hook_file.write(text.strip())
        hook_file.flush()
        hook_file.close()
        if not no_exec:
            os.chmod(hook_path, stat.S_IRWXU)
        return hook_path

    def wait_on_hook(self,
                     name=None,
                     count=None,
                     sequence=(),
                     debug=False,
                     executor=None):
        """Wait on the given named hook to be executed.

        @param: name: if specified only one hook name can be waited on
        at a given time.

        @param: count: Multiples of the same name can be captured by specifying
        the count parameter.

        @param: sequence: A list of hook names executed in sequence to
        be waited on

        @param: debug: This parameter enables debug stdout loogging.

        @param: executor: A HookExecutor instance to use instead of the default
        """
        d = Deferred()
        results = []
        assert name is not None or sequence, "Hook match must be specified"

        def observer(hook_path):
            hook_name = os.path.basename(hook_path)
            results.append(hook_name)
            if debug:
                print "-> exec hook", hook_name
            if d.called:
                return
            if results == sequence:
                d.callback(True)
            if hook_name == name and count is None:
                d.callback(True)
            if hook_name == name and results.count(hook_name) == count:
                d.callback(True)

        executor = executor or self.executor
        executor.set_observer(observer)
        return d

    def wait_on_state(self, workflow, state, debug=False):
        state_changed = Deferred()

        def observer(workflow_state, state_variables):
            if debug:
                print " workflow state", state, workflow
            if workflow_state == state:
                state_changed.callback(True)

        workflow.set_observer(observer)
        return state_changed

    def capture_output(self, stdout=True):
        """Convience method to capture log output.

        Useful tool for observing interaction between components.
        """
        if stdout:
            output = sys.stdout
        else:
            output = StringIO.StringIO()
        for log_name, indent in (("statemachine", 0), ("hook.executor", 2),
                                 ("hook.scheduler", 1), ("unit.lifecycle", 1),
                                 ("unit.relation.watch",
                                  1), ("unit.relation.lifecycle", 1)):
            formatter = logging.Formatter((" " * indent) +
                                          "%(name)s: %(message)s")
            self.capture_logging(log_name,
                                 level=logging.DEBUG,
                                 log_file=output,
                                 formatter=formatter)
        print
        return output