def perform_power_driver_change(system_id, hostname, power_type, power_change, context): """Execute power driver `power_change` method. On failure the node will be marked as broken and the error will be re-raised to the caller. """ power_driver = PowerDriverRegistry.get_item(power_type) if power_change == "on": d = power_driver.on(system_id, context) elif power_change == "off": d = power_driver.off(system_id, context) elif power_change == "cycle": d = power_driver.cycle(system_id, context) def power_change_failed(failure): message = "Power %s for the node failed: %s" % ( power_change, get_error_message(failure.value), ) df = power_change_failure(system_id, hostname, power_change, message) df.addCallback(lambda _: failure) # Propagate the original error. return df return d.addErrback(power_change_failed)
def change_power_state(system_id, hostname, power_type, power_change, context, clock=reactor): """Change the power state of a node. This monitors the result of the power change by querying the power state of the node, thus attempting to ensure that the requested change has taken place. Success is reported using `power_change_success`. Power-related failures are reported using `power_change_failure`. Other failures must be reported by the caller. """ yield power_change_starting(system_id, hostname, power_change) yield perform_power_driver_change(system_id, hostname, power_type, power_change, context) if power_type not in PowerDriverRegistry: returnValue(None) power_driver = PowerDriverRegistry.get_item(power_type) if not power_driver.queryable: returnValue(None) new_power_state = yield perform_power_driver_query(system_id, hostname, power_type, context) if new_power_state == "unknown" or new_power_state == power_change: yield power_change_success(system_id, hostname, power_change) elif new_power_state == "on" and power_change == "cycle": yield power_change_success(system_id, hostname, new_power_state) returnValue(new_power_state)
def get_power_state(system_id, hostname, power_type, context, clock=reactor): """Return the power state of the given node. :return: The string "on", "off" or "unknown". :raises PowerActionFail: When there's a failure querying the node's power state. """ def check_power_state(state): if state not in ("on", "off", "unknown"): # This is considered an error. raise PowerActionFail(state) # Capture errors as we go along. exc_info = None, None, None 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) ) try: power_state = yield perform_power_driver_query( system_id, hostname, power_type, context ) check_power_state(power_state) except Exception: # Hold the error; it will be reported later. exc_info = sys.exc_info() else: returnValue(power_state) # Reaching here means that things have gone wrong. assert exc_info != (None, None, None) exc_type, exc_value, exc_trace = exc_info raise exc_type(exc_value).with_traceback(exc_trace)
def scope_power_parameters(power_type, power_params): """Separate the global, bmc related power_parameters from the local, node-specific ones.""" if not power_type: # If there is no power type, treat all params as node params. return ({}, power_params) power_driver = PowerDriverRegistry.get_item(power_type) if power_driver is None: # If there is no power driver, treat all params as node params. return ({}, power_params) power_fields = power_driver.settings if not power_fields: # If there is no parameter info, treat all params as node params. return ({}, power_params) bmc_params = {} node_params = {} for param_name in power_params: power_field = power_driver.get_setting(param_name) if (power_field and power_field.get('scope') == SETTING_SCOPE.BMC): bmc_params[param_name] = power_params[param_name] else: node_params[param_name] = power_params[param_name] return (bmc_params, node_params)
def extract_ip_address(power_type, power_parameters): # Extract the ip_address from the power_parameters. If there is no # power_type, no power_parameters, or no valid value provided in the # power_address field, returns None. if not power_type or not power_parameters: return None power_driver = PowerDriverRegistry.get_item(power_type) if power_driver is None: return None power_type_parameters = power_driver.settings if not power_type_parameters: return None ip_extractor = power_driver.ip_extractor if not ip_extractor: return None field_value = power_parameters.get(ip_extractor.get("field_name")) if not field_value: return None extraction_pattern = ip_extractor.get("pattern") if not extraction_pattern: return None match = re.match(extraction_pattern, field_value) return match.group("address") if match else None
def extract_ip_address(power_type, power_parameters): """ Extract the ip_address from the power_parameters. If there is no power_type, no power_parameters, or no valid value provided in the power_address field, returns None. """ if not power_type or not power_parameters: # Nothing to extract. return None power_driver = PowerDriverRegistry.get_item(power_type) if power_driver is None: maaslog.warning("No power driver for power type %s" % power_type) return None power_type_parameters = power_driver.settings if not power_type_parameters: maaslog.warning("No power driver settings for power type %s" % power_type) return None ip_extractor = power_driver.ip_extractor if not ip_extractor: maaslog.info("No IP extractor configured for power type %s. " "IP will not be extracted." % power_type) return None field_value = power_parameters.get(ip_extractor.get('field_name')) if not field_value: maaslog.warning("IP extractor field_value missing for %s" % power_type) return None extraction_pattern = ip_extractor.get('pattern') if not extraction_pattern: maaslog.warning("IP extractor extraction_pattern missing for %s" % power_type) return None match = re.match(extraction_pattern, field_value) if match: return match.group('address') # no match found - return None return None
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))