def test_resolved_already_resolved(self): """ 'juju resolved <unit_name>' will report if the unit is already resolved. """ # Mark the unit as resolved and as in an error state. yield self.service_unit1.set_resolved(RETRY_HOOKS) yield self.unit1_workflow.set_state("start_error") unit2_workflow = UnitWorkflowState( self.client, self.service_unit1, None, self.makeDir()) unit2_workflow.set_state("start_error") self.assertEqual( (yield self.service_unit2.get_resolved()), None) self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() main(["resolved", "mysql/0"]) yield finished self.assertEqual( (yield self.service_unit1.get_resolved()), {"retry": RETRY_HOOKS}) self.assertNotIn( "Marked unit 'mysql/0' as resolved", self.output.getvalue()) self.assertIn( "Service unit 'mysql/0' is already marked as resolved.", self.stderr.getvalue(), "")
def test_resolved_already_running(self): """ 'juju resolved <unit_name>' will report if the unit is already running. """ # Just verify we don't accidentally mark up another unit of the service unit2_workflow = UnitWorkflowState( self.client, self.service_unit2, None, self.makeDir()) unit2_workflow.set_state("start_error") self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() main(["resolved", "mysql/0"]) yield finished self.assertEqual( (yield self.service_unit2.get_resolved()), None) self.assertEqual( (yield self.service_unit1.get_resolved()), None) self.assertNotIn( "Unit 'mysql/0 already running: started", self.output.getvalue())
def setUp(self): yield super(ControlResolvedTest, self).setUp() config = {"environments": {"firstenv": {"type": "dummy"}}} self.write_config(dump(config)) self.config.load() yield self.add_relation_state("wordpress", "mysql") yield self.add_relation_state("wordpress", "varnish") self.service1 = yield self.service_state_manager.get_service_state( "mysql") self.service_unit1 = yield self.service1.add_unit_state() self.service_unit2 = yield self.service1.add_unit_state() self.unit1_workflow = UnitWorkflowState(self.client, self.service_unit1, None, self.makeDir()) yield self.unit1_workflow.set_state("started") self.environment = self.config.get_default() self.provider = self.environment.get_machine_provider() self.output = self.capture_logging() self.stderr = self.capture_stream("stderr") self.executor = HookExecutor()
def setUp(self): yield super(RemoteUpgradeCharmTest, self).setUp() config = { "environments": {"firstenv": {"type": "dummy"}}} self.write_config(dump(config)) self.config.load() charm = CharmDirectory(os.path.join( test_repository_path, "series", "mysql")) self.charm_state_manager.add_charm_state( "cs:series/mysql-1", charm, "") self.service_state1 = yield self.add_service_from_charm( "mysql", "cs:series/mysql-1") self.service_unit1 = yield self.service_state1.add_unit_state() self.unit1_workflow = UnitWorkflowState( self.client, self.service_unit1, None, self.makeDir()) yield self.unit1_workflow.set_state("started") self.environment = self.config.get_default() self.provider = self.environment.get_machine_provider() self.output = self.capture_logging() self.stderr = self.capture_stream("stderr")
def test_resolved_already_resolved(self): """ 'juju resolved <unit_name>' will report if the unit is already resolved. """ # Mark the unit as resolved and as in an error state. yield self.service_unit1.set_resolved(RETRY_HOOKS) yield self.unit1_workflow.set_state("start_error") unit2_workflow = UnitWorkflowState(self.client, self.service_unit1, None, self.makeDir()) unit2_workflow.set_state("start_error") self.assertEqual((yield self.service_unit2.get_resolved()), None) self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() main(["resolved", "mysql/0"]) yield finished self.assertEqual((yield self.service_unit1.get_resolved()), {"retry": RETRY_HOOKS}) self.assertNotIn("Marked unit 'mysql/0' as resolved", self.output.getvalue()) self.assertIn("Service unit 'mysql/0' is already marked as resolved.", self.stderr.getvalue(), "")
def start(self): """Start the unit agent process.""" self.service_state_manager = ServiceStateManager(self.client) self.executor.start() # Retrieve our unit and configure working directories. service_name = self.unit_name.split("/")[0] service_state = yield self.service_state_manager.get_service_state( service_name) self.unit_state = yield service_state.get_unit_state(self.unit_name) self.unit_directory = os.path.join( self.config["juju_directory"], "units", self.unit_state.unit_name.replace("/", "-")) self.state_directory = os.path.join(self.config["juju_directory"], "state") # Setup the server portion of the cli api exposed to hooks. from twisted.internet import reactor self.api_socket = reactor.listenUNIX( os.path.join(self.unit_directory, HOOK_SOCKET_FILE), self.api_factory) # Setup the unit state's address address = yield get_unit_address(self.client) yield self.unit_state.set_public_address( (yield address.get_public_address())) yield self.unit_state.set_private_address( (yield address.get_private_address())) # Inform the system, we're alive. yield self.unit_state.connect_agent() self.lifecycle = UnitLifecycle(self.client, self.unit_state, service_state, self.unit_directory, self.executor) self.workflow = UnitWorkflowState(self.client, self.unit_state, self.lifecycle, self.state_directory) if self.get_watch_enabled(): yield self.unit_state.watch_resolved(self.cb_watch_resolved) yield self.unit_state.watch_hook_debug(self.cb_watch_hook_debug) yield service_state.watch_config_state( self.cb_watch_config_changed) # Fire initial transitions, only if successful if (yield self.workflow.transition_state("installed")): yield self.workflow.transition_state("started") # Upgrade can only be processed if we're in a running state so # for case of a newly started unit, do it after the unit is started. if self.get_watch_enabled(): yield self.unit_state.watch_upgrade_flag( self.cb_watch_upgrade_flag)
def setUp(self): yield super(UnitWorkflowTest, self).setUp() yield self.setup_default_test_relation() self.lifecycle = UnitLifecycle(self.client, self.states["unit"], self.states["service"], self.unit_directory, self.executor) self.juju_directory = self.makeDir() self.state_directory = self.makeDir( path=os.path.join(self.juju_directory, "state")) self.workflow = UnitWorkflowState(self.client, self.states["unit"], self.lifecycle, self.state_directory)
def setUp(self): yield super(ControlResolvedTest, self).setUp() config = { "environments": {"firstenv": {"type": "dummy"}}} self.write_config(dump(config)) self.config.load() yield self.add_relation_state("wordpress", "mysql") yield self.add_relation_state("wordpress", "varnish") self.service1 = yield self.service_state_manager.get_service_state( "mysql") self.service_unit1 = yield self.service1.add_unit_state() self.service_unit2 = yield self.service1.add_unit_state() self.unit1_workflow = UnitWorkflowState( self.client, self.service_unit1, None, self.makeDir()) yield self.unit1_workflow.set_state("started") self.environment = self.config.get_default() self.provider = self.environment.get_machine_provider() self.output = self.capture_logging() self.stderr = self.capture_stream("stderr") self.executor = HookExecutor()
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)
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)
def setUp(self): yield super(ControlCharmUpgradeTest, self).setUp() config = { "environments": {"firstenv": {"type": "dummy"}}} self.write_config(dump(config)) self.config.load() self.service_state1 = yield self.add_service_from_charm("mysql") self.service_unit1 = yield self.service_state1.add_unit_state() self.unit1_workflow = UnitWorkflowState( self.client, self.service_unit1, None, self.makeDir()) yield self.unit1_workflow.set_state("started") self.environment = self.config.get_default() self.provider = self.environment.get_machine_provider() self.output = self.capture_logging() self.stderr = self.capture_stream("stderr")
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_resolved_already_running(self): """ 'juju resolved <unit_name>' will report if the unit is already running. """ # Just verify we don't accidentally mark up another unit of the service unit2_workflow = UnitWorkflowState(self.client, self.service_unit2, None, self.makeDir()) unit2_workflow.set_state("start_error") self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() main(["resolved", "mysql/0"]) yield finished self.assertEqual((yield self.service_unit2.get_resolved()), None) self.assertEqual((yield self.service_unit1.get_resolved()), None) self.assertNotIn("Unit 'mysql/0 already running: started", self.output.getvalue())
def setUp(self): yield super(UnitWorkflowTest, self).setUp() yield self.setup_default_test_relation() self.lifecycle = UnitLifecycle( self.client, self.states["unit"], self.states["service"], self.unit_directory, self.executor) self.juju_directory = self.makeDir() self.state_directory = self.makeDir( path=os.path.join(self.juju_directory, "state")) self.workflow = UnitWorkflowState( self.client, self.states["unit"], self.lifecycle, self.state_directory)
def setUp(self): yield super(UnitWorkflowTestBase, self).setUp() yield self.setup_default_test_relation() self.lifecycle = UnitLifecycle( self.client, self.states["unit"], self.states["service"], self.unit_directory, self.state_directory, self.executor) self.workflow = UnitWorkflowState( self.client, self.states["unit"], self.lifecycle, self.state_directory) self.write_exit_hook("install") self.write_exit_hook("start") self.write_exit_hook("stop") self.write_exit_hook("config-changed") self.write_exit_hook("upgrade-charm")
def setUp(self): yield super(ControlCharmUpgradeTest, self).setUp() self.service_state1 = yield self.add_service_from_charm("mysql") self.service_unit1 = yield self.service_state1.add_unit_state() self.unit1_workflow = UnitWorkflowState( self.client, self.service_unit1, None, self.makeDir()) with (yield self.unit1_workflow.lock()): yield self.unit1_workflow.set_state("started") self.environment = self.config.get_default() self.provider = self.environment.get_machine_provider() self.output = self.capture_logging() self.stderr = self.capture_stream("stderr")
class UnitAgent(BaseAgent): """An juju Unit Agent. Provides for the management of a charm, via hook execution in response to external events in the coordination space (zookeeper). """ name = "juju-unit-agent" @classmethod def setup_options(cls, parser): super(UnitAgent, cls).setup_options(parser) unit_name = os.environ.get("JUJU_UNIT_NAME", "") parser.add_argument("--unit-name", default=unit_name) @property def unit_name(self): return self.config["unit_name"] def get_agent_name(self): return "unit:%s" % self.unit_name def configure(self, options): """Configure the unit agent.""" super(UnitAgent, self).configure(options) if not options.get("unit_name"): msg = ("--unit-name must be provided in the command line, " "or $JUJU_UNIT_NAME in the environment") raise JujuError(msg) self.executor = HookExecutor() self.api_factory = UnitSettingsFactory( self.executor.get_hook_context, logging.getLogger("unit.hook.api")) self.api_socket = None self.workflow = None @inlineCallbacks def start(self): """Start the unit agent process.""" self.service_state_manager = ServiceStateManager(self.client) self.executor.start() # Retrieve our unit and configure working directories. service_name = self.unit_name.split("/")[0] service_state = yield self.service_state_manager.get_service_state( service_name) self.unit_state = yield service_state.get_unit_state(self.unit_name) self.unit_directory = os.path.join( self.config["juju_directory"], "units", self.unit_state.unit_name.replace("/", "-")) self.state_directory = os.path.join(self.config["juju_directory"], "state") # Setup the server portion of the cli api exposed to hooks. from twisted.internet import reactor self.api_socket = reactor.listenUNIX( os.path.join(self.unit_directory, HOOK_SOCKET_FILE), self.api_factory) # Setup the unit state's address address = yield get_unit_address(self.client) yield self.unit_state.set_public_address( (yield address.get_public_address())) yield self.unit_state.set_private_address( (yield address.get_private_address())) # Inform the system, we're alive. yield self.unit_state.connect_agent() self.lifecycle = UnitLifecycle(self.client, self.unit_state, service_state, self.unit_directory, self.executor) self.workflow = UnitWorkflowState(self.client, self.unit_state, self.lifecycle, self.state_directory) if self.get_watch_enabled(): yield self.unit_state.watch_resolved(self.cb_watch_resolved) yield self.unit_state.watch_hook_debug(self.cb_watch_hook_debug) yield service_state.watch_config_state( self.cb_watch_config_changed) # Fire initial transitions, only if successful if (yield self.workflow.transition_state("installed")): yield self.workflow.transition_state("started") # Upgrade can only be processed if we're in a running state so # for case of a newly started unit, do it after the unit is started. if self.get_watch_enabled(): yield self.unit_state.watch_upgrade_flag( self.cb_watch_upgrade_flag) @inlineCallbacks def stop(self): """Stop the unit agent process.""" if self.workflow: yield self.workflow.transition_state("stopped") if self.api_socket: yield self.api_socket.stopListening() yield self.api_factory.stopFactory() @inlineCallbacks def cb_watch_resolved(self, change): """Update the unit's state, when its resolved. Resolved operations form the basis of error recovery for unit workflows. A resolved operation can optionally specify hook execution. The unit agent runs the error recovery transition if the unit is not in a running state. """ # Would be nice if we could fold this into an atomic # get and delete primitive. # Check resolved setting resolved = yield self.unit_state.get_resolved() if resolved is None: returnValue(None) # Clear out the setting yield self.unit_state.clear_resolved() # Verify its not already running if (yield self.workflow.get_state()) == "started": returnValue(None) log.info("Resolved detected, firing retry transition") # Fire a resolved transition try: if resolved["retry"] == RETRY_HOOKS: yield self.workflow.fire_transition_alias("retry_hook") else: yield self.workflow.fire_transition_alias("retry") except Exception: log.exception("Unknown error while transitioning for resolved") @inlineCallbacks def cb_watch_hook_debug(self, change): """Update the hooks to be debugged when the settings change. """ debug = yield self.unit_state.get_hook_debug() debug_hooks = debug and debug.get("debug_hooks") or None self.executor.set_debug(debug_hooks) @inlineCallbacks def cb_watch_upgrade_flag(self, change): """Update the unit's charm when requested. """ upgrade_flag = yield self.unit_state.get_upgrade_flag() if upgrade_flag: log.info("Upgrade detected, starting upgrade") upgrade = CharmUpgradeOperation(self) try: yield upgrade.run() except Exception: log.exception("Error while upgrading") @inlineCallbacks def cb_watch_config_changed(self, change): """Trigger hook on configuration change""" # Verify it is running current_state = yield self.workflow.get_state() log.debug("Configuration Changed") if current_state != "started": log.debug( "Configuration updated on service in a non-started state") returnValue(None) yield self.workflow.fire_transition("reconfigure")
class ControlResolvedTest(ServiceStateManagerTestBase, ControlToolTest, RepositoryTestBase): @inlineCallbacks def setUp(self): yield super(ControlResolvedTest, self).setUp() config = {"environments": {"firstenv": {"type": "dummy"}}} self.write_config(dump(config)) self.config.load() yield self.add_relation_state("wordpress", "mysql") yield self.add_relation_state("wordpress", "varnish") self.service1 = yield self.service_state_manager.get_service_state( "mysql") self.service_unit1 = yield self.service1.add_unit_state() self.service_unit2 = yield self.service1.add_unit_state() self.unit1_workflow = UnitWorkflowState(self.client, self.service_unit1, None, self.makeDir()) yield self.unit1_workflow.set_state("started") self.environment = self.config.get_default() self.provider = self.environment.get_machine_provider() self.output = self.capture_logging() self.stderr = self.capture_stream("stderr") self.executor = HookExecutor() @inlineCallbacks def add_relation_state(self, *service_names): for service_name in service_names: try: yield self.service_state_manager.get_service_state( service_name) except ServiceStateNotFound: yield self.add_service_from_charm(service_name) endpoint_pairs = yield self.service_state_manager.join_descriptors( *service_names) endpoints = endpoint_pairs[0] endpoints = endpoint_pairs[0] if endpoints[0] == endpoints[1]: endpoints = endpoints[0:1] relation_state = (yield self.relation_state_manager.add_relation_state( *endpoints))[0] returnValue(relation_state) @inlineCallbacks def get_named_service_relation(self, service_state, relation_name): if isinstance(service_state, str): service_state = yield self.service_state_manager.get_service_state( service_state) rels = yield self.relation_state_manager.get_relations_for_service( service_state) rels = [sr for sr in rels if sr.relation_name == relation_name] if len(rels) == 1: returnValue(rels[0]) returnValue(rels) @inlineCallbacks def setup_unit_relations(self, service_relation, *units): """ Given a service relation and set of unit tuples in the form unit_state, unit_relation_workflow_state, will add unit relations for these units and update their workflow state to the desired/given state. """ for unit, state in units: unit_relation = yield service_relation.add_unit_state(unit) lifecycle = UnitRelationLifecycle(self.client, unit.unit_name, unit_relation, service_relation.relation_name, self.makeDir(), self.executor) workflow_state = RelationWorkflowState(self.client, unit_relation, lifecycle, self.makeDir()) yield workflow_state.set_state(state) @inlineCallbacks def test_resolved(self): """ 'juju resolved <unit_name>' will schedule a unit for retrying from an error state. """ # Push the unit into an error state yield self.unit1_workflow.set_state("start_error") self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() self.assertEqual((yield self.service_unit1.get_resolved()), None) main(["resolved", "mysql/0"]) yield finished self.assertEqual((yield self.service_unit1.get_resolved()), {"retry": NO_HOOKS}) self.assertIn("Marked unit 'mysql/0' as resolved", self.output.getvalue()) @inlineCallbacks def test_resolved_retry(self): """ 'juju resolved --retry <unit_name>' will schedule a unit for retrying from an error state with a retry of hooks executions. """ yield self.unit1_workflow.set_state("start_error") self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() self.assertEqual((yield self.service_unit1.get_resolved()), None) main(["resolved", "--retry", "mysql/0"]) yield finished self.assertEqual((yield self.service_unit1.get_resolved()), {"retry": RETRY_HOOKS}) self.assertIn("Marked unit 'mysql/0' as resolved", self.output.getvalue()) @inlineCallbacks def test_relation_resolved(self): """ 'juju relation <unit_name> <rel_name>' will schedule the broken unit relations for being resolved. """ service_relation = yield self.get_named_service_relation( self.service1, "server") yield self.setup_unit_relations(service_relation, (self.service_unit1, "down"), (self.service_unit2, "up")) yield self.unit1_workflow.set_state("start_error") self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() self.assertEqual((yield self.service_unit1.get_relation_resolved()), None) main( ["resolved", "--retry", "mysql/0", service_relation.relation_name]) yield finished self.assertEqual((yield self.service_unit1.get_relation_resolved()), {service_relation.internal_relation_id: RETRY_HOOKS}) self.assertEqual((yield self.service_unit2.get_relation_resolved()), None) self.assertIn("Marked unit 'mysql/0' relation 'server' as resolved", self.output.getvalue()) @inlineCallbacks def test_resolved_relation_some_already_resolved(self): """ 'juju resolved <service_name> <rel_name>' will mark resolved all down units that are not already marked resolved. """ service2 = yield self.service_state_manager.get_service_state( "wordpress") service_unit1 = yield service2.add_unit_state() service_relation = yield self.get_named_service_relation( service2, "db") yield self.setup_unit_relations(service_relation, (service_unit1, "down")) service_relation2 = yield self.get_named_service_relation( service2, "cache") yield self.setup_unit_relations(service_relation2, (service_unit1, "down")) yield service_unit1.set_relation_resolved( {service_relation.internal_relation_id: NO_HOOKS}) self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() main(["resolved", "--retry", "wordpress/0", "cache"]) yield finished self.assertEqual( (yield service_unit1.get_relation_resolved()), { service_relation.internal_relation_id: NO_HOOKS, service_relation2.internal_relation_id: RETRY_HOOKS }) self.assertIn("Marked unit 'wordpress/0' relation 'cache' as resolved", self.output.getvalue()) @inlineCallbacks def test_resolved_relation_some_already_resolved_conflict(self): """ 'juju resolved <service_name> <rel_name>' will mark resolved all down units that are not already marked resolved. """ service2 = yield self.service_state_manager.get_service_state( "wordpress") service_unit1 = yield service2.add_unit_state() service_relation = yield self.get_named_service_relation( service2, "db") yield self.setup_unit_relations(service_relation, (service_unit1, "down")) yield service_unit1.set_relation_resolved( {service_relation.internal_relation_id: NO_HOOKS}) self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() main(["resolved", "--retry", "wordpress/0", "db"]) yield finished self.assertEqual((yield service_unit1.get_relation_resolved()), {service_relation.internal_relation_id: NO_HOOKS}) self.assertIn( "Service unit 'wordpress/0' already has relations marked as resol", self.output.getvalue()) @inlineCallbacks def test_resolved_unknown_service(self): """ 'juju resolved <unit_name>' will report if a service is invalid. """ self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() main(["resolved", "zebra/0"]) yield finished self.assertIn("Service 'zebra' was not found", self.stderr.getvalue()) @inlineCallbacks def test_resolved_unknown_unit(self): """ 'juju resolved <unit_name>' will report if a unit is invalid. """ self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() main(["resolved", "mysql/5"]) yield finished self.assertIn("Service unit 'mysql/5' was not found", self.output.getvalue()) @inlineCallbacks def test_resolved_unknown_unit_relation(self): """ 'juju resolved <unit_name>' will report if a relation is invalid. """ self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() self.assertEqual((yield self.service_unit1.get_resolved()), None) main(["resolved", "mysql/0", "magic"]) yield finished self.assertIn("Relation not found", self.output.getvalue()) @inlineCallbacks def test_resolved_already_running(self): """ 'juju resolved <unit_name>' will report if the unit is already running. """ # Just verify we don't accidentally mark up another unit of the service unit2_workflow = UnitWorkflowState(self.client, self.service_unit2, None, self.makeDir()) unit2_workflow.set_state("start_error") self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() main(["resolved", "mysql/0"]) yield finished self.assertEqual((yield self.service_unit2.get_resolved()), None) self.assertEqual((yield self.service_unit1.get_resolved()), None) self.assertNotIn("Unit 'mysql/0 already running: started", self.output.getvalue()) @inlineCallbacks def test_resolved_already_resolved(self): """ 'juju resolved <unit_name>' will report if the unit is already resolved. """ # Mark the unit as resolved and as in an error state. yield self.service_unit1.set_resolved(RETRY_HOOKS) yield self.unit1_workflow.set_state("start_error") unit2_workflow = UnitWorkflowState(self.client, self.service_unit1, None, self.makeDir()) unit2_workflow.set_state("start_error") self.assertEqual((yield self.service_unit2.get_resolved()), None) self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() main(["resolved", "mysql/0"]) yield finished self.assertEqual((yield self.service_unit1.get_resolved()), {"retry": RETRY_HOOKS}) self.assertNotIn("Marked unit 'mysql/0' as resolved", self.output.getvalue()) self.assertIn("Service unit 'mysql/0' is already marked as resolved.", self.stderr.getvalue(), "") @inlineCallbacks def test_resolved_relation_already_running(self): """ 'juju resolved <unit_name> <rel_name>' will report if the relation is already running. """ service2 = yield self.service_state_manager.get_service_state( "wordpress") service_unit1 = yield service2.add_unit_state() service_relation = yield self.get_named_service_relation( service2, "db") yield self.setup_unit_relations(service_relation, (service_unit1, "up")) self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() main(["resolved", "wordpress/0", "db"]) yield finished self.assertIn("Matched relations are all running", self.output.getvalue()) self.assertEqual((yield service_unit1.get_relation_resolved()), None)
def start(self): """Start the unit agent process.""" self.service_state_manager = ServiceStateManager(self.client) self.executor.start() # Retrieve our unit and configure working directories. service_name = self.unit_name.split("/")[0] service_state = yield self.service_state_manager.get_service_state( service_name) self.unit_state = yield service_state.get_unit_state( self.unit_name) self.unit_directory = os.path.join( self.config["juju_directory"], "units", self.unit_state.unit_name.replace("/", "-")) self.state_directory = os.path.join( self.config["juju_directory"], "state") # Setup the server portion of the cli api exposed to hooks. from twisted.internet import reactor self.api_socket = reactor.listenUNIX( os.path.join(self.unit_directory, HOOK_SOCKET_FILE), self.api_factory) # Setup the unit state's address address = yield get_unit_address(self.client) yield self.unit_state.set_public_address( (yield address.get_public_address())) yield self.unit_state.set_private_address( (yield address.get_private_address())) # Inform the system, we're alive. yield self.unit_state.connect_agent() self.lifecycle = UnitLifecycle( self.client, self.unit_state, service_state, self.unit_directory, self.executor) self.workflow = UnitWorkflowState( self.client, self.unit_state, self.lifecycle, self.state_directory) if self.get_watch_enabled(): yield self.unit_state.watch_resolved(self.cb_watch_resolved) yield self.unit_state.watch_hook_debug(self.cb_watch_hook_debug) yield service_state.watch_config_state( self.cb_watch_config_changed) # Fire initial transitions, only if successful if (yield self.workflow.transition_state("installed")): yield self.workflow.transition_state("started") # Upgrade can only be processed if we're in a running state so # for case of a newly started unit, do it after the unit is started. if self.get_watch_enabled(): yield self.unit_state.watch_upgrade_flag( self.cb_watch_upgrade_flag)
class UnitAgent(BaseAgent): """An juju Unit Agent. Provides for the management of a charm, via hook execution in response to external events in the coordination space (zookeeper). """ name = "juju-unit-agent" @classmethod def setup_options(cls, parser): super(UnitAgent, cls).setup_options(parser) unit_name = os.environ.get("JUJU_UNIT_NAME", "") parser.add_argument("--unit-name", default=unit_name) @property def unit_name(self): return self.config["unit_name"] def get_agent_name(self): return "unit:%s" % self.unit_name def configure(self, options): """Configure the unit agent.""" super(UnitAgent, self).configure(options) if not options.get("unit_name"): msg = ("--unit-name must be provided in the command line, " "or $JUJU_UNIT_NAME in the environment") raise JujuError(msg) self.executor = HookExecutor() self.api_factory = UnitSettingsFactory( self.executor.get_hook_context, logging.getLogger("unit.hook.api")) self.api_socket = None self.workflow = None @inlineCallbacks def start(self): """Start the unit agent process.""" self.service_state_manager = ServiceStateManager(self.client) self.executor.start() # Retrieve our unit and configure working directories. service_name = self.unit_name.split("/")[0] service_state = yield self.service_state_manager.get_service_state( service_name) self.unit_state = yield service_state.get_unit_state( self.unit_name) self.unit_directory = os.path.join( self.config["juju_directory"], "units", self.unit_state.unit_name.replace("/", "-")) self.state_directory = os.path.join( self.config["juju_directory"], "state") # Setup the server portion of the cli api exposed to hooks. from twisted.internet import reactor self.api_socket = reactor.listenUNIX( os.path.join(self.unit_directory, HOOK_SOCKET_FILE), self.api_factory) # Setup the unit state's address address = yield get_unit_address(self.client) yield self.unit_state.set_public_address( (yield address.get_public_address())) yield self.unit_state.set_private_address( (yield address.get_private_address())) # Inform the system, we're alive. yield self.unit_state.connect_agent() self.lifecycle = UnitLifecycle( self.client, self.unit_state, service_state, self.unit_directory, self.executor) self.workflow = UnitWorkflowState( self.client, self.unit_state, self.lifecycle, self.state_directory) if self.get_watch_enabled(): yield self.unit_state.watch_resolved(self.cb_watch_resolved) yield self.unit_state.watch_hook_debug(self.cb_watch_hook_debug) yield service_state.watch_config_state( self.cb_watch_config_changed) # Fire initial transitions, only if successful if (yield self.workflow.transition_state("installed")): yield self.workflow.transition_state("started") # Upgrade can only be processed if we're in a running state so # for case of a newly started unit, do it after the unit is started. if self.get_watch_enabled(): yield self.unit_state.watch_upgrade_flag( self.cb_watch_upgrade_flag) @inlineCallbacks def stop(self): """Stop the unit agent process.""" if self.workflow: yield self.workflow.transition_state("stopped") if self.api_socket: yield self.api_socket.stopListening() yield self.api_factory.stopFactory() @inlineCallbacks def cb_watch_resolved(self, change): """Update the unit's state, when its resolved. Resolved operations form the basis of error recovery for unit workflows. A resolved operation can optionally specify hook execution. The unit agent runs the error recovery transition if the unit is not in a running state. """ # Would be nice if we could fold this into an atomic # get and delete primitive. # Check resolved setting resolved = yield self.unit_state.get_resolved() if resolved is None: returnValue(None) # Clear out the setting yield self.unit_state.clear_resolved() # Verify its not already running if (yield self.workflow.get_state()) == "started": returnValue(None) log.info("Resolved detected, firing retry transition") # Fire a resolved transition try: if resolved["retry"] == RETRY_HOOKS: yield self.workflow.fire_transition_alias("retry_hook") else: yield self.workflow.fire_transition_alias("retry") except Exception: log.exception("Unknown error while transitioning for resolved") @inlineCallbacks def cb_watch_hook_debug(self, change): """Update the hooks to be debugged when the settings change. """ debug = yield self.unit_state.get_hook_debug() debug_hooks = debug and debug.get("debug_hooks") or None self.executor.set_debug(debug_hooks) @inlineCallbacks def cb_watch_upgrade_flag(self, change): """Update the unit's charm when requested. """ upgrade_flag = yield self.unit_state.get_upgrade_flag() if upgrade_flag: log.info("Upgrade detected, starting upgrade") upgrade = CharmUpgradeOperation(self) try: yield upgrade.run() except Exception: log.exception("Error while upgrading") @inlineCallbacks def cb_watch_config_changed(self, change): """Trigger hook on configuration change""" # Verify it is running current_state = yield self.workflow.get_state() log.debug("Configuration Changed") if current_state != "started": log.debug( "Configuration updated on service in a non-started state") returnValue(None) yield self.workflow.fire_transition("reconfigure")
class UnitWorkflowTest(WorkflowTestBase): @inlineCallbacks def setUp(self): yield super(UnitWorkflowTest, self).setUp() yield self.setup_default_test_relation() self.lifecycle = UnitLifecycle( self.client, self.states["unit"], self.states["service"], self.unit_directory, self.executor) self.juju_directory = self.makeDir() self.state_directory = self.makeDir( path=os.path.join(self.juju_directory, "state")) self.workflow = UnitWorkflowState( self.client, self.states["unit"], self.lifecycle, self.state_directory) @inlineCallbacks def test_install(self): file_path = self.makeFile() self.write_hook( "install", "#!/bin/bash\necho installed >> %s\n" % file_path) result = yield self.workflow.fire_transition("install") self.assertTrue(result) current_state = yield self.workflow.get_state() self.assertEqual(current_state, "installed") f_state, history, zk_state = yield self.read_persistent_state() self.assertEqual(f_state, zk_state) self.assertEqual(f_state, {"state": "installed", "state_variables": {}}) self.assertEqual(history, [{"state": "installed", "state_variables": {}}]) @inlineCallbacks def test_install_with_error_and_retry(self): """If the install hook fails, the workflow is transition to the install_error state. If the install is retried, a success transition will take us to the started state. """ self.write_hook("install", "#!/bin/bash\nexit 1") result = yield self.workflow.fire_transition("install") self.assertFalse(result) current_state = yield self.workflow.get_state() yield self.assertEqual(current_state, "install_error") result = yield self.workflow.fire_transition("retry_install") yield self.assertState(self.workflow, "started") @inlineCallbacks def test_install_error_with_retry_hook(self): """If the install hook fails, the workflow is transition to the install_error state. """ self.write_hook("install", "#!/bin/bash\nexit 1") result = yield self.workflow.fire_transition("install") self.assertFalse(result) current_state = yield self.workflow.get_state() yield self.assertEqual(current_state, "install_error") result = yield self.workflow.fire_transition("retry_install_hook") yield self.assertState(self.workflow, "install_error") self.write_hook("install", "#!/bin/bash\necho hello\n") hook_deferred = self.wait_on_hook("install") result = yield self.workflow.fire_transition_alias("retry_hook") yield hook_deferred yield self.assertState(self.workflow, "started") @inlineCallbacks def test_start(self): file_path = self.makeFile() self.write_hook( "install", "#!/bin/bash\necho installed >> %s\n" % file_path) self.write_hook( "start", "#!/bin/bash\necho start >> %s\n" % file_path) self.write_hook( "stop", "#!/bin/bash\necho stop >> %s\n" % file_path) result = yield self.workflow.fire_transition("install") self.assertTrue(result) result = yield self.workflow.fire_transition("start") self.assertTrue(result) current_state = yield self.workflow.get_state() self.assertEqual(current_state, "started") f_state, history, zk_state = yield self.read_persistent_state() self.assertEqual(f_state, zk_state) self.assertEqual(f_state, {"state": "started", "state_variables": {}}) self.assertEqual(history, [{"state": "installed", "state_variables": {}}, {"state": "started", "state_variables": {}}]) @inlineCallbacks def test_start_with_error(self): """Executing the start transition with a hook error, results in the workflow going to the start_error state. The start can be retried. """ self.write_hook("install", "#!/bin/bash\necho hello\n") result = yield self.workflow.fire_transition("install") self.assertTrue(result) self.write_hook("start", "#!/bin/bash\nexit 1") result = yield self.workflow.fire_transition("start") self.assertFalse(result) current_state = yield self.workflow.get_state() self.assertEqual(current_state, "start_error") result = yield self.workflow.fire_transition("retry_start") yield self.assertState(self.workflow, "started") @inlineCallbacks def test_start_error_with_retry_hook(self): """Executing the start transition with a hook error, results in the workflow going to the start_error state. The start can be retried. """ self.write_hook("install", "#!/bin/bash\necho hello\n") result = yield self.workflow.fire_transition("install") self.assertTrue(result) self.write_hook("start", "#!/bin/bash\nexit 1") result = yield self.workflow.fire_transition("start") self.assertFalse(result) current_state = yield self.workflow.get_state() self.assertEqual(current_state, "start_error") hook_deferred = self.wait_on_hook("start") result = yield self.workflow.fire_transition("retry_start_hook") yield hook_deferred yield self.assertState(self.workflow, "start_error") self.write_hook("start", "#!/bin/bash\nexit 0") hook_deferred = self.wait_on_hook("start") result = yield self.workflow.fire_transition_alias("retry_hook") yield hook_deferred yield self.assertState(self.workflow, "started") @inlineCallbacks def test_is_unit_running(self): running, state = yield is_unit_running( self.client, self.states["unit"]) self.assertIdentical(running, False) self.assertIdentical(state, None) yield self.workflow.fire_transition("install") yield self.workflow.fire_transition("start") running, state = yield is_unit_running( self.client, self.states["unit"]) self.assertIdentical(running, True) self.assertEqual(state, "started") @inlineCallbacks def test_configure(self): """Configuring a unit results in the config-changed hook being run. """ yield self.workflow.fire_transition("install") result = yield self.workflow.fire_transition("start") self.assertTrue(result) self.assertState(self.workflow, "started") hook_deferred = self.wait_on_hook("config-changed") file_path = self.makeFile() self.write_hook("config-changed", "#!/bin/bash\necho hello >> %s" % file_path) result = yield self.workflow.fire_transition("reconfigure") self.assertTrue(result) yield hook_deferred yield self.assertState(self.workflow, "started") self.assertEqual(open(file_path).read().strip(), "hello") @inlineCallbacks def test_configure_error_and_retry(self): """An error while configuring, transitions the unit and stops the lifecycle.""" yield self.workflow.fire_transition("install") result = yield self.workflow.fire_transition("start") self.assertTrue(result) self.assertState(self.workflow, "started") # Verify transition to error state hook_deferred = self.wait_on_hook("config-changed") self.write_hook("config-changed", "#!/bin/bash\nexit 1") result = yield self.workflow.fire_transition("reconfigure") yield hook_deferred self.assertFalse(result) yield self.assertState(self.workflow, "configure_error") # Verify recovery from error state result = yield self.workflow.fire_transition_alias("retry") self.assertTrue(result) yield self.assertState(self.workflow, "started") @inlineCallbacks def test_configure_error_and_retry_hook(self): """An error while configuring, transitions the unit and stops the lifecycle.""" #self.capture_output() yield self.workflow.fire_transition("install") result = yield self.workflow.fire_transition("start") self.assertTrue(result) self.assertState(self.workflow, "started") # Verify transition to error state hook_deferred = self.wait_on_hook("config-changed") self.write_hook("config-changed", "#!/bin/bash\nexit 1") result = yield self.workflow.fire_transition("reconfigure") yield hook_deferred self.assertFalse(result) yield self.assertState(self.workflow, "configure_error") # Verify retry hook with hook error stays in error state hook_deferred = self.wait_on_hook("config-changed") result = yield self.workflow.fire_transition("retry_configure_hook") self.assertFalse(result) yield hook_deferred yield self.assertState(self.workflow, "configure_error") hook_deferred = self.wait_on_hook("config-changed") self.write_hook("config-changed", "#!/bin/bash\nexit 0") result = yield self.workflow.fire_transition_alias("retry_hook") yield hook_deferred yield self.assertState(self.workflow, "started") @inlineCallbacks def test_upgrade(self): """Upgrading a workflow results in the upgrade hook being executed. """ self.makeFile() yield self.workflow.fire_transition("install") yield self.workflow.fire_transition("start") current_state = yield self.workflow.get_state() self.assertEqual(current_state, "started") file_path = self.makeFile() self.write_hook("upgrade-charm", ("#!/bin/bash\n" "echo upgraded >> %s") % file_path) self.executor.stop() yield self.workflow.fire_transition("upgrade_charm") current_state = yield self.workflow.get_state() self.assertEqual(current_state, "started") @inlineCallbacks def test_upgrade_without_stopping_hooks_errors(self): """Attempting to execute an upgrade without stopping the executor is an error. """ yield self.workflow.fire_transition("install") yield self.workflow.fire_transition("start") current_state = yield self.workflow.get_state() self.assertEqual(current_state, "started") yield self.assertFailure( self.workflow.fire_transition("upgrade_charm"), AssertionError) @inlineCallbacks def test_upgrade_error_retry(self): """A hook error during an upgrade transitions to upgrade_error. """ self.write_hook("upgrade-charm", "#!/bin/bash\nexit 1") yield self.workflow.fire_transition("install") yield self.workflow.fire_transition("start") current_state = yield self.workflow.get_state() self.assertEqual(current_state, "started") self.executor.stop() yield self.workflow.fire_transition("upgrade_charm") current_state = yield self.workflow.get_state() self.assertEqual(current_state, "charm_upgrade_error") file_path = self.makeFile() self.write_hook("upgrade-charm", ("#!/bin/bash\n" "echo upgraded >> %s") % file_path) # The upgrade error hook should ensure that the executor is stoppped. self.assertFalse(self.executor.running) yield self.workflow.fire_transition("retry_upgrade_charm") current_state = yield self.workflow.get_state() self.assertEqual(current_state, "started") @inlineCallbacks def test_upgrade_error_retry_hook(self): """A hook error during an upgrade transitions to upgrade_error, and can be re-tried with hook execution. """ yield self.workflow.fire_transition("install") yield self.workflow.fire_transition("start") current_state = yield self.workflow.get_state() self.assertEqual(current_state, "started") # Agent prepares this. self.executor.stop() self.write_hook("upgrade-charm", "#!/bin/bash\nexit 1") hook_deferred = self.wait_on_hook("upgrade-charm") yield self.workflow.fire_transition("upgrade_charm") yield hook_deferred current_state = yield self.workflow.get_state() self.assertEqual(current_state, "charm_upgrade_error") hook_deferred = self.wait_on_hook("upgrade-charm") self.write_hook("upgrade-charm", "#!/bin/bash\nexit 0") # The upgrade error hook should ensure that the executor is stoppped. self.assertFalse(self.executor.running) yield self.workflow.fire_transition_alias("retry_hook") yield hook_deferred current_state = yield self.workflow.get_state() self.assertEqual(current_state, "started") self.assertTrue(self.executor.running) @inlineCallbacks def test_stop(self): """Executing the stop transition, results in the workflow going to the down state. """ file_path = self.makeFile() self.write_hook( "install", "#!/bin/bash\necho installed >> %s\n" % file_path) self.write_hook( "start", "#!/bin/bash\necho start >> %s\n" % file_path) self.write_hook( "stop", "#!/bin/bash\necho stop >> %s\n" % file_path) result = yield self.workflow.fire_transition("install") result = yield self.workflow.fire_transition("start") result = yield self.workflow.fire_transition("stop") self.assertTrue(result) current_state = yield self.workflow.get_state() self.assertEqual(current_state, "stopped") f_state, history, zk_state = yield self.read_persistent_state() self.assertEqual(f_state, zk_state) self.assertEqual(f_state, {"state": "stopped", "state_variables": {}}) workflow_client = WorkflowStateClient(self.client, self.states["unit"]) value = yield workflow_client.get_state() self.assertEqual(value, "stopped") self.assertEqual(history, [{"state": "installed", "state_variables": {}}, {"state": "started", "state_variables": {}}, {"state": "stopped", "state_variables": {}}]) @inlineCallbacks def test_stop_with_error(self): self.write_hook("install", "#!/bin/bash\necho hello\n") self.write_hook("start", "#!/bin/bash\necho hello\n") result = yield self.workflow.fire_transition("install") self.assertTrue(result) result = yield self.workflow.fire_transition("start") self.assertTrue(result) self.write_hook("stop", "#!/bin/bash\nexit 1") result = yield self.workflow.fire_transition("stop") self.assertFalse(result) yield self.assertState(self.workflow, "stop_error") self.write_hook("stop", "#!/bin/bash\necho hello\n") result = yield self.workflow.fire_transition("retry_stop") yield self.assertState(self.workflow, "stopped") @inlineCallbacks def test_stop_error_with_retry_hook(self): self.write_hook("install", "#!/bin/bash\necho hello\n") self.write_hook("start", "#!/bin/bash\necho hello\n") result = yield self.workflow.fire_transition("install") self.assertTrue(result) result = yield self.workflow.fire_transition("start") self.assertTrue(result) self.write_hook("stop", "#!/bin/bash\nexit 1") result = yield self.workflow.fire_transition("stop") self.assertFalse(result) yield self.assertState(self.workflow, "stop_error") result = yield self.workflow.fire_transition_alias("retry_hook") yield self.assertState(self.workflow, "stop_error") self.write_hook("stop", "#!/bin/bash\nexit 0") result = yield self.workflow.fire_transition_alias("retry_hook") yield self.assertState(self.workflow, "stopped") @inlineCallbacks def test_client_with_no_state(self): workflow_client = WorkflowStateClient(self.client, self.states["unit"]) state = yield workflow_client.get_state() self.assertEqual(state, None) @inlineCallbacks def test_client_with_state(self): yield self.workflow.fire_transition("install") workflow_client = WorkflowStateClient(self.client, self.states["unit"]) self.assertEqual( (yield workflow_client.get_state()), "installed") @inlineCallbacks def test_client_readonly(self): yield self.workflow.fire_transition("install") workflow_client = WorkflowStateClient( self.client, self.states["unit"]) self.assertEqual( (yield workflow_client.get_state()), "installed") yield self.assertFailure( workflow_client.set_state("started"), NotImplementedError) self.assertEqual( (yield workflow_client.get_state()), "installed")
class UnitWorkflowTest(WorkflowTestBase): @inlineCallbacks def setUp(self): yield super(UnitWorkflowTest, self).setUp() yield self.setup_default_test_relation() self.lifecycle = UnitLifecycle(self.client, self.states["unit"], self.states["service"], self.unit_directory, self.executor) self.juju_directory = self.makeDir() self.state_directory = self.makeDir( path=os.path.join(self.juju_directory, "state")) self.workflow = UnitWorkflowState(self.client, self.states["unit"], self.lifecycle, self.state_directory) @inlineCallbacks def test_install(self): file_path = self.makeFile() self.write_hook("install", "#!/bin/bash\necho installed >> %s\n" % file_path) result = yield self.workflow.fire_transition("install") self.assertTrue(result) current_state = yield self.workflow.get_state() self.assertEqual(current_state, "installed") f_state, history, zk_state = yield self.read_persistent_state() self.assertEqual(f_state, zk_state) self.assertEqual(f_state, { "state": "installed", "state_variables": {} }) self.assertEqual(history, [{ "state": "installed", "state_variables": {} }]) @inlineCallbacks def test_install_with_error_and_retry(self): """If the install hook fails, the workflow is transition to the install_error state. If the install is retried, a success transition will take us to the started state. """ self.write_hook("install", "#!/bin/bash\nexit 1") result = yield self.workflow.fire_transition("install") self.assertFalse(result) current_state = yield self.workflow.get_state() yield self.assertEqual(current_state, "install_error") result = yield self.workflow.fire_transition("retry_install") yield self.assertState(self.workflow, "started") @inlineCallbacks def test_install_error_with_retry_hook(self): """If the install hook fails, the workflow is transition to the install_error state. """ self.write_hook("install", "#!/bin/bash\nexit 1") result = yield self.workflow.fire_transition("install") self.assertFalse(result) current_state = yield self.workflow.get_state() yield self.assertEqual(current_state, "install_error") result = yield self.workflow.fire_transition("retry_install_hook") yield self.assertState(self.workflow, "install_error") self.write_hook("install", "#!/bin/bash\necho hello\n") hook_deferred = self.wait_on_hook("install") result = yield self.workflow.fire_transition_alias("retry_hook") yield hook_deferred yield self.assertState(self.workflow, "started") @inlineCallbacks def test_start(self): file_path = self.makeFile() self.write_hook("install", "#!/bin/bash\necho installed >> %s\n" % file_path) self.write_hook("start", "#!/bin/bash\necho start >> %s\n" % file_path) self.write_hook("stop", "#!/bin/bash\necho stop >> %s\n" % file_path) result = yield self.workflow.fire_transition("install") self.assertTrue(result) result = yield self.workflow.fire_transition("start") self.assertTrue(result) current_state = yield self.workflow.get_state() self.assertEqual(current_state, "started") f_state, history, zk_state = yield self.read_persistent_state() self.assertEqual(f_state, zk_state) self.assertEqual(f_state, {"state": "started", "state_variables": {}}) self.assertEqual(history, [{ "state": "installed", "state_variables": {} }, { "state": "started", "state_variables": {} }]) @inlineCallbacks def test_start_with_error(self): """Executing the start transition with a hook error, results in the workflow going to the start_error state. The start can be retried. """ self.write_hook("install", "#!/bin/bash\necho hello\n") result = yield self.workflow.fire_transition("install") self.assertTrue(result) self.write_hook("start", "#!/bin/bash\nexit 1") result = yield self.workflow.fire_transition("start") self.assertFalse(result) current_state = yield self.workflow.get_state() self.assertEqual(current_state, "start_error") result = yield self.workflow.fire_transition("retry_start") yield self.assertState(self.workflow, "started") @inlineCallbacks def test_start_error_with_retry_hook(self): """Executing the start transition with a hook error, results in the workflow going to the start_error state. The start can be retried. """ self.write_hook("install", "#!/bin/bash\necho hello\n") result = yield self.workflow.fire_transition("install") self.assertTrue(result) self.write_hook("start", "#!/bin/bash\nexit 1") result = yield self.workflow.fire_transition("start") self.assertFalse(result) current_state = yield self.workflow.get_state() self.assertEqual(current_state, "start_error") hook_deferred = self.wait_on_hook("start") result = yield self.workflow.fire_transition("retry_start_hook") yield hook_deferred yield self.assertState(self.workflow, "start_error") self.write_hook("start", "#!/bin/bash\nexit 0") hook_deferred = self.wait_on_hook("start") result = yield self.workflow.fire_transition_alias("retry_hook") yield hook_deferred yield self.assertState(self.workflow, "started") @inlineCallbacks def test_is_unit_running(self): running, state = yield is_unit_running(self.client, self.states["unit"]) self.assertIdentical(running, False) self.assertIdentical(state, None) yield self.workflow.fire_transition("install") yield self.workflow.fire_transition("start") running, state = yield is_unit_running(self.client, self.states["unit"]) self.assertIdentical(running, True) self.assertEqual(state, "started") @inlineCallbacks def test_configure(self): """Configuring a unit results in the config-changed hook being run. """ yield self.workflow.fire_transition("install") result = yield self.workflow.fire_transition("start") self.assertTrue(result) self.assertState(self.workflow, "started") hook_deferred = self.wait_on_hook("config-changed") file_path = self.makeFile() self.write_hook("config-changed", "#!/bin/bash\necho hello >> %s" % file_path) result = yield self.workflow.fire_transition("reconfigure") self.assertTrue(result) yield hook_deferred yield self.assertState(self.workflow, "started") self.assertEqual(open(file_path).read().strip(), "hello") @inlineCallbacks def test_configure_error_and_retry(self): """An error while configuring, transitions the unit and stops the lifecycle.""" yield self.workflow.fire_transition("install") result = yield self.workflow.fire_transition("start") self.assertTrue(result) self.assertState(self.workflow, "started") # Verify transition to error state hook_deferred = self.wait_on_hook("config-changed") self.write_hook("config-changed", "#!/bin/bash\nexit 1") result = yield self.workflow.fire_transition("reconfigure") yield hook_deferred self.assertFalse(result) yield self.assertState(self.workflow, "configure_error") # Verify recovery from error state result = yield self.workflow.fire_transition_alias("retry") self.assertTrue(result) yield self.assertState(self.workflow, "started") @inlineCallbacks def test_configure_error_and_retry_hook(self): """An error while configuring, transitions the unit and stops the lifecycle.""" #self.capture_output() yield self.workflow.fire_transition("install") result = yield self.workflow.fire_transition("start") self.assertTrue(result) self.assertState(self.workflow, "started") # Verify transition to error state hook_deferred = self.wait_on_hook("config-changed") self.write_hook("config-changed", "#!/bin/bash\nexit 1") result = yield self.workflow.fire_transition("reconfigure") yield hook_deferred self.assertFalse(result) yield self.assertState(self.workflow, "configure_error") # Verify retry hook with hook error stays in error state hook_deferred = self.wait_on_hook("config-changed") result = yield self.workflow.fire_transition("retry_configure_hook") self.assertFalse(result) yield hook_deferred yield self.assertState(self.workflow, "configure_error") hook_deferred = self.wait_on_hook("config-changed") self.write_hook("config-changed", "#!/bin/bash\nexit 0") result = yield self.workflow.fire_transition_alias("retry_hook") yield hook_deferred yield self.assertState(self.workflow, "started") @inlineCallbacks def test_upgrade(self): """Upgrading a workflow results in the upgrade hook being executed. """ self.makeFile() yield self.workflow.fire_transition("install") yield self.workflow.fire_transition("start") current_state = yield self.workflow.get_state() self.assertEqual(current_state, "started") file_path = self.makeFile() self.write_hook("upgrade-charm", ("#!/bin/bash\n" "echo upgraded >> %s") % file_path) self.executor.stop() yield self.workflow.fire_transition("upgrade_charm") current_state = yield self.workflow.get_state() self.assertEqual(current_state, "started") @inlineCallbacks def test_upgrade_without_stopping_hooks_errors(self): """Attempting to execute an upgrade without stopping the executor is an error. """ yield self.workflow.fire_transition("install") yield self.workflow.fire_transition("start") current_state = yield self.workflow.get_state() self.assertEqual(current_state, "started") yield self.assertFailure( self.workflow.fire_transition("upgrade_charm"), AssertionError) @inlineCallbacks def test_upgrade_error_retry(self): """A hook error during an upgrade transitions to upgrade_error. """ self.write_hook("upgrade-charm", "#!/bin/bash\nexit 1") yield self.workflow.fire_transition("install") yield self.workflow.fire_transition("start") current_state = yield self.workflow.get_state() self.assertEqual(current_state, "started") self.executor.stop() yield self.workflow.fire_transition("upgrade_charm") current_state = yield self.workflow.get_state() self.assertEqual(current_state, "charm_upgrade_error") file_path = self.makeFile() self.write_hook("upgrade-charm", ("#!/bin/bash\n" "echo upgraded >> %s") % file_path) # The upgrade error hook should ensure that the executor is stoppped. self.assertFalse(self.executor.running) yield self.workflow.fire_transition("retry_upgrade_charm") current_state = yield self.workflow.get_state() self.assertEqual(current_state, "started") @inlineCallbacks def test_upgrade_error_retry_hook(self): """A hook error during an upgrade transitions to upgrade_error, and can be re-tried with hook execution. """ yield self.workflow.fire_transition("install") yield self.workflow.fire_transition("start") current_state = yield self.workflow.get_state() self.assertEqual(current_state, "started") # Agent prepares this. self.executor.stop() self.write_hook("upgrade-charm", "#!/bin/bash\nexit 1") hook_deferred = self.wait_on_hook("upgrade-charm") yield self.workflow.fire_transition("upgrade_charm") yield hook_deferred current_state = yield self.workflow.get_state() self.assertEqual(current_state, "charm_upgrade_error") hook_deferred = self.wait_on_hook("upgrade-charm") self.write_hook("upgrade-charm", "#!/bin/bash\nexit 0") # The upgrade error hook should ensure that the executor is stoppped. self.assertFalse(self.executor.running) yield self.workflow.fire_transition_alias("retry_hook") yield hook_deferred current_state = yield self.workflow.get_state() self.assertEqual(current_state, "started") self.assertTrue(self.executor.running) @inlineCallbacks def test_stop(self): """Executing the stop transition, results in the workflow going to the down state. """ file_path = self.makeFile() self.write_hook("install", "#!/bin/bash\necho installed >> %s\n" % file_path) self.write_hook("start", "#!/bin/bash\necho start >> %s\n" % file_path) self.write_hook("stop", "#!/bin/bash\necho stop >> %s\n" % file_path) result = yield self.workflow.fire_transition("install") result = yield self.workflow.fire_transition("start") result = yield self.workflow.fire_transition("stop") self.assertTrue(result) current_state = yield self.workflow.get_state() self.assertEqual(current_state, "stopped") f_state, history, zk_state = yield self.read_persistent_state() self.assertEqual(f_state, zk_state) self.assertEqual(f_state, {"state": "stopped", "state_variables": {}}) workflow_client = WorkflowStateClient(self.client, self.states["unit"]) value = yield workflow_client.get_state() self.assertEqual(value, "stopped") self.assertEqual(history, [{ "state": "installed", "state_variables": {} }, { "state": "started", "state_variables": {} }, { "state": "stopped", "state_variables": {} }]) @inlineCallbacks def test_stop_with_error(self): self.write_hook("install", "#!/bin/bash\necho hello\n") self.write_hook("start", "#!/bin/bash\necho hello\n") result = yield self.workflow.fire_transition("install") self.assertTrue(result) result = yield self.workflow.fire_transition("start") self.assertTrue(result) self.write_hook("stop", "#!/bin/bash\nexit 1") result = yield self.workflow.fire_transition("stop") self.assertFalse(result) yield self.assertState(self.workflow, "stop_error") self.write_hook("stop", "#!/bin/bash\necho hello\n") result = yield self.workflow.fire_transition("retry_stop") yield self.assertState(self.workflow, "stopped") @inlineCallbacks def test_stop_error_with_retry_hook(self): self.write_hook("install", "#!/bin/bash\necho hello\n") self.write_hook("start", "#!/bin/bash\necho hello\n") result = yield self.workflow.fire_transition("install") self.assertTrue(result) result = yield self.workflow.fire_transition("start") self.assertTrue(result) self.write_hook("stop", "#!/bin/bash\nexit 1") result = yield self.workflow.fire_transition("stop") self.assertFalse(result) yield self.assertState(self.workflow, "stop_error") result = yield self.workflow.fire_transition_alias("retry_hook") yield self.assertState(self.workflow, "stop_error") self.write_hook("stop", "#!/bin/bash\nexit 0") result = yield self.workflow.fire_transition_alias("retry_hook") yield self.assertState(self.workflow, "stopped") @inlineCallbacks def test_client_with_no_state(self): workflow_client = WorkflowStateClient(self.client, self.states["unit"]) state = yield workflow_client.get_state() self.assertEqual(state, None) @inlineCallbacks def test_client_with_state(self): yield self.workflow.fire_transition("install") workflow_client = WorkflowStateClient(self.client, self.states["unit"]) self.assertEqual((yield workflow_client.get_state()), "installed") @inlineCallbacks def test_client_readonly(self): yield self.workflow.fire_transition("install") workflow_client = WorkflowStateClient(self.client, self.states["unit"]) self.assertEqual((yield workflow_client.get_state()), "installed") yield self.assertFailure(workflow_client.set_state("started"), NotImplementedError) self.assertEqual((yield workflow_client.get_state()), "installed")
class ControlResolvedTest( ServiceStateManagerTestBase, ControlToolTest, RepositoryTestBase): @inlineCallbacks def setUp(self): yield super(ControlResolvedTest, self).setUp() config = { "environments": {"firstenv": {"type": "dummy"}}} self.write_config(dump(config)) self.config.load() yield self.add_relation_state("wordpress", "mysql") yield self.add_relation_state("wordpress", "varnish") self.service1 = yield self.service_state_manager.get_service_state( "mysql") self.service_unit1 = yield self.service1.add_unit_state() self.service_unit2 = yield self.service1.add_unit_state() self.unit1_workflow = UnitWorkflowState( self.client, self.service_unit1, None, self.makeDir()) yield self.unit1_workflow.set_state("started") self.environment = self.config.get_default() self.provider = self.environment.get_machine_provider() self.output = self.capture_logging() self.stderr = self.capture_stream("stderr") self.executor = HookExecutor() @inlineCallbacks def add_relation_state(self, *service_names): for service_name in service_names: try: yield self.service_state_manager.get_service_state( service_name) except ServiceStateNotFound: yield self.add_service_from_charm(service_name) endpoint_pairs = yield self.service_state_manager.join_descriptors( *service_names) endpoints = endpoint_pairs[0] endpoints = endpoint_pairs[0] if endpoints[0] == endpoints[1]: endpoints = endpoints[0:1] relation_state = (yield self.relation_state_manager.add_relation_state( *endpoints))[0] returnValue(relation_state) @inlineCallbacks def get_named_service_relation(self, service_state, relation_name): if isinstance(service_state, str): service_state = yield self.service_state_manager.get_service_state( service_state) rels = yield self.relation_state_manager.get_relations_for_service( service_state) rels = [sr for sr in rels if sr.relation_name == relation_name] if len(rels) == 1: returnValue(rels[0]) returnValue(rels) @inlineCallbacks def setup_unit_relations(self, service_relation, *units): """ Given a service relation and set of unit tuples in the form unit_state, unit_relation_workflow_state, will add unit relations for these units and update their workflow state to the desired/given state. """ for unit, state in units: unit_relation = yield service_relation.add_unit_state(unit) lifecycle = UnitRelationLifecycle(self.client, unit.unit_name, unit_relation, service_relation.relation_name, self.makeDir(), self.executor) workflow_state = RelationWorkflowState( self.client, unit_relation, lifecycle, self.makeDir()) yield workflow_state.set_state(state) @inlineCallbacks def test_resolved(self): """ 'juju resolved <unit_name>' will schedule a unit for retrying from an error state. """ # Push the unit into an error state yield self.unit1_workflow.set_state("start_error") self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() self.assertEqual( (yield self.service_unit1.get_resolved()), None) main(["resolved", "mysql/0"]) yield finished self.assertEqual( (yield self.service_unit1.get_resolved()), {"retry": NO_HOOKS}) self.assertIn( "Marked unit 'mysql/0' as resolved", self.output.getvalue()) @inlineCallbacks def test_resolved_retry(self): """ 'juju resolved --retry <unit_name>' will schedule a unit for retrying from an error state with a retry of hooks executions. """ yield self.unit1_workflow.set_state("start_error") self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() self.assertEqual( (yield self.service_unit1.get_resolved()), None) main(["resolved", "--retry", "mysql/0"]) yield finished self.assertEqual( (yield self.service_unit1.get_resolved()), {"retry": RETRY_HOOKS}) self.assertIn( "Marked unit 'mysql/0' as resolved", self.output.getvalue()) @inlineCallbacks def test_relation_resolved(self): """ 'juju relation <unit_name> <rel_name>' will schedule the broken unit relations for being resolved. """ service_relation = yield self.get_named_service_relation( self.service1, "server") yield self.setup_unit_relations( service_relation, (self.service_unit1, "down"), (self.service_unit2, "up")) yield self.unit1_workflow.set_state("start_error") self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() self.assertEqual( (yield self.service_unit1.get_relation_resolved()), None) main(["resolved", "--retry", "mysql/0", service_relation.relation_name]) yield finished self.assertEqual( (yield self.service_unit1.get_relation_resolved()), {service_relation.internal_relation_id: RETRY_HOOKS}) self.assertEqual( (yield self.service_unit2.get_relation_resolved()), None) self.assertIn( "Marked unit 'mysql/0' relation 'server' as resolved", self.output.getvalue()) @inlineCallbacks def test_resolved_relation_some_already_resolved(self): """ 'juju resolved <service_name> <rel_name>' will mark resolved all down units that are not already marked resolved. """ service2 = yield self.service_state_manager.get_service_state( "wordpress") service_unit1 = yield service2.add_unit_state() service_relation = yield self.get_named_service_relation( service2, "db") yield self.setup_unit_relations( service_relation, (service_unit1, "down")) service_relation2 = yield self.get_named_service_relation( service2, "cache") yield self.setup_unit_relations( service_relation2, (service_unit1, "down")) yield service_unit1.set_relation_resolved( {service_relation.internal_relation_id: NO_HOOKS}) self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() main(["resolved", "--retry", "wordpress/0", "cache"]) yield finished self.assertEqual( (yield service_unit1.get_relation_resolved()), {service_relation.internal_relation_id: NO_HOOKS, service_relation2.internal_relation_id: RETRY_HOOKS}) self.assertIn( "Marked unit 'wordpress/0' relation 'cache' as resolved", self.output.getvalue()) @inlineCallbacks def test_resolved_relation_some_already_resolved_conflict(self): """ 'juju resolved <service_name> <rel_name>' will mark resolved all down units that are not already marked resolved. """ service2 = yield self.service_state_manager.get_service_state( "wordpress") service_unit1 = yield service2.add_unit_state() service_relation = yield self.get_named_service_relation( service2, "db") yield self.setup_unit_relations( service_relation, (service_unit1, "down")) yield service_unit1.set_relation_resolved( {service_relation.internal_relation_id: NO_HOOKS}) self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() main(["resolved", "--retry", "wordpress/0", "db"]) yield finished self.assertEqual( (yield service_unit1.get_relation_resolved()), {service_relation.internal_relation_id: NO_HOOKS}) self.assertIn( "Service unit 'wordpress/0' already has relations marked as resol", self.output.getvalue()) @inlineCallbacks def test_resolved_unknown_service(self): """ 'juju resolved <unit_name>' will report if a service is invalid. """ self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() main(["resolved", "zebra/0"]) yield finished self.assertIn("Service 'zebra' was not found", self.stderr.getvalue()) @inlineCallbacks def test_resolved_unknown_unit(self): """ 'juju resolved <unit_name>' will report if a unit is invalid. """ self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() main(["resolved", "mysql/5"]) yield finished self.assertIn( "Service unit 'mysql/5' was not found", self.output.getvalue()) @inlineCallbacks def test_resolved_unknown_unit_relation(self): """ 'juju resolved <unit_name>' will report if a relation is invalid. """ self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() self.assertEqual( (yield self.service_unit1.get_resolved()), None) main(["resolved", "mysql/0", "magic"]) yield finished self.assertIn("Relation not found", self.output.getvalue()) @inlineCallbacks def test_resolved_already_running(self): """ 'juju resolved <unit_name>' will report if the unit is already running. """ # Just verify we don't accidentally mark up another unit of the service unit2_workflow = UnitWorkflowState( self.client, self.service_unit2, None, self.makeDir()) unit2_workflow.set_state("start_error") self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() main(["resolved", "mysql/0"]) yield finished self.assertEqual( (yield self.service_unit2.get_resolved()), None) self.assertEqual( (yield self.service_unit1.get_resolved()), None) self.assertNotIn( "Unit 'mysql/0 already running: started", self.output.getvalue()) @inlineCallbacks def test_resolved_already_resolved(self): """ 'juju resolved <unit_name>' will report if the unit is already resolved. """ # Mark the unit as resolved and as in an error state. yield self.service_unit1.set_resolved(RETRY_HOOKS) yield self.unit1_workflow.set_state("start_error") unit2_workflow = UnitWorkflowState( self.client, self.service_unit1, None, self.makeDir()) unit2_workflow.set_state("start_error") self.assertEqual( (yield self.service_unit2.get_resolved()), None) self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() main(["resolved", "mysql/0"]) yield finished self.assertEqual( (yield self.service_unit1.get_resolved()), {"retry": RETRY_HOOKS}) self.assertNotIn( "Marked unit 'mysql/0' as resolved", self.output.getvalue()) self.assertIn( "Service unit 'mysql/0' is already marked as resolved.", self.stderr.getvalue(), "") @inlineCallbacks def test_resolved_relation_already_running(self): """ 'juju resolved <unit_name> <rel_name>' will report if the relation is already running. """ service2 = yield self.service_state_manager.get_service_state( "wordpress") service_unit1 = yield service2.add_unit_state() service_relation = yield self.get_named_service_relation( service2, "db") yield self.setup_unit_relations( service_relation, (service_unit1, "up")) self.setup_exit(0) finished = self.setup_cli_reactor() self.mocker.replay() main(["resolved", "wordpress/0", "db"]) yield finished self.assertIn("Matched relations are all running", self.output.getvalue()) self.assertEqual( (yield service_unit1.get_relation_resolved()), None)
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() config = { "environments": {"firstenv": {"type": "dummy"}}} self.write_config(dump(config)) self.config.load() self.service_state1 = yield self.add_service_from_charm("mysql") self.service_unit1 = yield self.service_state1.add_unit_state() self.unit1_workflow = UnitWorkflowState( self.client, self.service_unit1, None, self.makeDir()) yield self.unit1_workflow.set_state("started") self.environment = self.config.get_default() self.provider = self.environment.get_machine_provider() self.output = self.capture_logging() self.stderr = self.capture_stream("stderr") @inlineCallbacks def test_charm_upgrade(self): """ 'juju charm-upgrade <service_name>' will schedule a charm for upgrade. """ repository = self.increment_charm(self.charm) mock_environment = self.mocker.patch(Environment) mock_environment.get_machine_provider() self.mocker.result(self.provider) finished = self.setup_cli_reactor() self.setup_exit(0) self.mocker.replay() main(["upgrade-charm", "--repository", repository.path, "mysql"]) yield finished # Verify the service has a new charm reference charm_id = yield self.service_state1.get_charm_id() self.assertEqual(charm_id, "local:series/mysql-2") # Verify the provider storage has been updated charm = yield repository.find(CharmURL.parse("local:series/mysql")) storage = self.provider.get_file_storage() try: yield storage.get( "local_3a_series_2f_mysql-2_3a_%s" % charm.get_sha256()) except FileNotFound: self.fail("New charm not uploaded") # Verify the upgrade flag on the service units. upgrade_flag = yield self.service_unit1.get_upgrade_flag() self.assertTrue(upgrade_flag) @inlineCallbacks def test_missing_repository(self): finished = self.setup_cli_reactor() self.setup_exit(0) self.mocker.replay() main(["upgrade-charm", "mysql"]) yield finished self.assertIn("No repository specified", self.output.getvalue()) @inlineCallbacks def test_upgrade_charm_with_unupgradeable_units(self): """If there are units that won't be upgraded, they will be reported, other units will be upgraded. """ repository = self.increment_charm(self.charm) service_unit2 = yield self.service_state1.add_unit_state() finished = self.setup_cli_reactor() self.setup_exit(0) self.mocker.replay() main(["upgrade-charm", "--repository", repository.path, "mysql"]) yield finished # Verify report of unupgradeable units self.assertIn( ("Unit 'mysql/1' is not in a running state " "(state: 'uninitialized'), won't upgrade"), self.output.getvalue()) # Verify flags only set on upgradeable unit. value = (yield service_unit2.get_upgrade_flag()) self.assertFalse(value) value = (yield self.service_unit1.get_upgrade_flag()) self.assertTrue(value) @inlineCallbacks def test_upgrade_charm_unknown_service(self): finished = self.setup_cli_reactor() self.setup_exit(0) self.mocker.replay() main(["upgrade-charm", "--repository", self.makeDir(), "volcano"]) yield finished self.assertIn( "Service 'volcano' was not found", self.stderr.getvalue()) @inlineCallbacks def test_upgrade_charm_unknown_charm(self): """If a charm is not found in the repository, an error is given. """ finished = self.setup_cli_reactor() self.setup_exit(0) self.mocker.replay() repository_dir = self.makeDir() os.mkdir(os.path.join(repository_dir, "series")) main(["upgrade-charm", "--repository", repository_dir, "mysql"]) yield finished self.assertIn( "Charm 'local:series/mysql' not found in repository", self.output.getvalue()) @inlineCallbacks def test_upgrade_charm_unknown_charm_dryrun(self): """If a charm is not found in the repository, an error is given. """ finished = self.setup_cli_reactor() self.setup_exit(0) self.mocker.replay() repository_dir = self.makeDir() os.mkdir(os.path.join(repository_dir, "series")) main(["upgrade-charm", "--repository", repository_dir, "mysql", "--dry-run"]) yield finished self.assertIn( "Charm 'local:series/mysql' not found in repository", self.output.getvalue()) @inlineCallbacks def test_upgrade_charm_dryrun_reports_unupgradeable_units(self): """If there are units that won't be upgraded, dry-run will report them. """ repository = self.increment_charm(self.charm) service_unit2 = yield self.service_state1.add_unit_state() finished = self.setup_cli_reactor() self.setup_exit(0) self.mocker.replay() main(["upgrade-charm", "-n", "--repository", repository.path, "mysql"]) yield finished # Verify dry run self.assertIn( "Service would be upgraded from charm", self.output.getvalue()) # Verify report of unupgradeable units self.assertIn( ("Unit 'mysql/1' is not in a running state " "(state: 'uninitialized'), won't upgrade"), self.output.getvalue()) # Verify no flags have been set. value = (yield service_unit2.get_upgrade_flag()) self.assertFalse(value) value = (yield self.service_unit1.get_upgrade_flag()) self.assertFalse(value) @inlineCallbacks def test_latest_local_dry_run(self): """Do nothing; log that local charm would be re-revisioned and used""" finished = self.setup_cli_reactor() self.setup_exit(0) self.mocker.replay() metadata = self.charm.metadata.get_serialization_data() metadata["name"] = "mysql" repository = self.add_charm(metadata, 1) main(["upgrade-charm", "--dry-run", "--repository", repository.path, "mysql"]) yield finished charm_path = os.path.join(repository.path, "series", "mysql") self.assertIn( "%s would be set to revision 2" % charm_path, self.output.getvalue()) self.assertIn( "Service would be upgraded from charm 'local:series/mysql-1' to " "'local:series/mysql-2'", self.output.getvalue()) with open(os.path.join(charm_path, "revision")) as f: self.assertEquals(f.read(), "1") upgrade_flag = yield self.service_unit1.get_upgrade_flag() self.assertFalse(upgrade_flag) @inlineCallbacks def test_latest_local_live_fire(self): """Local charm should be re-revisioned and used; log that it was""" finished = self.setup_cli_reactor() self.setup_exit(0) self.mocker.replay() metadata = self.charm.metadata.get_serialization_data() metadata["name"] = "mysql" repository = self.add_charm(metadata, 1) main(["upgrade-charm", "--repository", repository.path, "mysql"]) yield finished charm_path = os.path.join(repository.path, "series", "mysql") self.assertIn( "Setting %s to revision 2" % charm_path, self.output.getvalue()) with open(os.path.join(charm_path, "revision")) as f: self.assertEquals(f.read(), "2\n") upgrade_flag = yield self.service_unit1.get_upgrade_flag() self.assertTrue(upgrade_flag) @inlineCallbacks def test_latest_local_leapfrog_dry_run(self): """Do nothing; log that local charm would be re-revisioned and used""" finished = self.setup_cli_reactor() self.setup_exit(0) self.mocker.replay() metadata = self.charm.metadata.get_serialization_data() metadata["name"] = "mysql" repository = self.add_charm(metadata, 0) main(["upgrade-charm", "--dry-run", "--repository", repository.path, "mysql"]) yield finished charm_path = os.path.join(repository.path, "series", "mysql") self.assertIn( "%s would be set to revision 2" % charm_path, self.output.getvalue()) self.assertIn( "Service would be upgraded from charm 'local:series/mysql-1' to " "'local:series/mysql-2'", self.output.getvalue()) with open(os.path.join(charm_path, "revision")) as f: self.assertEquals(f.read(), "0") upgrade_flag = yield self.service_unit1.get_upgrade_flag() self.assertFalse(upgrade_flag) @inlineCallbacks def test_latest_local_leapfrog_live_fire(self): """Local charm should be re-revisioned and used; log that it was""" finished = self.setup_cli_reactor() self.setup_exit(0) self.mocker.replay() metadata = self.charm.metadata.get_serialization_data() metadata["name"] = "mysql" repository = self.add_charm(metadata, 0) main(["upgrade-charm", "--repository", repository.path, "mysql"]) yield finished charm_path = os.path.join(repository.path, "series", "mysql") self.assertIn( "Setting %s to revision 2" % charm_path, self.output.getvalue()) with open(os.path.join(charm_path, "revision")) as f: self.assertEquals(f.read(), "2\n") upgrade_flag = yield self.service_unit1.get_upgrade_flag() self.assertTrue(upgrade_flag) @inlineCallbacks def test_latest_local_bundle_dry_run(self): """Do nothing; log that nothing would be done""" finished = self.setup_cli_reactor() self.setup_exit(0) self.mocker.replay() metadata = self.charm.metadata.get_serialization_data() metadata["name"] = "mysql" repository = self.add_charm(metadata, 1, bundle=True) main(["upgrade-charm", "--dry-run", "--repository", repository.path, "mysql"]) yield finished self.assertIn( "Service already running latest charm", self.output.getvalue()) upgrade_flag = yield self.service_unit1.get_upgrade_flag() self.assertFalse(upgrade_flag) @inlineCallbacks def test_latest_local_bundle_live_fire(self): """Do nothing; log that nothing was done""" finished = self.setup_cli_reactor() self.setup_exit(0) self.mocker.replay() metadata = self.charm.metadata.get_serialization_data() metadata["name"] = "mysql" repository = self.add_charm(metadata, 1, bundle=True) main(["upgrade-charm", "--repository", repository.path, "mysql"]) yield finished self.assertIn( "Charm 'local:series/mysql-1' is the latest revision known", self.output.getvalue()) upgrade_flag = yield self.service_unit1.get_upgrade_flag() self.assertFalse(upgrade_flag)
class RemoteUpgradeCharmTest(MachineControlToolTest): @inlineCallbacks def setUp(self): yield super(RemoteUpgradeCharmTest, self).setUp() config = { "environments": {"firstenv": {"type": "dummy"}}} self.write_config(dump(config)) self.config.load() charm = CharmDirectory(os.path.join( test_repository_path, "series", "mysql")) self.charm_state_manager.add_charm_state( "cs:series/mysql-1", charm, "") self.service_state1 = yield self.add_service_from_charm( "mysql", "cs:series/mysql-1") self.service_unit1 = yield self.service_state1.add_unit_state() self.unit1_workflow = UnitWorkflowState( self.client, self.service_unit1, None, self.makeDir()) yield self.unit1_workflow.set_state("started") self.environment = self.config.get_default() self.provider = self.environment.get_machine_provider() self.output = self.capture_logging() self.stderr = self.capture_stream("stderr") @inlineCallbacks def test_latest_dry_run(self): """Do nothing; log that nothing would be done""" finished = self.setup_cli_reactor() self.setup_exit(0) getPage = self.mocker.replace("twisted.web.client.getPage") getPage( "https://store.juju.ubuntu.com/" "charm-info?charms=cs%3Aseries/mysql") self.mocker.result(succeed(json.dumps( {"cs:series/mysql": {"revision": 1, "sha256": "whatever"}}))) self.mocker.replay() main(["upgrade-charm", "--dry-run", "mysql"]) yield finished self.assertIn( "Service already running latest charm", self.output.getvalue()) upgrade_flag = yield self.service_unit1.get_upgrade_flag() self.assertFalse(upgrade_flag) @inlineCallbacks def test_latest_live_fire(self): """Do nothing; log that nothing was done""" finished = self.setup_cli_reactor() self.setup_exit(0) getPage = self.mocker.replace("twisted.web.client.getPage") getPage( "https://store.juju.ubuntu.com/charm-info?charms=cs%3Aseries/mysql") self.mocker.result(succeed(json.dumps( {"cs:series/mysql": {"revision": 1, "sha256": "whatever"}}))) self.mocker.replay() main(["upgrade-charm", "mysql"]) yield finished self.assertIn( "Charm 'cs:series/mysql-1' is the latest revision known", self.output.getvalue()) upgrade_flag = yield self.service_unit1.get_upgrade_flag() self.assertFalse(upgrade_flag)