Example #1
0
    def test_resolved_already_resolved(self):
        """
        'juju resolved <unit_name>' will report if
        the unit is already resolved.
        """
        # Mark the unit as resolved and as in an error state.
        yield self.service_unit1.set_resolved(RETRY_HOOKS)
        yield self.unit1_workflow.set_state("start_error")

        unit2_workflow = UnitWorkflowState(
            self.client, self.service_unit1, None, self.makeDir())
        unit2_workflow.set_state("start_error")

        self.assertEqual(
            (yield self.service_unit2.get_resolved()), None)

        self.setup_exit(0)
        finished = self.setup_cli_reactor()
        self.mocker.replay()

        main(["resolved", "mysql/0"])
        yield finished

        self.assertEqual(
            (yield self.service_unit1.get_resolved()),
            {"retry": RETRY_HOOKS})
        self.assertNotIn(
            "Marked unit 'mysql/0' as resolved",
            self.output.getvalue())
        self.assertIn(
            "Service unit 'mysql/0' is already marked as resolved.",
            self.stderr.getvalue(), "")
Example #2
0
    def test_resolved_already_running(self):
        """
        'juju resolved <unit_name>' will report if
        the unit is already running.
        """
        # Just verify we don't accidentally mark up another unit of the service
        unit2_workflow = UnitWorkflowState(
            self.client, self.service_unit2, None, self.makeDir())
        unit2_workflow.set_state("start_error")

        self.setup_exit(0)
        finished = self.setup_cli_reactor()
        self.mocker.replay()

        main(["resolved", "mysql/0"])
        yield finished

        self.assertEqual(
            (yield self.service_unit2.get_resolved()), None)
        self.assertEqual(
            (yield self.service_unit1.get_resolved()), None)

        self.assertNotIn(
            "Unit 'mysql/0 already running: started",
            self.output.getvalue())
Example #3
0
    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()
Example #4
0
    def setUp(self):
        yield super(RemoteUpgradeCharmTest, self).setUp()
        config = {
            "environments": {"firstenv": {"type": "dummy"}}}

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

        charm = CharmDirectory(os.path.join(
            test_repository_path, "series", "mysql"))
        self.charm_state_manager.add_charm_state(
            "cs:series/mysql-1", charm, "")
        self.service_state1 = yield self.add_service_from_charm(
            "mysql", "cs:series/mysql-1")
        self.service_unit1 = yield self.service_state1.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")
Example #5
0
    def test_resolved_already_resolved(self):
        """
        'juju resolved <unit_name>' will report if
        the unit is already resolved.
        """
        # Mark the unit as resolved and as in an error state.
        yield self.service_unit1.set_resolved(RETRY_HOOKS)
        yield self.unit1_workflow.set_state("start_error")

        unit2_workflow = UnitWorkflowState(self.client, self.service_unit1,
                                           None, self.makeDir())
        unit2_workflow.set_state("start_error")

        self.assertEqual((yield self.service_unit2.get_resolved()), None)

        self.setup_exit(0)
        finished = self.setup_cli_reactor()
        self.mocker.replay()

        main(["resolved", "mysql/0"])
        yield finished

        self.assertEqual((yield self.service_unit1.get_resolved()),
                         {"retry": RETRY_HOOKS})
        self.assertNotIn("Marked unit 'mysql/0' as resolved",
                         self.output.getvalue())
        self.assertIn("Service unit 'mysql/0' is already marked as resolved.",
                      self.stderr.getvalue(), "")
Example #6
0
    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)
Example #7
0
    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)
Example #8
0
    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()
Example #9
0
    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)
Example #10
0
    def test_agent_start_from_started_workflow(self):
        lifecycle = UnitLifecycle(
            self.client, self.states["unit"], self.states["service"],
            self.unit_directory, self.state_directory, self.executor)
        workflow = UnitWorkflowState(
            self.client, self.states["unit"], lifecycle,
            os.path.join(self.juju_directory, "state"))

        with (yield workflow.lock()):
            yield workflow.fire_transition("install")
        yield lifecycle.stop(fire_hooks=False, stop_relations=False)

        yield self.agent.startService()
        current_state = yield self.agent.workflow.get_state()
        self.assertEqual(current_state, "started")
        self.assertTrue(self.agent.lifecycle.running)
        self.assertTrue(self.agent.executor.running)
Example #11
0
    def setUp(self):
        yield super(ControlCharmUpgradeTest, self).setUp()
        config = {
            "environments": {"firstenv": {"type": "dummy"}}}

        self.write_config(dump(config))
        self.config.load()
        self.service_state1 = yield self.add_service_from_charm("mysql")
        self.service_unit1 = yield self.service_state1.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")
Example #12
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)
Example #13
0
    def test_resolved_already_running(self):
        """
        'juju resolved <unit_name>' will report if
        the unit is already running.
        """
        # Just verify we don't accidentally mark up another unit of the service
        unit2_workflow = UnitWorkflowState(self.client, self.service_unit2,
                                           None, self.makeDir())
        unit2_workflow.set_state("start_error")

        self.setup_exit(0)
        finished = self.setup_cli_reactor()
        self.mocker.replay()

        main(["resolved", "mysql/0"])
        yield finished

        self.assertEqual((yield self.service_unit2.get_resolved()), None)
        self.assertEqual((yield self.service_unit1.get_resolved()), None)

        self.assertNotIn("Unit 'mysql/0 already running: started",
                         self.output.getvalue())
Example #14
0
    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)
Example #15
0
    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")
    def setUp(self):
        yield super(ControlCharmUpgradeTest, self).setUp()

        self.service_state1 = yield self.add_service_from_charm("mysql")
        self.service_unit1 = yield self.service_state1.add_unit_state()

        self.unit1_workflow = UnitWorkflowState(
            self.client, self.service_unit1, None, self.makeDir())
        with (yield self.unit1_workflow.lock()):
            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")
Example #17
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")
Example #18
0
class ControlResolvedTest(ServiceStateManagerTestBase, ControlToolTest,
                          RepositoryTestBase):
    @inlineCallbacks
    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()

    @inlineCallbacks
    def add_relation_state(self, *service_names):
        for service_name in service_names:
            try:
                yield self.service_state_manager.get_service_state(
                    service_name)
            except ServiceStateNotFound:
                yield self.add_service_from_charm(service_name)

        endpoint_pairs = yield self.service_state_manager.join_descriptors(
            *service_names)
        endpoints = endpoint_pairs[0]
        endpoints = endpoint_pairs[0]
        if endpoints[0] == endpoints[1]:
            endpoints = endpoints[0:1]
        relation_state = (yield self.relation_state_manager.add_relation_state(
            *endpoints))[0]
        returnValue(relation_state)

    @inlineCallbacks
    def get_named_service_relation(self, service_state, relation_name):
        if isinstance(service_state, str):
            service_state = yield self.service_state_manager.get_service_state(
                service_state)

        rels = yield self.relation_state_manager.get_relations_for_service(
            service_state)

        rels = [sr for sr in rels if sr.relation_name == relation_name]
        if len(rels) == 1:
            returnValue(rels[0])
        returnValue(rels)

    @inlineCallbacks
    def setup_unit_relations(self, service_relation, *units):
        """
        Given a service relation and set of unit tuples in the form
        unit_state, unit_relation_workflow_state, will add unit relations
        for these units and update their workflow state to the desired/given
        state.
        """
        for unit, state in units:
            unit_relation = yield service_relation.add_unit_state(unit)
            lifecycle = UnitRelationLifecycle(self.client, unit.unit_name,
                                              unit_relation,
                                              service_relation.relation_name,
                                              self.makeDir(), self.executor)
            workflow_state = RelationWorkflowState(self.client, unit_relation,
                                                   lifecycle, self.makeDir())
            yield workflow_state.set_state(state)

    @inlineCallbacks
    def test_resolved(self):
        """
        'juju resolved <unit_name>' will schedule a unit for
        retrying from an error state.
        """
        # Push the unit into an error state
        yield self.unit1_workflow.set_state("start_error")
        self.setup_exit(0)
        finished = self.setup_cli_reactor()
        self.mocker.replay()

        self.assertEqual((yield self.service_unit1.get_resolved()), None)

        main(["resolved", "mysql/0"])
        yield finished

        self.assertEqual((yield self.service_unit1.get_resolved()),
                         {"retry": NO_HOOKS})
        self.assertIn("Marked unit 'mysql/0' as resolved",
                      self.output.getvalue())

    @inlineCallbacks
    def test_resolved_retry(self):
        """
        'juju resolved --retry <unit_name>' will schedule a unit
        for retrying from an error state with a retry of hooks
        executions.
        """
        yield self.unit1_workflow.set_state("start_error")
        self.setup_exit(0)
        finished = self.setup_cli_reactor()
        self.mocker.replay()

        self.assertEqual((yield self.service_unit1.get_resolved()), None)

        main(["resolved", "--retry", "mysql/0"])
        yield finished

        self.assertEqual((yield self.service_unit1.get_resolved()),
                         {"retry": RETRY_HOOKS})
        self.assertIn("Marked unit 'mysql/0' as resolved",
                      self.output.getvalue())

    @inlineCallbacks
    def test_relation_resolved(self):
        """
        'juju relation <unit_name> <rel_name>' will schedule
        the broken unit relations for being resolved.
        """
        service_relation = yield self.get_named_service_relation(
            self.service1, "server")

        yield self.setup_unit_relations(service_relation,
                                        (self.service_unit1, "down"),
                                        (self.service_unit2, "up"))

        yield self.unit1_workflow.set_state("start_error")
        self.setup_exit(0)
        finished = self.setup_cli_reactor()
        self.mocker.replay()

        self.assertEqual((yield self.service_unit1.get_relation_resolved()),
                         None)

        main(
            ["resolved", "--retry", "mysql/0", service_relation.relation_name])
        yield finished

        self.assertEqual((yield self.service_unit1.get_relation_resolved()),
                         {service_relation.internal_relation_id: RETRY_HOOKS})
        self.assertEqual((yield self.service_unit2.get_relation_resolved()),
                         None)
        self.assertIn("Marked unit 'mysql/0' relation 'server' as resolved",
                      self.output.getvalue())

    @inlineCallbacks
    def test_resolved_relation_some_already_resolved(self):
        """
        'juju resolved <service_name> <rel_name>' will mark
        resolved all down units that are not already marked resolved.
        """

        service2 = yield self.service_state_manager.get_service_state(
            "wordpress")
        service_unit1 = yield service2.add_unit_state()

        service_relation = yield self.get_named_service_relation(
            service2, "db")
        yield self.setup_unit_relations(service_relation,
                                        (service_unit1, "down"))

        service_relation2 = yield self.get_named_service_relation(
            service2, "cache")
        yield self.setup_unit_relations(service_relation2,
                                        (service_unit1, "down"))

        yield service_unit1.set_relation_resolved(
            {service_relation.internal_relation_id: NO_HOOKS})

        self.setup_exit(0)
        finished = self.setup_cli_reactor()
        self.mocker.replay()

        main(["resolved", "--retry", "wordpress/0", "cache"])
        yield finished

        self.assertEqual(
            (yield service_unit1.get_relation_resolved()), {
                service_relation.internal_relation_id: NO_HOOKS,
                service_relation2.internal_relation_id: RETRY_HOOKS
            })

        self.assertIn("Marked unit 'wordpress/0' relation 'cache' as resolved",
                      self.output.getvalue())

    @inlineCallbacks
    def test_resolved_relation_some_already_resolved_conflict(self):
        """
        'juju resolved <service_name> <rel_name>' will mark
        resolved all down units that are not already marked resolved.
        """

        service2 = yield self.service_state_manager.get_service_state(
            "wordpress")
        service_unit1 = yield service2.add_unit_state()

        service_relation = yield self.get_named_service_relation(
            service2, "db")
        yield self.setup_unit_relations(service_relation,
                                        (service_unit1, "down"))

        yield service_unit1.set_relation_resolved(
            {service_relation.internal_relation_id: NO_HOOKS})

        self.setup_exit(0)
        finished = self.setup_cli_reactor()
        self.mocker.replay()

        main(["resolved", "--retry", "wordpress/0", "db"])
        yield finished

        self.assertEqual((yield service_unit1.get_relation_resolved()),
                         {service_relation.internal_relation_id: NO_HOOKS})

        self.assertIn(
            "Service unit 'wordpress/0' already has relations marked as resol",
            self.output.getvalue())

    @inlineCallbacks
    def test_resolved_unknown_service(self):
        """
        'juju resolved <unit_name>' will report if a service is
        invalid.
        """
        self.setup_exit(0)
        finished = self.setup_cli_reactor()
        self.mocker.replay()
        main(["resolved", "zebra/0"])
        yield finished
        self.assertIn("Service 'zebra' was not found", self.stderr.getvalue())

    @inlineCallbacks
    def test_resolved_unknown_unit(self):
        """
        'juju resolved <unit_name>' will report if a unit is
        invalid.
        """
        self.setup_exit(0)
        finished = self.setup_cli_reactor()
        self.mocker.replay()
        main(["resolved", "mysql/5"])
        yield finished
        self.assertIn("Service unit 'mysql/5' was not found",
                      self.output.getvalue())

    @inlineCallbacks
    def test_resolved_unknown_unit_relation(self):
        """
        'juju resolved <unit_name>' will report if a relation is
        invalid.
        """
        self.setup_exit(0)
        finished = self.setup_cli_reactor()
        self.mocker.replay()

        self.assertEqual((yield self.service_unit1.get_resolved()), None)

        main(["resolved", "mysql/0", "magic"])
        yield finished

        self.assertIn("Relation not found", self.output.getvalue())

    @inlineCallbacks
    def test_resolved_already_running(self):
        """
        'juju resolved <unit_name>' will report if
        the unit is already running.
        """
        # Just verify we don't accidentally mark up another unit of the service
        unit2_workflow = UnitWorkflowState(self.client, self.service_unit2,
                                           None, self.makeDir())
        unit2_workflow.set_state("start_error")

        self.setup_exit(0)
        finished = self.setup_cli_reactor()
        self.mocker.replay()

        main(["resolved", "mysql/0"])
        yield finished

        self.assertEqual((yield self.service_unit2.get_resolved()), None)
        self.assertEqual((yield self.service_unit1.get_resolved()), None)

        self.assertNotIn("Unit 'mysql/0 already running: started",
                         self.output.getvalue())

    @inlineCallbacks
    def test_resolved_already_resolved(self):
        """
        'juju resolved <unit_name>' will report if
        the unit is already resolved.
        """
        # Mark the unit as resolved and as in an error state.
        yield self.service_unit1.set_resolved(RETRY_HOOKS)
        yield self.unit1_workflow.set_state("start_error")

        unit2_workflow = UnitWorkflowState(self.client, self.service_unit1,
                                           None, self.makeDir())
        unit2_workflow.set_state("start_error")

        self.assertEqual((yield self.service_unit2.get_resolved()), None)

        self.setup_exit(0)
        finished = self.setup_cli_reactor()
        self.mocker.replay()

        main(["resolved", "mysql/0"])
        yield finished

        self.assertEqual((yield self.service_unit1.get_resolved()),
                         {"retry": RETRY_HOOKS})
        self.assertNotIn("Marked unit 'mysql/0' as resolved",
                         self.output.getvalue())
        self.assertIn("Service unit 'mysql/0' is already marked as resolved.",
                      self.stderr.getvalue(), "")

    @inlineCallbacks
    def test_resolved_relation_already_running(self):
        """
        'juju resolved <unit_name> <rel_name>' will report
        if the relation is already running.
        """
        service2 = yield self.service_state_manager.get_service_state(
            "wordpress")
        service_unit1 = yield service2.add_unit_state()

        service_relation = yield self.get_named_service_relation(
            service2, "db")
        yield self.setup_unit_relations(service_relation,
                                        (service_unit1, "up"))

        self.setup_exit(0)
        finished = self.setup_cli_reactor()
        self.mocker.replay()

        main(["resolved", "wordpress/0", "db"])
        yield finished

        self.assertIn("Matched relations are all running",
                      self.output.getvalue())
        self.assertEqual((yield service_unit1.get_relation_resolved()), None)
Example #19
0
    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)
Example #20
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")
Example #21
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")
Example #22
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")
Example #23
0
class ControlResolvedTest(
    ServiceStateManagerTestBase, ControlToolTest, RepositoryTestBase):

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

    @inlineCallbacks
    def add_relation_state(self, *service_names):
        for service_name in service_names:
            try:
                yield self.service_state_manager.get_service_state(
                    service_name)
            except ServiceStateNotFound:
                yield self.add_service_from_charm(service_name)

        endpoint_pairs = yield self.service_state_manager.join_descriptors(
            *service_names)
        endpoints = endpoint_pairs[0]
        endpoints = endpoint_pairs[0]
        if endpoints[0] == endpoints[1]:
            endpoints = endpoints[0:1]
        relation_state = (yield self.relation_state_manager.add_relation_state(
            *endpoints))[0]
        returnValue(relation_state)

    @inlineCallbacks
    def get_named_service_relation(self, service_state, relation_name):
        if isinstance(service_state, str):
            service_state = yield self.service_state_manager.get_service_state(
                service_state)

        rels = yield self.relation_state_manager.get_relations_for_service(
            service_state)

        rels = [sr for sr in rels if sr.relation_name == relation_name]
        if len(rels) == 1:
            returnValue(rels[0])
        returnValue(rels)

    @inlineCallbacks
    def setup_unit_relations(self, service_relation, *units):
        """
        Given a service relation and set of unit tuples in the form
        unit_state, unit_relation_workflow_state, will add unit relations
        for these units and update their workflow state to the desired/given
        state.
        """
        for unit, state in units:
            unit_relation = yield service_relation.add_unit_state(unit)
            lifecycle = UnitRelationLifecycle(self.client,
                                              unit.unit_name, unit_relation,
                                              service_relation.relation_name,
                                              self.makeDir(), self.executor)
            workflow_state = RelationWorkflowState(
                self.client, unit_relation, lifecycle, self.makeDir())
            yield workflow_state.set_state(state)

    @inlineCallbacks
    def test_resolved(self):
        """
        'juju resolved <unit_name>' will schedule a unit for
        retrying from an error state.
        """
        # Push the unit into an error state
        yield self.unit1_workflow.set_state("start_error")
        self.setup_exit(0)
        finished = self.setup_cli_reactor()
        self.mocker.replay()

        self.assertEqual(
            (yield self.service_unit1.get_resolved()), None)

        main(["resolved", "mysql/0"])
        yield finished

        self.assertEqual(
            (yield self.service_unit1.get_resolved()), {"retry": NO_HOOKS})
        self.assertIn(
            "Marked unit 'mysql/0' as resolved",
            self.output.getvalue())

    @inlineCallbacks
    def test_resolved_retry(self):
        """
        'juju resolved --retry <unit_name>' will schedule a unit
        for retrying from an error state with a retry of hooks
        executions.
        """
        yield self.unit1_workflow.set_state("start_error")
        self.setup_exit(0)
        finished = self.setup_cli_reactor()
        self.mocker.replay()

        self.assertEqual(
            (yield self.service_unit1.get_resolved()), None)

        main(["resolved", "--retry", "mysql/0"])
        yield finished

        self.assertEqual(
            (yield self.service_unit1.get_resolved()), {"retry": RETRY_HOOKS})
        self.assertIn(
            "Marked unit 'mysql/0' as resolved",
            self.output.getvalue())

    @inlineCallbacks
    def test_relation_resolved(self):
        """
        'juju relation <unit_name> <rel_name>' will schedule
        the broken unit relations for being resolved.
        """
        service_relation = yield self.get_named_service_relation(
            self.service1, "server")

        yield self.setup_unit_relations(
            service_relation,
            (self.service_unit1, "down"),
            (self.service_unit2, "up"))

        yield self.unit1_workflow.set_state("start_error")
        self.setup_exit(0)
        finished = self.setup_cli_reactor()
        self.mocker.replay()

        self.assertEqual(
            (yield self.service_unit1.get_relation_resolved()), None)

        main(["resolved", "--retry", "mysql/0",
              service_relation.relation_name])
        yield finished

        self.assertEqual(
            (yield self.service_unit1.get_relation_resolved()),
            {service_relation.internal_relation_id: RETRY_HOOKS})
        self.assertEqual(
            (yield self.service_unit2.get_relation_resolved()),
            None)
        self.assertIn(
            "Marked unit 'mysql/0' relation 'server' as resolved",
            self.output.getvalue())

    @inlineCallbacks
    def test_resolved_relation_some_already_resolved(self):
        """
        'juju resolved <service_name> <rel_name>' will mark
        resolved all down units that are not already marked resolved.
        """

        service2 = yield self.service_state_manager.get_service_state(
            "wordpress")
        service_unit1 = yield service2.add_unit_state()

        service_relation = yield self.get_named_service_relation(
            service2, "db")
        yield self.setup_unit_relations(
            service_relation, (service_unit1, "down"))

        service_relation2 = yield self.get_named_service_relation(
            service2, "cache")
        yield self.setup_unit_relations(
            service_relation2, (service_unit1, "down"))

        yield service_unit1.set_relation_resolved(
            {service_relation.internal_relation_id: NO_HOOKS})

        self.setup_exit(0)
        finished = self.setup_cli_reactor()
        self.mocker.replay()

        main(["resolved", "--retry", "wordpress/0", "cache"])
        yield finished

        self.assertEqual(
            (yield service_unit1.get_relation_resolved()),
            {service_relation.internal_relation_id: NO_HOOKS,
             service_relation2.internal_relation_id: RETRY_HOOKS})

        self.assertIn(
            "Marked unit 'wordpress/0' relation 'cache' as resolved",
            self.output.getvalue())

    @inlineCallbacks
    def test_resolved_relation_some_already_resolved_conflict(self):
        """
        'juju resolved <service_name> <rel_name>' will mark
        resolved all down units that are not already marked resolved.
        """

        service2 = yield self.service_state_manager.get_service_state(
            "wordpress")
        service_unit1 = yield service2.add_unit_state()

        service_relation = yield self.get_named_service_relation(
            service2, "db")
        yield self.setup_unit_relations(
            service_relation, (service_unit1, "down"))

        yield service_unit1.set_relation_resolved(
            {service_relation.internal_relation_id: NO_HOOKS})

        self.setup_exit(0)
        finished = self.setup_cli_reactor()
        self.mocker.replay()

        main(["resolved", "--retry", "wordpress/0", "db"])
        yield finished

        self.assertEqual(
            (yield service_unit1.get_relation_resolved()),
            {service_relation.internal_relation_id: NO_HOOKS})

        self.assertIn(
            "Service unit 'wordpress/0' already has relations marked as resol",
            self.output.getvalue())

    @inlineCallbacks
    def test_resolved_unknown_service(self):
        """
        'juju resolved <unit_name>' will report if a service is
        invalid.
        """
        self.setup_exit(0)
        finished = self.setup_cli_reactor()
        self.mocker.replay()
        main(["resolved", "zebra/0"])
        yield finished
        self.assertIn("Service 'zebra' was not found", self.stderr.getvalue())

    @inlineCallbacks
    def test_resolved_unknown_unit(self):
        """
        'juju resolved <unit_name>' will report if a unit is
        invalid.
        """
        self.setup_exit(0)
        finished = self.setup_cli_reactor()
        self.mocker.replay()
        main(["resolved", "mysql/5"])
        yield finished
        self.assertIn(
            "Service unit 'mysql/5' was not found", self.output.getvalue())

    @inlineCallbacks
    def test_resolved_unknown_unit_relation(self):
        """
        'juju resolved <unit_name>' will report if a relation is
        invalid.
        """
        self.setup_exit(0)
        finished = self.setup_cli_reactor()
        self.mocker.replay()

        self.assertEqual(
            (yield self.service_unit1.get_resolved()), None)

        main(["resolved", "mysql/0", "magic"])
        yield finished

        self.assertIn("Relation not found", self.output.getvalue())

    @inlineCallbacks
    def test_resolved_already_running(self):
        """
        'juju resolved <unit_name>' will report if
        the unit is already running.
        """
        # Just verify we don't accidentally mark up another unit of the service
        unit2_workflow = UnitWorkflowState(
            self.client, self.service_unit2, None, self.makeDir())
        unit2_workflow.set_state("start_error")

        self.setup_exit(0)
        finished = self.setup_cli_reactor()
        self.mocker.replay()

        main(["resolved", "mysql/0"])
        yield finished

        self.assertEqual(
            (yield self.service_unit2.get_resolved()), None)
        self.assertEqual(
            (yield self.service_unit1.get_resolved()), None)

        self.assertNotIn(
            "Unit 'mysql/0 already running: started",
            self.output.getvalue())

    @inlineCallbacks
    def test_resolved_already_resolved(self):
        """
        'juju resolved <unit_name>' will report if
        the unit is already resolved.
        """
        # Mark the unit as resolved and as in an error state.
        yield self.service_unit1.set_resolved(RETRY_HOOKS)
        yield self.unit1_workflow.set_state("start_error")

        unit2_workflow = UnitWorkflowState(
            self.client, self.service_unit1, None, self.makeDir())
        unit2_workflow.set_state("start_error")

        self.assertEqual(
            (yield self.service_unit2.get_resolved()), None)

        self.setup_exit(0)
        finished = self.setup_cli_reactor()
        self.mocker.replay()

        main(["resolved", "mysql/0"])
        yield finished

        self.assertEqual(
            (yield self.service_unit1.get_resolved()),
            {"retry": RETRY_HOOKS})
        self.assertNotIn(
            "Marked unit 'mysql/0' as resolved",
            self.output.getvalue())
        self.assertIn(
            "Service unit 'mysql/0' is already marked as resolved.",
            self.stderr.getvalue(), "")

    @inlineCallbacks
    def test_resolved_relation_already_running(self):
        """
        'juju resolved <unit_name> <rel_name>' will report
        if the relation is already running.
        """
        service2 = yield self.service_state_manager.get_service_state(
            "wordpress")
        service_unit1 = yield service2.add_unit_state()

        service_relation = yield self.get_named_service_relation(
            service2, "db")
        yield self.setup_unit_relations(
            service_relation, (service_unit1, "up"))

        self.setup_exit(0)
        finished = self.setup_cli_reactor()
        self.mocker.replay()

        main(["resolved", "wordpress/0", "db"])
        yield finished

        self.assertIn("Matched relations are all running",
                      self.output.getvalue())
        self.assertEqual(
            (yield service_unit1.get_relation_resolved()),
            None)
Example #24
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")
Example #25
0
class ControlCharmUpgradeTest(
    MachineControlToolTest, CharmUpgradeTestBase):

    @inlineCallbacks
    def setUp(self):
        yield super(ControlCharmUpgradeTest, self).setUp()
        config = {
            "environments": {"firstenv": {"type": "dummy"}}}

        self.write_config(dump(config))
        self.config.load()
        self.service_state1 = yield self.add_service_from_charm("mysql")
        self.service_unit1 = yield self.service_state1.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")

    @inlineCallbacks
    def test_charm_upgrade(self):
        """
        'juju charm-upgrade <service_name>' will schedule
        a charm for upgrade.
        """
        repository = self.increment_charm(self.charm)

        mock_environment = self.mocker.patch(Environment)
        mock_environment.get_machine_provider()
        self.mocker.result(self.provider)

        finished = self.setup_cli_reactor()
        self.setup_exit(0)
        self.mocker.replay()
        main(["upgrade-charm", "--repository", repository.path, "mysql"])
        yield finished

        # Verify the service has a new charm reference
        charm_id = yield self.service_state1.get_charm_id()
        self.assertEqual(charm_id, "local:series/mysql-2")

        # Verify the provider storage has been updated
        charm = yield repository.find(CharmURL.parse("local:series/mysql"))
        storage = self.provider.get_file_storage()
        try:
            yield storage.get(
                "local_3a_series_2f_mysql-2_3a_%s" % charm.get_sha256())
        except FileNotFound:
            self.fail("New charm not uploaded")

        # Verify the upgrade flag on the service units.
        upgrade_flag = yield self.service_unit1.get_upgrade_flag()
        self.assertTrue(upgrade_flag)

    @inlineCallbacks
    def test_missing_repository(self):
        finished = self.setup_cli_reactor()
        self.setup_exit(0)
        self.mocker.replay()

        main(["upgrade-charm", "mysql"])
        yield finished

        self.assertIn("No repository specified", self.output.getvalue())

    @inlineCallbacks
    def test_upgrade_charm_with_unupgradeable_units(self):
        """If there are units that won't be upgraded, they will be reported,
        other units will be upgraded.
        """
        repository = self.increment_charm(self.charm)
        service_unit2 = yield self.service_state1.add_unit_state()

        finished = self.setup_cli_reactor()
        self.setup_exit(0)
        self.mocker.replay()

        main(["upgrade-charm", "--repository", repository.path, "mysql"])
        yield finished

        # Verify report of unupgradeable units
        self.assertIn(
            ("Unit 'mysql/1' is not in a running state "
            "(state: 'uninitialized'), won't upgrade"),
            self.output.getvalue())

        # Verify flags only set on upgradeable unit.
        value = (yield service_unit2.get_upgrade_flag())
        self.assertFalse(value)
        value = (yield self.service_unit1.get_upgrade_flag())
        self.assertTrue(value)

    @inlineCallbacks
    def test_upgrade_charm_unknown_service(self):
        finished = self.setup_cli_reactor()
        self.setup_exit(0)
        self.mocker.replay()
        main(["upgrade-charm", "--repository", self.makeDir(), "volcano"])
        yield finished
        self.assertIn(
            "Service 'volcano' was not found", self.stderr.getvalue())

    @inlineCallbacks
    def test_upgrade_charm_unknown_charm(self):
        """If a charm is not found in the repository, an error is given.
        """
        finished = self.setup_cli_reactor()
        self.setup_exit(0)
        self.mocker.replay()
        repository_dir = self.makeDir()
        os.mkdir(os.path.join(repository_dir, "series"))
        main(["upgrade-charm", "--repository", repository_dir, "mysql"])
        yield finished
        self.assertIn(
            "Charm 'local:series/mysql' not found in repository",
            self.output.getvalue())

    @inlineCallbacks
    def test_upgrade_charm_unknown_charm_dryrun(self):
        """If a charm is not found in the repository, an error is given.
        """
        finished = self.setup_cli_reactor()
        self.setup_exit(0)
        self.mocker.replay()
        repository_dir = self.makeDir()
        os.mkdir(os.path.join(repository_dir, "series"))
        main(["upgrade-charm", "--repository", repository_dir, "mysql", "--dry-run"])
        yield finished
        self.assertIn(
            "Charm 'local:series/mysql' not found in repository",
            self.output.getvalue())

    @inlineCallbacks
    def test_upgrade_charm_dryrun_reports_unupgradeable_units(self):
        """If there are units that won't be upgraded, dry-run will report them.
        """
        repository = self.increment_charm(self.charm)
        service_unit2 = yield self.service_state1.add_unit_state()

        finished = self.setup_cli_reactor()
        self.setup_exit(0)
        self.mocker.replay()

        main(["upgrade-charm", "-n",
              "--repository", repository.path, "mysql"])
        yield finished

        # Verify dry run
        self.assertIn(
            "Service would be upgraded from charm", self.output.getvalue())
        # Verify report of unupgradeable units
        self.assertIn(
            ("Unit 'mysql/1' is not in a running state "
            "(state: 'uninitialized'), won't upgrade"),
            self.output.getvalue())

        # Verify no flags have been set.
        value = (yield service_unit2.get_upgrade_flag())
        self.assertFalse(value)
        value = (yield self.service_unit1.get_upgrade_flag())
        self.assertFalse(value)


    @inlineCallbacks
    def test_latest_local_dry_run(self):
        """Do nothing; log that local charm would be re-revisioned and used"""
        finished = self.setup_cli_reactor()
        self.setup_exit(0)
        self.mocker.replay()

        metadata = self.charm.metadata.get_serialization_data()
        metadata["name"] = "mysql"
        repository = self.add_charm(metadata, 1)
        main(["upgrade-charm", "--dry-run",
                  "--repository", repository.path, "mysql"])
        yield finished
        charm_path = os.path.join(repository.path, "series", "mysql")
        self.assertIn(
            "%s would be set to revision 2" % charm_path,
            self.output.getvalue())
        self.assertIn(
            "Service would be upgraded from charm 'local:series/mysql-1' to "
            "'local:series/mysql-2'",
            self.output.getvalue())

        with open(os.path.join(charm_path, "revision")) as f:
            self.assertEquals(f.read(), "1")

        upgrade_flag = yield self.service_unit1.get_upgrade_flag()
        self.assertFalse(upgrade_flag)

    @inlineCallbacks
    def test_latest_local_live_fire(self):
        """Local charm should be re-revisioned and used; log that it was"""
        finished = self.setup_cli_reactor()
        self.setup_exit(0)
        self.mocker.replay()

        metadata = self.charm.metadata.get_serialization_data()
        metadata["name"] = "mysql"
        repository = self.add_charm(metadata, 1)
        main(["upgrade-charm", "--repository", repository.path, "mysql"])

        yield finished
        charm_path = os.path.join(repository.path, "series", "mysql")
        self.assertIn(
            "Setting %s to revision 2" % charm_path,
            self.output.getvalue())

        with open(os.path.join(charm_path, "revision")) as f:
            self.assertEquals(f.read(), "2\n")

        upgrade_flag = yield self.service_unit1.get_upgrade_flag()
        self.assertTrue(upgrade_flag)

    @inlineCallbacks
    def test_latest_local_leapfrog_dry_run(self):
        """Do nothing; log that local charm would be re-revisioned and used"""
        finished = self.setup_cli_reactor()
        self.setup_exit(0)
        self.mocker.replay()

        metadata = self.charm.metadata.get_serialization_data()
        metadata["name"] = "mysql"
        repository = self.add_charm(metadata, 0)
        main(["upgrade-charm", "--dry-run",
                  "--repository", repository.path, "mysql"])
        yield finished
        charm_path = os.path.join(repository.path, "series", "mysql")
        self.assertIn(
            "%s would be set to revision 2" % charm_path,
            self.output.getvalue())
        self.assertIn(
            "Service would be upgraded from charm 'local:series/mysql-1' to "
            "'local:series/mysql-2'",
            self.output.getvalue())

        with open(os.path.join(charm_path, "revision")) as f:
            self.assertEquals(f.read(), "0")

        upgrade_flag = yield self.service_unit1.get_upgrade_flag()
        self.assertFalse(upgrade_flag)

    @inlineCallbacks
    def test_latest_local_leapfrog_live_fire(self):
        """Local charm should be re-revisioned and used; log that it was"""
        finished = self.setup_cli_reactor()
        self.setup_exit(0)
        self.mocker.replay()

        metadata = self.charm.metadata.get_serialization_data()
        metadata["name"] = "mysql"
        repository = self.add_charm(metadata, 0)
        main(["upgrade-charm", "--repository", repository.path, "mysql"])

        yield finished
        charm_path = os.path.join(repository.path, "series", "mysql")
        self.assertIn(
            "Setting %s to revision 2" % charm_path,
            self.output.getvalue())

        with open(os.path.join(charm_path, "revision")) as f:
            self.assertEquals(f.read(), "2\n")

        upgrade_flag = yield self.service_unit1.get_upgrade_flag()
        self.assertTrue(upgrade_flag)

    @inlineCallbacks
    def test_latest_local_bundle_dry_run(self):
        """Do nothing; log that nothing would be done"""
        finished = self.setup_cli_reactor()
        self.setup_exit(0)
        self.mocker.replay()

        metadata = self.charm.metadata.get_serialization_data()
        metadata["name"] = "mysql"
        repository = self.add_charm(metadata, 1, bundle=True)
        main(["upgrade-charm", "--dry-run",
              "--repository", repository.path, "mysql"])
        yield finished
        self.assertIn(
            "Service already running latest charm",
            self.output.getvalue())

        upgrade_flag = yield self.service_unit1.get_upgrade_flag()
        self.assertFalse(upgrade_flag)

    @inlineCallbacks
    def test_latest_local_bundle_live_fire(self):
        """Do nothing; log that nothing was done"""
        finished = self.setup_cli_reactor()
        self.setup_exit(0)
        self.mocker.replay()

        metadata = self.charm.metadata.get_serialization_data()
        metadata["name"] = "mysql"
        repository = self.add_charm(metadata, 1, bundle=True)
        main(["upgrade-charm", "--repository", repository.path, "mysql"])

        yield finished
        self.assertIn(
            "Charm 'local:series/mysql-1' is the latest revision known",
            self.output.getvalue())

        upgrade_flag = yield self.service_unit1.get_upgrade_flag()
        self.assertFalse(upgrade_flag)
Example #26
0
class RemoteUpgradeCharmTest(MachineControlToolTest):

    @inlineCallbacks
    def setUp(self):
        yield super(RemoteUpgradeCharmTest, self).setUp()
        config = {
            "environments": {"firstenv": {"type": "dummy"}}}

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

        charm = CharmDirectory(os.path.join(
            test_repository_path, "series", "mysql"))
        self.charm_state_manager.add_charm_state(
            "cs:series/mysql-1", charm, "")
        self.service_state1 = yield self.add_service_from_charm(
            "mysql", "cs:series/mysql-1")
        self.service_unit1 = yield self.service_state1.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")

    @inlineCallbacks
    def test_latest_dry_run(self):
        """Do nothing; log that nothing would be done"""
        finished = self.setup_cli_reactor()
        self.setup_exit(0)
        getPage = self.mocker.replace("twisted.web.client.getPage")
        getPage(
            "https://store.juju.ubuntu.com/"
            "charm-info?charms=cs%3Aseries/mysql")
        self.mocker.result(succeed(json.dumps(
            {"cs:series/mysql": {"revision": 1, "sha256": "whatever"}})))
        self.mocker.replay()

        main(["upgrade-charm", "--dry-run", "mysql"])
        yield finished
        self.assertIn(
            "Service already running latest charm",
            self.output.getvalue())

        upgrade_flag = yield self.service_unit1.get_upgrade_flag()
        self.assertFalse(upgrade_flag)

    @inlineCallbacks
    def test_latest_live_fire(self):
        """Do nothing; log that nothing was done"""
        finished = self.setup_cli_reactor()
        self.setup_exit(0)

        getPage = self.mocker.replace("twisted.web.client.getPage")
        getPage(
            "https://store.juju.ubuntu.com/charm-info?charms=cs%3Aseries/mysql")
        self.mocker.result(succeed(json.dumps(
            {"cs:series/mysql": {"revision": 1, "sha256": "whatever"}})))
        self.mocker.replay()

        main(["upgrade-charm", "mysql"])

        yield finished
        self.assertIn(
            "Charm 'cs:series/mysql-1' is the latest revision known",
            self.output.getvalue())

        upgrade_flag = yield self.service_unit1.get_upgrade_flag()
        self.assertFalse(upgrade_flag)