diff -Nru juju-0.5+bzr447/debian/changelog juju-0.5+bzr457/debian/changelog --- juju-0.5+bzr447/debian/changelog 2012-01-23 18:16:56.000000000 +0000 +++ juju-0.5+bzr457/debian/changelog 2012-02-16 11:44:10.000000000 +0000 @@ -1,3 +1,9 @@ +juju (0.5+bzr457-0ubuntu1) precise; urgency=low + + * New upstream snapshot. + + -- Clint Byrum Thu, 16 Feb 2012 03:44:02 -0800 + juju (0.5+bzr447-0ubuntu1) precise; urgency=low * d/control: Recommend components necessary for local provider except diff -Nru juju-0.5+bzr447/docs/source/internals/unit-workflow-lifecycle.rst juju-0.5+bzr457/docs/source/internals/unit-workflow-lifecycle.rst --- juju-0.5+bzr447/docs/source/internals/unit-workflow-lifecycle.rst 1970-01-01 00:00:00.000000000 +0000 +++ juju-0.5+bzr457/docs/source/internals/unit-workflow-lifecycle.rst 2012-01-10 14:03:56.000000000 +0000 @@ -0,0 +1,138 @@ +Notes on unit agent persistence +=============================== + +Introduction +------------ + +This was first written to explain the extensive changes made in the branch +lp:~fwereade/juju/restart-transitions; that branch has been split out into +four separate branches, but this discussion should remain a useful guide to +the changes made to the unit and relation workflows and lifecycles in the +course of making the unit agent resumable. + + +Glossary +-------- + +UA = UnitAgent +UL = UnitLifecycle +UWS = UnitWorkflowState +URL = UnitRelationLifecycle +RWS = RelationWorkflowState +URS = UnitRelationState +SRS = ServiceRelationState + + +Technical discussion +-------------------- + +Probably the most fundamental change is the addition of a "synchronize" method +to both UWS and RWS. Calling "synchronize" should generally be *all* you need +to do to put the workflow and associated components into "the right state"; ie +ZK state will be restored, the appropriate lifecycle will be started (or not), +and any initial transitons will automatically be fired ("start" for RWS; +"install", "start" for UWS). + +The synchronize method keeps responsibility for the lifecycle's state purely in +the hands of the workflow; once a workflow is synced, the *only* necessary +interactions with it should be in response to changes in ZK. + +The disadvantage is that lifecycle "start" and "stop" methods have become a +touch overloaded: + +* UL.stop(): now takes "stop_relations" in addition to "fire_hooks", in which + "stop_relations" being True causes the orginal behaviour (transition "up" + RWSs to "down", as when transitioning the UWS to a "stopped" or "error" + state), but False simply causes them to stop watching for changes (in + preparation for an orderly shutdown, for example). + +* UL.start(): now takes "start_relations" in addition to "fire_hooks", in which + the "start_relations" flag being True causes the original behaviour + (automatically transition "down" RWSs to "up", as when restarting/resolving + the UWS), while False causes the RWSs only to be synced. + +* URL.start(): now takes "scheduler" in addition to "watches", allowing the + watching and the contained HookScheduler to be controlled separately + (allowing us to actually perform the RWS synchronise correctly). + +* URL.stop(): still just takes "watches", because there wasn't a scenario in + which I wanted to stop the watches but not the HookScheduler. + +I still think it's a win, though: and I don't think that turning them into +separate methods is the right way to go; "start" and "stop" remain perfectly +decent and appropriate names for what they do. + +Now this has been done, we can always launch directly into whatever state we +shut down in, and that's great, because sudden process death doesn't hurt us +any more [0] [1]. Except... when we're upgrading a charm. It emerges that the +charm upgrade state transition only covers the process of firing the hook, and +not the process of actually upgrading the charm. + +In short, we had a mechanism, completely outside the workflow's purview, for +potentially *brutal* modifications of state (both in terms of the charm itself, +on disk, and also in that the hook executor should remain stopped forever while +in "charm_upgrade_error" state); and this rather scuppered the "restart in the +same state" goal. The obvious thing to do was to move the charm upgrade +operation into the "charm_upgrade" transition, so we had a *chance* of being +able to start in the correct state. + +UL.upgrade_charm, called by UWS, does itself have subtleties, but it should be +reasonably clear when examined in context; the most important point is that it +will call back at the start and end of the risky period, and that the UWS's +handler for this callback sets a flag in "started"'s state_vars for the +duration of the upgrade. If that flag is set when we subsequently start up +again and synchronize the UWS, then we know to immediately force the +charm_upgrade_error state and work from there. + +[0] Well, it does, because we need to persist more than just the (already- +persisted) workflow state. This branch includes RWS persistence in the UL, as +requested in this branch's first pre-review (back in the day...), but does not +include HookScheduler persistence in the URLs, so it remains possible for +relation hooks which have been queued, but not yet executed, to be lost if the +process executes before the queue empties. That will be coming in another +branch (resolve-unit-relation-diffs). + +[1] This seems like a good time to mention the UL's relation-broken handling +for relations that went away while the process was stopped: every time +._relations is changed, it writes out enough state to recreate a Frankenstein's +URS object, which it can then use on load to reconstruct the necessary URL and +hence RWS. + +We don't strictly need to *reconstruct* it in every case -- we can just use +SRS.get_unit_state if the relation still exists -- but given that sometimes we +do, it seemed senseless to have two code paths for the same operations. Of the +RWSs we reconstruct, those with existing SRSs will be synchronized (because we +know it's safe to do so), and the remainder will be stored untouched (because +we know that _process_service_changes will fire the "depart" transition for us +before doing anything else... and the "relation-broken" hook will be executed +in a DepartedRelationHookContext, which is rather restricted, and so shouldn't +cause the Frankenstein's URS to hit state we can't be sure exists). + + +Appendix: a rough history of changes to restart-transitions +----------------------------------------------------------- + +* Add UWS transitions from "stopped" to "started", so that process restarts can + be made to restart UWSs. +* Upon review, add RWS persistence to UL, to ensure we can't miss + relation-broken hooks; as part of this, as discussed, add + DepartedRelationHookContext in which to execute them. +* Upon discussion, discover that original UWS "started" -> "stopped" behaviour + on process shutdown is not actually the desired behaviour (and that the + associated RWS "up" -> "down" shouldn't happen either. +* Make changes to UL.start/stop, and add UWS/RWS.synchronize, to allow us to + shut down workflows cleanly without transitions and bring them up again in + the same state. +* Discover that we don't have any other reason to transition UWS to "stopped"; + to actually fire stop hooks at the right time, we need a more sophisticated + system (possibly involving the machine agent telling the unit agent to shut + itself down). Remove the newly-added "restart" transitions, because they're + meaningless now; ponder what good it does us to have a "stopped" state that + we never actually enter; chicken out of actually removing it. +* Realise that charm upgrades do an end-run around the whole UWS mechanism, and + resolve to integrate them so I can actually detect upgrades left incomplete + due to process death. +* Move charm upgrade operation from agent into UL; come to appreciate the + subtleties of the charm upgrade process; make necessary tweaks to + UL.upgrade_charm, and UWS, to allow for synchronization of incomplete + upgrades. diff -Nru juju-0.5+bzr447/docs/source/provider-configuration-local.rst juju-0.5+bzr457/docs/source/provider-configuration-local.rst --- juju-0.5+bzr447/docs/source/provider-configuration-local.rst 2012-01-23 18:02:19.000000000 +0000 +++ juju-0.5+bzr457/docs/source/provider-configuration-local.rst 2011-10-13 00:42:45.000000000 +0000 @@ -41,11 +41,51 @@ Provider specific options ========================= - data-dir: - Directory for zookeeper state and log files. - + data-dir: + Directory for zookeeper state and log files. - +Troubleshooting +=============== +Local development is still a young and evolving feature and because of +the many complex system interactions its possible you'll encounter +some speed bumps along your path. There are a number of log files you +should be aware of when trying to understand how the system behaves. + +Once``juju bootstrap`` has run you'll have a directory in place on +your filesystem at the location specified in your +environments.yaml. In here you'll find a number of files related to +the runtime of the local deployment system. The first step is to +verify that the machine-agent.log includes a line similar to:: + + 2011-10-05 14:43:56,327: juju.agents.machine@INFO: Machine agent started id:0 deploy: provider:'local' + +When you first attempt to deploy a unit the time consuming process of +creating a unit will begin. The first time you run this it can taGke +quite some time and if your impatient and need to check on the process +`lxc-ls` will show you which templates are created or being created +and:: + + pgrep lxc| head -1| xargs watch pstree -alU + +will give you a refeshing view of the first container as its built. + +Once a container is built you should have access to it via ssh through +the `juju ssh` as normal. + +If services are not running properly as exposed by `juju status` the +log file in *data-dir/units/master-customize.log* should provide insight +into the nature of the error. + +When providing bug reports or including issues the output of the both +the master-customize.log mentioned above and the result of the +devel-tools/juju-inspect-local-provider script. If you are not running +a development checkout this script should be located at +*/usr/lib/juju/juju/misc/devel-tools/juju-inspect-local-provider* + +By passing the name of the container as an argument to that script +additional information abou the container will be included in the +output as well. To find the name of the container in question `lxc-ls` +can be used. diff -Nru juju-0.5+bzr447/juju/agents/tests/test_unit.py juju-0.5+bzr457/juju/agents/tests/test_unit.py --- juju-0.5+bzr447/juju/agents/tests/test_unit.py 2012-01-23 18:02:19.000000000 +0000 +++ juju-0.5+bzr457/juju/agents/tests/test_unit.py 2011-12-16 09:23:31.000000000 +0000 @@ -14,6 +14,8 @@ from juju.state.environment import GlobalSettingsStateManager from juju.state.errors import ServiceStateNotFound from juju.state.service import NO_HOOKS, RETRY_HOOKS +from juju.unit.lifecycle import UnitLifecycle +from juju.unit.workflow import UnitWorkflowState from juju.agents.tests.common import AgentTestBase from juju.control.tests.test_upgrade_charm import CharmUpgradeTestBase @@ -84,15 +86,83 @@ class UnitAgentTest(UnitAgentTestBase): @inlineCallbacks - def test_agent_start_stop_service(self): + def test_agent_start_stop_start_service(self): """Verify workflow state when starting and stopping the unit agent.""" self.write_empty_hooks() 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) + workflow = self.agent.lifecycle.get_relation_workflow( + self.states["unit_relation"].internal_relation_id) + relation_state = yield workflow.get_state() + self.assertEquals(relation_state, "up") + yield self.agent.stopService() current_state = yield self.agent.workflow.get_state() - self.assertEqual(current_state, "stopped") + # NOTE: stopping the unit agent does *not* imply that the service + # should not continue to run; ie don't transition to "stopped", and + # don't mark the relation states as "down" + self.assertEqual(current_state, "started") + self.assertFalse(self.agent.lifecycle.running) + self.assertFalse(self.agent.executor.running) + relation_state = yield workflow.get_state() + self.assertEquals(relation_state, "up") + + # and check we can restart as well + 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) + relation_state = yield workflow.get_state() + self.assertEquals(relation_state, "up") + + yield self.agent.stopService() + current_state = yield self.agent.workflow.get_state() + self.assertEqual(current_state, "started") + self.assertFalse(self.agent.lifecycle.running) + self.assertFalse(self.agent.executor.running) + relation_state = yield workflow.get_state() + self.assertEquals(relation_state, "up") + + @inlineCallbacks + 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")) + + 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) + + @inlineCallbacks + def test_agent_start_from_error_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")) + + yield workflow.fire_transition("install") + self.write_exit_hook("stop", 1) + yield workflow.fire_transition("stop") + + yield self.agent.startService() + current_state = yield self.agent.workflow.get_state() + self.assertEqual(current_state, "stop_error") + self.assertFalse(self.agent.lifecycle.running) + self.assertTrue(self.agent.executor.running) def test_agent_unit_name_environment_extraction(self): """Verify extraction of unit name from the environment.""" @@ -163,7 +233,7 @@ "localhost") @inlineCallbacks - def test_agent_agent_executes_install_and_start_hooks_on_startup(self): + def test_agent_executes_install_and_start_hooks_on_startup(self): """On initial startup, the unit agent executes install and start hooks. """ output_file = self.write_empty_hooks() @@ -195,26 +265,6 @@ yield self.assertState(self.agent.workflow, "install_error") @inlineCallbacks - def test_agent_executes_stop_hook_on_shutdown(self): - """On shutdown, the agent stops the hook.""" - output_file = self.write_empty_hooks() - yield self.agent.startService() - hooks_complete = self.wait_on_hook( - "stop", executor=self.agent.executor) - yield self.agent.stopService() - # Verify the hook has executed. - yield hooks_complete - self.assertEqual(self.parse_output(output_file), - ["install", "start", "stop"]) - yield self.assertState(self.agent.workflow, "stopped") - - # verify workflow state. - f_state, history, zk_state = yield self.read_persistent_state( - workflow=self.agent.workflow) - self.assertEqual(f_state, zk_state) - self.assertEqual(f_state, {"state": "stopped", "state_variables": {}}) - - @inlineCallbacks def test_agent_executes_relation_changed_hook(self): """If a relation changes after the unit is started, a relation change hook is executed.""" diff -Nru juju-0.5+bzr447/juju/agents/unit.py juju-0.5+bzr457/juju/agents/unit.py --- juju-0.5+bzr447/juju/agents/unit.py 2012-01-23 18:02:19.000000000 +0000 +++ juju-0.5+bzr457/juju/agents/unit.py 2012-01-10 14:14:28.000000000 +0000 @@ -22,6 +22,11 @@ log = logging.getLogger("juju.agents.unit") +def unit_path(juju_path, unit_state): + return os.path.join( + juju_path, "units", unit_state.unit_name.replace("/", "-")) + + class UnitAgent(BaseAgent): """An juju Unit Agent. @@ -62,7 +67,6 @@ 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] @@ -72,10 +76,8 @@ self.unit_state = yield service_state.get_unit_state( self.unit_name) self.unit_directory = os.path.join( - self.config["juju_directory"], - "units", + self.config["juju_directory"], "units", self.unit_state.unit_name.replace("/", "-")) - self.state_directory = os.path.join( self.config["juju_directory"], "state") @@ -92,43 +94,36 @@ 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() self.lifecycle = UnitLifecycle( - self.client, - self.unit_state, - service_state, - self.unit_directory, - self.executor) + self.client, self.unit_state, service_state, self.unit_directory, + self.state_directory, self.executor) self.workflow = UnitWorkflowState( - self.client, - self.unit_state, - self.lifecycle, - self.state_directory) + 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. + yield self.workflow.synchronize(self.executor) 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.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() @@ -232,7 +227,7 @@ # Verify the workflow state workflow_state = yield self._agent.workflow.get_state() - if not workflow_state in ("started",): + if workflow_state != "started": self._log.warning( "Unit not in an upgradeable state: %s", workflow_state) # Upgrades can only be supported while the unit is diff -Nru juju-0.5+bzr447/juju/control/deploy.py juju-0.5+bzr457/juju/control/deploy.py --- juju-0.5+bzr447/juju/control/deploy.py 2012-01-23 18:02:19.000000000 +0000 +++ juju-0.5+bzr457/juju/control/deploy.py 2012-01-26 19:39:06.000000000 +0000 @@ -65,7 +65,7 @@ num_units=options.num_units) -def parse_config_options(config_file, service_name): +def parse_config_options(config_file, service_name, charm): if not os.path.exists(config_file) or \ not os.access(config_file, os.R_OK): raise ServiceConfigValueError( @@ -77,8 +77,9 @@ raise ServiceConfigValueError( "Invalid options file passed to --config.\n" "Expected a YAML dict with service name (%r)." % service_name) - # remove the service name prefix for application to state - return options[service_name] + + # Validate and type service options and return + return charm.config.validate(options[service_name]) @inlineCallbacks @@ -93,14 +94,15 @@ repo, charm_url = resolve( charm_name, repository_path, environment.default_series) + charm = yield repo.find(charm_url) + charm_id = str(charm_url.with_revision(charm.get_revision())) + # Validate config options prior to deployment attempt service_options = {} service_name = service_name or charm_url.name if config_file: - service_options = parse_config_options(config_file, service_name) - - charm = yield repo.find(charm_url) - charm_id = str(charm_url.with_revision(charm.get_revision())) + service_options = parse_config_options( + config_file, service_name, charm) provider = environment.get_machine_provider() placement_policy = provider.get_placement_policy() diff -Nru juju-0.5+bzr447/juju/control/tests/test_add_relation.py juju-0.5+bzr457/juju/control/tests/test_add_relation.py --- juju-0.5+bzr447/juju/control/tests/test_add_relation.py 2012-01-23 18:02:19.000000000 +0000 +++ juju-0.5+bzr457/juju/control/tests/test_add_relation.py 2012-01-23 23:17:03.000000000 +0000 @@ -201,7 +201,9 @@ self.mocker.replay() main(["add-relation", "wordpress", "mysql"]) yield wait_on_reactor_stopped - self.assertIn("Relation already exists", self.stderr.getvalue()) + self.assertIn( + "Relation mysql already exists between wordpress and mysql", + self.stderr.getvalue()) @inlineCallbacks def test_invalid_environment(self): diff -Nru juju-0.5+bzr447/juju/control/tests/test_deploy.py juju-0.5+bzr457/juju/control/tests/test_deploy.py --- juju-0.5+bzr447/juju/control/tests/test_deploy.py 2012-01-23 18:02:19.000000000 +0000 +++ juju-0.5+bzr457/juju/control/tests/test_deploy.py 2012-01-26 19:39:06.000000000 +0000 @@ -8,7 +8,7 @@ from juju.environment.config import EnvironmentsConfig from juju.charm.errors import ServiceConfigValueError from juju.state.environment import EnvironmentStateManager -from juju.state.errors import ServiceStateNameInUse +from juju.state.errors import ServiceStateNameInUse, ServiceStateNotFound from juju.state.service import ServiceStateManager from juju.state.relation import RelationStateManager @@ -290,6 +290,24 @@ "Expected a YAML dict with service name ('myblog').", str(error)) @inlineCallbacks + def test_deploy_with_invalid_config(self): + """Can't deploy with config that doesn't pass charm validation.""" + config_file = self.makeFile( + yaml.dump(dict(myblog=dict(application_file="foo")))) + environment = self.config.get("firstenv") + + failure = deploy.deploy( + self.config, environment, self.unbundled_repo_path, "local:sample", + "myblog", logging.getLogger("deploy"), config_file) + error = yield self.assertFailure(failure, ServiceConfigValueError) + self.assertIn( + "application_file is not a valid configuration option", + str(error)) + yield self.assertFailure( + ServiceStateManager(self.client).get_service_state("myblog"), + ServiceStateNotFound) + + @inlineCallbacks def test_deploy_with_config(self): """Valid config options should be available to the deployed service.""" diff -Nru juju-0.5+bzr447/juju/control/tests/test_resolved.py juju-0.5+bzr457/juju/control/tests/test_resolved.py --- juju-0.5+bzr447/juju/control/tests/test_resolved.py 2012-01-23 18:02:19.000000000 +0000 +++ juju-0.5+bzr457/juju/control/tests/test_resolved.py 2012-01-12 10:18:07.000000000 +0000 @@ -93,7 +93,8 @@ service_relation.relation_name, self.makeDir(), self.executor) workflow_state = RelationWorkflowState( - self.client, unit_relation, lifecycle, self.makeDir()) + self.client, unit_relation, service_relation.relation_name, + lifecycle, self.makeDir()) yield workflow_state.set_state(state) @inlineCallbacks diff -Nru juju-0.5+bzr447/juju/hooks/scheduler.py juju-0.5+bzr457/juju/hooks/scheduler.py --- juju-0.5+bzr447/juju/hooks/scheduler.py 2012-01-23 18:02:19.000000000 +0000 +++ juju-0.5+bzr457/juju/hooks/scheduler.py 2011-12-12 01:56:05.000000000 +0000 @@ -53,6 +53,10 @@ # Artifical clock sequence self._clock_sequence = 0 + @property + def running(self): + return self._running is True + @inlineCallbacks def run(self): """Run the hook scheduler and execution.""" diff -Nru juju-0.5+bzr447/juju/hooks/tests/test_invoker.py juju-0.5+bzr457/juju/hooks/tests/test_invoker.py --- juju-0.5+bzr447/juju/hooks/tests/test_invoker.py 2012-01-23 18:02:19.000000000 +0000 +++ juju-0.5+bzr457/juju/hooks/tests/test_invoker.py 2012-02-08 22:19:19.000000000 +0000 @@ -176,7 +176,7 @@ def create_hook(self, hook, arguments): bin_path = self.get_cli_hook(hook) - fn = self.makeFile("#!/bin/sh\n%s %s" % (bin_path, arguments)) + fn = self.makeFile("#!/bin/sh\n'%s' %s" % (bin_path, arguments)) # make the hook executable os.chmod(fn, stat.S_IEXEC | stat.S_IREAD) return fn diff -Nru juju-0.5+bzr447/juju/hooks/tests/test_scheduler.py juju-0.5+bzr457/juju/hooks/tests/test_scheduler.py --- juju-0.5+bzr447/juju/hooks/tests/test_scheduler.py 2012-01-23 18:02:19.000000000 +0000 +++ juju-0.5+bzr457/juju/hooks/tests/test_scheduler.py 2011-12-12 01:56:05.000000000 +0000 @@ -99,13 +99,17 @@ # Other stuff. @inlineCallbacks def test_start_stop(self): + self.assertFalse(self.scheduler.running) d = self.scheduler.run() + self.assertTrue(self.scheduler.running) # starting multiple times results in an error self.assertFailure(self.scheduler.run(), AssertionError) self.scheduler.stop() + self.assertFalse(self.scheduler.running) yield d # stopping multiple times is not an error yield self.scheduler.stop() + self.assertFalse(self.scheduler.running) @inlineCallbacks def test_membership_visibility_per_change(self): diff -Nru juju-0.5+bzr447/juju/lib/lxc/data/juju-create juju-0.5+bzr457/juju/lib/lxc/data/juju-create --- juju-0.5+bzr447/juju/lib/lxc/data/juju-create 2012-01-23 18:02:19.000000000 +0000 +++ juju-0.5+bzr457/juju/lib/lxc/data/juju-create 2012-02-16 01:15:19.000000000 +0000 @@ -1,6 +1,8 @@ #!/bin/bash set -x +APT_OPTIONS="-o Dpkg::Options::= --force-confnew --force-yes -fuy" + setup_apt_cache() { # We ask the host to run apt-cache-server by default speeding the deployment of n units @@ -17,6 +19,9 @@ setup_networking() { + # Ensure we have the resolvconf installed before configuring it. + apt-get install $APT_OPTIONS resolvconf + # Use dnsmasq to resolve names from the bridge # This script executes from a chroot, so directly modify the output file. cat < /etc/resolvconf/run/resolv.conf @@ -39,17 +44,17 @@ setup_users() { id ubuntu - if [ $? == 0 ]; then - return 0 + if [ $? != 0 ]; then + # add the ubuntu user. + adduser ubuntu --disabled-password --shell /bin/bash --gecos "" fi - # add the ubuntu user and allow for ssh - adduser ubuntu --disabled-password --shell /bin/bash --gecos "" - - # SSH access + # Allow SSH access if [ -n "$JUJU_PUBLIC_KEY" ]; then - mkdir /home/ubuntu/.ssh - chmod 700 /home/ubuntu/.ssh + if [ ! -e /home/ubuntu/.ssh ]; then + mkdir /home/ubuntu/.ssh + chmod 700 /home/ubuntu/.ssh + fi echo $JUJU_PUBLIC_KEY >> /home/ubuntu/.ssh/authorized_keys chmod 700 /home/ubuntu/.ssh/authorized_keys chown -R ubuntu:ubuntu /home/ubuntu/.ssh @@ -89,8 +94,6 @@ fi export DEBIAN_FRONTEND=noninteractive - APT_OPTIONS="-o Dpkg::Options::= --force-confnew --force-yes -fuy" - echo "Setting up juju in container" apt-get install $APT_OPTIONS bzr tmux sudo python-software-properties python-yaml @@ -146,3 +149,5 @@ setup_juju # setup_juju ensures sudo is installed which is needed for setup_users setup_users + +echo "Container Customization Complete" \ No newline at end of file diff -Nru juju-0.5+bzr447/juju/lib/lxc/__init__.py juju-0.5+bzr457/juju/lib/lxc/__init__.py --- juju-0.5+bzr447/juju/lib/lxc/__init__.py 2012-01-23 18:02:19.000000000 +0000 +++ juju-0.5+bzr457/juju/lib/lxc/__init__.py 2012-02-11 15:49:41.000000000 +0000 @@ -148,6 +148,7 @@ def __init__(self, container_name, public_key, + series, origin, origin_source=None, network_name="virbr0", @@ -161,6 +162,8 @@ :param public_key: SSH public key + :param series: distro release series (oneiric, precise, etc) + :param origin: distro|ppa|branch :param origin_source: when origin is branch supply a valid bzr branch @@ -186,6 +189,7 @@ self.public_key = public_key self.origin = origin self.source = origin_source + self.series = series self.network_name = network_name @property @@ -260,7 +264,10 @@ """Create the container synchronously.""" if not self.is_constructed(): lxc_config = self._make_lxc_config(self.network_name) - _lxc_create(self.container_name, config_file=lxc_config) + _lxc_create( + self.container_name, + config_file=lxc_config, + release=self.series) os.unlink(lxc_config) self._customize_container() @@ -286,7 +293,8 @@ debug_log=self.debug_log, console_log=self.console_log, customize_script=self.customize_script, - network_name=self.network_name) + network_name=self.network_name, + series=self.series) if not container.is_constructed(): _lxc_clone(self.container_name, container_name) diff -Nru juju-0.5+bzr447/juju/lib/lxc/tests/test_lxc.py juju-0.5+bzr457/juju/lib/lxc/tests/test_lxc.py --- juju-0.5+bzr447/juju/lib/lxc/tests/test_lxc.py 2012-01-23 18:02:19.000000000 +0000 +++ juju-0.5+bzr457/juju/lib/lxc/tests/test_lxc.py 2012-02-11 15:49:41.000000000 +0000 @@ -69,7 +69,6 @@ self.addCleanup(self.clean_container, DEFAULT_CONTAINER) _lxc_create(DEFAULT_CONTAINER, config_file=self.config) - _lxc_start(DEFAULT_CONTAINER) _lxc_stop(DEFAULT_CONTAINER) @@ -84,12 +83,15 @@ def test_lxc_container(self): self.addCleanup(self.clean_container, DEFAULT_CONTAINER) customize_log = self.makeFile() - c = LXCContainer( - DEFAULT_CONTAINER, "dsa...", "ppa", customize_log=customize_log) - + c = LXCContainer(DEFAULT_CONTAINER, + "dsa...", + "precise", + "ppa", + customize_log=customize_log) running = yield c.is_running() self.assertFalse(running) self.assertFalse(c.is_constructed()) + # verify we can't run a non-constructed container failure = c.run() yield self.assertFailure(failure, LXCError) @@ -107,13 +109,13 @@ output = _lxc_ls() self.assertIn(DEFAULT_CONTAINER, output) - # verify we have a path into the container + # Verify we have a path into the container self.assertTrue(os.path.exists(c.rootfs)) self.assertTrue(c.is_constructed()) - self.verify_container(c, "dsa...", "ppa") + self.verify_container(c, "dsa...", "precise", "ppa") - # verify that we are in containers + # Verify that we are in containers containers = yield get_containers(None) self.assertEqual(containers[DEFAULT_CONTAINER], True) @@ -158,7 +160,8 @@ master_container = LXCContainer(DEFAULT_CONTAINER, origin="ppa", - public_key="dsa...") + public_key="dsa...", + series="oneiric") # verify that we cannot clone an unconstructed container failure = master_container.clone("test_lxc_fail") @@ -182,7 +185,7 @@ output = _lxc_ls() self.assertIn(DEFAULT_CONTAINER, output) - self.verify_container(c, "dsa...", "ppa") + self.verify_container(c, "dsa...", "oneiric", "ppa") # verify that we are in containers containers = yield get_containers(None) @@ -202,7 +205,7 @@ yield master_container.destroy() - def verify_container(self, c, public_key, origin): + def verify_container(self, c, public_key, series, origin): """Verify properties of an LXCContainer""" def p(path): @@ -259,6 +262,11 @@ self.assertIn('Acquire::http { Proxy "http://192.168.122.1:3142"; };', apt_proxy) + # Verify the container release series. + with open(os.path.join(c.rootfs, "etc", "lsb-release")) as fh: + lsb_info = fh.read() + self.assertIn(series, lsb_info) + # check basic juju installation # these could be more through if origin == "ppa": diff -Nru juju-0.5+bzr447/juju/machine/tests/test_unit_deployment.py juju-0.5+bzr457/juju/machine/tests/test_unit_deployment.py --- juju-0.5+bzr447/juju/machine/tests/test_unit_deployment.py 2012-01-23 18:02:19.000000000 +0000 +++ juju-0.5+bzr457/juju/machine/tests/test_unit_deployment.py 2012-02-11 16:28:50.000000000 +0000 @@ -348,6 +348,8 @@ # Setup unit namespace environ = dict(os.environ) environ["JUJU_UNIT_NS"] = "ns1" + environ["JUJU_SERIES"] = "precise" + self.change_environment(**environ) self.unit_deploy = UnitContainerDeployment( @@ -411,7 +413,7 @@ @inlineCallbacks def test_start(self): - container = LXCContainer(self.unit_name, None, None, None) + container = LXCContainer(self.unit_name, None, "precise", None) rootfs = self.makeDir() env = dict(os.environ) env["JUJU_PUBLIC_KEY"] = "dsa ..." @@ -442,7 +444,6 @@ self.assertIn('JUJU_ZOOKEEPER="127.0.1.1:2181"', job) self.assertIn('JUJU_MACHINE_ID="0"', job) self.assertIn('JUJU_UNIT_NAME="riak/0"', job) - # Verify the symlink exists self.assertTrue(os.path.lexists(os.path.join( self.unit_deploy.juju_home, "units", diff -Nru juju-0.5+bzr447/juju/machine/unit.py juju-0.5+bzr457/juju/machine/unit.py --- juju-0.5+bzr447/juju/machine/unit.py 2012-01-23 18:02:19.000000000 +0000 +++ juju-0.5+bzr457/juju/machine/unit.py 2012-02-11 16:28:50.000000000 +0000 @@ -189,7 +189,9 @@ self._unit_namespace = os.environ.get("JUJU_UNIT_NS") self._juju_origin = os.environ.get("JUJU_ORIGIN") + self._juju_series = os.environ.get("JUJU_SERIES") assert self._unit_namespace is not None, "Required unit ns not found" + assert self._juju_series is not None, "Required juju series not found" self.pid_file = None self.container = LXCContainer(self.container_name, None, None, None) @@ -250,7 +252,7 @@ master_template = LXCContainer( container_template_name, origin=self._juju_origin, - public_key=public_key) + public_key=public_key, series=self._juju_series) # Debug log for the customize script, customize is only run on master. customize_log_path = os.path.join( diff -Nru juju-0.5+bzr447/juju/providers/local/agent.py juju-0.5+bzr457/juju/providers/local/agent.py --- juju-0.5+bzr447/juju/providers/local/agent.py 2012-01-23 18:02:19.000000000 +0000 +++ juju-0.5+bzr457/juju/providers/local/agent.py 2012-02-11 15:49:41.000000000 +0000 @@ -15,11 +15,12 @@ agent_module = "juju.agents.machine" def __init__( - self, pid_file, zookeeper_hosts=None, machine_id="0", + self, pid_file, juju_series=None, zookeeper_hosts=None, machine_id="0", log_file=None, juju_directory="/var/lib/juju", juju_unit_namespace="", public_key=None, juju_origin="ppa"): """ :param pid_file: Path to file used to store process id. + :param juju_series: The release series to use (maverick, natty, etc). :param machine_id: machine id for the local machine. :param zookeeper_hosts: Zookeeper hosts to connect. :param log_file: A file to use for the agent logs. @@ -39,6 +40,7 @@ self._log_file = log_file self._public_key = public_key self._juju_origin = juju_origin + self._juju_series = juju_series if self._juju_origin is None: origin, source = get_default_origin() @@ -54,7 +56,7 @@ def start(self): """Start the machine agent. """ - assert self._zookeeper_hosts and self._log_file + assert self._zookeeper_hosts and self._log_file and self._juju_series if (yield self.is_running()): return @@ -67,6 +69,7 @@ "JUJU_MACHINE_ID=%s" % self._machine_id, "JUJU_HOME=%s" % self._juju_directory, "JUJU_UNIT_NS=%s" % self._juju_unit_namespace, + "JUJU_SERIES=%s" % self._juju_series, "PYTHONPATH=%s" % ":".join(sys.path), sys.executable, "-m", self.agent_module, "-n", "--pidfile", self._pid_file, diff -Nru juju-0.5+bzr447/juju/providers/local/__init__.py juju-0.5+bzr457/juju/providers/local/__init__.py --- juju-0.5+bzr447/juju/providers/local/__init__.py 2012-01-23 18:02:19.000000000 +0000 +++ juju-0.5+bzr457/juju/providers/local/__init__.py 2012-02-16 01:15:19.000000000 +0000 @@ -70,6 +70,13 @@ raise ProviderError("Missing packages %s" % ( ", ".join(sorted(list(missing))))) + # Store user credentials from the running user + try: + public_key = get_user_authorized_keys(self.config) + public_key = public_key.strip() + except LookupError, e: + raise ProviderError(str(e)) + # Get/create directory for zookeeper and files zookeeper_dir = os.path.join(self._directory, "zookeeper") if not os.path.exists(zookeeper_dir): @@ -122,13 +129,6 @@ hierarchy = StateHierarchy(client, admin_identity, "local", "local") yield hierarchy.initialize() - # Store user credentials from the running user - try: - public_key = get_user_authorized_keys(self.config) - public_key = public_key.strip() - except LookupError, e: - raise ProviderError(str(e)) - # Startup the machine agent pid_file = os.path.join(self._directory, "machine-agent.pid") log_file = os.path.join(self._directory, "machine-agent.log") @@ -141,7 +141,8 @@ log_file=log_file, juju_origin=juju_origin, juju_unit_namespace=self._qualified_name, - public_key=public_key) + public_key=public_key, + juju_series=self.config["default-series"]) log.info( "Starting machine agent (origin: %s)... ", agent.juju_origin) yield agent.start() diff -Nru juju-0.5+bzr447/juju/providers/local/tests/test_agent.py juju-0.5+bzr457/juju/providers/local/tests/test_agent.py --- juju-0.5+bzr447/juju/providers/local/tests/test_agent.py 2012-01-23 18:02:19.000000000 +0000 +++ juju-0.5+bzr457/juju/providers/local/tests/test_agent.py 2012-02-11 15:49:41.000000000 +0000 @@ -27,7 +27,7 @@ log_file = self.makeFile() agent = ManagedMachineAgent( - pid_file, get_test_zookeeper_address(), + pid_file, "precise", get_test_zookeeper_address(), juju_directory=juju_directory, log_file=log_file, juju_unit_namespace="ns1", juju_origin="lp:juju/trunk") @@ -50,7 +50,8 @@ JUJU_MACHINE_ID="0", JUJU_HOME=juju_directory, JUJU_ORIGIN="lp:juju/trunk", - JUJU_UNIT_NS="ns1")) + JUJU_UNIT_NS="ns1", + JUJU_SERIES="precise")) @inlineCallbacks def test_managed_agent_root(self): @@ -66,14 +67,13 @@ self.addCleanup(cleanup_root_file, log_file) agent = ManagedMachineAgent( - pid_file, machine_id="0", log_file=log_file, + pid_file, "precise", machine_id="0", log_file=log_file, zookeeper_hosts=get_test_zookeeper_address(), juju_directory=juju_directory) agent.agent_module = "juju.agents.dummy" self.assertFalse((yield agent.is_running())) yield agent.start() - # Give a moment for the process to start and write its config yield self.sleep(0.1) self.assertTrue((yield agent.is_running())) diff -Nru juju-0.5+bzr447/juju/providers/local/tests/test_provider.py juju-0.5+bzr457/juju/providers/local/tests/test_provider.py --- juju-0.5+bzr447/juju/providers/local/tests/test_provider.py 2012-01-23 18:02:19.000000000 +0000 +++ juju-0.5+bzr457/juju/providers/local/tests/test_provider.py 2012-01-25 17:34:19.000000000 +0000 @@ -29,7 +29,7 @@ self.provider = MachineProvider( "local-test", { "admin-secret": "admin:abc", "data-dir": self.makeDir(), - "authorized-keys": "fooabc123"}) + "authorized-keys": "fooabc123", "default-series": "oneiric"}) self.output = self.capture_logging( "juju.local-dev", level=logging.DEBUG) zookeeper.set_debug_level(0) diff -Nru juju-0.5+bzr447/juju/state/errors.py juju-0.5+bzr457/juju/state/errors.py --- juju-0.5+bzr447/juju/state/errors.py 2012-01-23 18:02:19.000000000 +0000 +++ juju-0.5+bzr457/juju/state/errors.py 2012-01-28 07:57:35.000000000 +0000 @@ -187,11 +187,18 @@ class RelationAlreadyExists(StateError): - def __init__(self, *endpoints): + def __init__(self, endpoints): self.endpoints = endpoints def __str__(self): - return "Relation already exists %r" % (self.endpoints,) + services = [endpoint.service_name for endpoint in self.endpoints] + if len(services) > 1: + return "Relation %s already exists between %s" % ( + self.endpoints[0].relation_type, + " and ".join(services)) + else: + return "Relation %s already exists for %s" % ( + self.endpoints[0].relation_type, services[0]) class RelationStateNotFound(StateError): @@ -308,3 +315,7 @@ requested = "%s %s" % self.requested_pair return "Ambiguous relation %r; could refer to:\n%s" % ( requested, "\n".join(sorted(relations))) + + +class RelationBrokenContextError(StateError): + """An inappropriate operation was attempted in a relation-broken hook""" diff -Nru juju-0.5+bzr447/juju/state/hook.py juju-0.5+bzr457/juju/state/hook.py --- juju-0.5+bzr447/juju/state/hook.py 2012-01-23 18:02:19.000000000 +0000 +++ juju-0.5+bzr457/juju/state/hook.py 2011-12-15 10:08:48.000000000 +0000 @@ -1,6 +1,8 @@ -from twisted.internet.defer import inlineCallbacks, returnValue +from twisted.internet.defer import inlineCallbacks, returnValue, succeed, fail + from juju.state.base import StateBase -from juju.state.errors import (UnitRelationStateNotFound, StateNotFound) +from juju.state.errors import ( + UnitRelationStateNotFound, StateNotFound, RelationBrokenContextError) from juju.state.service import ServiceStateManager, parse_service_name from juju.state.utils import YAMLState @@ -71,7 +73,8 @@ def get_local_unit_state(self): """Return ServiceUnitState for the local service unit.""" service_state_manager = ServiceStateManager(self._client) - unit_state = yield service_state_manager.get_unit_state(self._unit_name) + unit_state = yield service_state_manager.get_unit_state( + self._unit_name) returnValue(unit_state) @inlineCallbacks @@ -84,10 +87,6 @@ parse_service_name(self._unit_name))) returnValue(self._service) - def _settings_path(self, unit_id): - return "/relations/%s/settings/%s" % ( - self._unit_relation.internal_relation_id, unit_id) - @inlineCallbacks def get_config(self): """Gather the configuration options. @@ -131,6 +130,10 @@ # A cache of related units in the relation. self._members = members + def _settings_path(self, unit_id): + return "/relations/%s/settings/%s" % ( + self._unit_relation.internal_relation_id, unit_id) + @inlineCallbacks def get_members(self): """Gets the related unit members of the relation with caching.""" @@ -248,3 +251,66 @@ yield super(RelationHookContext, self).flush() returnValue(relation_setting_changes) + +class DepartedRelationHookContext(HookContext): + """A hook execution context suitable for running a relation-broken hook. + + This context exposes the same interface as RelationHookContext, but: + + * relation settings cannot be changed + * no remote units are reported to exist + * remote unit settings are not accessible + """ + + def __init__(self, client, unit_name, unit_id, relation_name, relation_id): + super(DepartedRelationHookContext, self).__init__(client, unit_name) + self._relation_name = relation_name + self._relation_id = relation_id + self._settings_path = "/relations/%s/settings/%s" % ( + relation_id, unit_id) + + # Cache of relation settings for the local unit + self._relation_cache = None + + def get_members(self): + return succeed([]) + + @inlineCallbacks + def get(self, unit_name): + # Only this unit's settings should be accessible. + if unit_name not in (None, self._unit_name): + raise RelationBrokenContextError( + "Cannot access other units in broken relation") + + if self._relation_cache is None: + relation_data = YAMLState(self._client, self._settings_path) + try: + yield relation_data.read(required=True) + self._relation_cache = dict(relation_data) + except StateNotFound: + self._relation_cache = {} + returnValue(self._relation_cache) + + @inlineCallbacks + def get_value(self, unit_name, key): + settings = yield self.get(unit_name) + returnValue(settings.get(key, "")) + + def set(self, data): + return fail(RelationBrokenContextError( + "Cannot change settings in broken relation")) + + def set_value(self, key, value): + return fail(RelationBrokenContextError( + "Cannot change settings in broken relation")) + + def delete_value(self, key): + return fail(RelationBrokenContextError( + "Cannot change settings in broken relation")) + + def has_read(self, unit_name): + """Has the context been used to access the settings of the unit. + """ + if unit_name in (None, self._unit_name): + return self._relation_cache is not None + return False diff -Nru juju-0.5+bzr447/juju/state/relation.py juju-0.5+bzr457/juju/state/relation.py --- juju-0.5+bzr447/juju/state/relation.py 2012-01-23 18:02:19.000000000 +0000 +++ juju-0.5+bzr457/juju/state/relation.py 2011-12-12 01:56:05.000000000 +0000 @@ -484,6 +484,10 @@ self._unit_name_map = None self._log = logging.getLogger("unit.relation.watch") + @property + def running(self): + return self._stopped is False + def _watch_container(self, watch_established_callback=None): """Watch the service role container, for related units. """ diff -Nru juju-0.5+bzr447/juju/state/service.py juju-0.5+bzr457/juju/state/service.py --- juju-0.5+bzr447/juju/state/service.py 2012-01-23 18:02:19.000000000 +0000 +++ juju-0.5+bzr457/juju/state/service.py 2012-02-04 00:01:06.000000000 +0000 @@ -1335,7 +1335,7 @@ data = yaml.load(content) if data is None: data = {} - open_ports = data.setdefault("open", ()) + open_ports = data.setdefault("open", []) port_proto = dict(port=port, proto=proto) if port_proto in open_ports: open_ports.remove(port_proto) diff -Nru juju-0.5+bzr447/juju/state/tests/test_errors.py juju-0.5+bzr457/juju/state/tests/test_errors.py --- juju-0.5+bzr447/juju/state/tests/test_errors.py 2012-01-23 18:02:19.000000000 +0000 +++ juju-0.5+bzr457/juju/state/tests/test_errors.py 2012-01-28 07:57:35.000000000 +0000 @@ -15,7 +15,8 @@ NoMatchingEndpoints, AmbiguousRelation, ServiceUnitStateMachineNotAssigned, ServiceUnitDebugAlreadyEnabled, ServiceUnitResolvedAlreadyEnabled, - ServiceUnitRelationResolvedAlreadyEnabled, PrincipalNotFound) + ServiceUnitRelationResolvedAlreadyEnabled, PrincipalNotFound, + RelationBrokenContextError) class StateErrorsTest(TestCase): @@ -139,11 +140,12 @@ def test_relation_already_exists(self): error = RelationAlreadyExists( - RelationEndpoint("wordpress", "mysql", "mysql", "client"), - RelationEndpoint("mysql", "mysql", "db", "server")) + (RelationEndpoint("wordpress", "mysql", "mysql", "client"), + RelationEndpoint("mysql", "mysql", "db", "server"))) self.assertIsStateError(error) - self.assertTrue("wordpress" in str(error)) - self.assertTrue("mysql" in str(error)) + self.assertEqual( + str(error), + "Relation mysql already exists between wordpress and mysql") def test_relation_state_not_found(self): error = RelationStateNotFound() @@ -213,3 +215,8 @@ Ambiguous relation 'myblog mydb'; could refer to: 'myblog:db mydb:db' (mysql client / mysql server) 'myblog:db mydb:db-admin' (mysql client / mysql server)""")) + + def test_relation_broken_context(self): + error = RelationBrokenContextError("+++ OUT OF CHEESE ERROR +++") + self.assertIsStateError(error) + self.assertEquals(str(error), "+++ OUT OF CHEESE ERROR +++") diff -Nru juju-0.5+bzr447/juju/state/tests/test_hook.py juju-0.5+bzr457/juju/state/tests/test_hook.py --- juju-0.5+bzr447/juju/state/tests/test_hook.py 2012-01-23 18:02:19.000000000 +0000 +++ juju-0.5+bzr457/juju/state/tests/test_hook.py 2012-01-23 18:25:40.000000000 +0000 @@ -5,8 +5,10 @@ from juju.state.endpoint import RelationEndpoint from juju.state.hook import ( - HookContext, RelationChange, RelationHookContext) -from juju.state.errors import UnitRelationStateNotFound + DepartedRelationHookContext, HookContext, RelationChange, + RelationHookContext) +from juju.state.errors import ( + UnitRelationStateNotFound, RelationBrokenContextError) from juju.state.tests.test_relation import RelationTestBase from juju.state.utils import AddedItem, DeletedItem, ModifiedItem @@ -21,11 +23,67 @@ self.assertEqual(change.unit_name, "mysql/0") -class ExecutionContextTest(RelationTestBase): +class CommonHookContextTestsMixin(object): + + @inlineCallbacks + def test_config_get(self): + """Verify we can get config settings. + + This is a simple test that basic I/O works through the + context. + """ + config = yield self.service.get_config() + config.update({"hello": "world"}) + yield config.write() + + data = yield self.context.get_config() + self.assertEqual(data, {"hello": "world", "title": "My Title", + "username": "admin001"}) + + # Verify that context.flush triggers writes as well + data["goodbye"] = "goodnight" + yield self.context.flush() + # get a new yamlstate from the service itself + config = yield self.service.get_config() + self.assertEqual(config["goodbye"], "goodnight") + + @inlineCallbacks + def test_config_get_cache(self): + """Verify we can get config settings. + + This is a simple test that basic I/O works through the + context. + """ + config = yield self.service.get_config() + config.update({"hello": "world"}) + yield config.write() + + data = yield self.context.get_config() + self.assertEqual(data, {"hello": "world", + "title": "My Title", + "username": "admin001"}) + + d2 = yield self.context.get_config() + self.assertIs(data, d2) + + @inlineCallbacks + def test_hook_knows_service(self): + """Verify that hooks can get their local service.""" + service = yield self.context.get_local_service() + self.assertEqual(service.service_name, self.service.service_name) + + @inlineCallbacks + def test_hook_knows_unit_state(self): + """Verify that hook has access to its local unit state.""" + unit = yield self.context.get_local_unit_state() + self.assertEqual(unit.unit_name, self.unit.unit_name) + + +class HookContextTestBase(RelationTestBase): @inlineCallbacks def setUp(self): - yield super(ExecutionContextTest, self).setUp() + yield super(HookContextTestBase, self).setUp() wordpress_ep = RelationEndpoint( "wordpress", "client-server", "", "client") mysql_ep = RelationEndpoint( @@ -37,18 +95,39 @@ self.wordpress_states) self.relation = self.mysql_states["relation"] - def get_execution_context(self, states, change_type, unit_name): + +class HookContextTest(HookContextTestBase, CommonHookContextTestsMixin): + + @inlineCallbacks + def setUp(self): + yield super(HookContextTest, self).setUp() + self.service = self.wordpress_states["service"] + self.unit = self.wordpress_states["unit"] + self.context = HookContext( + self.client, unit_name=self.unit.unit_name) + + +class RelationHookContextTest(HookContextTestBase, CommonHookContextTestsMixin): + + @inlineCallbacks + def setUp(self): + yield super(RelationHookContextTest, self).setUp() + self.service = self.wordpress_states["service"] + self.unit = self.wordpress_states["unit"] + self.reset_context() + + def reset_context(self): + self.context = self.get_context( + self.wordpress_states, "modified", "mysql/0") + + def get_context(self, states, change_type, unit_name): change = RelationChange( states["service_relation"].relation_name, change_type, unit_name) - return RelationHookContext(self.client, - states["unit_relation"], - change, - unit_name=unit_name) - - def get_config_execution_context(self, states): - return HookContext(self.client, unit_name=states["unit"].unit_name) + return RelationHookContext( + self.client, states["unit_relation"], change, + unit_name=self.unit.unit_name) @inlineCallbacks def test_get(self): @@ -61,10 +140,7 @@ self.mocker.passthrough() self.mocker.replay() - context = self.get_execution_context( - self.wordpress_states, "modified", "mysql/0") - - data = yield context.get("mysql/0") + data = yield self.context.get("mysql/0") self.assertEqual(data, {"hello": "world"}) @inlineCallbacks @@ -73,26 +149,24 @@ values does not modify the underlying write buffer. They must be explicitly set. """ - context = self.get_execution_context( - self.wordpress_states, "modified", "mysql/0") - yield context.set_value("hello", u"world") - data = yield context.get("wordpress/0") + yield self.context.set_value("hello", u"world") + data = yield self.context.get("wordpress/0") self.assertEqual( data, {"hello": "world", "private-address": "wordpress-0.example.com"}) del data["hello"] - current_data = yield context.get("wordpress/0") + current_data = yield self.context.get("wordpress/0") self.assertNotEqual(current_data, data) self.client.set(self.get_unit_settings_path(self.mysql_states), yaml.dump({"hello": "world"})) - data = yield context.get("mysql/0") + data = yield self.context.get("mysql/0") data["abc"] = 1 - data = yield context.get("mysql/0") + data = yield self.context.get("mysql/0") del data["hello"] - current_data = yield context.get("mysql/0") + current_data = yield self.context.get("mysql/0") self.assertEqual(current_data, {"hello": "world"}) @inlineCallbacks @@ -101,17 +175,14 @@ # Getting a value from an existing empty unit is returns empty # strings for all keys. - context = self.get_execution_context( - self.wordpress_states, "modified", "mysql/0") - port = yield context.get_value("mysql/0", "port") + port = yield self.context.get_value("mysql/0", "port") self.assertEqual(port, "") # Write some data to retrieve and refetch the context yield self.mysql_states["unit_relation"].set_data({ "host": "xe.example.com", "port": 2222}) - context = self.get_execution_context( - self.wordpress_states, "modified", "mysql/0") + self.reset_context() # use mocker to verify we only access the node once. mock_client = self.mocker.patch(self.client) @@ -119,18 +190,18 @@ self.mocker.passthrough() self.mocker.replay() - port = yield context.get_value("mysql/0", "port") + port = yield self.context.get_value("mysql/0", "port") self.assertEqual(port, 2222) - host = yield context.get_value("mysql/0", "host") + host = yield self.context.get_value("mysql/0", "host") self.assertEqual(host, "xe.example.com") - magic = yield context.get_value("mysql/0", "unknown") + magic = yield self.context.get_value("mysql/0", "unknown") self.assertEqual(magic, "") # fetching from a value from a non existent unit raises an error. yield self.assertFailure( - context.get_value("mysql/5", "zebra"), + self.context.get_value("mysql/5", "zebra"), UnitRelationStateNotFound) @inlineCallbacks @@ -139,25 +210,20 @@ This is also holds true for values locally modified on the context. """ - context = self.get_execution_context( - self.wordpress_states, "modified", "mysql/0") - - data = yield context.get_value("wordpress/0", "magic") + data = yield self.context.get_value("wordpress/0", "magic") self.assertEqual(data, "") - yield context.set_value("magic", "room") - data = yield context.get_value("wordpress/0", "magic") + yield self.context.set_value("magic", "room") + data = yield self.context.get_value("wordpress/0", "magic") self.assertEqual(data, "room") @inlineCallbacks def test_set(self): """The unit relation settings can be done as a blob.""" - context = self.get_execution_context( - self.wordpress_states, "modified", "mysql/0") - yield self.assertFailure(context.set("abc"), TypeError) + yield self.assertFailure(self.context.set("abc"), TypeError) data = {"abc": 12, "bar": "21"} - yield context.set(data) - changes = yield context.flush() + yield self.context.set(data) + changes = yield self.context.flush() content, stat = yield self.client.get( self.get_unit_settings_path(self.wordpress_states)) data["private-address"] = "wordpress-0.example.com" @@ -168,17 +234,14 @@ @inlineCallbacks def test_set_value(self): """Values can be set by name, and are written at flush time.""" - context = self.get_execution_context( - self.wordpress_states, "modified", "mysql/0") - - yield context.set_value("zebra", 12) - yield context.set_value("donkey", u"abc") + yield self.context.set_value("zebra", 12) + yield self.context.set_value("donkey", u"abc") data, stat = yield self.client.get( self.get_unit_settings_path(self.wordpress_states)) self.assertEqual(yaml.load(data), {"private-address": "wordpress-0.example.com"}) - changes = yield context.flush() + changes = yield self.context.flush() data, stat = yield self.client.get( self.get_unit_settings_path(self.wordpress_states)) @@ -200,10 +263,8 @@ self.wordpress_states), yaml.dump({"key": "secret"})) - context = self.get_execution_context( - self.wordpress_states, "modified", "mysql/0") - yield context.delete_value("key") - changes = yield context.flush() + yield self.context.delete_value("key") + changes = yield self.context.flush() data, stat = yield self.client.get( self.get_unit_settings_path(self.wordpress_states)) @@ -218,10 +279,8 @@ self.get_unit_settings_path(self.wordpress_states), yaml.dump({"lantern": "green"})) - context = self.get_execution_context( - self.wordpress_states, "modified", "mysql/0") - yield context.delete_value("key") - changes = yield context.flush() + yield self.context.delete_value("key") + changes = yield self.context.flush() data, stat = yield self.client.get( self.get_unit_settings_path(self.wordpress_states)) @@ -234,9 +293,7 @@ yield self.client.set( self.get_unit_settings_path(self.wordpress_states), yaml.dump({"key": "secret"})) - context = self.get_execution_context( - self.wordpress_states, "modified", "mysql/0") - changes = yield context.flush() + changes = yield self.context.flush() data, stat = yield self.client.get( self.get_unit_settings_path(self.wordpress_states)) @@ -264,20 +321,17 @@ self.get_unit_settings_path(self.wordpress_states), yaml.dump(data)) - context = self.get_execution_context( - self.wordpress_states, "modified", "mysql/0") - # On the context: # - add a new key # - modify an existing key # - delete an old key/value - yield context.set_value("home", "good") - yield context.set_value("db", 21) - yield context.delete_value("seed") + yield self.context.set_value("home", "good") + yield self.context.set_value("db", 21) + yield self.context.delete_value("seed") # Also test conflict on delete, modify, and add - yield context.delete_value("castle") - yield context.set_value("tower", "rock") - yield context.set_value("zoo", "keeper") + yield self.context.delete_value("castle") + yield self.context.set_value("tower", "rock") + yield self.context.set_value("zoo", "keeper") # Outside of the context: # - add a new key/value. @@ -295,7 +349,7 @@ self.get_unit_settings_path(self.wordpress_states), yaml.dump(data)) - changes = yield context.flush() + changes = yield self.context.flush() data, stat = yield self.client.get( self.get_unit_settings_path(self.wordpress_states)) @@ -318,25 +372,21 @@ yield self.client.set( self.get_unit_settings_path(self.wordpress_states), yaml.dump({"key": "secret"})) - context = self.get_execution_context( - self.wordpress_states, "modified", "mysql/0") - yield context.set_value("magic", "room") - value = yield context.get_value("wordpress/0", "key") + yield self.context.set_value("magic", "room") + value = yield self.context.get_value("wordpress/0", "key") self.assertEqual(value, "secret") - value = yield context.get_value("wordpress/0", "magic") + value = yield self.context.get_value("wordpress/0", "magic") self.assertEqual(value, "room") @inlineCallbacks def test_get_members(self): """The related units of a relation can be retrieved.""" - context = self.get_execution_context( - self.wordpress_states, "modified", "mysql/0") - members = yield context.get_members() + members = yield self.context.get_members() self.assertEqual(members, ["mysql/0"]) # Add a new member and refetch yield self.add_related_service_unit(self.mysql_states) - members2 = yield context.get_members() + members2 = yield self.context.get_members() # There should be no change in the retrieved members. self.assertEqual(members, members2) @@ -349,8 +399,7 @@ "riak", "riak", "peer", "peer") riak2_states = yield self.add_related_service_unit( riak1_states) - context = self.get_execution_context( - riak1_states, "modified", "riak/1") + context = self.get_context(riak1_states, "modified", "riak/1") members = yield context.get_members() self.assertEqual(members, [riak2_states["unit"].unit_name]) @@ -361,103 +410,123 @@ read a value that may have subsequently been modified, and act accordingly. """ - context = self.get_execution_context( - self.wordpress_states, "modified", "mysql/0") - # notify the context of a change - self.assertFalse(context.has_read("mysql/0")) + self.assertFalse(self.context.has_read("mysql/0")) # read the node data - yield context.get("mysql/0") + yield self.context.get("mysql/0") # Now verify we've read it - self.assertTrue(context.has_read("mysql/0")) + self.assertTrue(self.context.has_read("mysql/0")) # And only it. - self.assertFalse(context.has_read("mysql/1")) + self.assertFalse(self.context.has_read("mysql/1")) - @inlineCallbacks - def test_hook_knows_service(self): - """Verify that hooks can get their local service.""" - context = self.get_execution_context( - self.wordpress_states, "modified", "wordpress/0") - service = yield context.get_local_service() - self.assertEqual(service.service_name, "wordpress") +class DepartedRelationHookContextTest( + HookContextTestBase, CommonHookContextTestsMixin): @inlineCallbacks - def test_hook_knows_unit_state(self): - """Verify that hook has access to its local unit state.""" - context = self.get_execution_context( - self.wordpress_states, "modified", "wordpress/0") - - unit = yield context.get_local_unit_state() - self.assertEqual(unit.unit_name, "wordpress/0") + def setUp(self): + yield super(DepartedRelationHookContextTest, self).setUp() + self.service = self.wordpress_states["service"] + self.unit = self.wordpress_states["unit"] + relation = self.wordpress_states["service_relation"] + self.context = DepartedRelationHookContext( + self.client, self.unit.unit_name, self.unit.internal_id, + relation.relation_name, relation.internal_relation_id) @inlineCallbacks - def test_config_get(self): - """Verify we can get config settings. - - This is a simple test that basic I/O works through the - context. - """ - config = yield self.wordpress_states["service"].get_config() - config.update({"hello": "world"}) - yield config.write() - - context = self.get_config_execution_context(self.wordpress_states) - - data = yield context.get_config() - self.assertEqual(data, {"hello": "world", "title": "My Title", - "username": "admin001"}) - - # Verify that context.flush triggers writes as well - data["goodbye"] = "goodnight" - yield context.flush() - # get a new yamlstate from the service itself - config = yield self.wordpress_states["service"].get_config() - self.assertEqual(config["goodbye"], "goodnight") + def test_get_members(self): + """Related units cannot be retrieved.""" + members = yield self.context.get_members() + self.assertEqual(members, []) + # Add a new member and refetch + yield self.add_related_service_unit(self.mysql_states) + members2 = yield self.context.get_members() + # There should be no change in the retrieved members. + self.assertEqual(members2, []) @inlineCallbacks - def test_config_get_cache(self): - """Verify we can get config settings. - - This is a simple test that basic I/O works through the - context. - """ - config = yield self.wordpress_states["service"].get_config() - config.update({"hello": "world"}) - yield config.write() + def test_get_self(self): + """Own settings can be retrieved.""" + self.client.set(self.get_unit_settings_path(self.wordpress_states), + yaml.dump({"hello": "world"})) + data = yield self.context.get(None) + self.assertEquals(data, {"hello": "world"}) - context = self.get_config_execution_context(self.wordpress_states) + @inlineCallbacks + def test_get_self_by_name(self): + """Own settings can be retrieved by name.""" + self.client.set(self.get_unit_settings_path(self.wordpress_states), + yaml.dump({"hello": "world"})) + data = yield self.context.get("wordpress/0") + self.assertEquals(data, {"hello": "world"}) - data = yield context.get_config() - self.assertEqual(data, {"hello": "world", - "title": "My Title", - "username": "admin001"}) + @inlineCallbacks + def test_get_other(self): + """Other unit settings cannot be retrieved.""" + e = yield self.assertFailure( + self.context.get("mysql/0"), RelationBrokenContextError) + self.assertEquals( + str(e), "Cannot access other units in broken relation") - d2 = yield context.get_config() - self.assertIs(data, d2) + @inlineCallbacks + def test_get_value_self(self): + """Own settings can be retrieved.""" + self.client.set(self.get_unit_settings_path(self.wordpress_states), + yaml.dump({"hello": "world"})) + self.assertEquals( + (yield self.context.get_value("wordpress/0", "hello")), "world") + self.assertEquals( + (yield self.context.get_value("wordpress/0", "goodbye")), "") @inlineCallbacks - def test_config_get_with_relation_context(self): - """Verify we can get config settings. + def test_get_value_other(self): + """Other unit settings cannot be retrieved.""" + e = yield self.assertFailure( + self.context.get_value("mysql/0", "anything"), + RelationBrokenContextError) + self.assertEquals( + str(e), "Cannot access other units in broken relation") - This is a simple test that basic I/O works through the - context. This variant tests this through the Relation based - hook context. - """ - config = yield self.wordpress_states["service"].get_config() - config.update({"hello": "world"}) - yield config.write() + @inlineCallbacks + def test_set(self): + """Own settings cannot be changed.""" + e = yield self.assertFailure( + self.context.set({"anything": "anything"}), + RelationBrokenContextError) + self.assertEquals( + str(e), "Cannot change settings in broken relation") - context = self.get_execution_context(self.wordpress_states, - "modified", "wordpress/0") + @inlineCallbacks + def test_set_value(self): + """Own settings cannot be changed.""" + e = yield self.assertFailure( + self.context.set_value("anything", "anything"), + RelationBrokenContextError) + self.assertEquals( + str(e), "Cannot change settings in broken relation") - data = yield context.get_config() - self.assertEqual(data, {"hello": "world", - "username": "admin001", - "title": "My Title"}) + @inlineCallbacks + def test_delete_value(self): + """Own settings cannot be changed.""" + e = yield self.assertFailure( + self.context.delete_value("anything"), + RelationBrokenContextError) + self.assertEquals( + str(e), "Cannot change settings in broken relation") - d2 = yield context.get_config() - self.assertIs(data, d2) + @inlineCallbacks + def test_has_read(self): + """We can tell whether settings have been read""" + self.assertFalse(self.context.has_read("wordpress/0")) + self.assertFalse(self.context.has_read("mysql/0")) + yield self.context.get(None) + self.assertTrue(self.context.has_read("wordpress/0")) + self.assertFalse(self.context.has_read("mysql/0")) + yield self.assertFailure( + self.context.get_value("mysql/0", "anything"), + RelationBrokenContextError) + self.assertTrue(self.context.has_read("wordpress/0")) + self.assertFalse(self.context.has_read("mysql/0")) diff -Nru juju-0.5+bzr447/juju/state/tests/test_relation.py juju-0.5+bzr457/juju/state/tests/test_relation.py --- juju-0.5+bzr447/juju/state/tests/test_relation.py 2012-01-23 18:02:19.000000000 +0000 +++ juju-0.5+bzr457/juju/state/tests/test_relation.py 2012-01-28 08:45:10.000000000 +0000 @@ -301,12 +301,18 @@ yield self.add_service("mysql") yield self.add_service("wordpress") yield self.relation_manager.add_relation_state(mysql_ep, blog_ep) - yield self.assertFailure( + e = yield self.assertFailure( self.relation_manager.add_relation_state(blog_ep, mysql_ep), RelationAlreadyExists) - yield self.assertFailure( + self.assertEqual( + str(e), + "Relation mysql already exists between wordpress and mysql") + e = yield self.assertFailure( self.relation_manager.add_relation_state(mysql_ep, blog_ep), RelationAlreadyExists) + self.assertEqual( + str(e), + "Relation mysql already exists between mysql and wordpress") @inlineCallbacks def test_add_peer_relation_state_twice(self): @@ -314,9 +320,10 @@ riak_ep = RelationEndpoint("riak", "riak", "ring", "peer") yield self.add_service("riak") yield self.relation_manager.add_relation_state(riak_ep) - yield self.assertFailure( + e = yield self.assertFailure( self.relation_manager.add_relation_state(riak_ep), RelationAlreadyExists) + self.assertEqual(str(e), "Relation riak already exists for riak") @inlineCallbacks def test_add_relation_state_no_endpoints(self): @@ -840,7 +847,9 @@ watcher = yield wordpress_states["unit_relation"].watch_related_units( watch_related) + self.assertFalse(watcher.running) yield watcher.start() + self.assertTrue(watcher.running) yield wait_callback[0] @inlineCallbacks @@ -1281,6 +1290,7 @@ watcher = yield mysql_states["unit_relation"].watch_related_units( watch_related) yield watcher.start() + self.assertTrue(watcher.running) yield wait_callback[0] self.verify_unit_watch_result( @@ -1288,6 +1298,7 @@ # Stop watching watcher.stop() + self.assertFalse(watcher.running) # Add a new unit wordpress2_states = yield self.add_related_service_unit( @@ -1302,6 +1313,7 @@ # Start watching yield watcher.start() + self.assertTrue(watcher.running) # Verify we see the addition. yield wait_callback[1] @@ -1341,9 +1353,11 @@ watcher = yield mysql_states["unit_relation"].watch_related_units( watch_related) yield watcher.start() + self.assertTrue(watcher.running) # Stop watching watcher.stop() + self.assertFalse(watcher.running) # Add the new service and a unit wordpress_states = yield self.add_opposite_service_unit( @@ -1362,6 +1376,7 @@ # Start watching yield watcher.start() + self.assertTrue(watcher.running) # Wait a moment for callback yield wait_callback[0] diff -Nru juju-0.5+bzr447/juju/state/tests/test_service.py juju-0.5+bzr457/juju/state/tests/test_service.py --- juju-0.5+bzr447/juju/state/tests/test_service.py 2012-01-23 18:02:19.000000000 +0000 +++ juju-0.5+bzr457/juju/state/tests/test_service.py 2012-02-04 00:00:08.000000000 +0000 @@ -2413,6 +2413,23 @@ {"port": 443, "proto": "tcp"}]) @inlineCallbacks + def test_close_open_port(self): + """Verify closing an unopened port, then actually opening it, works.""" + service_state = yield self.add_service("wordpress") + unit_state = yield service_state.add_unit_state() + unit_name = unit_state.unit_name + + yield unit_state.close_port(80, "tcp") + self.assertEqual( + (yield unit_state.get_open_ports()), + []) + + yield unit_state.open_port(80, "tcp") + self.assertEqual( + (yield unit_state.get_open_ports()), + [{"port": 80, "proto": "tcp"}]) + + @inlineCallbacks def test_open_ports_znode_representation(self): """Verify the specific representation of open ports in ZK.""" service_state = yield self.add_service("wordpress") diff -Nru juju-0.5+bzr447/juju/unit/lifecycle.py juju-0.5+bzr457/juju/unit/lifecycle.py --- juju-0.5+bzr447/juju/unit/lifecycle.py 2012-01-23 18:02:19.000000000 +0000 +++ juju-0.5+bzr457/juju/unit/lifecycle.py 2012-01-28 08:45:10.000000000 +0000 @@ -1,14 +1,16 @@ import os import logging +import yaml from twisted.internet.defer import ( inlineCallbacks, DeferredLock, DeferredList, returnValue) - from juju.hooks.invoker import Invoker from juju.hooks.scheduler import HookScheduler -from juju.state.hook import RelationChange, HookContext +from juju.state.hook import ( + DepartedRelationHookContext, RelationChange, HookContext) from juju.state.errors import StopWatcher, UnitRelationStateNotFound +from juju.state.relation import RelationStateManager, UnitRelationState from juju.unit.workflow import RelationWorkflowState @@ -23,21 +25,29 @@ Primarily used by the workflow interaction, to modify unit behavior according to the current unit workflow state and transitions. + + See docs/source/internals/unit-workflow-lifecycle.rst for a brief + discussion of some of the more interesting implementation decisions. """ - def __init__(self, client, unit, service, unit_path, executor): + def __init__(self, client, unit, service, unit_dir, state_dir, executor): self._client = client self._unit = unit self._service = service self._executor = executor - self._unit_path = unit_path - self._relations = {} + self._unit_dir = unit_dir + self._state_dir = state_dir + self._relations = None self._running = False self._watching_relation_memberships = False self._watching_relation_resolved = False self._run_lock = DeferredLock() self._log = logging.getLogger("unit.lifecycle") + @property + def running(self): + return self._running + def get_relation_workflow(self, relation_id): """Accessor to a unit relation workflow, by relation id. @@ -63,8 +73,15 @@ self._executor.start() @inlineCallbacks - def start(self, fire_hooks=True): + def start(self, fire_hooks=True, start_relations=True): """Invoke the start hook, and setup relation watching. + + :param fire_hooks: False to skip running config-change and start hooks. + Will not affect any relation hooks that happen to be fired as a + consequence of starting up. + + :param start_relations: True to transition all "down" relation + workflows to "up". """ self._log.debug("pre-start acquire, running:%s", self._running) yield self._run_lock.acquire() @@ -80,12 +97,16 @@ yield self._execute_hook("config-changed") yield self._execute_hook("start") - # If we have any existing relations in memory, start them. - if self._relations: - self._log.debug("starting relation lifecycles") + if self._relations is None: + yield self._load_relations() - for workflow in self._relations.values(): - yield workflow.transition_state("up") + if start_relations: + # We actually want to transition from "down" to "up" where + # applicable (ie a stopped unit is starting up again) + for workflow in self._relations.values(): + state = yield workflow.get_state() + if state == "down": + yield workflow.transition_state("up") # Establish a watch on the existing relations. if not self._watching_relation_memberships: @@ -117,8 +138,14 @@ self._log.debug("started unit lifecycle") @inlineCallbacks - def stop(self, fire_hooks=True): + def stop(self, fire_hooks=True, stop_relations=True): """Stop the unit, executes the stop hook, and stops relation watching. + + :param fire_hooks: False to skip running stop hooks. + + :param stop_relations: True to transition all "up" relation + workflows to "down"; when False, simply shut down relation + lifecycles (in preparation for process shutdown, for example). """ self._log.debug("pre-stop acquire, running:%s", self._running) yield self._run_lock.acquire() @@ -126,12 +153,17 @@ # Verify state assert self._running, "Already Stopped" - # Stop relation lifecycles - if self._relations: + if stop_relations: + # We actually want to transition relation states + # (probably because the unit workflow state is stopped/error) + for workflow in self._relations.values(): + yield workflow.transition_state("down") + else: + # We just want to stop the relations from acting + # (probably because the process is going down) self._log.debug("stopping relation lifecycles") - - for workflow in self._relations.values(): - yield workflow.transition_state("down") + for workflow in self._relations.values(): + yield workflow.lifecycle.stop() if fire_hooks: yield self._execute_hook("stop") @@ -234,71 +266,140 @@ def _process_service_changes(self, old_relations, new_relations): """Add and remove unit lifecycles per the service relations Determine. """ - # changes relation delta of global zk state with our memory state. - new_relations = dict([(service_relation.internal_relation_id, - service_relation) for - service_relation in new_relations]) + # Calculate delta between zookeeper state and our stored state. + new_relations = dict( + (service_relation.internal_relation_id, service_relation) + for service_relation in new_relations) added = set(new_relations.keys()) - set(self._relations.keys()) removed = set(self._relations.keys()) - set(new_relations.keys()) - # Stop and remove, old ones. - - # Trying to directly transition this causes additional yielding - # operations, which means that concurrent events for subsequent - # watch firings will be executed. ie. if the relation - # is broken, but a subsequent modify comes in for a related unit, - # it will cause the modify to have a hook execution. To prevent - # this we stop the lifecycle immediately before executing the - # transition. see UnitLifecycleTest.test_removed_relation_depart + # Once we know a relation is departed, *immediately* stop running + # its hooks. We can't really handle the case in which a hook is + # *already* running, but we can at least make sure it doesn't run + # any *more* hooks (which could have been queued in the past, but + # not yet executed).# This isn't *currently* an exceptionally big + # deal, because: + # + # (1) The ZK state won't actually be deleted, so an inappropriate + # hook will still run happily. + # (2) Even if the state is deleted, and the hook errors out, the + # only actual consequence is that we'll eventually run the + # error_depart transition rather than depart or down_depart. + # + # However, (1) will certainly change in the future, and (2) is not + # necessarily a watertight guarantee. for relation_id in removed: yield self._relations[relation_id].lifecycle.stop() + # Actually depart old relations. for relation_id in removed: workflow = self._relations.pop(relation_id) yield workflow.transition_state("departed") + self._store_relations() # Process new relations. for relation_id in added: service_relation = new_relations[relation_id] - try: - unit_relation = yield service_relation.get_unit_state( - self._unit) - except UnitRelationStateNotFound: - # This unit has not yet been assigned a unit relation state, - # Go ahead and add one. - unit_relation = yield service_relation.add_unit_state( - self._unit) - - self._log.debug( - "Starting new relation: %s", service_relation.relation_name) - - workflow = self._get_unit_relation_workflow(unit_relation, - service_relation) - # Start it before storing it. - yield workflow.fire_transition("start") - self._relations[service_relation.internal_relation_id] = workflow - - def _get_unit_path(self): - """Retrieve the root path of the unit. - """ - return self._unit_path - - def _get_unit_relation_workflow(self, unit_relation, service_relation): - - lifecycle = UnitRelationLifecycle(self._client, - self._unit.unit_name, - unit_relation, - service_relation.relation_name, - self._get_unit_path(), - self._executor) + yield self._add_relation(service_relation) + self._store_relations() + + @property + def _known_relations_path(self): + return os.path.join( + self._state_dir, "%s.lifecycle.relations" % self._unit.internal_id) + + @inlineCallbacks + def _load_relations(self): + """Recreate workflows for any relation we had previously stored. + + All relations (including those already departed) are stored in + ._relations (and will be added or departed as usual); but only + relations *not* already departed will be synchronized, to avoid + errors caused by trying to access ZK state that may not exist any + more. + """ + self._relations = {} + if not os.path.exists(self._known_relations_path): + return - state_directory = os.path.abspath(os.path.join( - self._unit_path, "../../state")) + rsm = RelationStateManager(self._client) + current_relations = yield rsm.get_relations_for_service(self._service) + current_ids = set(r.internal_relation_id for r in current_relations) + + with open(self._known_relations_path) as f: + known_relations = yaml.load(f.read()) + + for relation_id, relation_name in known_relations.items(): + workflow = self._reconstruct_workflow(relation_id, relation_name) + + # Don't try to sync departed relations; if they happen to be "up" + # (according to latest stored state) they will try to start, and + # it won't go well (no way to watch related units). + if relation_id in current_ids: + yield workflow.synchronize() + + # Put everything into self._relations; adds/departs will be handled + # as usual in the first call to _process_service_changes. + self._relations[relation_id] = workflow + + def _store_relations(self): + """Store *just* enough information to recreate RelationWorkflowStates. + + Note that we don't need to store the actual states -- if we can + reconstruct the RWS, it will be responsible for finding its own state + -- but we *do* need to store the fact of their existence, so that we + can still depart broken relations even if they break while we're not + running. + """ + state = yaml.dump(dict( + (relation_id, workflow.relation_name) + for (relation_id, workflow) in self._relations.items())) + temp_path = self._known_relations_path + "~" + with open(temp_path, "w") as f: + f.write(state) + os.rename(temp_path, self._known_relations_path) + + def _reconstruct_workflow(self, relation_id, relation_name): + """Create a RelationWorkflowState which may refer to outdated state. + + This means that *if* this service has already departed the relevant + relation, it is not safe to synchronize the resultant workflow, + because its lifecycle may attempt to watch state that doesn't exist. + + Since synchronization is a one-time occurrence, and this method has + only one client, this shouldn't be too hard to keep track of. + """ + unit_relation = UnitRelationState( + self._client, self._service.internal_id, self._unit.internal_id, + relation_id) + lifecycle = UnitRelationLifecycle( + self._client, self._unit.unit_name, unit_relation, relation_name, + self._unit_dir, self._executor) + return RelationWorkflowState( + self._client, unit_relation, relation_name, lifecycle, + self._state_dir) + + @inlineCallbacks + def _add_relation(self, service_relation): + try: + unit_relation = yield service_relation.get_unit_state( + self._unit) + except UnitRelationStateNotFound: + # This unit has not yet been assigned a unit relation state, + # Go ahead and add one. + unit_relation = yield service_relation.add_unit_state( + self._unit) + + lifecycle = UnitRelationLifecycle( + self._client, self._unit.unit_name, unit_relation, + service_relation.relation_name, self._unit_dir, self._executor) workflow = RelationWorkflowState( - self._client, unit_relation, lifecycle, state_directory) + self._client, unit_relation, service_relation.relation_name, + lifecycle, self._state_dir) - return workflow + self._relations[service_relation.internal_relation_id] = workflow + yield workflow.synchronize() @inlineCallbacks def _execute_hook(self, hook_name, now=False): @@ -307,13 +408,12 @@ For priority hooks, the hook is scheduled and then the executioner started, before wait on the result. """ - unit_path = self._get_unit_path() - hook_path = os.path.join(unit_path, "charm", "hooks", hook_name) - socket_path = os.path.join(unit_path, HOOK_SOCKET_FILE) - - invoker = Invoker(HookContext(self._client, self._unit.unit_name), - None, "constant", socket_path, - self._unit_path, hook_log) + hook_path = os.path.join(self._unit_dir, "charm", "hooks", hook_name) + socket_path = os.path.join(self._unit_dir, HOOK_SOCKET_FILE) + invoker = Invoker( + HookContext(self._client, self._unit.unit_name), None, + "constant", socket_path, self._unit_dir, hook_log) + if now: yield self._executor.run_priority_hook(invoker, hook_path) else: @@ -347,72 +447,37 @@ determinism across relations**. They only maintain ordering and determinism within a relation. A shared scheduler across relations would be needed to maintain such behavior. + + See docs/source/internals/unit-workflow-lifecycle.rst for a brief + discussion of some of the more interesting implementation decisions. """ - def __init__(self, client, unit_name, unit_relation, relation_name, unit_path, executor): + def __init__(self, client, unit_name, unit_relation, relation_name, + unit_dir, executor): self._client = client - self._unit_path = unit_path + self._unit_dir = unit_dir self._relation_name = relation_name self._unit_relation = unit_relation + self._unit_name = unit_name self._executor = executor self._run_lock = DeferredLock() self._log = logging.getLogger("unit.relation.lifecycle") self._error_handler = None - self._scheduler = HookScheduler(client, - self._execute_change_hook, - self._unit_relation, - self._relation_name, - unit_name=unit_name) + self._scheduler = HookScheduler( + client, self._execute_change_hook, self._unit_relation, + self._relation_name, unit_name=unit_name) self._watcher = None - @inlineCallbacks - def _execute_change_hook(self, context, change, hook_name=None): - """Invoked by the contained HookScheduler, to execute a hook. - - We utilize the HookExecutor to execute the hook, if an - error occurs, it will be reraised, unless an error handler - is specified see ``set_hook_error_handler``. - """ - socket_path = os.path.join(self._unit_path, HOOK_SOCKET_FILE) - if hook_name is None: - if change.change_type == "departed": - hook_names = [ - "%s-relation-departed" % self._relation_name] - elif change.change_type == "joined": - hook_names = [ - "%s-relation-joined" % self._relation_name, - "%s-relation-changed" % self._relation_name] - else: - hook_names = ["%s-relation-changed" % self._relation_name] - else: - hook_names = [hook_name] - - invoker = RelationInvoker( - context, change, "constant", socket_path, self._unit_path, - hook_log) - - for hook_name in hook_names: - hook_path = os.path.join( - self._unit_path, "charm", "hooks", hook_name) - yield self._run_lock.acquire() - self._log.debug("Executing hook %s", hook_name) - try: - yield self._executor(invoker, hook_path) - except Exception, e: - yield self._run_lock.release() - self._log.warn("Error in %s hook: %s", hook_name, e) - - if not self._error_handler: - raise - self._log.info( - "Invoked error handler for %s hook", hook_name) - # We can't hold the run lock, when we invoke the error - # handler, or we get a deadlock if the handler - # manipulates the lifecycle. - yield self._error_handler(change, e) - else: - yield self._run_lock.release() + @property + def watching(self): + """Are we queuing up hook executions in response to state changes?""" + return self._watcher and self._watcher.running + + @property + def executing(self): + """Are we currently dequeuing and executing any queued hooks?""" + return self._scheduler.running def set_hook_error_handler(self, handler): """Set an error handler to be invoked if a hook errors. @@ -422,22 +487,25 @@ self._error_handler = handler @inlineCallbacks - def start(self, watches=True): + def start(self, start_watches=True, start_scheduler=True): """Start watching related units and executing change hooks. - @param watches: boolean parameter denoting if relation watches - should be started. + :param bool start_watches: True to start relation watches + + :param bool start_scheduler: True to run the scheduler and actually + react to any changes delivered by the watcher """ yield self._run_lock.acquire() try: # Start the hook execution scheduler. - self._scheduler.run() + if start_scheduler and not self.executing: + self._scheduler.run() # Create a watcher if we don't have one yet. if self._watcher is None: self._watcher = yield self._unit_relation.watch_related_units( self._scheduler.notify_change) # And start the watcher. - if watches: + if start_watches and not self.watching: yield self._watcher.start() finally: self._run_lock.release() @@ -445,17 +513,19 @@ "started relation:%s lifecycle", self._relation_name) @inlineCallbacks - def stop(self, watches=True): - """Stop watching changes and stop executing relation change hooks. + def stop(self, stop_watches=True): + """Stop executing relation change hooks; maybe stop watching changes. - @param watches: boolean parameter denoting if relation watches - should be stopped. + :param bool stop_watches: True to stop watches as well as scheduler + (which will prevent changes from being detected and queued, as well + as stopping them being executed). """ yield self._run_lock.acquire() try: - if watches and self._watcher: + if stop_watches and self.watching: self._watcher.stop() - self._scheduler.stop() + if self._scheduler.running: + self._scheduler.stop() finally: yield self._run_lock.release() self._log.debug("stopped relation:%s lifecycle", self._relation_name) @@ -465,7 +535,63 @@ """Inform the charm that the service has departed the relation. """ self._log.debug("depart relation lifecycle") + unit_id = self._unit_relation.internal_unit_id + relation_id = self._unit_relation.internal_relation_id + context = DepartedRelationHookContext( + self._client, self._unit_name, unit_id, self._relation_name, + relation_id) change = RelationChange(self._relation_name, "departed", "") - context = self._scheduler.get_hook_context(change) + invoker = self._get_invoker(context, change) hook_name = "%s-relation-broken" % self._relation_name - yield self._execute_change_hook(context, change, hook_name) + yield self._execute_hook(invoker, hook_name, change) + + def _get_invoker(self, context, change): + socket_path = os.path.join(self._unit_dir, HOOK_SOCKET_FILE) + return RelationInvoker( + context, change, "constant", socket_path, self._unit_dir, + hook_log) + + @inlineCallbacks + def _execute_change_hook(self, context, change): + """Invoked by the contained HookScheduler, to execute a hook. + + We utilize the HookExecutor to execute the hook, if an + error occurs, it will be reraised, unless an error handler + is specified see ``set_hook_error_handler``. + """ + if change.change_type == "departed": + hook_names = [ + "%s-relation-departed" % self._relation_name] + elif change.change_type == "joined": + hook_names = [ + "%s-relation-joined" % self._relation_name, + "%s-relation-changed" % self._relation_name] + else: + hook_names = ["%s-relation-changed" % self._relation_name] + + invoker = self._get_invoker(context, change) + for hook_name in hook_names: + yield self._execute_hook(invoker, hook_name, change) + + @inlineCallbacks + def _execute_hook(self, invoker, hook_name, change): + hook_path = os.path.join( + self._unit_dir, "charm", "hooks", hook_name) + yield self._run_lock.acquire() + self._log.debug("Executing hook %s", hook_name) + try: + yield self._executor(invoker, hook_path) + except Exception, e: + # We can't hold the run lock when we invoke the error + # handler, or we get a deadlock if the handler + # manipulates the lifecycle. + yield self._run_lock.release() + self._log.warn("Error in %s hook: %s", hook_name, e) + + if not self._error_handler: + raise + self._log.info( + "Invoked error handler for %s hook", hook_name) + yield self._error_handler(change, e) + else: + yield self._run_lock.release() diff -Nru juju-0.5+bzr447/juju/unit/tests/test_lifecycle.py juju-0.5+bzr457/juju/unit/tests/test_lifecycle.py --- juju-0.5+bzr447/juju/unit/tests/test_lifecycle.py 2012-01-23 18:02:19.000000000 +0000 +++ juju-0.5+bzr457/juju/unit/tests/test_lifecycle.py 2012-01-11 09:37:48.000000000 +0000 @@ -12,8 +12,6 @@ from juju.unit.lifecycle import ( UnitLifecycle, UnitRelationLifecycle, RelationInvoker) -from juju.unit.workflow import RelationWorkflowState - from juju.hooks.invoker import Invoker from juju.hooks.executor import HookExecutor @@ -63,10 +61,15 @@ "units", self.states["unit"].unit_name.replace("/", "-")) os.makedirs(os.path.join(self.unit_directory, "charm", "hooks")) - os.makedirs(os.path.join(self.juju_directory, "state")) + self.state_directory = os.path.join(self.juju_directory, "state") + os.makedirs(self.state_directory) - def write_hook(self, name, text, no_exec=False): - hook_path = os.path.join(self.unit_directory, "charm", "hooks", name) + def write_hook(self, name, text, no_exec=False, hooks_dir=None): + if hooks_dir is None: + hooks_dir = os.path.join(self.unit_directory, "charm", "hooks") + if not os.path.exists(hooks_dir): + os.makedirs(hooks_dir) + hook_path = os.path.join(hooks_dir, name) hook_file = open(hook_path, "w") hook_file.write(text.strip()) hook_file.flush() @@ -159,24 +162,7 @@ yield self.setup_default_test_relation() self.lifecycle = UnitLifecycle( self.client, self.states["unit"], self.states["service"], - self.unit_directory, self.executor) - - def get_unit_relation_workflow(self, states): - state_dir = os.path.join(self.juju_directory, "state") - lifecycle = UnitRelationLifecycle( - self.client, - states["unit_relation"], - states["service_relation"].relation_name, - self.unit_directory, - self.executor) - - workflow = RelationWorkflowState( - self.client, - states["unit_relation"], - lifecycle, - state_dir) - - return (workflow, lifecycle) + self.unit_directory, self.state_directory, self.executor) @inlineCallbacks def wb_test_start_with_relation_errors(self): @@ -242,10 +228,8 @@ yield self.states["unit"].set_relation_resolved( {self.states["unit_relation"].internal_relation_id: NO_HOOKS}) - # Give a moment for the watch to fire erroneously - yield self.sleep(0.2) - # Ensure we didn't attempt a transition. + yield self.sleep(0.1) self.assertFalse(resolved.called) self.assertEqual( {self.states["unit_relation"].internal_relation_id: NO_HOOKS}, @@ -366,7 +350,7 @@ yield self.setup_default_test_relation() self.lifecycle = UnitLifecycle( self.client, self.states["unit"], self.states["service"], - self.unit_directory, self.executor) + self.unit_directory, self.state_directory, self.executor) @inlineCallbacks def test_hook_invocation(self): @@ -435,6 +419,16 @@ self.assertFalse(upgrade_executed.called) self.assertTrue(self.executor.running) + @inlineCallbacks + def test_running(self): + self.assertFalse(self.lifecycle.running) + yield self.lifecycle.install() + self.assertFalse(self.lifecycle.running) + yield self.lifecycle.start() + self.assertTrue(self.lifecycle.running) + yield self.lifecycle.stop() + self.assertFalse(self.lifecycle.running) + def test_hook_error(self): """Verify hook execution error, raises an exception.""" self.write_hook("install", '#!/bin/sh\n exit 1') @@ -746,6 +740,119 @@ self.assertEqual([True, True, True, True], [x.called for x in execution_callbacks]) + @inlineCallbacks + def test_start_stop_relations(self): + yield self.lifecycle.start() + + # Simulate relation down on an individual unit relation + workflow = self.lifecycle.get_relation_workflow( + self.states["unit_relation"].internal_relation_id) + self.assertEqual("up", (yield workflow.get_state())) + + # Stop the unit lifecycle + yield self.lifecycle.stop() + self.assertEqual("down", (yield workflow.get_state())) + + # Start again + yield self.lifecycle.start() + self.assertEqual("up", (yield workflow.get_state())) + + @inlineCallbacks + def test_start_without_relations(self): + yield self.lifecycle.start() + + # Simulate relation down on an individual unit relation + workflow = self.lifecycle.get_relation_workflow( + self.states["unit_relation"].internal_relation_id) + self.assertEqual("up", (yield workflow.get_state())) + yield workflow.transition_state("down") + resolved = self.wait_on_state(workflow, "up") + + # Stop the unit lifecycle + yield self.lifecycle.stop() + self.assertEqual("down", (yield workflow.get_state())) + + # Start again without start_relations + yield self.lifecycle.start(start_relations=False) + self.assertEqual("down", (yield workflow.get_state())) + + # Give a moment for the watch to fire erroneously + yield self.sleep(0.1) + + # Ensure we didn't attempt a transition. + self.assertFalse(resolved.called) + + @inlineCallbacks + def test_stop_without_relations(self): + yield self.lifecycle.start() + + # Simulate relation down on an individual unit relation + workflow = self.lifecycle.get_relation_workflow( + self.states["unit_relation"].internal_relation_id) + self.assertEqual("up", (yield workflow.get_state())) + + # Stop the unit lifecycle + yield self.lifecycle.stop(stop_relations=False) + self.assertEqual("up", (yield workflow.get_state())) + + # Start again without start_relations + yield self.lifecycle.start(start_relations=False) + self.assertEqual("up", (yield workflow.get_state())) + + @inlineCallbacks + def test_remembers_relation_removes(self): + # Add another relation that *won't* be trashed; there's no way to tell + # the diference between a running relation that's loaded from disk (as + # this one should be) and one that's just picked up from the call to + # watch_relation_states, but this should ensure the tests at least hit + # the correct path. + other_states = yield self.add_opposite_service_unit( + (yield self.add_relation_service_unit_to_another_endpoint( + self.states, + RelationEndpoint( + "wordpress-2", "client-server", "db", "client")))) + + yield self.lifecycle.start() + going_workflow = self.lifecycle.get_relation_workflow( + self.states["unit_relation"].internal_relation_id) + staying_workflow = self.lifecycle.get_relation_workflow( + other_states["unit_relation"].internal_relation_id) + + # Stop the lifecycle as though the process were being shut down + yield self.lifecycle.stop(fire_hooks=False, stop_relations=False) + self.assertEqual("up", (yield going_workflow.get_state())) + self.assertEqual("up", (yield staying_workflow.get_state())) + + # This lifecycle is not responding to events; while it's not looking, + # trash one of the relations. + broken_complete = self.wait_on_hook("app-relation-broken") + yield self.relation_manager.remove_relation_state( + self.states["relation"]) + + # Check it's really not responding to events. + yield self.sleep(0.1) + self.assertFalse(broken_complete.called) + + # Create a new lifecycle with the same params; ie one that doesn't + # share memory state. + new_lifecycle = UnitLifecycle( + self.client, self.states["unit"], self.states["service"], + self.unit_directory, self.state_directory, self.executor) + + # Demonstrate that state was stored by the first one and picked up + # by the second one, by showing that "missing" relations still have + # depart hooks fired despite the second one having no direct knowledge + # of the departed relation's existence.. + yield new_lifecycle.start(fire_hooks=False, start_relations=False) + yield broken_complete + + # The workflow, which we grabbed earlier from the original lifecycle, + # should now be "departed" (rather than "up", which it was when we + # stopped the original lifecycle). + self.assertEqual("departed", (yield going_workflow.get_state())) + self.assertEqual("up", (yield staying_workflow.get_state())) + yield new_lifecycle.stop() + class RelationInvokerTest(TestCase): @@ -800,6 +907,8 @@ ("/bin/bash\n" "echo executed >> %s\n" % file_path)) yield self.lifecycle.start() self.assertFalse(os.path.exists(file_path)) + self.assertTrue(self.lifecycle.watching) + self.assertTrue(self.lifecycle.executing) @inlineCallbacks def test_stop_can_continue_watching(self): @@ -811,15 +920,24 @@ ("#!/bin/bash\n" "echo executed >> %s\n" % file_path)) rel_states = yield self.add_opposite_service_unit(self.states) yield self.lifecycle.start() + self.assertTrue(self.lifecycle.watching) + self.assertTrue(self.lifecycle.executing) + yield self.wait_on_hook( sequence=["app-relation-joined", "app-relation-changed"]) changed_executed = self.wait_on_hook("app-relation-changed") - yield self.lifecycle.stop(watches=False) + yield self.lifecycle.stop(stop_watches=False) + self.assertTrue(self.lifecycle.watching) + self.assertFalse(self.lifecycle.executing) + rel_states["unit_relation"].set_data(yaml.dump(dict(hello="world"))) # Sleep to give an error a chance. yield self.sleep(0.1) self.assertFalse(changed_executed.called) - yield self.lifecycle.start(watches=False) + + yield self.lifecycle.start(start_watches=False) + self.assertTrue(self.lifecycle.watching) + self.assertTrue(self.lifecycle.executing) yield changed_executed @inlineCallbacks @@ -866,9 +984,13 @@ # starting is async yield self.lifecycle.start() + self.assertTrue(self.lifecycle.watching) + self.assertTrue(self.lifecycle.executing) # stopping is sync. self.lifecycle.stop() + self.assertFalse(self.lifecycle.watching) + self.assertFalse(self.lifecycle.executing) # Add a related unit. yield self.add_opposite_service_unit(self.states) @@ -880,6 +1002,8 @@ # Now start again yield self.lifecycle.start() + self.assertTrue(self.lifecycle.watching) + self.assertTrue(self.lifecycle.executing) # Verify we get our join event. yield self.wait_on_hook("app-relation-changed") @@ -932,10 +1056,6 @@ """If a relation is departed, the depart hook is executed. """ file_path = self.makeFile() - self.write_hook("%s-relation-joined" % self.relation_name, - "#!/bin/bash\n echo joined") - self.write_hook("%s-relation-changed" % self.relation_name, - "#!/bin/bash\n echo hello") self.write_hook("%s-relation-broken" % self.relation_name, self.hook_template % dict(change_type="broken", file_path=file_path)) @@ -950,7 +1070,9 @@ hook_complete = self.wait_on_hook("app-relation-broken") yield self.lifecycle.depart() yield hook_complete - self.assertTrue(os.path.exists(file_path)) + contents = open(file_path).read() + self.assertEqual( + contents, "broken\nJUJU_RELATION=app\nJUJU_REMOTE_UNIT=\n") @inlineCallbacks def test_lock_start_stop(self): @@ -976,8 +1098,6 @@ start_complete = self.lifecycle.start() stop_complete = self.lifecycle.stop() - # Sadly this sleeping is the easiest way to verify that - # the stop hasn't procesed prior to the start. yield self.sleep(0.1) self.assertFalse(start_complete.called) self.assertFalse(stop_complete.called) @@ -985,3 +1105,29 @@ yield start_complete self.assertTrue(stop_complete.called) + + @inlineCallbacks + def test_start_scheduler(self): + yield self.lifecycle.start(start_scheduler=False) + self.assertTrue(self.lifecycle.watching) + self.assertFalse(self.lifecycle.executing) + hooks_complete = self.wait_on_hook( + sequence=["app-relation-joined", "app-relation-changed"]) + + # Watches are firing, but scheduler is not running hooks + yield self.add_opposite_service_unit(self.states) + yield self.sleep(0.1) + self.assertFalse(hooks_complete.called) + + # Shut down everything + yield self.lifecycle.stop() + self.assertFalse(self.lifecycle.watching) + self.assertFalse(self.lifecycle.executing) + + # Start the scheduler only, without the watches + yield self.lifecycle.start(start_watches=False) + self.assertFalse(self.lifecycle.watching) + self.assertTrue(self.lifecycle.executing) + + # The scheduler should run the hooks it queued up earlier + yield hooks_complete diff -Nru juju-0.5+bzr447/juju/unit/tests/test_workflow.py juju-0.5+bzr457/juju/unit/tests/test_workflow.py --- juju-0.5+bzr447/juju/unit/tests/test_workflow.py 2012-01-23 18:02:19.000000000 +0000 +++ juju-0.5+bzr457/juju/unit/tests/test_workflow.py 2012-01-11 09:37:48.000000000 +0000 @@ -1,3 +1,4 @@ +import itertools import logging import yaml import csv @@ -16,6 +17,11 @@ class WorkflowTestBase(LifecycleTestBase): @inlineCallbacks + def setUp(self): + yield super(WorkflowTestBase, self).setUp() + self.output = self.makeFile() + + @inlineCallbacks def assertState(self, workflow, state): workflow_state = yield workflow.get_state() self.assertEqual(workflow_state, state) @@ -34,44 +40,70 @@ [yaml.load(r[0]) for r in csv.reader(history)], yaml.load(zk_state[history_id]))) + def write_exit_hook(self, name, code=0, hooks_dir=None): + self.write_hook( + name, + "#!/bin/bash\necho %s >> %s\n exit %s" % (name, self.output, code), + hooks_dir=hooks_dir) + -class UnitWorkflowTest(WorkflowTestBase): +class UnitWorkflowTestBase(WorkflowTestBase): @inlineCallbacks def setUp(self): - yield super(UnitWorkflowTest, self).setUp() + 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.executor) - - self.juju_directory = self.makeDir() - self.state_directory = self.makeDir( - path=os.path.join(self.juju_directory, "state")) - + 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 test_install(self): - file_path = self.makeFile() - self.write_hook( - "install", "#!/bin/bash\necho installed >> %s\n" % file_path) + def assert_transition(self, transition, success=True): + result = yield self.workflow.fire_transition(transition) + self.assertEquals(result, success) + + @inlineCallbacks + def assert_transition_alias(self, transition, success=True): + result = yield self.workflow.fire_transition_alias(transition) + self.assertEquals(result, success) - result = yield self.workflow.fire_transition("install") + @inlineCallbacks + def assert_state(self, expected): + actual = yield self.workflow.get_state() + self.assertEquals(actual, expected) - self.assertTrue(result) - current_state = yield self.workflow.get_state() - self.assertEqual(current_state, "installed") + def assert_hooks(self, *hooks): + with open(self.output) as f: + lines = tuple(l.strip() for l in f) + self.assertEquals(lines, hooks) + @inlineCallbacks + def assert_history(self, expected): f_state, history, zk_state = yield self.read_persistent_state() + self.assertEquals(f_state, zk_state) + self.assertEquals(f_state, history[-1]) + self.assertEquals(history, expected) - self.assertEqual(f_state, zk_state) - self.assertEqual(f_state, - {"state": "installed", "state_variables": {}}) - self.assertEqual(history, - [{"state": "installed", "state_variables": {}}]) + +class UnitWorkflowTest(UnitWorkflowTestBase): + + @inlineCallbacks + def test_install(self): + yield self.assert_transition("install") + yield self.assert_state("started") + self.assert_hooks("install", "config-changed", "start") + yield self.assert_history([ + {"state": "installed", "state_variables": {}}, + {"state": "started", "state_variables": {}}]) @inlineCallbacks def test_install_with_error_and_retry(self): @@ -79,191 +111,282 @@ 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") + self.write_exit_hook("install", 1) + + yield self.assert_transition("install", False) + yield self.assert_state("install_error") + yield self.assert_transition("retry_install") + yield self.assert_state("started") + self.assert_hooks("install", "config-changed", "start") + yield self.assert_history([ + {"state": "install_error", "state_variables": {}}, + {"state": "installed", "state_variables": {}}, + {"state": "started", "state_variables": {}}]) @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") + self.write_exit_hook("install", 1) + + yield self.assert_transition("install", False) + yield self.assert_state("install_error") + yield self.assert_transition("retry_install_hook", False) + yield self.assert_state("install_error") + self.write_exit_hook("install") + yield self.assert_transition_alias("retry_hook") + yield self.assert_state("started") + self.assert_hooks( + "install", "install", "install", "config-changed", "start") + yield self.assert_history([ + {"state": "install_error", "state_variables": {}}, + {"state": "installed", "state_variables": {}}, + {"state": "started", "state_variables": {}}]) @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) + yield self.workflow.set_state("installed") - 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": {}}]) + yield self.assert_transition("start") + yield self.assert_state("started") + self.assert_hooks("config-changed", "start") + yield self.assert_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") + self.write_exit_hook("start", 1) + # The install transition succeeded; error from success_transition + # is ignored in StateMachine + yield self.assert_transition("install") + yield self.assert_state("start_error") + + yield self.assert_transition("retry_start") + yield self.assert_state("started") + self.assert_hooks("install", "config-changed", "start") + yield self.assert_history([ + {"state": "installed", "state_variables": {}}, + {"state": "start_error", "state_variables": {}}, + {"state": "started", "state_variables": {}}]) @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") + self.write_exit_hook("start", 1) + yield self.assert_transition("install") + yield self.assert_state("start_error") + + yield self.assert_transition("retry_start_hook", False) + yield self.assert_state("start_error") + self.write_exit_hook("start") + yield self.assert_transition_alias("retry_hook") + yield self.assert_state("started") + self.assert_hooks( + "install", "config-changed", "start", "config-changed", "start", + "config-changed", "start") + yield self.assert_history([ + {"state": "installed", "state_variables": {}}, + {"state": "start_error", "state_variables": {}}, + {"state": "started", "state_variables": {}}]) - 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") + @inlineCallbacks + def test_stop(self): + """Executing the stop transition, results in the workflow going + to the down state. + """ + yield self.assert_transition("install") - 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") + yield self.assert_transition("stop") + yield self.assert_state("stopped") + self.assert_hooks("install", "config-changed", "start", "stop") + yield self.assert_history([ + {"state": "installed", "state_variables": {}}, + {"state": "started", "state_variables": {}}, + {"state": "stopped", "state_variables": {}}]) @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") + def test_stop_with_error(self): + self.write_exit_hook("stop", 1) + yield self.assert_transition("install") + + yield self.assert_transition("stop", False) + yield self.assert_state("stop_error") + yield self.assert_transition("retry_stop") + yield self.assert_state("stopped") + self.assert_hooks("install", "config-changed", "start", "stop") + yield self.assert_history([ + {"state": "installed", "state_variables": {}}, + {"state": "started", "state_variables": {}}, + {"state": "stop_error", "state_variables": {}}, + {"state": "stopped", "state_variables": {}}]) + + @inlineCallbacks + def test_stop_error_with_retry_hook(self): + self.write_exit_hook("stop", 1) + yield self.assert_transition("install") + + yield self.assert_transition("stop", False) + yield self.assert_state("stop_error") + yield self.assert_transition("retry_stop_hook", False) + yield self.assert_state("stop_error") + self.write_exit_hook("stop") + yield self.assert_transition_alias("retry_hook") + yield self.assert_state("stopped") + self.assert_hooks( + "install", "config-changed", "start", "stop", "stop", "stop") + yield self.assert_history([ + {"state": "installed", "state_variables": {}}, + {"state": "started", "state_variables": {}}, + {"state": "stop_error", "state_variables": {}}, + {"state": "stopped", "state_variables": {}}]) @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") + yield self.assert_transition("install") - 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") + yield self.assert_transition("reconfigure") + yield self.assert_state("started") + self.assert_hooks( + "install", "config-changed", "start", "config-changed") + yield self.assert_history([ + {"state": "installed", "state_variables": {}}, + {"state": "started", "state_variables": {}}, + {"state": "started", "state_variables": {}}]) @inlineCallbacks def test_configure_error_and_retry(self): """An error while configuring, transitions the unit and stops the lifecycle.""" + yield self.assert_transition("install") + self.write_exit_hook("config-changed", 1) - 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") + yield self.assert_transition("reconfigure", False) + yield self.assert_state("configure_error") + yield self.assert_transition("retry_configure") + yield self.assert_state("started") + self.assert_hooks( + "install", "config-changed", "start", "config-changed") + yield self.assert_history([ + {"state": "installed", "state_variables": {}}, + {"state": "started", "state_variables": {}}, + {"state": "configure_error", "state_variables": {}}, + {"state": "started", "state_variables": {}}]) @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.assert_transition("install") + self.write_exit_hook("config-changed", 1) + + yield self.assert_transition("reconfigure", False) + yield self.assert_state("configure_error") + yield self.assert_transition("retry_configure_hook", False) + yield self.assert_state("configure_error") + self.write_exit_hook("config-changed") + yield self.assert_transition_alias("retry_hook") + yield self.assert_state("started") + self.assert_hooks( + "install", "config-changed", "start", + "config-changed", "config-changed", "config-changed") + yield self.assert_history([ + {"state": "installed", "state_variables": {}}, + {"state": "started", "state_variables": {}}, + {"state": "configure_error", "state_variables": {}}, + {"state": "configure_error", "state_variables": {}}, + {"state": "started", "state_variables": {}}]) + + @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") - 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") + running, state = yield is_unit_running( + self.client, self.states["unit"]) + self.assertIdentical(running, True) + self.assertEqual(state, "started") + yield self.workflow.fire_transition("stop") + running, state = yield is_unit_running( + self.client, self.states["unit"]) + self.assertIdentical(running, False) + self.assertEqual(state, "stopped") - # 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") + @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) - self.assertFalse(result) - yield hook_deferred - yield self.assertState(self.workflow, "configure_error") + @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()), "started") - 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_client_readonly(self): + yield self.workflow.fire_transition("install") + workflow_client = WorkflowStateClient( + self.client, self.states["unit"]) + + self.assertEqual( + (yield workflow_client.get_state()), "started") + yield self.assertFailure( + workflow_client.set_state("stopped"), NotImplementedError) + self.assertEqual( + (yield workflow_client.get_state()), "started") + + @inlineCallbacks + def assert_synchronize( + self, start_state, start_vars, + expect_state, expect_lifecycle, expect_executor): + all_start_states = itertools.product((True, False), (True, False)) + for lifecycle, executor in all_start_states: + if executor and not self.executor.running: + self.executor.start() + if lifecycle and not self.lifecycle.running: + yield self.lifecycle.start(fire_hooks=False) + yield self.workflow.set_state(start_state, **start_vars) + yield self.workflow.synchronize(self.executor) + + state = yield self.workflow.get_state() + self.assertEquals(state, expect_state) + vars = yield self.workflow.get_state_variables() + self.assertEquals(vars, {}) + self.assertEquals(self.lifecycle.running, expect_lifecycle) + self.assertEquals(self.executor.running, expect_executor) + + def assert_default_synchronize(self, state): + return self.assert_synchronize(state, {}, state, False, True) + + @inlineCallbacks + def test_synchronize(self): + yield self.assert_synchronize( + None, {}, "started", True, True) + yield self.assert_synchronize( + "installed", {}, "started", True, True) + yield self.assert_synchronize( + "started", {}, "started", True, True) + yield self.assert_synchronize( + "charm_upgrade_error", {}, "charm_upgrade_error", True, False) + yield self.assert_default_synchronize("install_error") + yield self.assert_default_synchronize("start_error") + yield self.assert_default_synchronize("configure_error") + yield self.assert_default_synchronize("stop_error") + yield self.assert_default_synchronize("stopped") @inlineCallbacks def test_upgrade(self): @@ -272,7 +395,6 @@ """ 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() @@ -290,7 +412,6 @@ 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( @@ -304,7 +425,6 @@ """ 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() @@ -329,7 +449,6 @@ 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") @@ -353,108 +472,6 @@ self.assertEqual(current_state, "started") self.assertTrue(self.executor.running) - @inlineCallbacks - def test_stop(self): - """Executing the stop transition, results in the workflow going - to the down state. - """ - file_path = self.makeFile() - self.write_hook( - "install", "#!/bin/bash\necho installed >> %s\n" % file_path) - self.write_hook( - "start", "#!/bin/bash\necho start >> %s\n" % file_path) - self.write_hook( - "stop", "#!/bin/bash\necho stop >> %s\n" % file_path) - result = yield self.workflow.fire_transition("install") - result = yield self.workflow.fire_transition("start") - result = yield self.workflow.fire_transition("stop") - self.assertTrue(result) - current_state = yield self.workflow.get_state() - self.assertEqual(current_state, "stopped") - f_state, history, zk_state = yield self.read_persistent_state() - self.assertEqual(f_state, zk_state) - self.assertEqual(f_state, - {"state": "stopped", "state_variables": {}}) - - workflow_client = WorkflowStateClient(self.client, self.states["unit"]) - value = yield workflow_client.get_state() - self.assertEqual(value, "stopped") - - self.assertEqual(history, - [{"state": "installed", "state_variables": {}}, - {"state": "started", "state_variables": {}}, - {"state": "stopped", "state_variables": {}}]) - - @inlineCallbacks - def test_stop_with_error(self): - self.write_hook("install", "#!/bin/bash\necho hello\n") - self.write_hook("start", "#!/bin/bash\necho hello\n") - result = yield self.workflow.fire_transition("install") - self.assertTrue(result) - result = yield self.workflow.fire_transition("start") - self.assertTrue(result) - - self.write_hook("stop", "#!/bin/bash\nexit 1") - result = yield self.workflow.fire_transition("stop") - self.assertFalse(result) - - yield self.assertState(self.workflow, "stop_error") - self.write_hook("stop", "#!/bin/bash\necho hello\n") - result = yield self.workflow.fire_transition("retry_stop") - - yield self.assertState(self.workflow, "stopped") - - @inlineCallbacks - def test_stop_error_with_retry_hook(self): - self.write_hook("install", "#!/bin/bash\necho hello\n") - self.write_hook("start", "#!/bin/bash\necho hello\n") - result = yield self.workflow.fire_transition("install") - self.assertTrue(result) - result = yield self.workflow.fire_transition("start") - self.assertTrue(result) - - self.write_hook("stop", "#!/bin/bash\nexit 1") - result = yield self.workflow.fire_transition("stop") - self.assertFalse(result) - yield self.assertState(self.workflow, "stop_error") - - result = yield self.workflow.fire_transition_alias("retry_hook") - yield self.assertState(self.workflow, "stop_error") - - self.write_hook("stop", "#!/bin/bash\nexit 0") - result = yield self.workflow.fire_transition_alias("retry_hook") - yield self.assertState(self.workflow, "stopped") - - @inlineCallbacks - def test_client_with_no_state(self): - workflow_client = WorkflowStateClient(self.client, self.states["unit"]) - state = yield workflow_client.get_state() - self.assertEqual(state, None) - - @inlineCallbacks - def test_client_with_state(self): - yield self.workflow.fire_transition("install") - workflow_client = WorkflowStateClient(self.client, self.states["unit"]) - self.assertEqual( - (yield workflow_client.get_state()), - "installed") - - @inlineCallbacks - def test_client_readonly(self): - yield self.workflow.fire_transition("install") - workflow_client = WorkflowStateClient( - self.client, self.states["unit"]) - - self.assertEqual( - (yield workflow_client.get_state()), - "installed") - yield self.assertFailure( - workflow_client.set_state("started"), - NotImplementedError) - self.assertEqual( - (yield workflow_client.get_state()), - "installed") - class UnitRelationWorkflowTest(WorkflowTestBase): @@ -479,8 +496,8 @@ path=os.path.join(self.juju_directory, "state")) self.workflow = RelationWorkflowState( - self.client, self.states["unit_relation"], self.lifecycle, - self.state_directory) + self.client, self.states["unit_relation"], + self.states["unit"].unit_name, self.lifecycle, self.state_directory) @inlineCallbacks def test_is_relation_running(self): @@ -585,18 +602,6 @@ broken hook is executed, and the unit stops responding to relation changes. """ - self.write_hook("%s-relation-joined" % self.relation_name, - "#!/bin/bash\necho hello\n") - self.write_hook("%s-relation-changed" % self.relation_name, - "#!/bin/bash\necho hello\n") - self.write_hook("%s-relation-broken" % self.relation_name, - "#!/bin/bash\necho hello\n") - - results = [] - - def collect_executions(*args): - results.append(args) - yield self.workflow.fire_transition("start") yield self.assertState(self.workflow, "up") @@ -612,8 +617,14 @@ # verify further changes to the related unit, don't result in # hook executions. + results = [] + + def collect_executions(*args): + results.append(args) + self.executor.set_observer(collect_executions) yield states["unit_relation"].set_data(dict(a=1)) + # Sleep to give errors a chance. yield self.sleep(0.1) self.assertFalse(results) @@ -654,22 +665,42 @@ NotImplementedError) @inlineCallbacks + def assert_synchronize(self, state, expect_state, watches, scheduler): + start_states = itertools.product((True, False), (True, False)) + for (initial_watches, initial_scheduler) in start_states: + yield self.workflow.lifecycle.stop() + yield self.workflow.lifecycle.start( + start_watches=initial_watches, + start_scheduler=initial_scheduler) + self.assertEquals( + self.workflow.lifecycle.watching, initial_watches) + self.assertEquals( + self.workflow.lifecycle.executing, initial_scheduler) + yield self.workflow.set_state(state) + + yield self.workflow.synchronize() + new_state = yield self.workflow.get_state() + self.assertEquals(new_state, expect_state) + self.assertEquals(self.workflow.lifecycle.watching, watches) + self.assertEquals(self.workflow.lifecycle.executing, scheduler) + + @inlineCallbacks + def test_synchronize(self): + yield self.assert_synchronize(None, "up", True, True) + yield self.assert_synchronize("down", "down", False, False) + yield self.assert_synchronize("departed", "departed", False, False) + yield self.assert_synchronize("error", "error", True, False) + yield self.assert_synchronize("up", "up", True, True) + + @inlineCallbacks def test_depart_hook_error(self): """A depart hook error, still results in a transition to the departed state with a state variable noting the error.""" - self.write_hook("%s-relation-changed" % self.relation_name, - "#!/bin/bash\necho hello\n") self.write_hook("%s-relation-broken" % self.relation_name, "#!/bin/bash\nexit 1\n") - error_output = self.capture_logging("unit.relation.workflow") - results = [] - - def collect_executions(*args): - results.append(args) - yield self.workflow.fire_transition("start") yield self.assertState(self.workflow, "up") @@ -683,10 +714,16 @@ yield wait_on_hook yield wait_on_state - # verify further changes to the related unit, don't result in + # verify further changes to the related unit don't result in # hook executions. + results = [] + + def collect_executions(*args): + results.append(args) + self.executor.set_observer(collect_executions) yield states["unit_relation"].set_data(dict(a=1)) + # Sleep to give errors a chance. yield self.sleep(0.1) self.assertFalse(results) @@ -715,39 +752,59 @@ "error_message": error_msg}}) def test_depart_down(self): - """When the workflow is transition to the down state, a relation + """When the workflow is transitioned from down to departed, a relation broken hook is executed, and the unit stops responding to relation changes. """ - self.write_hook("%s-relation-changed" % self.relation_name, - "#!/bin/bash\necho hello\n") - self.write_hook("%s-relation-broken" % self.relation_name, - "#!/bin/bash\necho hello\n") + yield self.workflow.fire_transition("start") + yield self.assertState(self.workflow, "up") + yield self.workflow.fire_transition("stop") + yield self.assertState(self.workflow, "down") + states = yield self.add_opposite_service_unit(self.states) + wait_on_hook = self.wait_on_hook("app-relation-broken") + wait_on_state = self.wait_on_state(self.workflow, "departed") + yield self.workflow.fire_transition("depart") + yield wait_on_hook + yield wait_on_state + + # Verify further changes to the related unit, don't result in + # hook executions. results = [] def collect_executions(*args): results.append(args) + self.executor.set_observer(collect_executions) + yield states["unit_relation"].set_data(dict(a=1)) + + # Sleep to give errors a chance. + yield self.sleep(0.1) + self.assertFalse(results) + + def test_depart_error(self): yield self.workflow.fire_transition("start") yield self.assertState(self.workflow, "up") - - yield self.workflow.fire_transition("stop") - yield self.assertState(self.workflow, "down") + yield self.workflow.fire_transition("error") + yield self.assertState(self.workflow, "error") states = yield self.add_opposite_service_unit(self.states) - wait_on_hook = self.wait_on_hook("app-relation-broken") wait_on_state = self.wait_on_state(self.workflow, "departed") - yield self.workflow.fire_transition("depart") yield wait_on_hook yield wait_on_state # Verify further changes to the related unit, don't result in # hook executions. + results = [] + + def collect_executions(*args): + results.append(args) + self.executor.set_observer(collect_executions) yield states["unit_relation"].set_data(dict(a=1)) + # Sleep to give errors a chance. yield self.sleep(0.1) self.assertFalse(results) diff -Nru juju-0.5+bzr447/juju/unit/workflow.py juju-0.5+bzr457/juju/unit/workflow.py --- juju-0.5+bzr447/juju/unit/workflow.py 2012-01-23 18:02:19.000000000 +0000 +++ juju-0.5+bzr457/juju/unit/workflow.py 2012-01-11 09:37:48.000000000 +0000 @@ -16,7 +16,8 @@ UnitWorkflow = Workflow( # Install transitions Transition("install", "Install", None, "installed", - error_transition_id="error_install"), + error_transition_id="error_install", + success_transition_id="start"), Transition("error_install", "Install error", None, "install_error"), Transition("retry_install", "Retry install", "install_error", "installed", alias="retry", success_transition_id="start"), @@ -42,13 +43,6 @@ Transition("retry_stop_hook", "Retry stop with hook", "stop_error", "stopped", alias="retry_hook"), - # Restart transitions - Transition("restart", "Restart", "stop", "start", - error_transition_id="error_start", alias="retry"), - Transition("restart_with_hook", "Restart with hook", - "stop", "start", alias="retry_hook", - error_transition_id="error_start"), - # Upgrade transitions Transition( "upgrade_charm", "Upgrade", "started", "started", @@ -123,6 +117,7 @@ Transition("reset", "Recover from hook error", "error", "up"), Transition("depart", "Relation broken", "up", "departed"), Transition("down_depart", "Relation broken", "down", "departed"), + Transition("error_depart", "Relation broken", "error", "departed"), ) @@ -342,6 +337,46 @@ raise TransitionError(e) returnValue(result) + @inlineCallbacks + def synchronize(self, executor): + """Ensure the workflow's lifecycle is in the correct state, given + current zookeeper state. + + :param executor: the unit agent's shared HookExecutor, which should not + run if we come up in (or detect and switch to) the + "charm_upgrade_error" state. + + In addition, if the lifecycle has never been started before, the + necessary state transitions are run. + """ + state = yield self.get_state() + run_executor, run_lifecycle = True, False + if state == "started": + run_lifecycle = True + elif state == "charm_upgrade_error": + run_executor, run_lifecycle = False, True + + if run_executor: + if not executor.running: + executor.start() + elif executor.running: + yield executor.stop() + + if run_lifecycle: + if not self._lifecycle.running: + yield self._lifecycle.start( + fire_hooks=False, start_relations=False) + elif self._lifecycle.running: + yield self._lifecycle.stop(fire_hooks=False) + + # At this point, prior state (if any) has been fully restored, and + # we can run state transitions as usual; fire the standard startup ones + # if they haven't completed yet. + if state is None: + yield self.fire_transition("install") + if state == "installed": + yield self.fire_transition("start") + # Install transitions def do_install(self): return self._invoke_lifecycle(self._lifecycle.install) @@ -412,14 +447,40 @@ _workflow = RelationWorkflow - def __init__(self, client, unit_relation, lifecycle, state_directory): + def __init__( + self, client, unit_relation, relation_name, lifecycle, state_dir): super(RelationWorkflowState, self).__init__( - client, unit_relation, state_directory) + client, unit_relation, state_dir) self._lifecycle = lifecycle + self.relation_name = relation_name # Catch any related-change hook errors self._lifecycle.set_hook_error_handler(self.on_hook_error) self._log = logging.getLogger("unit.relation.workflow") + @inlineCallbacks + def synchronize(self): + """Ensure the workflow's lifecycle is in the correct state, given + current zookeeper state. + + In addition, if the lifecycle has never been started before, the + necessary state transitions are run. + """ + state = yield self.get_state() + if state == "up": + watches, scheduler = True, True + elif state in (None, "down", "departed"): + watches, scheduler = False, False + elif state == "error": + watches, scheduler = True, False + + yield self._lifecycle.stop() + if watches or scheduler: + yield self._lifecycle.start( + start_watches=watches, start_scheduler=scheduler) + + if state is None: + yield self.fire_transition("start") + @property def lifecycle(self): return self._lifecycle @@ -460,14 +521,14 @@ Turns on the unit-relation lifecycle monitoring and hook execution. """ - yield self._lifecycle.start(watches=False) + yield self._lifecycle.start(start_watches=False) @inlineCallbacks def do_error(self, **error_info): """A relation hook error, stops further execution hooks but continues to watch for changes. """ - yield self._lifecycle.stop(watches=False) + yield self._lifecycle.stop(stop_watches=False) @inlineCallbacks def do_restart(self): diff -Nru juju-0.5+bzr447/misc/devel-tools/juju-inspect-local-provider juju-0.5+bzr457/misc/devel-tools/juju-inspect-local-provider --- juju-0.5+bzr447/misc/devel-tools/juju-inspect-local-provider 1970-01-01 00:00:00.000000000 +0000 +++ juju-0.5+bzr457/misc/devel-tools/juju-inspect-local-provider 2011-10-14 23:44:09.000000000 +0000 @@ -0,0 +1,63 @@ +#!/bin/bash -x + +# Gather a collection of data into an output file to make +# debugging any possible issues with the local provider simpler + +if [ ! `id -u` == '0' ]; then + echo "This script should be run as root" + exit 1; +fi + +if [ ${#} = 1 ]; then + image_name=$1 +fi + +# 11.10 (Oneiric) is the first supported release for the local provider due +# to its improved LXC support +source /etc/lsb-release +major_release=`echo $DISTRIB_RELEASE | cut -d . -f 1` +minor_release=`echo $DISTRIB_RELEASE | cut -d . -f 2` + +if [ $major_release -lt 11 -o $minor_release -lt 10 ]; then + echo "Oneiric 11.10 is the first supported release of the local provider" + exit 1; +fi + + +# Collect various status information about the system +echo "#Local provider inspection" +uname -a + +ifconfig +virsh net-list --all + +ls /var/cache/lxc +ls /var/cache/lxc/* + +lxc-ls + +# guess about the users data-dir +if [ -n "${SUDO_USER}" ]; then + user=$SUDO_USER +fi + + +image=/var/lib/lxc/$image_name +if [ -n "$image_name" -a -e "$image" ]; then + cat "$image/config" + chroot "$image/rootfs" bash -xc " + cat /etc/juju/juju.conf; + ls /usr/lib/juju/juju; + dpkg-query -s juju; + cat /etc/hostname; + cat /etc/resolv.conf; + cat /etc/hosts; + tail -n 100 /var/log/juju/*.log + " +fi + + + + + +