def test_handles_PowerActionAlreadyInProgress(self): request = factory.make_fake_request(factory.make_string(), "POST") error_message = ("Unable to execute power action: another action is " "already in progress for node %s" % factory.make_name("node")) error = PowerActionAlreadyInProgress(error_message) response = self.process_request(request, error) # The response is a redirect. self.assertEqual(request.path, extract_redirect(response))
def test__raises_power_problem(self): node = factory.make_Node() client = Mock() client.return_value = fail( PowerActionAlreadyInProgress('Houston, we have a problem.')) with ExpectedException(PowerProblem, "Houston, we have a problem."): wait_for_reactor(self.power_func)(client, node.system_id, node.hostname, node.get_effective_power_info())
def test_POST_power_off_returns_503_when_power_already_in_progress(self): machine = factory.make_Node(owner=self.user) exc_text = factory.make_name("exc_text") self.patch(node_module.Machine, "stop").side_effect = PowerActionAlreadyInProgress(exc_text) response = self.client.post(self.get_node_uri(machine), {"op": "power_off"}) self.assertThat(response, HasStatusCode(http.client.SERVICE_UNAVAILABLE)) self.assertIn(exc_text, response.content.decode(settings.DEFAULT_CHARSET))
def test_power_action_already_in_progress_returned_as_503(self): request = factory.make_fake_request( "/MAAS/api/2.0/" + factory.make_string(), 'POST') error_message = ( "Unable to execute power action: another action is already in " "progress for node %s" % factory.make_name('node')) error = PowerActionAlreadyInProgress(error_message) response = self.process_request(request, error) self.assertEqual((http.client.SERVICE_UNAVAILABLE, error_message), (response.status_code, response.content.decode(settings.DEFAULT_CHARSET)))
def test_handles_PowerActionAlreadyInProgress(self): request = factory.make_fake_request(factory.make_string(), 'POST') error_message = ("Unable to execute power action: another action is " "already in progress for node %s" % factory.make_name('node')) error = PowerActionAlreadyInProgress(error_message) response = self.process_request(request, error) # The response is a redirect. self.assertEqual(request.path, extract_redirect(response)) # An error message has been published. self.assertEqual([(constants.ERROR, "Error: %s" % error_message, '')], request._messages.messages)
def maybe_change_power_state(system_id, hostname, power_type, power_change, context, clock=reactor): """Attempt to change the power state of a node. If there is no power action already in progress, register this action and then pass change_power_state() to the reactor to call later and then return. This function exists to guarantee that PowerActionAlreadyInProgress errors will be raised promptly, before any work is done to power the node on. :raises: PowerActionAlreadyInProgress if there's already a power action in progress for this node. """ assert power_change in ("on", "off", "cycle"), ("Unknown power change: %s" % power_change) power_driver = PowerDriverRegistry.get_item(power_type) if power_driver is None: raise PowerActionFail("Unknown power_type '%s'" % power_type) missing_packages = power_driver.detect_missing_packages() if len(missing_packages): raise PowerActionFail("'%s' package(s) are not installed" % " ".join(missing_packages)) # There should be one and only one power change for each system ID. if system_id in power_action_registry: current_power_change, d = power_action_registry[system_id] else: current_power_change, d = None, None if current_power_change is None: # Arrange for the power change to happen later; do not make the caller # wait, because it might take a long time. We set a timeout so that if # the power action doesn't return in a timely fashion (or fails # silently or some such) it doesn't block other actions on the node. d = deferLater( clock, 0, deferWithTimeout, CHANGE_POWER_STATE_TIMEOUT, change_power_state, system_id, hostname, power_type, power_change, context, clock, ) power_action_registry[system_id] = power_change, d # Whether we succeed or fail, we need to remove the action from the # registry of actions, otherwise subsequent actions will fail. d.addBoth(callOut, power_action_registry.pop, system_id, None) # Log cancellations distinctly from other errors. def eb_cancelled(failure): failure.trap(CancelledError) log.msg("%s: Power could not be set to %s; timed out." % (hostname, power_change)) return power_change_failure(system_id, hostname, power_change, "Timed out") d.addErrback(eb_cancelled) # Catch-all log. d.addErrback(log.err, "%s: Power %s failed." % (hostname, power_change)) # LP: 1761600 - Returning d will cause d to be resolved with the # caller. This causes power actions in the UI/API such as deploy, # commission, or release to wait for the power action to complete # before finishing. elif current_power_change == power_change: # What we want is already happening; let it continue. pass else: # Right now we reject conflicting power changes. However, we have the # Deferred (in `d`) along which the current power change is occurring, # so the option to cancel is available if we want it. raise PowerActionAlreadyInProgress( "Unable to change power state to '%s' for node %s: another " "action is already in progress for that node." % (power_change, hostname))