def test_resolved_already_resolved(self): """ 'juju resolved <unit_name>' will report if the unit is already resolved. """ # Mark the unit as resolved and as in an error state. yield self.service_unit1.set_resolved(RETRY_HOOKS) with (yield self.unit1_workflow.lock()): yield self.unit1_workflow.set_state("start_error") unit2_workflow = UnitWorkflowState( self.client, self.service_unit1, None, self.makeDir()) with (yield unit2_workflow.lock()): unit2_workflow.set_state("start_error") self.assertEqual( (yield self.service_unit2.get_resolved()), None) self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() main(["resolved", "mysql/0"]) yield finished self.assertEqual( (yield self.service_unit1.get_resolved()), {"retry": RETRY_HOOKS}) self.assertNotIn( "Marked unit 'mysql/0' as resolved", self.output.getvalue()) self.assertIn( "Service unit 'mysql/0' is already marked as resolved.", self.stderr.getvalue(), "")
def test_resolved_already_running(self): """ 'juju resolved <unit_name>' will report if the unit is already running. """ # Just verify we don't accidentally mark up another unit of the service unit2_workflow = UnitWorkflowState( self.client, self.service_unit2, None, self.makeDir()) with (yield unit2_workflow.lock()): unit2_workflow.set_state("start_error") self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() main(["resolved", "mysql/0"]) yield finished self.assertEqual( (yield self.service_unit2.get_resolved()), None) self.assertEqual( (yield self.service_unit1.get_resolved()), None) self.assertNotIn( "Unit 'mysql/0 already running: started", self.output.getvalue())
class UnitWorkflowTestBase(WorkflowTestBase): @inlineCallbacks def setUp(self): yield super(UnitWorkflowTestBase, self).setUp() yield self.setup_default_test_relation() self.lifecycle = UnitLifecycle( self.client, self.states["unit"], self.states["service"], self.unit_directory, self.state_directory, self.executor) self.workflow = UnitWorkflowState( self.client, self.states["unit"], self.lifecycle, self.state_directory) self.write_exit_hook("install") self.write_exit_hook("start") self.write_exit_hook("stop") self.write_exit_hook("config-changed") self.write_exit_hook("upgrade-charm") @inlineCallbacks def assert_transition(self, transition, success=True): with (yield self.workflow.lock()): result = yield self.workflow.fire_transition(transition) self.assertEquals(result, success) @inlineCallbacks def assert_transition_alias(self, transition, success=True): with (yield self.workflow.lock()): result = yield self.workflow.fire_transition_alias(transition) self.assertEquals(result, success) @inlineCallbacks def assert_state(self, expected): actual = yield self.workflow.get_state() self.assertEquals(actual, expected) def assert_hooks(self, *hooks): with open(self.output) as f: lines = tuple(l.strip() for l in f) self.assertEquals(lines, hooks)
def test_agent_start_from_started_workflow(self): lifecycle = UnitLifecycle( self.client, self.states["unit"], self.states["service"], self.unit_directory, self.state_directory, self.executor) workflow = UnitWorkflowState( self.client, self.states["unit"], lifecycle, os.path.join(self.juju_directory, "state")) with (yield workflow.lock()): yield workflow.fire_transition("install") yield lifecycle.stop(fire_hooks=False, stop_relations=False) yield self.agent.startService() current_state = yield self.agent.workflow.get_state() self.assertEqual(current_state, "started") self.assertTrue(self.agent.lifecycle.running) self.assertTrue(self.agent.executor.running)
class UnitAgent(BaseAgent): """An juju Unit Agent. Provides for the management of a charm, via hook execution in response to external events in the coordination space (zookeeper). """ name = "juju-unit-agent" @classmethod def setup_options(cls, parser): super(UnitAgent, cls).setup_options(parser) unit_name = os.environ.get("JUJU_UNIT_NAME", "") parser.add_argument("--unit-name", default=unit_name) @property def unit_name(self): return self.config["unit_name"] def get_agent_name(self): return "unit:%s" % self.unit_name def configure(self, options): """Configure the unit agent.""" super(UnitAgent, self).configure(options) if not options.get("unit_name"): msg = ("--unit-name must be provided in the command line, " "or $JUJU_UNIT_NAME in the environment") raise JujuError(msg) self.executor = HookExecutor() self.api_factory = UnitSettingsFactory( self.executor.get_hook_context, self.executor.get_invoker, logging.getLogger("unit.hook.api")) self.api_socket = None self.workflow = None @inlineCallbacks def start(self): """Start the unit agent process.""" service_state_manager = ServiceStateManager(self.client) # Retrieve our unit and configure working directories. service_name = self.unit_name.split("/")[0] self.service_state = yield service_state_manager.get_service_state( service_name) self.unit_state = yield self.service_state.get_unit_state( self.unit_name) self.unit_directory = os.path.join( self.config["juju_directory"], "units", self.unit_state.unit_name.replace("/", "-")) self.state_directory = os.path.join( self.config["juju_directory"], "state") # Setup the server portion of the cli api exposed to hooks. socket_path = os.path.join(self.unit_directory, HOOK_SOCKET_FILE) if os.path.exists(socket_path): os.unlink(socket_path) from twisted.internet import reactor self.api_socket = reactor.listenUNIX(socket_path, self.api_factory) # Setup the unit state's address address = yield get_unit_address(self.client) yield self.unit_state.set_public_address( (yield address.get_public_address())) yield self.unit_state.set_private_address( (yield address.get_private_address())) if self.get_watch_enabled(): yield self.unit_state.watch_hook_debug(self.cb_watch_hook_debug) # Inform the system, we're alive. yield self.unit_state.connect_agent() # Start paying attention to the debug-log setting if self.get_watch_enabled(): yield self.unit_state.watch_hook_debug(self.cb_watch_hook_debug) self.lifecycle = UnitLifecycle( self.client, self.unit_state, self.service_state, self.unit_directory, self.state_directory, self.executor) self.workflow = UnitWorkflowState( self.client, self.unit_state, self.lifecycle, self.state_directory) # Set up correct lifecycle and executor state given the persistent # unit workflow state, and fire any starting transitions if necessary. with (yield self.workflow.lock()): yield self.workflow.synchronize(self.executor) if self.get_watch_enabled(): yield self.unit_state.watch_resolved(self.cb_watch_resolved) yield self.service_state.watch_config_state( self.cb_watch_config_changed) yield self.unit_state.watch_upgrade_flag( self.cb_watch_upgrade_flag) @inlineCallbacks def stop(self): """Stop the unit agent process.""" if self.lifecycle.running: yield self.lifecycle.stop(fire_hooks=False, stop_relations=False) yield self.executor.stop() if self.api_socket: yield self.api_socket.stopListening() yield self.api_factory.stopFactory() @inlineCallbacks def cb_watch_resolved(self, change): """Update the unit's state, when its resolved. Resolved operations form the basis of error recovery for unit workflows. A resolved operation can optionally specify hook execution. The unit agent runs the error recovery transition if the unit is not in a running state. """ # Would be nice if we could fold this into an atomic # get and delete primitive. # Check resolved setting resolved = yield self.unit_state.get_resolved() if resolved is None: returnValue(None) # Clear out the setting yield self.unit_state.clear_resolved() with (yield self.workflow.lock()): if (yield self.workflow.get_state()) == "started": returnValue(None) try: log.info("Resolved detected, firing retry transition") if resolved["retry"] == RETRY_HOOKS: yield self.workflow.fire_transition_alias("retry_hook") else: yield self.workflow.fire_transition_alias("retry") except Exception: log.exception("Unknown error while transitioning for resolved") @inlineCallbacks def cb_watch_hook_debug(self, change): """Update the hooks to be debugged when the settings change. """ debug = yield self.unit_state.get_hook_debug() debug_hooks = debug and debug.get("debug_hooks") or None self.executor.set_debug(debug_hooks) @inlineCallbacks def cb_watch_upgrade_flag(self, change): """Update the unit's charm when requested. """ upgrade_flag = yield self.unit_state.get_upgrade_flag() if not upgrade_flag: log.info("No upgrade flag set.") return log.info("Upgrade detected") # Clear the flag immediately; this means that upgrade requests will # be *ignored* by units which are not "started", and will need to be # reissued when the units are in acceptable states. yield self.unit_state.clear_upgrade_flag() new_id = yield self.service_state.get_charm_id() old_id = yield self.unit_state.get_charm_id() if new_id == old_id: log.info("Upgrade ignored: already running latest charm") return with (yield self.workflow.lock()): state = yield self.workflow.get_state() if state != "started": if upgrade_flag["force"]: yield self.lifecycle.upgrade_charm( fire_hooks=False, force=True) log.info("Forced upgrade complete") return log.warning( "Cannot upgrade: unit is in non-started state %s. Reissue " "upgrade command to try again.", state) return log.info("Starting upgrade") if (yield self.workflow.fire_transition("upgrade_charm")): log.info("Upgrade complete") else: log.info("Upgrade failed") @inlineCallbacks def cb_watch_config_changed(self, change): """Trigger hook on configuration change""" # Verify it is running with (yield self.workflow.lock()): current_state = yield self.workflow.get_state() log.debug("Configuration Changed") if current_state != "started": log.debug( "Configuration updated on service in a non-started state") returnValue(None) yield self.workflow.fire_transition("configure")
class ControlCharmUpgradeTest( MachineControlToolTest, CharmUpgradeTestBase): @inlineCallbacks def setUp(self): yield super(ControlCharmUpgradeTest, self).setUp() self.service_state1 = yield self.add_service_from_charm("mysql") self.service_unit1 = yield self.service_state1.add_unit_state() self.unit1_workflow = UnitWorkflowState( self.client, self.service_unit1, None, self.makeDir()) with (yield self.unit1_workflow.lock()): yield self.unit1_workflow.set_state("started") self.environment = self.config.get_default() self.provider = self.environment.get_machine_provider() self.output = self.capture_logging() self.stderr = self.capture_stream("stderr") @inlineCallbacks def test_charm_upgrade(self): """ 'juju charm-upgrade <service_name>' will schedule a charm for upgrade. """ repository = self.increment_charm(self.charm) mock_environment = self.mocker.patch(Environment) mock_environment.get_machine_provider() self.mocker.result(self.provider) finished = self.setup_cli_reactor() self.setup_exit(0) self.mocker.replay() main(["upgrade-charm", "--repository", repository.path, "mysql"]) yield finished # Verify the service has a new charm reference charm_id = yield self.service_state1.get_charm_id() self.assertEqual(charm_id, "local:series/mysql-2") # Verify the provider storage has been updated charm = yield repository.find(CharmURL.parse("local:series/mysql")) storage = self.provider.get_file_storage() try: yield storage.get( "local_3a_series_2f_mysql-2_3a_%s" % charm.get_sha256()) except FileNotFound: self.fail("New charm not uploaded") # Verify the upgrade flag on the service units. upgrade_flag = yield self.service_unit1.get_upgrade_flag() self.assertTrue(upgrade_flag) @inlineCallbacks def test_missing_repository(self): finished = self.setup_cli_reactor() self.setup_exit(0) self.mocker.replay() main(["upgrade-charm", "mysql"]) yield finished self.assertIn("No repository specified", self.output.getvalue()) @inlineCallbacks def test_repository_from_environ(self): repository = self.increment_charm(self.charm) self.change_environment(JUJU_REPOSITORY=repository.path) finished = self.setup_cli_reactor() self.setup_exit(0) self.mocker.replay() main(["upgrade-charm", "mysql"]) yield finished self.assertNotIn("No repository specified", self.output.getvalue()) @inlineCallbacks def test_upgrade_charm_with_unupgradeable_units(self): """If there are units that won't be upgraded, they will be reported, other units will be upgraded. """ repository = self.increment_charm(self.charm) service_unit2 = yield self.service_state1.add_unit_state() finished = self.setup_cli_reactor() self.setup_exit(0) self.mocker.replay() main(["upgrade-charm", "--repository", repository.path, "mysql"]) yield finished # Verify report of unupgradeable units self.assertIn( ("Unit 'mysql/1' is not in a running state " "(state: 'uninitialized'), won't upgrade"), self.output.getvalue()) # Verify flags only set on upgradeable unit. value = (yield service_unit2.get_upgrade_flag()) self.assertFalse(value) value = (yield self.service_unit1.get_upgrade_flag()) self.assertTrue(value) @inlineCallbacks def test_force_charm_upgrade(self): repository = self.increment_charm(self.charm) with (yield self.unit1_workflow.lock()): yield self.unit1_workflow.set_state("start_error") finished = self.setup_cli_reactor() self.setup_exit(0) self.mocker.replay() main(["upgrade-charm", "--repository", repository.path, "--force", "mysql"]) yield finished value = (yield self.service_unit1.get_upgrade_flag()) self.assertEqual(value, {'force': True}) @inlineCallbacks def test_upgrade_charm_unknown_service(self): finished = self.setup_cli_reactor() self.setup_exit(0) self.mocker.replay() main(["upgrade-charm", "--repository", self.makeDir(), "volcano"]) yield finished self.assertIn( "Service 'volcano' was not found", self.stderr.getvalue()) @inlineCallbacks def test_upgrade_charm_unknown_charm(self): """If a charm is not found in the repository, an error is given. """ finished = self.setup_cli_reactor() self.setup_exit(0) self.mocker.replay() repository_dir = self.makeDir() os.mkdir(os.path.join(repository_dir, "series")) main(["upgrade-charm", "--repository", repository_dir, "mysql"]) yield finished self.assertIn( "Charm 'local:series/mysql' not found in repository", self.output.getvalue()) @inlineCallbacks def test_upgrade_charm_unknown_charm_dryrun(self): """If a charm is not found in the repository, an error is given. """ finished = self.setup_cli_reactor() self.setup_exit(0) self.mocker.replay() repository_dir = self.makeDir() os.mkdir(os.path.join(repository_dir, "series")) main(["upgrade-charm", "--repository", repository_dir, "mysql", "--dry-run"]) yield finished self.assertIn( "Charm 'local:series/mysql' not found in repository", self.output.getvalue()) @inlineCallbacks def test_upgrade_charm_dryrun_reports_unupgradeable_units(self): """If there are units that won't be upgraded, dry-run will report them. """ repository = self.increment_charm(self.charm) service_unit2 = yield self.service_state1.add_unit_state() finished = self.setup_cli_reactor() self.setup_exit(0) self.mocker.replay() main(["upgrade-charm", "-n", "--repository", repository.path, "mysql"]) yield finished # Verify dry run self.assertIn( "Service would be upgraded from charm", self.output.getvalue()) # Verify report of unupgradeable units self.assertIn( ("Unit 'mysql/1' is not in a running state " "(state: 'uninitialized'), won't upgrade"), self.output.getvalue()) # Verify no flags have been set. value = (yield service_unit2.get_upgrade_flag()) self.assertFalse(value) value = (yield self.service_unit1.get_upgrade_flag()) self.assertFalse(value) @inlineCallbacks def test_apply_new_charm_defaults(self): finished = self.setup_cli_reactor() self.setup_exit(0) self.mocker.replay() # Add a charm and its service. metadata = {"name": "haiku", "summary": "its short", "description": "but with cadence"} repository = self.add_charm( metadata=metadata, revision=1, config={ "options": { "foo": {"type": "string", "default": "foo-default", "description": "Foo"}, "bar": {"type": "string", "default": "bar-default", "description": "Bar"}, } }) charm_dir = yield repository.find(CharmURL.parse("local:series/haiku")) service_state = yield self.add_service_from_charm( "haiku", charm_dir=charm_dir) # Update a config value config = yield service_state.get_config() config["foo"] = "abc" yield config.write() # Upgrade the charm repository = self.add_charm( metadata=metadata, revision=2, config={ "options": { "foo": {"type": "string", "default": "foo-default", "description": "Foo"}, "bar": {"type": "string", "default": "bar-default", "description": "Bar"}, "dca": {"type": "string", "default": "default-dca", "description": "Airport"}, } }) main(["upgrade-charm", "--repository", repository.path, "haiku"]) yield finished config = yield service_state.get_config() self.assertEqual( config, {"foo": "abc", "dca": "default-dca", "bar": "bar-default"}) @inlineCallbacks def test_latest_local_dry_run(self): """Do nothing; log that local charm would be re-revisioned and used""" finished = self.setup_cli_reactor() self.setup_exit(0) self.mocker.replay() metadata = self.charm.metadata.get_serialization_data() metadata["name"] = "mysql" repository = self.add_charm(metadata, 1) main(["upgrade-charm", "--dry-run", "--repository", repository.path, "mysql"]) yield finished charm_path = os.path.join(repository.path, "series", "mysql") self.assertIn( "%s would be set to revision 2" % charm_path, self.output.getvalue()) self.assertIn( "Service would be upgraded from charm 'local:series/mysql-1' to " "'local:series/mysql-2'", self.output.getvalue()) with open(os.path.join(charm_path, "revision")) as f: self.assertEquals(f.read(), "1") upgrade_flag = yield self.service_unit1.get_upgrade_flag() self.assertFalse(upgrade_flag) @inlineCallbacks def test_latest_local_live_fire(self): """Local charm should be re-revisioned and used; log that it was""" finished = self.setup_cli_reactor() self.setup_exit(0) self.mocker.replay() metadata = self.charm.metadata.get_serialization_data() metadata["name"] = "mysql" repository = self.add_charm(metadata, 1) main(["upgrade-charm", "--repository", repository.path, "mysql"]) yield finished charm_path = os.path.join(repository.path, "series", "mysql") self.assertIn( "Setting %s to revision 2" % charm_path, self.output.getvalue()) with open(os.path.join(charm_path, "revision")) as f: self.assertEquals(f.read(), "2\n") upgrade_flag = yield self.service_unit1.get_upgrade_flag() self.assertTrue(upgrade_flag) @inlineCallbacks def test_latest_local_leapfrog_dry_run(self): """Do nothing; log that local charm would be re-revisioned and used""" finished = self.setup_cli_reactor() self.setup_exit(0) self.mocker.replay() metadata = self.charm.metadata.get_serialization_data() metadata["name"] = "mysql" repository = self.add_charm(metadata, 0) main(["upgrade-charm", "--dry-run", "--repository", repository.path, "mysql"]) yield finished charm_path = os.path.join(repository.path, "series", "mysql") self.assertIn( "%s would be set to revision 2" % charm_path, self.output.getvalue()) self.assertIn( "Service would be upgraded from charm 'local:series/mysql-1' to " "'local:series/mysql-2'", self.output.getvalue()) with open(os.path.join(charm_path, "revision")) as f: self.assertEquals(f.read(), "0") upgrade_flag = yield self.service_unit1.get_upgrade_flag() self.assertFalse(upgrade_flag) @inlineCallbacks def test_latest_local_leapfrog_live_fire(self): """Local charm should be re-revisioned and used; log that it was""" finished = self.setup_cli_reactor() self.setup_exit(0) self.mocker.replay() metadata = self.charm.metadata.get_serialization_data() metadata["name"] = "mysql" repository = self.add_charm(metadata, 0) main(["upgrade-charm", "--repository", repository.path, "mysql"]) yield finished charm_path = os.path.join(repository.path, "series", "mysql") self.assertIn( "Setting %s to revision 2" % charm_path, self.output.getvalue()) with open(os.path.join(charm_path, "revision")) as f: self.assertEquals(f.read(), "2\n") upgrade_flag = yield self.service_unit1.get_upgrade_flag() self.assertTrue(upgrade_flag) @inlineCallbacks def test_latest_local_bundle_dry_run(self): """Do nothing; log that nothing would be done""" finished = self.setup_cli_reactor() self.setup_exit(0) self.mocker.replay() metadata = self.charm.metadata.get_serialization_data() metadata["name"] = "mysql" repository = self.add_charm(metadata, 1, bundle=True) main(["upgrade-charm", "--dry-run", "--repository", repository.path, "mysql"]) yield finished self.assertIn( "Service already running latest charm", self.output.getvalue()) upgrade_flag = yield self.service_unit1.get_upgrade_flag() self.assertFalse(upgrade_flag) @inlineCallbacks def test_latest_local_bundle_live_fire(self): """Do nothing; log that nothing was done""" finished = self.setup_cli_reactor() self.setup_exit(0) self.mocker.replay() metadata = self.charm.metadata.get_serialization_data() metadata["name"] = "mysql" repository = self.add_charm(metadata, 1, bundle=True) main(["upgrade-charm", "--repository", repository.path, "mysql"]) yield finished self.assertIn( "Charm 'local:series/mysql-1' is the latest revision known", self.output.getvalue()) upgrade_flag = yield self.service_unit1.get_upgrade_flag() self.assertFalse(upgrade_flag)
class RemoteUpgradeCharmTest(MachineControlToolTest): @inlineCallbacks def setUp(self): yield super(RemoteUpgradeCharmTest, self).setUp() charm = CharmDirectory(os.path.join( test_repository_path, "series", "mysql")) self.charm_state_manager.add_charm_state( "cs:series/mysql-1", charm, "") self.service_state1 = yield self.add_service_from_charm( "mysql", "cs:series/mysql-1") self.service_unit1 = yield self.service_state1.add_unit_state() self.unit1_workflow = UnitWorkflowState( self.client, self.service_unit1, None, self.makeDir()) with (yield self.unit1_workflow.lock()): yield self.unit1_workflow.set_state("started") self.environment = self.config.get_default() self.provider = self.environment.get_machine_provider() self.output = self.capture_logging() self.stderr = self.capture_stream("stderr") @inlineCallbacks def test_latest_dry_run(self): """Do nothing; log that nothing would be done""" finished = self.setup_cli_reactor() self.setup_exit(0) getPage = self.mocker.replace("twisted.web.client.getPage") getPage( CS_STORE_URL + "/charm-info?charms=cs%3Aseries/mysql") self.mocker.result(succeed(json.dumps( {"cs:series/mysql": {"revision": 1, "sha256": "whatever"}}))) self.mocker.replay() main(["upgrade-charm", "--dry-run", "mysql"]) yield finished self.assertIn( "Service already running latest charm", self.output.getvalue()) upgrade_flag = yield self.service_unit1.get_upgrade_flag() self.assertFalse(upgrade_flag) @inlineCallbacks def test_latest_live_fire(self): """Do nothing; log that nothing was done""" finished = self.setup_cli_reactor() self.setup_exit(0) getPage = self.mocker.replace("twisted.web.client.getPage") getPage(CS_STORE_URL + "/charm-info?charms=cs%3Aseries/mysql") self.mocker.result(succeed(json.dumps( {"cs:series/mysql": {"revision": 1, "sha256": "whatever"}}))) self.mocker.replay() main(["upgrade-charm", "mysql"]) yield finished self.assertIn( "Charm 'cs:series/mysql-1' is the latest revision known", self.output.getvalue()) upgrade_flag = yield self.service_unit1.get_upgrade_flag() self.assertFalse(upgrade_flag)
class ControlResolvedTest( ServiceStateManagerTestBase, ControlToolTest, RepositoryTestBase): @inlineCallbacks def setUp(self): yield super(ControlResolvedTest, self).setUp() yield self.add_relation_state("wordpress", "mysql") yield self.add_relation_state("wordpress", "varnish") self.service1 = yield self.service_state_manager.get_service_state( "mysql") self.service_unit1 = yield self.service1.add_unit_state() self.service_unit2 = yield self.service1.add_unit_state() self.unit1_workflow = UnitWorkflowState( self.client, self.service_unit1, None, self.makeDir()) with (yield self.unit1_workflow.lock()): yield self.unit1_workflow.set_state("started") self.environment = self.config.get_default() self.provider = self.environment.get_machine_provider() self.output = self.capture_logging() self.stderr = self.capture_stream("stderr") self.executor = HookExecutor() @inlineCallbacks def add_relation_state(self, *service_names): for service_name in service_names: try: yield self.service_state_manager.get_service_state( service_name) except ServiceStateNotFound: yield self.add_service_from_charm(service_name) endpoint_pairs = yield self.service_state_manager.join_descriptors( *service_names) endpoints = endpoint_pairs[0] endpoints = endpoint_pairs[0] if endpoints[0] == endpoints[1]: endpoints = endpoints[0:1] relation_state = (yield self.relation_state_manager.add_relation_state( *endpoints))[0] returnValue(relation_state) @inlineCallbacks def get_named_service_relation(self, service_state, relation_name): if isinstance(service_state, str): service_state = yield self.service_state_manager.get_service_state( service_state) rels = yield self.relation_state_manager.get_relations_for_service( service_state) rels = [sr for sr in rels if sr.relation_name == relation_name] if len(rels) == 1: returnValue(rels[0]) returnValue(rels) @inlineCallbacks def setup_unit_relations(self, service_relation, *units): """ Given a service relation and set of unit tuples in the form unit_state, unit_relation_workflow_state, will add unit relations for these units and update their workflow state to the desired/given state. """ for unit, state in units: unit_relation = yield service_relation.add_unit_state(unit) lifecycle = UnitRelationLifecycle( self.client, unit.unit_name, unit_relation, service_relation.relation_ident, self.makeDir(), self.makeDir(), self.executor) workflow_state = RelationWorkflowState( self.client, unit_relation, service_relation.relation_name, lifecycle, self.makeDir()) with (yield workflow_state.lock()): yield workflow_state.set_state(state) @inlineCallbacks def test_resolved(self): """ 'juju resolved <unit_name>' will schedule a unit for retrying from an error state. """ # Push the unit into an error state with (yield self.unit1_workflow.lock()): yield self.unit1_workflow.set_state("start_error") self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() self.assertEqual( (yield self.service_unit1.get_resolved()), None) main(["resolved", "mysql/0"]) yield finished self.assertEqual( (yield self.service_unit1.get_resolved()), {"retry": NO_HOOKS}) self.assertIn( "Marked unit 'mysql/0' as resolved", self.output.getvalue()) @inlineCallbacks def test_resolved_retry(self): """ 'juju resolved --retry <unit_name>' will schedule a unit for retrying from an error state with a retry of hooks executions. """ with (yield self.unit1_workflow.lock()): yield self.unit1_workflow.set_state("start_error") self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() self.assertEqual( (yield self.service_unit1.get_resolved()), None) main(["resolved", "--retry", "mysql/0"]) yield finished self.assertEqual( (yield self.service_unit1.get_resolved()), {"retry": RETRY_HOOKS}) self.assertIn( "Marked unit 'mysql/0' as resolved", self.output.getvalue()) @inlineCallbacks def test_relation_resolved(self): """ 'juju relation <unit_name> <rel_name>' will schedule the broken unit relations for being resolved. """ service_relation = yield self.get_named_service_relation( self.service1, "server") yield self.setup_unit_relations( service_relation, (self.service_unit1, "down"), (self.service_unit2, "up")) with (yield self.unit1_workflow.lock()): yield self.unit1_workflow.set_state("start_error") self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() self.assertEqual( (yield self.service_unit1.get_relation_resolved()), None) main(["resolved", "--retry", "mysql/0", service_relation.relation_name]) yield finished self.assertEqual( (yield self.service_unit1.get_relation_resolved()), {service_relation.internal_relation_id: RETRY_HOOKS}) self.assertEqual( (yield self.service_unit2.get_relation_resolved()), None) self.assertIn( "Marked unit 'mysql/0' relation 'server' as resolved", self.output.getvalue()) @inlineCallbacks def test_resolved_relation_some_already_resolved(self): """ 'juju resolved <service_name> <rel_name>' will mark resolved all down units that are not already marked resolved. """ service2 = yield self.service_state_manager.get_service_state( "wordpress") service_unit1 = yield service2.add_unit_state() service_relation = yield self.get_named_service_relation( service2, "db") yield self.setup_unit_relations( service_relation, (service_unit1, "down")) service_relation2 = yield self.get_named_service_relation( service2, "cache") yield self.setup_unit_relations( service_relation2, (service_unit1, "down")) yield service_unit1.set_relation_resolved( {service_relation.internal_relation_id: NO_HOOKS}) self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() main(["resolved", "--retry", "wordpress/0", "cache"]) yield finished self.assertEqual( (yield service_unit1.get_relation_resolved()), {service_relation.internal_relation_id: NO_HOOKS, service_relation2.internal_relation_id: RETRY_HOOKS}) self.assertIn( "Marked unit 'wordpress/0' relation 'cache' as resolved", self.output.getvalue()) @inlineCallbacks def test_resolved_relation_some_already_resolved_conflict(self): """ 'juju resolved <service_name> <rel_name>' will mark resolved all down units that are not already marked resolved. """ service2 = yield self.service_state_manager.get_service_state( "wordpress") service_unit1 = yield service2.add_unit_state() service_relation = yield self.get_named_service_relation( service2, "db") yield self.setup_unit_relations( service_relation, (service_unit1, "down")) yield service_unit1.set_relation_resolved( {service_relation.internal_relation_id: NO_HOOKS}) self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() main(["resolved", "--retry", "wordpress/0", "db"]) yield finished self.assertEqual( (yield service_unit1.get_relation_resolved()), {service_relation.internal_relation_id: NO_HOOKS}) self.assertIn( "Service unit 'wordpress/0' already has relations marked as resol", self.output.getvalue()) @inlineCallbacks def test_resolved_unknown_service(self): """ 'juju resolved <unit_name>' will report if a service is invalid. """ self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() main(["resolved", "zebra/0"]) yield finished self.assertIn("Service 'zebra' was not found", self.stderr.getvalue()) @inlineCallbacks def test_resolved_unknown_unit(self): """ 'juju resolved <unit_name>' will report if a unit is invalid. """ self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() main(["resolved", "mysql/5"]) yield finished self.assertIn( "Service unit 'mysql/5' was not found", self.output.getvalue()) @inlineCallbacks def test_resolved_unknown_unit_relation(self): """ 'juju resolved <unit_name>' will report if a relation is invalid. """ self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() self.assertEqual( (yield self.service_unit1.get_resolved()), None) main(["resolved", "mysql/0", "magic"]) yield finished self.assertIn("Relation not found", self.output.getvalue()) @inlineCallbacks def test_resolved_already_running(self): """ 'juju resolved <unit_name>' will report if the unit is already running. """ # Just verify we don't accidentally mark up another unit of the service unit2_workflow = UnitWorkflowState( self.client, self.service_unit2, None, self.makeDir()) with (yield unit2_workflow.lock()): unit2_workflow.set_state("start_error") self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() main(["resolved", "mysql/0"]) yield finished self.assertEqual( (yield self.service_unit2.get_resolved()), None) self.assertEqual( (yield self.service_unit1.get_resolved()), None) self.assertNotIn( "Unit 'mysql/0 already running: started", self.output.getvalue()) @inlineCallbacks def test_resolved_already_resolved(self): """ 'juju resolved <unit_name>' will report if the unit is already resolved. """ # Mark the unit as resolved and as in an error state. yield self.service_unit1.set_resolved(RETRY_HOOKS) with (yield self.unit1_workflow.lock()): yield self.unit1_workflow.set_state("start_error") unit2_workflow = UnitWorkflowState( self.client, self.service_unit1, None, self.makeDir()) with (yield unit2_workflow.lock()): unit2_workflow.set_state("start_error") self.assertEqual( (yield self.service_unit2.get_resolved()), None) self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() main(["resolved", "mysql/0"]) yield finished self.assertEqual( (yield self.service_unit1.get_resolved()), {"retry": RETRY_HOOKS}) self.assertNotIn( "Marked unit 'mysql/0' as resolved", self.output.getvalue()) self.assertIn( "Service unit 'mysql/0' is already marked as resolved.", self.stderr.getvalue(), "") @inlineCallbacks def test_resolved_relation_already_running(self): """ 'juju resolved <unit_name> <rel_name>' will report if the relation is already running. """ service2 = yield self.service_state_manager.get_service_state( "wordpress") service_unit1 = yield service2.add_unit_state() service_relation = yield self.get_named_service_relation( service2, "db") yield self.setup_unit_relations( service_relation, (service_unit1, "up")) self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() main(["resolved", "wordpress/0", "db"]) yield finished self.assertIn("Matched relations are all running", self.output.getvalue()) self.assertEqual( (yield service_unit1.get_relation_resolved()), None)