def amttool_query_state(self, ip_address, power_pass): """Ask for node's power state: 'on' or 'off', via amttool.""" # Retry the state if it fails because it often fails the first time for _ in range(10): output = self._issue_amttool_command("info", ip_address, power_pass) if output: break # Wait 1 second between retries. AMT controllers are generally # very light and may not be comfortable with more frequent # queries. sleep(1) if not output: raise PowerActionError("amttool power querying failed.") # Ensure that from this point forward that output is a str. output = output.decode("utf-8") # Wide awake (S0), or asleep (S1-S4), but not a clean slate that # will lead to a fresh boot. if "S5" in output: return "off" for state in ("S0", "S1", "S2", "S3", "S4"): if state in output: return "on" raise PowerActionError("Got unknown power state from node: %s" % state)
def cb(response_data): parsed_data = json.loads(response_data) vms = parsed_data["data"] if not vms: raise PowerActionError( "No VMs returned! Are permissions set correctly?" ) for vm in vms: if power_vm_name in (str(vm.get("vmid")), vm.get("name")): return vm raise PowerActionError("Unable to find virtual machine")
def run_process(self, *command): """Run SNMP command in subprocess.""" result = shell.run_command(*command) if result.returncode != 0: raise PowerActionError( "APC Power Driver external process error for command %s: %s" % ("".join(command), result.stderr)) match = re.search(r"INTEGER:\s*([1-2])", result.stdout) if match is None: raise PowerActionError( "APC Power Driver unable to extract outlet power state" " from: %s" % result.stdout) else: return match.group(1)
def power_query(self, system_id, context): """Power query MSCM node.""" try: # Retreive node power state # # Example of output from running "show node power <node_id>": # "show node power c1n1\r\r\n\r\nCartridge #1\r\n Node #1\r\n # Power State: On\r\n" output = self.run_mscm_command( "show node power %s" % context["node_id"], **context) except PowerConnError as e: raise PowerActionError( "MSCM Power Driver unable to power query node %s: %s" % (context["node_id"], e)) match = re.search(r"Power State:\s*((O[\w]+|U[\w]+))", output) if match is None: raise PowerFatalError( "MSCM Power Driver unable to extract node power state from: %s" % output) else: power_state = match.group(1) if power_state in MSCMState.OFF: return "off" elif power_state == MSCMState.ON: return "on"
def _set_outlet_state(self, power_change, outlet_id=None, power_user=None, power_pass=None, power_address=None, **extra): """Power DLI outlet ON/OFF.""" try: url = "http://%s:%s@%s/outlet?%s=%s" % ( power_user, power_pass, power_address, outlet_id, power_change, ) # --auth-no-challenge: send Basic HTTP authentication # information without first waiting for the server's challenge. call_and_check( ["wget", "--auth-no-challenge", "-O", "/dev/null", url], env=get_env_with_locale(), ) except ExternalProcessError as e: raise PowerActionError( "Failed to power %s outlet %s: %s" % (power_change, outlet_id, e.output_as_unicode))
def _power_control_seamicro15k_ipmi(self, ip, username, password, server_id, power_change): """Power on/off SeaMicro node via ipmitool.""" power_mode = 1 if power_change == 'on' else 6 try: call_and_check([ 'ipmitool', '-I', 'lanplus', '-H', ip, '-U', username, '-P', password, 'raw', '0x2E', '1', '0x00', '0x7d', '0xab', power_mode, '0', server_id, ]) except ExternalProcessError as e: raise PowerActionError( "Failed to power %s %s at %s: %s" % (power_change, server_id, ip, e.output_as_unicode))
def power_off(self, system_id, context): """Power off Wedge.""" try: self.run_wedge_command("/usr/local/bin/wedge_power.sh off", **context) except PowerConnError: raise PowerActionError("Wedge Power Driver unable to power off")
def _get_amt_command(self, ip_address, power_pass): """Retrieve AMT command to use, either amttool or wsman (if AMT version > 8), for the given system. """ # XXX bug=1331214 # Check if the AMT ver > 8 # If so, we need wsman, not amttool env = self._get_amt_environment(power_pass) process = Popen( ('amttool', ip_address, 'info'), stdout=PIPE, stderr=PIPE, env=env) stdout, stderr = process.communicate() stdout = stdout.decode("utf-8") stderr = stderr.decode("utf-8") if stdout == "" or stdout.isspace(): for error, error_info in AMT_ERRORS.items(): if error in stderr: raise error_info.get( 'exception')(error_info.get('message')) raise PowerConnError( "Unable to retrieve AMT version: %s" % stderr) else: match = re.search("AMT version:\s*([0-9]+)", stdout) if match is None: raise PowerActionError( "Unable to extract AMT version from " "amttool output: %s" % stdout) else: version = match.group(1) if int(version) > 8: return 'wsman' else: return 'amttool'
def _issue_ipmitool_command(self, power_change, power_address=None, power_user=None, power_pass=None, power_hwaddress=None, **extra): """Issue ipmitool command for HP Moonshot cartridge.""" command = ('ipmitool', '-I', 'lanplus', '-H', power_address, '-U', power_user, '-P', power_pass) + tuple( power_hwaddress.split()) if power_change == 'pxe': command += ('chassis', 'bootdev', 'pxe') else: command += ('power', power_change) try: stdout = call_and_check(command, env=select_c_utf8_locale()) stdout = stdout.decode('utf-8') except ExternalProcessError as e: raise PowerActionError( "Failed to execute %s for cartridge %s at %s: %s" % (command, power_hwaddress, power_address, e.output_as_unicode)) else: # Return output if power query if power_change == 'status': match = re.search(r'\b(on|off)\b$', stdout) return stdout if match is None else match.group(0)
class TestGetErrorMessage(MAASTestCase): scenarios = [ ('auth', dict( exception=PowerAuthError('auth'), message="Could not authenticate to node's BMC: auth", )), ('conn', dict( exception=PowerConnError('conn'), message="Could not contact node's BMC: conn", )), ('setting', dict( exception=PowerSettingError('setting'), message="Missing or invalid power setting: setting", )), ('tool', dict( exception=PowerToolError('tool'), message="Missing power tool: tool", )), ('action', dict( exception=PowerActionError('action'), message="Failed to complete power action: action", )), ('unknown', dict( exception=PowerError('unknown error'), message="Failed talking to node's BMC: unknown error", )), ] def test_return_msg(self): self.assertEqual(self.message, get_error_message(self.exception))
def _power_control_seamicro15k_ipmi(self, ip, username, password, server_id, power_change): """Power on/off SeaMicro node via ipmitool.""" power_mode = 1 if power_change == "on" else 6 try: call_and_check([ "ipmitool", "-I", "lanplus", "-H", ip, "-U", username, "-P", password, "-L", "OPERATOR", "raw", "0x2E", "1", "0x00", "0x7d", "0xab", power_mode, "0", server_id, ]) except ExternalProcessError as e: raise PowerActionError( "Failed to power %s %s at %s: %s" % (power_change, server_id, ip, e.output_as_unicode))
def render_response(response): """Render the HTTPS response received.""" def eb_catch_partial(failure): # Twisted is raising PartialDownloadError because the responses # do not contain a Content-Length header. Since every response # holds the whole body we just take the result. failure.trap(PartialDownloadError) if int(failure.value.status) == HTTPStatus.OK: return failure.value.response else: return failure def cb_json_decode(data): data = data.decode('utf-8') # Only decode non-empty response bodies. if data: return json.loads(data) def cb_attach_headers(data, headers): return data, headers # Error out if the response has a status code of 400 or above. if response.code >= int(HTTPStatus.BAD_REQUEST): raise PowerActionError( "Redfish request failed with response status code:" " %s." % response.code) d = readBody(response) d.addErrback(eb_catch_partial) d.addCallback(cb_json_decode) d.addCallback(cb_attach_headers, headers=response.headers) return d
def render_response(response): """Render the HTTPS response received.""" def eb_catch_partial(failure): # Twisted is raising PartialDownloadError because the responses # do not contain a Content-Length header. Since every response # holds the whole body we just take the result. failure.trap(PartialDownloadError) if int(failure.value.status) == HTTPStatus.OK: return failure.value.response else: return failure # Error out if the response has a status code of 400 or above. if response.code >= int(HTTPStatus.BAD_REQUEST): # if there was no trailing slash, retry with a trailing slash # because of varying requirements of BMC manufacturers if response.code == HTTPStatus.NOT_FOUND and uri[-1] != b"/": d = agent.request( method, uri + b"/", headers=headers, bodyProducer=bodyProducer, ) else: raise PowerActionError( "Request failed with response status code: " "%s." % response.code) d = readBody(response) d.addErrback(eb_catch_partial) return d
def _query_outlet_state( self, outlet_id=None, power_user=None, power_pass=None, power_address=None, **extra ): """Query DLI outlet power state. Sample snippet of query output from DLI: ... <!-- function reg() { window.open('http://www.digital-loggers.com/reg.html?SN=LPC751740'); } //--> </script> </head> <!-- state=02 lock=00 --> <body alink="#0000FF" vlink="#0000FF"> <FONT FACE="Arial, Helvetica, Sans-Serif"> ... """ try: url = "http://%s:%s@%s/index.htm" % ( power_user, power_pass, power_address, ) # --auth-no-challenge: send Basic HTTP authentication # information without first waiting for the server's challenge. wget_output = call_and_check( ["wget", "--auth-no-challenge", "-qO-", url], env=get_env_with_locale(), ) wget_output = wget_output.decode("utf-8") match = re.search("<!-- state=([0-9a-fA-F]+)", wget_output) if match is None: raise PowerError( "Unable to extract power state for outlet %s from " "wget output: %s" % (outlet_id, wget_output) ) else: state = match.group(1) # state is a bitmap of the DLI's oulet states, where bit 0 # corresponds to oulet 1's power state, bit 1 corresponds to # outlet 2's power state, etc., encoded as hexadecimal. if (int(state, 16) & (1 << int(outlet_id) - 1)) > 0: return "on" else: return "off" except ExternalProcessError as e: raise PowerActionError( "Failed to power query outlet %s: %s" % (outlet_id, e.output_as_unicode) )
def run_process(self, command): """Run SNMP command in subprocess.""" proc = Popen( command.split(), stdout=PIPE, stderr=PIPE, env=get_env_with_locale()) stdout, stderr = proc.communicate() stdout = stdout.decode("utf-8") stderr = stderr.decode("utf-8") if proc.returncode != 0: raise PowerActionError( "APC Power Driver external process error for command %s: %s" % (command, stderr)) match = re.search("INTEGER:\s*([1-2])", stdout) if match is None: raise PowerActionError( "APC Power Driver unable to extract outlet power state" " from: %s" % stdout) else: return match.group(1)
def power_off(self, system_id, context): """Power off MSCM node.""" try: # Power node off self.run_mscm_command( "set node power off force %s" % context["node_id"], **context) except PowerConnError as e: raise PowerActionError( "MSCM Power Driver unable to power off node %s: %s" % (context["node_id"], e))
def power_off(self, system_id, context): """Power off MicrosoftOCS blade.""" try: # Power off blade self.get("SetBladeOff", context, ["bladeid=%s" % context["blade_id"]]) except PowerConnError as e: raise PowerActionError( "MicrosoftOCS Power Driver unable to power off blade_id %s: %s" % (context["blade_id"], e))
def render_response(response): """Render the HTTPS response received.""" def eb_catch_partial(failure): # Twisted is raising PartialDownloadError because the responses # do not contain a Content-Length header. Since every response # holds the whole body we just take the result. failure.trap(PartialDownloadError) if int(failure.value.status) == HTTPStatus.OK: return failure.value.response else: return failure def cb_json_decode(data): data = data.decode("utf-8") # Only decode non-empty response bodies. if data: # occasionally invalid json is returned. provide a clear # error in that case try: return json.loads(data) except ValueError as error: raise PowerActionError( "Redfish request failed from a JSON parse error:" " %s." % error ) def cb_attach_headers(data, headers): return data, headers # Error out if the response has a status code of 400 or above. if response.code >= int(HTTPStatus.BAD_REQUEST): # if there was no trailing slash, retry with a trailing slash # because of varying requirements of BMC manufacturers if ( response.code == HTTPStatus.NOT_FOUND and uri.decode("utf-8")[-1] != "/" ): d = agent.request( method, uri + "/".encode("utf-8"), headers=headers, bodyProducer=bodyProducer, ) else: raise PowerActionError( "Redfish request failed with response status code:" " %s." % response.code ) d = readBody(response) d.addErrback(eb_catch_partial) d.addCallback(cb_json_decode) d.addCallback(cb_attach_headers, headers=response.headers) return d
def power_off(self, system_id, context): """Power off HMC lpar.""" try: # Power lpar off self.run_hmc_command( "chsysstate -r lpar -m %s -o shutdown -n %s --immed" % (context['server_name'], context['lpar']), **context) except PowerConnError as e: raise PowerActionError( "HMC Power Driver unable to power off lpar %s: %s" % (context['lpar'], e))
def process_vms(data): extra_headers, response_data = data vms = json.loads(response_data)["data"] if not vms: raise PowerActionError( "No VMs returned! Are permissions set correctly?" ) for vm in vms: if prefix_filter and not vm["name"].startswith(prefix_filter): continue # Proxmox doesn't have an easy way to get the MAC address, it # includes it with a bunch of other data in the config. vm_config_data = yield proxmox._webhook_request( b"GET", proxmox._get_url( context, f"nodes/{vm['node']}/{vm['type']}/{vm['vmid']}/config", ), proxmox._make_auth_headers("", {}, extra_headers), verify_ssl, ) macs = [ mac[0] for mac in mac_regex.findall(vm_config_data.decode()) ] system_id = yield create_node( macs, "amd64", "proxmox", {"power_vm_name": vm["vmid"], **context}, domain, hostname=vm["name"].replace(" ", "-"), ) # If the system_id is None an error occured when creating the machine. # Most likely the error is the node already exists. if system_id is None: continue if vm["status"] != "stopped": yield proxmox._webhook_request( b"POST", proxmox._get_url( context, f"nodes/{vm['node']}/{vm['type']}/{vm['vmid']}/" "status/stop", ), proxmox._make_auth_headers(system_id, {}, extra_headers), context.get("power_verify_ssl") == SSL_INSECURE_YES, ) if accept_all: yield commission_node(system_id, user)
def cb_json_decode(data): data = data.decode("utf-8") # Only decode non-empty response bodies. if data: # occasionally invalid json is returned. provide a clear # error in that case try: return json.loads(data) except ValueError as error: raise PowerActionError( "Redfish request failed from a JSON parse error:" " %s." % error)
def wsman_power_off(self, ip_address, power_pass): """Power off the node via wsman.""" # Issue the wsman command to change power state. self._issue_wsman_command("off", ip_address, power_pass) # Check power state several times. It usually takes a second or # two to get the correct state. for _ in range(10): if self.wsman_query_state(ip_address, power_pass) == "off": return # Success. Machine is off. else: sleep(1) raise PowerActionError("Machine is not powering off. Giving up.")
def wsman_power_on(self, ip_address, power_pass, restart=False): """Power on the node via wsman.""" power_command = "restart" if restart else "on" self._set_pxe_boot(ip_address, power_pass) self._issue_wsman_command(power_command, ip_address, power_pass) # Check power state several times. It usually takes a second or # two to get the correct state. for _ in range(10): if self.wsman_query_state(ip_address, power_pass) == "on": return # Success. Machine is on. sleep(1) raise PowerActionError("Machine is not powering on. Giving up.")
def amttool_power_on(self, ip_address, power_pass, amttool_boot_mode): """Power on the node via amttool.""" # Try several times. Power commands often fail the first time. for _ in range(10): # Issue the AMT command; amttool will prompt for confirmation. self._issue_amttool_command( 'powerup', ip_address, power_pass, amttool_boot_mode=amttool_boot_mode, stdin=b'yes') if self.amttool_query_state(ip_address, power_pass) == 'on': return sleep(1) raise PowerActionError("Machine is not powering on. Giving up.")
def amttool_power_off(self, ip_address, power_pass): """Power off the node via amttool.""" # Try several times. Power commands often fail the first time. for _ in range(10): if self.amttool_query_state(ip_address, power_pass) == 'off': # Success. Machine is off. return # Issue the AMT command; amttool will prompt for confirmation. self._issue_amttool_command( 'powerdown', ip_address, power_pass, stdin=b'yes') sleep(1) raise PowerActionError("Machine is not powering off. Giving up.")
def power_query(self, system_id, context): """Power query APC outlet.""" power_state = self.run_process( 'snmpget ' + COMMON_ARGS % (context['power_address'], context['node_outlet'])) if power_state == APCState.OFF: return 'off' elif power_state == APCState.ON: return 'on' else: raise PowerActionError( "APC Power Driver retrieved unknown power state: %r" % power_state)
def _run( self, command: tuple, power_pass: str, stdin: bytes=None) -> bytes: """Run a subprocess with stdin.""" env = self._get_amt_environment(power_pass) process = Popen( command, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env) stdout, stderr = process.communicate(stdin) if process.returncode != 0: raise PowerActionError( "Failed to run command: %s with error: %s" % ( command, stderr.decode("utf-8", "replace"))) return stdout
def wsman_query_state(self, ip_address, power_pass): """Ask for node's power state: 'on' or 'off', via wsman.""" # Retry the state if it fails because it often fails the first time. output = None for _ in range(10): output = self._issue_wsman_command('query', ip_address, power_pass) if output is not None and len(output) > 0: break # Wait 1 second between retries. AMT controllers are generally # very light and may not be comfortable with more frequent # queries. sleep(1) if output is None: raise PowerActionError("wsman power querying failed.") else: state = self.get_power_state(output) # There are a LOT of possible power states # 1: Other 9: Power Cycle (Off-Hard) # 2: On 10: Master Bus Reset # 3: Sleep - Light 11: Diagnostic Interrupt (NMI) # 4: Sleep - Deep 12: Off - Soft Graceful # 5: Power Cycle (Off - Soft) 13: Off - Hard Graceful # 6: Off - Hard 14: Master Bus Reset Graceful # 7: Hibernate (Off - Soft) 15: Power Cycle (Off-Soft Graceful) # 8: Off - Soft 16: Power Cycle (Off-Hard Graceful) # 17: Diagnostic Interrupt (INIT) # These are all power states that indicate that the system is # either ON or will resume function in an ON or Powered Up # state (e.g. being power cycled currently) if state in ( '2', '3', '4', '5', '7', '9', '10', '14', '15', '16'): return 'on' elif state in ('6', '8', '12', '13'): return 'off' else: raise PowerActionError( "Got unknown power state from node: %s" % state)
def power_on(self, system_id, context): """Power on HMC lpar.""" if self.power_query(system_id, context) in HMCState.ON: self.power_off(system_id, context) try: # Power lpar on self.run_hmc_command( "chsysstate -r lpar -m %s -o on -n %s --bootstring network-all" % (context['server_name'], context['lpar']), **context) except PowerConnError as e: raise PowerActionError( "HMC Power Driver unable to power on lpar %s: %s" % (context['lpar'], e))
def power_query(self, system_id, context): """Power query APC outlet.""" power_state = self.run_process( "snmpget", *_get_common_args(context["power_address"], context["node_outlet"]), ) if power_state == APCState.OFF: return "off" elif power_state == APCState.ON: return "on" else: raise PowerActionError( "APC Power Driver retrieved unknown power state: %r" % power_state)