class MoonshotIPMIPowerDriver(PowerDriver): name = 'moonshot' description = "HP Moonshot - iLO4 (IPMI)" settings = [ make_setting_field('power_address', "Power address", required=True), make_setting_field('power_user', "Power user"), make_setting_field('power_pass', "Power password", field_type='password'), make_setting_field('power_hwaddress', "Power hardware address", scope=SETTING_SCOPE.NODE, required=True), ] ip_extractor = make_ip_extractor('power_address') def detect_missing_packages(self): if not shell.has_command_available('ipmitool'): return ['ipmitool'] return [] def _issue_ipmitool_command(self, power_change, ipmitool=None, 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) def power_on(self, system_id, context): self._issue_ipmitool_command('pxe', **context) self._issue_ipmitool_command('on', **context) def power_off(self, system_id, context): self._issue_ipmitool_command('off', **context) def power_query(self, system_id, context): return self._issue_ipmitool_command('status', **context)
class VMwarePowerDriver(PowerDriver): name = 'vmware' chassis = True description = "VMware" settings = [ make_setting_field('power_vm_name', "VM Name (if UUID unknown)", required=False, scope=SETTING_SCOPE.NODE), make_setting_field('power_uuid', "VM UUID (if known)", required=False, scope=SETTING_SCOPE.NODE), make_setting_field('power_address', "VMware hostname", required=True), make_setting_field('power_user', "VMware username", required=True), make_setting_field('power_pass', "VMware password", field_type='password', required=True), make_setting_field('power_port', "VMware API port (optional)", required=False), make_setting_field('power_protocol', "VMware API protocol (optional)", required=False), ] ip_extractor = make_ip_extractor('power_address') def detect_missing_packages(self): if not vmware.try_pyvmomi_import(): return ["python3-pyvmomi"] return [] def power_on(self, system_id, context): """Power on VMware node.""" power_change = 'on' host, username, password, vm_name, uuid, port, protocol = ( extract_vmware_parameters(context)) power_control_vmware(host, username, password, vm_name, uuid, power_change, port, protocol) def power_off(self, system_id, context): """Power off VMware node.""" power_change = 'off' host, username, password, vm_name, uuid, port, protocol = ( extract_vmware_parameters(context)) power_control_vmware(host, username, password, vm_name, uuid, power_change, port, protocol) def power_query(self, system_id, context): """Power query VMware node.""" host, username, password, vm_name, uuid, port, protocol = ( extract_vmware_parameters(context)) return power_query_vmware(host, username, password, vm_name, uuid, port, protocol)
class HS300PowerDriver(PowerDriver): name = 'hs300' chassis = True description = "HS300" settings = [ make_setting_field('power_address', "IP address", required=True), make_setting_field('outlet_id', "Outlet ID, 1-6", scope=SETTING_SCOPE.NODE, required=True), make_setting_field('min_power', "Min ON power, W (optional)", scope=SETTING_SCOPE.NODE, required=False), ] ip_extractor = make_ip_extractor('power_address') def detect_missing_packages(self): return [] def power_on(self, system_id, context): self._set_outlet_state(1, **context) def power_off(self, system_id, context): self._set_outlet_state(0, **context) def power_query(self, system_id, context): return self._query_outlet_state(**context) def _set_outlet_state(self, state, power_address=None, outlet_id=None, **extra): client = HS300(power_address) client.set_relay_state(int(outlet_id) - 1, state) def _query_outlet_state(self, power_address=None, outlet_id=None, min_power=None, **extra): client = HS300(power_address) if min_power is not None: min_power = float(min_power) if client.get_relay_state(int(outlet_id) - 1, min_power) == 0: return "off" return "on"
class VirshPowerDriver(PowerDriver): name = "virsh" chassis = True description = "Virsh (virtual systems)" settings = [ make_setting_field("power_address", "Power address", required=True), make_setting_field( "power_id", "Power ID", scope=SETTING_SCOPE.NODE, required=True ), make_setting_field( "power_pass", "Power password (optional)", required=False, field_type="password", ), ] ip_extractor = make_ip_extractor( "power_address", IP_EXTRACTOR_PATTERNS.URL ) def detect_missing_packages(self): missing_packages = set() for binary, package in REQUIRED_PACKAGES: if not shell.has_command_available(binary): missing_packages.add(package) return list(missing_packages) def power_on(self, system_id, context): """Power on Virsh node.""" power_change = "on" poweraddr, machine, password = extract_virsh_parameters(context) power_control_virsh(poweraddr, machine, power_change, password) def power_off(self, system_id, context): """Power off Virsh node.""" power_change = "off" poweraddr, machine, password = extract_virsh_parameters(context) power_control_virsh(poweraddr, machine, power_change, password) def power_query(self, system_id, context): """Power query Virsh node.""" poweraddr, machine, password = extract_virsh_parameters(context) return power_state_virsh(poweraddr, machine, password)
class UCSMPowerDriver(PowerDriver): name = "ucsm" chassis = True can_probe = True can_set_boot_order = False description = "Cisco UCS Manager" settings = [ make_setting_field("uuid", "Server UUID", scope=SETTING_SCOPE.NODE, required=True), make_setting_field("power_address", "URL for XML API", required=True), make_setting_field("power_user", "API user"), make_setting_field("power_pass", "API password", field_type="password"), ] ip_extractor = make_ip_extractor("power_address", IP_EXTRACTOR_PATTERNS.URL) def detect_missing_packages(self): # uses urllib2 http client - nothing to look for! return [] def power_on(self, system_id, context): """Power on UCSM node.""" url, username, password, uuid = extract_ucsm_parameters(context) power_control_ucsm(url, username, password, uuid, maas_power_mode="on") def power_off(self, system_id, context): """Power off UCSM node.""" url, username, password, uuid = extract_ucsm_parameters(context) power_control_ucsm(url, username, password, uuid, maas_power_mode="off") def power_query(self, system_id, context): """Power query UCSM node.""" url, username, password, uuid = extract_ucsm_parameters(context) return power_state_ucsm(url, username, password, uuid)
class AMTPowerDriver(PowerDriver): name = "amt" chassis = False can_probe = False description = "Intel AMT" settings = [ make_setting_field("power_pass", "Power password", field_type="password"), make_setting_field("power_address", "Power address", required=True), ] ip_extractor = make_ip_extractor("power_address") def detect_missing_packages(self): missing_packages = [] for binary, package in REQUIRED_PACKAGES: if not shell.has_command_available(binary): missing_packages.append(package) return missing_packages @typed def _render_wsman_state_xml(self, power_change) -> bytes: """Render wsman state XML.""" wsman_state_filename = join(dirname(__file__), "amt.wsman-state.xml") wsman_state_ns = { "p": ("http://schemas.dmtf.org/wbem/wscim/1/cim-schema" "/2/CIM_PowerManagementService") } tree = etree.parse(wsman_state_filename) [ps] = tree.xpath("//p:PowerState", namespaces=wsman_state_ns) power_states = {"on": "2", "off": "8", "restart": "10"} ps.text = power_states[power_change] return etree.tostring(tree) @typed def _parse_multiple_xml_docs(self, xml: bytes): """Parse multiple XML documents. Each document must commence with an XML document declaration, i.e. <?xml ... Works around a weird decision in `wsman` where it returns multiple XML documents in a single stream. """ xmldecl = re.compile(b"<[?]xml\\s") xmldecls = xmldecl.finditer(xml) starts = [match.start() for match in xmldecls] ends = starts[1:] + [len(xml)] frags = (xml[start:end] for start, end in zip(starts, ends)) return (etree.fromstring(frag) for frag in frags) @typed def get_power_state(self, xml: bytes) -> str: """Get PowerState text from XML.""" namespaces = { "h": ("http://schemas.dmtf.org/wbem/wscim/1/cim-schema" "/2/CIM_AssociatedPowerManagementService") } state = next( chain.from_iterable( doc.xpath("//h:PowerState/text()", namespaces=namespaces) for doc in self._parse_multiple_xml_docs(xml))) return state def _set_pxe_boot(self, ip_address, power_pass): """Set to PXE for next boot.""" wsman_pxe_options = { "ChangeBootOrder": ( join(dirname(__file__), "amt.wsman-pxe.xml"), ("http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/" 'CIM_BootConfigSetting?InstanceID="Intel(r) ' 'AMT: Boot Configuration 0"'), ), "SetBootConfigRole": ( join(dirname(__file__), "amt.wsman-boot-config.xml"), ("http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/" "CIM_BootService?SystemCreationClassName=" '"CIM_ComputerSystem"&SystemName="Intel(r) AMT"' '&CreationClassName="CIM_BootService"&Name="Intel(r)' ' AMT Boot Service"'), ), } wsman_opts = ( "--port", "16992", "--hostname", ip_address, "--username", "admin", "--password", power_pass, "--noverifypeer", "--noverifyhost", ) # Change boot order to PXE and enable boot config request for method, (schema_file, schema_uri) in wsman_pxe_options.items(): with open(schema_file, "rb") as fd: wsman_opts += ("--input", "-") action = ("invoke", "--method", method, schema_uri) command = ("wsman", ) + wsman_opts + action self._run(command, power_pass, stdin=fd.read()) @typed def _run(self, command: tuple, power_pass: str, stdin: bytes = None) -> bytes: """Run a subprocess with stdin.""" result = shell.run_command( *command, stdin=stdin, extra_environ={"AMT_PASSWORD": power_pass}, decode=False, ) if result.returncode != 0: raise PowerActionError( "Failed to run command: %s with error: %s" % (command, result.stderr.decode("utf-8", "replace"))) return result.stdout @typed def _issue_amttool_command( self, cmd: str, ip_address: str, power_pass: str, amttool_boot_mode=None, stdin=None, ) -> bytes: """Perform a command using amttool.""" command = ("amttool", ip_address, cmd) if cmd in ("power-cycle", "powerup"): command += (amttool_boot_mode, ) return self._run(command, power_pass, stdin=stdin) @typed def _issue_wsman_command(self, power_change: str, ip_address: str, power_pass: str) -> bytes: """Perform a command using wsman.""" wsman_power_schema_uri = ( "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/" "CIM_PowerManagementService?SystemCreationClassName=" '"CIM_ComputerSystem"&SystemName="Intel(r) AMT"' '&CreationClassName="CIM_PowerManagementService"&Name=' '"Intel(r) AMT Power Management Service"') wsman_query_schema_uri = ( "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/" "CIM_AssociatedPowerManagementService") wsman_opts = ( "--port", "16992", "--hostname", ip_address, "--username", "admin", "--password", power_pass, "--noverifypeer", "--noverifyhost", ) if power_change in ("on", "off", "restart"): stdin = self._render_wsman_state_xml(power_change) wsman_opts += ("--input", "-") action = ( "invoke", "--method", "RequestPowerStateChange", wsman_power_schema_uri, ) command = ("wsman", ) + wsman_opts + action elif power_change == "query": stdin = None # No input for query wsman_opts += ("--optimize", "--encoding", "utf-8") action = ("enumerate", wsman_query_schema_uri) command = ("wsman", ) + wsman_opts + action return self._run(command, power_pass, stdin=stdin) 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 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. for _ in range(10): output = self._issue_wsman_command("query", 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("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 amttool_restart(self, ip_address, power_pass, amttool_boot_mode): """Restart the node via amttool.""" self._issue_amttool_command( "power_cycle", ip_address, power_pass, amttool_boot_mode=amttool_boot_mode, stdin=b"yes", ) 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 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_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 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 _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 result = shell.run_command( "wsman", "identify", "--port", "16992", "--hostname", ip_address, "--username", "admin", "--password", power_pass, ) if not result.stdout: for error, error_info in AMT_ERRORS.items(): if error in result.stderr: raise error_info.get("exception")( error_info.get("message")) raise PowerConnError( f"Unable to retrieve AMT version: {result.stderr}") else: match = re.search(r"ProductVersion>AMT\s*([0-9]+)", result.stdout) if match is None: raise PowerActionError("Unable to extract AMT version from " f"amttool output: {result.stdout}") else: version = match.group(1) if int(version) > 8: return "wsman" else: return "amttool" def _get_amttool_boot_mode(self, boot_mode): """Set amttool boot mode.""" # boot_mode tells us whether we're pxe booting or local booting. # For local booting, the argument to amttool must be empty # (NOT 'hd', it doesn't work!). if boot_mode == "local": return "" else: return boot_mode def _get_ip_address(self, power_address, ip_address): """Get the IP address of the AMT BMC.""" # The user specified power_address overrides any automatically # determined ip_address. if is_power_parameter_set( power_address) and not is_power_parameter_set(ip_address): return power_address elif is_power_parameter_set(ip_address): return ip_address else: raise PowerSettingError( "No IP address provided. " "Please update BMC configuration and try again.") def power_on(self, system_id, context): """Power on AMT node.""" ip_address = self._get_ip_address(context.get("power_address"), context.get("ip_address")) power_pass = context.get("power_pass") amt_command = self._get_amt_command(ip_address, power_pass) if amt_command == "amttool": amttool_boot_mode = self._get_amttool_boot_mode( context.get("boot_mode")) if self.amttool_query_state(ip_address, power_pass) == "on": self.amttool_restart(ip_address, power_pass, amttool_boot_mode) else: self.amttool_power_on(ip_address, power_pass, amttool_boot_mode) elif amt_command == "wsman": if self.wsman_query_state(ip_address, power_pass) == "on": self.wsman_power_on(ip_address, power_pass, restart=True) else: self.wsman_power_on(ip_address, power_pass) def power_off(self, system_id, context): """Power off AMT node.""" ip_address = self._get_ip_address(context.get("power_address"), context.get("ip_address")) power_pass = context.get("power_pass") amt_command = self._get_amt_command(ip_address, power_pass) if amt_command == "amttool": if self.amttool_query_state(ip_address, power_pass) != "off": self.amttool_power_off(ip_address, power_pass) elif amt_command == "wsman": if self.wsman_query_state(ip_address, power_pass) != "off": self.wsman_power_off(ip_address, power_pass) def power_query(self, system_id, context): """Power query AMT node.""" ip_address = self._get_ip_address(context.get("power_address"), context.get("ip_address")) power_pass = context.get("power_pass") amt_command = self._get_amt_command(ip_address, power_pass) if amt_command == "amttool": return self.amttool_query_state(ip_address, power_pass) elif amt_command == "wsman": return self.wsman_query_state(ip_address, power_pass)
class SeaMicroPowerDriver(PowerDriver): name = "sm15k" chassis = True can_probe = True can_set_boot_order = False description = "SeaMicro 15000" settings = [ make_setting_field("system_id", "System ID", scope=SETTING_SCOPE.NODE, required=True), make_setting_field("power_address", "Power address", required=True), make_setting_field("power_user", "Power user"), make_setting_field("power_pass", "Power password", field_type="password"), make_setting_field( "power_control", "Power control type", field_type="choice", choices=SM15K_POWER_CONTROL_CHOICES, default="ipmi", required=True, ), ] ip_extractor = make_ip_extractor("power_address") def detect_missing_packages(self): if not shell.has_command_available("ipmitool"): return ["ipmitool"] return [] 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 _power(self, power_change, context): """Power SeaMicro node.""" ( ip, username, password, server_id, power_control, ) = extract_seamicro_parameters(context) if power_control == "ipmi": self._power_control_seamicro15k_ipmi(ip, username, password, server_id, power_change=power_change) elif power_control == "restapi": power_control_seamicro15k_v09(ip, username, password, server_id, power_change=power_change) elif power_control == "restapi2": power_control_seamicro15k_v2(ip, username, password, server_id, power_change=power_change) def power_on(self, system_id, context): """Power on SeaMicro node.""" self._power("on", context) def power_off(self, system_id, context): """Power off SeaMicro node.""" self._power("off", context) def power_query(self, system_id, context): """Power query SeaMicro node.""" # Query the state. # Only supported by REST v2. ( ip, username, password, server_id, power_control, ) = extract_seamicro_parameters(context) if power_control == "restapi2": return power_query_seamicro15k_v2(ip, username, password, server_id) else: return "unknown"
class ProxmoxPowerDriver(PowerDriver): name = 'proxmox' chassis = True description = "Proxmox (virtual systems)" settings = [ make_setting_field( 'power_vm_name', "VM id or name", required=True, scope=SETTING_SCOPE.NODE), make_setting_field('power_address', "Proxmox host name or ip", required=True), make_setting_field('power_user', "Proxmox username (user@realm)", required=True), make_setting_field( 'power_pass', "Proxmox password", field_type='password', required=True), make_setting_field('power_ssl_validate', "Validate ssl", field_type='choice', required=True, choices=PROXMOX_VALIDATE_SSL_CHOICES, default=PROXMOX_NO), ] ip_extractor = make_ip_extractor('power_address') def detect_missing_packages(self): if not PROXMOXER_IMPORTED: return ["python3-proxmoxer"] return [] def power_on(self, system_id, context): """Power on Proxmox node.""" vm=self.__proxmox_login(system_id,context) vm.status.start.post(); def power_off(self, system_id, context): """Power off Proxmox node.""" vm=self.__proxmox_login(system_id,context) vm.status.stop.post(); def power_query(self, system_id, context): """Power query Proxmox node.""" vm=self.__proxmox_login(system_id,context) ncd=vm.status.current.get() if ncd['status'] == 'running': return "on" else: return "off" def __proxmox_login(self,system_id,context): """Login to proxmox server.""" api_host = context.get('power_address') api_user = context.get('power_user') api_password = context.get('power_pass') vm_id = context.get('power_vm_name') api_ssl_val = (context.get('power_validate_ssl')==PROXMOX_YES) try: api = ProxmoxAPI(api_host, user=api_user, password=api_password, verify_ssl=api_ssl_val) con_vm=None for vm in api.cluster.resources.get(type="vm"): if (str(vm['vmid'])==vm_id) or (vm['name']==vm_id): con_vm=vm break except Exception: raise ProxmoxError( "Can't connect to proxmox cluster %s" % (api_host)) if con_vm is None: """vm not found""" raise ProxmoxError( "Virtual machine %s not found on proxmox cluster %s" % (vm_id, api_host)) #extract node object vm_obj=getattr(getattr(getattr(api.nodes,con_vm['node']), con_vm['type']), str(con_vm['vmid'])) return vm_obj
class NovaPowerDriver(PowerDriver): name = 'nova' chassis = True description = "OpenStack Nova" settings = [ make_setting_field('nova_id', "Host UUID", required=True, scope=SETTING_SCOPE.NODE), make_setting_field('os_tenantname', "Tenant name", required=True), make_setting_field('os_username', "Username", required=True), make_setting_field('os_password', "Password", field_type='password', required=True), make_setting_field('os_authurl', "Auth URL", required=True), # Since OpenStack Queens Keystone supports ONLY v3 auth # hence parameters below are required to work with that version # but old one are left as an option for backward compatibility # with prev OpenStack versions make_setting_field('user_domain_name', "User Domain name"), make_setting_field('project_domain_name', "Project Domain name"), ] ip_extractor = make_ip_extractor('os_authurl', IP_EXTRACTOR_PATTERNS.URL) nova_api = None def power_control_nova(self, power_change, nova_id=None, os_tenantname=None, os_username=None, os_password=None, os_authurl=None, **extra): """Control power of nova instances.""" if not self.try_novaapi_import(): raise PowerToolError("Missing the python3-novaclient package.") if user_domain_name != "" and project_domain_name != "": nova = self.nova_api.Client( 2, username=os_username, password=os_password, project_name=os_tenantname, auth_url=os_authurl, user_domain_name=user_domain_name, project_domain_name=project_domain_name) else: nova = self.nova_api.Client(2, os_username, os_password, os_tenantname, os_authurl) try: urllib.request.urlopen(os_authurl) except urllib.error.URLError: raise PowerError('%s: URL error' % os_authurl) try: nova.authenticate() except self.nova_api.exceptions.Unauthorized: raise PowerAuthError('Failed to authenticate with OpenStack') try: pwr_stateStr = "OS-EXT-STS:power_state" tsk_stateStr = "OS-EXT-STS:task_state" vm_stateStr = "OS-EXT-STS:vm_state" power_state = getattr(nova.servers.get(nova_id), pwr_stateStr) task_state = getattr(nova.servers.get(nova_id), tsk_stateStr) vm_state = getattr(nova.servers.get(nova_id), vm_stateStr) except self.nova_api.exceptions.NotFound: raise PowerError('%s: Instance id not found' % nova_id) if power_state == NovaPowerState.NOSTATE: raise PowerFatalError('%s: Failed to get power state' % nova_id) if power_state == NovaPowerState.RUNNING: if (power_change == 'off' and task_state != 'powering-off' and vm_state != 'stopped'): nova.servers.get(nova_id).stop() elif power_change == 'query': return 'on' if power_state == NovaPowerState.SHUTDOWN: if (power_change == 'on' and task_state != 'powering-on' and vm_state != 'active'): nova.servers.get(nova_id).start() elif power_change == 'query': return 'off' def try_novaapi_import(self): """Attempt to import the novaclient API. This API is provided by the python3-novaclient package; if it doesn't work out, we need to notify the user so they can install it. """ invalidate_caches() try: if self.nova_api is None: self.nova_api = import_module('novaclient.client') except ImportError: return False else: return True def detect_missing_packages(self): """Detect missing package python3-novaclient.""" if not self.try_novaapi_import(): return ["python3-novaclient"] return [] def power_on(self, system_id, context): """Power on nova instance.""" self.power_control_nova('on', **context) def power_off(self, system_id, context): """Power off nova instance.""" self.power_control_nova('off', **context) def power_query(self, system_id, context): """Power query nova instance.""" return self.power_control_nova('query', **context)
class NovaPowerDriver(PowerDriver): name = "nova" chassis = True can_probe = False can_set_boot_order = False description = "OpenStack Nova" settings = [ make_setting_field( "nova_id", "Host UUID", required=True, scope=SETTING_SCOPE.NODE ), make_setting_field("os_tenantname", "Tenant name", required=True), make_setting_field("os_username", "Username", required=True), make_setting_field( "os_password", "Password", field_type="password", required=True ), make_setting_field("os_authurl", "Auth URL", required=True), ] ip_extractor = make_ip_extractor("os_authurl", IP_EXTRACTOR_PATTERNS.URL) nova_api = None def power_control_nova( self, power_change, nova_id=None, os_tenantname=None, os_username=None, os_password=None, os_authurl=None, **extra ): """Control power of nova instances.""" if not self.try_novaapi_import(): raise PowerToolError("Missing the python3-novaclient package.") nova = self.nova_api.Client( 2, os_username, os_password, os_tenantname, os_authurl ) try: urllib.request.urlopen(os_authurl) except urllib.error.URLError: raise PowerError("%s: URL error" % os_authurl) try: nova.authenticate() except self.nova_api.exceptions.Unauthorized: raise PowerAuthError("Failed to authenticate with OpenStack") try: pwr_stateStr = "OS-EXT-STS:power_state" tsk_stateStr = "OS-EXT-STS:task_state" vm_stateStr = "OS-EXT-STS:vm_state" power_state = getattr(nova.servers.get(nova_id), pwr_stateStr) task_state = getattr(nova.servers.get(nova_id), tsk_stateStr) vm_state = getattr(nova.servers.get(nova_id), vm_stateStr) except self.nova_api.exceptions.NotFound: raise PowerError("%s: Instance id not found" % nova_id) if power_state == NovaPowerState.NOSTATE: raise PowerFatalError("%s: Failed to get power state" % nova_id) if power_state == NovaPowerState.RUNNING: if ( power_change == "off" and task_state != "powering-off" and vm_state != "stopped" ): nova.servers.get(nova_id).stop() elif power_change == "query": return "on" if power_state == NovaPowerState.SHUTDOWN: if ( power_change == "on" and task_state != "powering-on" and vm_state != "active" ): nova.servers.get(nova_id).start() elif power_change == "query": return "off" def try_novaapi_import(self): """Attempt to import the novaclient API. This API is provided by the python3-novaclient package; if it doesn't work out, we need to notify the user so they can install it. """ invalidate_caches() try: if self.nova_api is None: self.nova_api = import_module("novaclient.client") except ImportError: return False else: return True def detect_missing_packages(self): """Detect missing package python3-novaclient.""" if not self.try_novaapi_import(): return ["python3-novaclient"] return [] def power_on(self, system_id, context): """Power on nova instance.""" self.power_control_nova("on", **context) def power_off(self, system_id, context): """Power off nova instance.""" self.power_control_nova("off", **context) def power_query(self, system_id, context): """Power query nova instance.""" return self.power_control_nova("query", **context)
class WedgePowerDriver(PowerDriver): name = 'wedge' chassis = False description = "Facebook's Wedge" settings = [ make_setting_field('power_address', "IP address", required=True), make_setting_field('power_user', "Power user"), make_setting_field( 'power_pass', "Power password", field_type='password'), ] ip_extractor = make_ip_extractor('power_address') def detect_missing_packages(self): # uses pure-python paramiko ssh client - nothing to look for! return [] def run_wedge_command(self, command, power_address=None, power_user=None, power_pass=None, **extra): """Run a single command and return unparsed text from stdout.""" try: ssh_client = SSHClient() ssh_client.set_missing_host_key_policy(AutoAddPolicy()) ssh_client.connect(power_address, username=power_user, password=power_pass) _, stdout, _ = ssh_client.exec_command(command) output = stdout.read().decode('utf-8').strip() except (SSHException, EOFError, SOCKETError) as e: raise PowerConnError( "Could not make SSH connection to Wedge for " "%s on %s - %s" % (power_user, power_address, e)) finally: ssh_client.close() return output def power_on(self, system_id, context): """Power on Wedge.""" try: self.run_wedge_command( "/usr/local/bin/wedge_power.sh on", **context) except PowerConnError: raise PowerActionError( "Wedge Power Driver unable to power on") 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 power_query(self, system_id, context): """Power query Wedge.""" try: power_state = self.run_wedge_command( "/usr/local/bin/wedge_power.sh status", **context) except PowerConnError: raise PowerActionError( "Wedge Power Driver unable to power query") else: if power_state in WedgeState.OFF: return 'off' elif power_state in WedgeState.ON: return 'on' else: raise PowerFatalError( "Wedge Power Driver retrieved unknown power response %s" % power_state)
class VirshPodDriver(PodDriver): name = 'virsh' description = "Virsh (virtual systems)" settings = [ make_setting_field( 'power_address', "Virsh address", required=True), make_setting_field( 'power_pass', "Virsh password (optional)", required=False, field_type='password'), make_setting_field( 'power_id', "Virsh VM ID", scope=SETTING_SCOPE.NODE, required=True), ] ip_extractor = make_ip_extractor( 'power_address', IP_EXTRACTOR_PATTERNS.URL) def detect_missing_packages(self): missing_packages = set() for binary, package in REQUIRED_PACKAGES: if not shell.has_command_available(binary): missing_packages.add(package) return list(missing_packages) @inlineCallbacks def power_control_virsh( self, power_address, power_id, power_change, power_pass=None, **kwargs): """Powers controls a VM using virsh.""" # Force password to None if blank, as the power control # script will send a blank password if one is not set. if power_pass == '': power_pass = None conn = VirshSSH() logged_in = yield deferToThread(conn.login, power_address, power_pass) if not logged_in: raise VirshError('Failed to login to virsh console.') state = yield deferToThread(conn.get_machine_state, power_id) if state is None: raise VirshError('%s: Failed to get power state' % power_id) if state == VirshVMState.OFF: if power_change == 'on': powered_on = yield deferToThread(conn.poweron, power_id) if powered_on is False: raise VirshError('%s: Failed to power on VM' % power_id) elif state == VirshVMState.ON: if power_change == 'off': powered_off = yield deferToThread(conn.poweroff, power_id) if powered_off is False: raise VirshError('%s: Failed to power off VM' % power_id) @inlineCallbacks def power_state_virsh( self, power_address, power_id, power_pass=None, **kwargs): """Return the power state for the VM using virsh.""" # Force password to None if blank, as the power control # script will send a blank password if one is not set. if power_pass == '': power_pass = None conn = VirshSSH() logged_in = yield deferToThread(conn.login, power_address, power_pass) if not logged_in: raise VirshError('Failed to login to virsh console.') state = yield deferToThread(conn.get_machine_state, power_id) if state is None: raise VirshError('Failed to get domain: %s' % power_id) try: return VM_STATE_TO_POWER_STATE[state] except KeyError: raise VirshError('Unknown state: %s' % state) @asynchronous def power_on(self, system_id, context): """Power on Virsh node.""" return self.power_control_virsh(power_change='on', **context) @asynchronous def power_off(self, system_id, context): """Power off Virsh node.""" return self.power_control_virsh(power_change='off', **context) @asynchronous def power_query(self, system_id, context): """Power query Virsh node.""" return self.power_state_virsh(**context) @inlineCallbacks def get_virsh_connection(self, context): """Connect and return the virsh connection.""" power_address = context.get('power_address') power_pass = context.get('power_pass') # Login to Virsh console. conn = VirshSSH() logged_in = yield deferToThread(conn.login, power_address, power_pass) if not logged_in: raise VirshError('Failed to login to virsh console.') return conn @inlineCallbacks def discover(self, system_id, context): """Discover all resources. Returns a defer to a DiscoveredPod object. """ conn = yield self.get_virsh_connection(context) # Discover pod resources. discovered_pod = yield deferToThread(conn.get_pod_resources) # Discovered pod hints. discovered_pod.hints = yield deferToThread(conn.get_pod_hints) # Discover VMs. machines = [] virtual_machines = yield deferToThread(conn.list_machines) for vm in virtual_machines: discovered_machine = yield deferToThread( conn.get_discovered_machine, vm) if discovered_machine is not None: discovered_machine.cpu_speed = discovered_pod.cpu_speed machines.append(discovered_machine) discovered_pod.machines = machines # Return the DiscoveredPod return discovered_pod @inlineCallbacks def compose(self, system_id, context, request): """Compose machine.""" conn = yield self.get_virsh_connection(context) created_machine = yield deferToThread(conn.create_domain, request) hints = yield deferToThread(conn.get_pod_hints) return created_machine, hints @inlineCallbacks def decompose(self, system_id, context): """Decompose machine.""" conn = yield self.get_virsh_connection(context) yield deferToThread(conn.delete_domain, context['power_id']) hints = yield deferToThread(conn.get_pod_hints) return hints
class IPMIPowerDriver(PowerDriver): name = 'ipmi' description = "IPMI" settings = [ make_setting_field( 'power_driver', "Power driver", field_type='choice', choices=IPMI_DRIVER_CHOICES, default=IPMI_DRIVER.LAN_2_0, required=True), make_setting_field('power_address', "IP address", required=True), make_setting_field('power_user', "Power user"), make_setting_field( 'power_pass', "Power password", field_type='password'), make_setting_field( 'mac_address', "Power MAC", scope=SETTING_SCOPE.NODE) ] ip_extractor = make_ip_extractor('power_address') wait_time = (4, 8, 16, 32) def detect_missing_packages(self): if not shell.has_command_available('ipmipower'): return ['freeipmi-tools'] return [] @staticmethod def _issue_ipmi_chassis_config_command( command, power_change, power_address): env = shell.select_c_utf8_locale() with NamedTemporaryFile("w+", encoding="utf-8") as tmp_config: # Write out the chassis configuration. tmp_config.write(IPMI_CONFIG) tmp_config.flush() # Use it when running the chassis config command. # XXX: Not using call_and_check here because we # need to check stderr. command = tuple(command) + ("--filename", tmp_config.name) process = Popen(command, stdout=PIPE, stderr=PIPE, env=env) _, stderr = process.communicate() stderr = stderr.decode("utf-8").strip() # XXX newell 2016-11-21 bug=1516065: Some IPMI hardware have timeout # issues when trying to set the boot order to PXE. We want to # continue and not raise an error here. ipmi_errors = { key: IPMI_ERRORS[key] for key in IPMI_ERRORS if IPMI_ERRORS[key]['exception'] == PowerAuthError } for error, error_info in ipmi_errors.items(): if error in stderr: raise error_info.get('exception')(error_info.get('message')) if process.returncode != 0: maaslog.warning( "Failed to change the boot order to PXE %s: %s" % ( power_address, stderr)) @staticmethod def _issue_ipmipower_command(command, power_change, power_address): env = shell.select_c_utf8_locale() command = tuple(command) # For consistency when testing. process = Popen(command, stdout=PIPE, stderr=PIPE, env=env) stdout, _ = process.communicate() stdout = stdout.decode("utf-8").strip() for error, error_info in IPMI_ERRORS.items(): # ipmipower dumps errors to stdout if error in stdout: raise error_info.get('exception')(error_info.get('message')) if process.returncode != 0: raise PowerError( "Failed to power %s %s: %s" % ( power_change, power_address, stdout)) match = re.search(":\s*(on|off)", stdout) return stdout if match is None else match.group(1) def _issue_ipmi_command( self, power_change, power_address=None, power_user=None, power_pass=None, power_driver=None, power_off_mode=None, ipmipower=None, ipmi_chassis_config=None, mac_address=None, **extra): """Issue command to ipmipower, for the given system.""" # This script deliberately does not check the current power state # before issuing the requested power command. See bug 1171418 for an # explanation. if (is_power_parameter_set(mac_address) and not is_power_parameter_set(power_address)): power_address = find_ip_via_arp(mac_address) # The `-W opensesspriv` workaround is required on many BMCs, and # should have no impact on BMCs that don't require it. # See https://bugs.launchpad.net/maas/+bug/1287964 ipmi_chassis_config_command = [ ipmi_chassis_config, '-W', 'opensesspriv'] ipmipower_command = [ ipmipower, '-W', 'opensesspriv'] # Arguments in common between chassis config and power control. See # https://launchpad.net/bugs/1053391 for details of modifying the # command for power_driver and power_user. common_args = [] if is_power_parameter_set(power_driver): common_args.extend(("--driver-type", power_driver)) common_args.extend(('-h', power_address)) if is_power_parameter_set(power_user): common_args.extend(("-u", power_user)) common_args.extend(('-p', power_pass)) # Update the chassis config and power commands. ipmi_chassis_config_command.extend(common_args) ipmi_chassis_config_command.append('--commit') ipmipower_command.extend(common_args) # Before changing state run the chassis config command. if power_change in ("on", "off"): self._issue_ipmi_chassis_config_command( ipmi_chassis_config_command, power_change, power_address) # Additional arguments for the power command. if power_change == 'on': ipmipower_command.append('--cycle') ipmipower_command.append('--on-if-off') elif power_change == 'off': if power_off_mode == 'soft': ipmipower_command.append('--soft') else: ipmipower_command.append('--off') elif power_change == 'query': ipmipower_command.append('--stat') # Update or query the power state. return self._issue_ipmipower_command( ipmipower_command, power_change, power_address) def power_on(self, system_id, context): self._issue_ipmi_command('on', **context) def power_off(self, system_id, context): self._issue_ipmi_command('off', **context) def power_query(self, system_id, context): return self._issue_ipmi_command('query', **context)
class IPMIPowerDriver(PowerDriver): name = "ipmi" chassis = False can_probe = False description = "IPMI" settings = [ make_setting_field( "power_driver", "Power driver", field_type="choice", choices=IPMI_DRIVER_CHOICES, default=IPMI_DRIVER.LAN_2_0, required=True, ), make_setting_field( "power_boot_type", "Power boot type", field_type="choice", choices=IPMI_BOOT_TYPE_CHOICES, default=IPMI_BOOT_TYPE.DEFAULT, required=False, ), make_setting_field("power_address", "IP address", required=True), make_setting_field("power_user", "Power user"), make_setting_field("power_pass", "Power password", field_type="password"), make_setting_field("k_g", "K_g BMC key", field_type="password"), make_setting_field( "cipher_suite_id", "Cipher Suite ID", field_type="choice", choices=IPMI_CIPHER_SUITE_ID_CHOICES, # freeipmi-tools defaults to 3, not all IPMI BMCs support 17. default="3", ), make_setting_field( "privilege_level", "Privilege Level", field_type="choice", choices=IPMI_PRIVILEGE_LEVEL_CHOICES, # All MAAS operations can be done as operator. default=IPMI_PRIVILEGE_LEVEL.OPERATOR.name, ), make_setting_field("mac_address", "Power MAC", scope=SETTING_SCOPE.NODE), ] ip_extractor = make_ip_extractor("power_address") wait_time = (4, 8, 16, 32) def detect_missing_packages(self): if not shell.has_command_available("ipmipower"): return ["freeipmi-tools"] return [] @staticmethod def _issue_ipmi_chassis_config_command(command, power_change, power_address, power_boot_type=None): with NamedTemporaryFile("w+", encoding="utf-8") as tmp_config: # Write out the chassis configuration. if (power_boot_type is None or power_boot_type == IPMI_BOOT_TYPE.DEFAULT): tmp_config.write(IPMI_CONFIG) else: tmp_config.write(IPMI_CONFIG_WITH_BOOT_TYPE % IPMI_BOOT_TYPE_MAPPING[power_boot_type]) tmp_config.flush() # Use it when running the chassis config command. # XXX: Not using call_and_check here because we # need to check stderr. command = tuple(command) + ("--filename", tmp_config.name) result = shell.run_command(*command) # XXX newell 2016-11-21 bug=1516065: Some IPMI hardware have timeout # issues when trying to set the boot order to PXE. We want to # continue and not raise an error here. ipmi_errors = { key: IPMI_ERRORS[key] for key in IPMI_ERRORS if IPMI_ERRORS[key]["exception"] == PowerAuthError } for error, error_info in ipmi_errors.items(): if error in result.stderr: raise error_info.get("exception")(error_info.get("message")) if result.returncode != 0: maaslog.warning("Failed to change the boot order to PXE %s: %s" % (power_address, result.stderr)) @staticmethod def _issue_ipmipower_command(command, power_change, power_address): result = shell.run_command(*command) for error, error_info in IPMI_ERRORS.items(): # ipmipower dumps errors to stdout if error in result.stdout: raise error_info.get("exception")(error_info.get("message")) if result.returncode != 0: raise PowerError("Failed to power %s %s: %s" % (power_change, power_address, result.stdout)) match = re.search(r":\s*(on|off)", result.stdout) return result.stdout if match is None else match.group(1) def _issue_ipmi_command(self, power_change, power_address=None, power_user=None, power_pass=None, power_driver=None, power_off_mode=None, mac_address=None, power_boot_type=None, k_g=None, cipher_suite_id=None, privilege_level=None, **extra): """Issue command to ipmipower, for the given system.""" # This script deliberately does not check the current power state # before issuing the requested power command. See bug 1171418 for an # explanation. if is_power_parameter_set( mac_address) and not is_power_parameter_set(power_address): power_address = find_ip_via_arp(mac_address) # The `-W opensesspriv` workaround is required on many BMCs, and # should have no impact on BMCs that don't require it. # See https://bugs.launchpad.net/maas/+bug/1287964 ipmi_chassis_config_command = [ "ipmi-chassis-config", "-W", "opensesspriv", ] ipmipower_command = [ "ipmipower", "-W", "opensesspriv", ] # Arguments in common between chassis config and power control. See # https://launchpad.net/bugs/1053391 for details of modifying the # command for power_driver and power_user. common_args = [] if is_power_parameter_set(power_driver): common_args.extend(("--driver-type", power_driver)) common_args.extend(("-h", power_address)) if is_power_parameter_set(power_user): common_args.extend(("-u", power_user)) common_args.extend(("-p", power_pass)) if is_power_parameter_set(k_g): common_args.extend(("-k", k_g)) if is_power_parameter_set(cipher_suite_id): common_args.extend(("-I", cipher_suite_id)) if is_power_parameter_set(privilege_level): common_args.extend(("-l", privilege_level)) else: # LP:1889788 - Default to communicate at operator level. common_args.extend(("-l", IPMI_PRIVILEGE_LEVEL.OPERATOR.name)) # Update the power commands with common args. ipmipower_command.extend(common_args) # Additional arguments for the power command. if power_change == "on": # Update the chassis config commands and call it just when # powering on the machine. ipmi_chassis_config_command.extend(common_args) ipmi_chassis_config_command.append("--commit") self._issue_ipmi_chassis_config_command( ipmi_chassis_config_command, power_change, power_address, power_boot_type, ) ipmipower_command.append("--cycle") ipmipower_command.append("--on-if-off") elif power_change == "off": if power_off_mode == "soft": ipmipower_command.append("--soft") else: ipmipower_command.append("--off") elif power_change == "query": ipmipower_command.append("--stat") # Update or query the power state. return self._issue_ipmipower_command(ipmipower_command, power_change, power_address) def power_on(self, system_id, context): self._issue_ipmi_command("on", **context) def power_off(self, system_id, context): self._issue_ipmi_command("off", **context) def power_query(self, system_id, context): return self._issue_ipmi_command("query", **context)
class SeaMicroPowerDriver(PowerDriver): name = 'sm15k' chassis = True description = "SeaMicro 15000" settings = [ make_setting_field('system_id', "System ID", scope=SETTING_SCOPE.NODE, required=True), make_setting_field('power_address', "Power address", required=True), make_setting_field('power_user', "Power user"), make_setting_field('power_pass', "Power password", field_type='password'), make_setting_field('power_control', "Power control type", field_type='choice', choices=SM15K_POWER_CONTROL_CHOICES, default='ipmi', required=True), ] ip_extractor = make_ip_extractor('power_address') def detect_missing_packages(self): if not shell.has_command_available('ipmitool'): return ['ipmitool'] return [] 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(self, power_change, context): """Power SeaMicro node.""" ip, username, password, server_id, power_control = ( extract_seamicro_parameters(context)) if power_control == 'ipmi': self._power_control_seamicro15k_ipmi(ip, username, password, server_id, power_change=power_change) elif power_control == 'restapi': power_control_seamicro15k_v09(ip, username, password, server_id, power_change=power_change) elif power_control == 'restapi2': power_control_seamicro15k_v2(ip, username, password, server_id, power_change=power_change) def power_on(self, system_id, context): """Power on SeaMicro node.""" self._power('on', context) def power_off(self, system_id, context): """Power off SeaMicro node.""" self._power('off', context) def power_query(self, system_id, context): """Power query SeaMicro node.""" # Query the state. # Only supported by REST v2. ip, username, password, server_id, power_control = ( extract_seamicro_parameters(context)) if power_control == 'restapi2': return power_query_seamicro15k_v2(ip, username, password, server_id) else: return 'unknown'
class LXDPodDriver(PodDriver): name = "lxd" chassis = True can_probe = False description = "LXD (virtual systems)" settings = [ make_setting_field("power_address", "LXD address", required=True), make_setting_field( "instance_name", "Instance name", scope=SETTING_SCOPE.NODE, required=True, ), make_setting_field( "password", "LXD password (optional)", required=False, field_type="password", ), ] ip_extractor = make_ip_extractor("power_address", IP_EXTRACTOR_PATTERNS.URL) def detect_missing_packages(self): # python3-pylxd is a required package # for maas and is installed by default. return [] @typed def get_url(self, context: dict): """Return url for the LXD host.""" power_address = context.get("power_address") url = urlparse(power_address) if not url.scheme: # When the scheme is not included in the power address # urlparse puts the url into path. url = url._replace(scheme="https", netloc="%s" % url.path, path="") if not url.port: if url.netloc: url = url._replace(netloc="%s:8443" % url.netloc) else: # Similar to above, we need to swap netloc and path. url = url._replace(netloc="%s:8443" % url.path, path="") return url.geturl() @typed @inlineCallbacks def get_client(self, pod_id: str, context: dict): """Connect pylxd client.""" endpoint = self.get_url(context) password = context.get("password") try: client = yield deferToThread( Client, endpoint=endpoint, cert=get_maas_cert_tuple(), verify=False, ) if not client.trusted: if password: yield deferToThread(client.authenticate, password) else: raise LXDPodError( f"Pod {pod_id}: Certificate is not trusted and no password was given." ) except ClientConnectionFailed: raise LXDPodError( f"Pod {pod_id}: Failed to connect to the LXD REST API.") return client @typed @inlineCallbacks def get_machine(self, pod_id: str, context: dict): """Retrieve LXD VM.""" client = yield self.get_client(pod_id, context) instance_name = context.get("instance_name") try: machine = yield deferToThread(client.virtual_machines.get, instance_name) except NotFound: raise LXDPodError( f"Pod {pod_id}: LXD VM {instance_name} not found.") return machine async def get_discovered_machine(self, client, machine, storage_pools, request=None): """Get the discovered machine.""" # Check the power state first. state = machine.status_code try: power_state = LXD_VM_POWER_STATE[state] except KeyError: maaslog.error( f"{machine.name}: Unknown power status code: {state}") power_state = "unknown" expanded_config = machine.expanded_config expanded_devices = machine.expanded_devices # Discover block devices. block_devices = [] for idx, device in enumerate(expanded_devices): # Block device. # When request is provided map the tags from the request block # devices to the discovered block devices. This ensures that # composed machine has the requested tags on the block device. tags = [] if (request is not None and expanded_devices[device]["type"] == "disk"): tags = request.block_devices[0].tags device_info = expanded_devices[device] if device_info["type"] == "disk": # When LXD creates a QEMU disk the serial is always # lxd_{device name}. The device_name is defined by # the LXD profile or when adding a device. This is # commonly "root" for the first disk. The model and # serial must be correctly defined here otherwise # MAAS will delete the disk created during composition # which results in losing the storage pool link. Without # the storage pool link MAAS can't determine how much # of the storage pool has been used. serial = f"lxd_{device}" # Default disk size is 10GB. size = convert_lxd_byte_suffixes( device_info.get("size", "10GB")) storage_pool = device_info.get("pool") block_devices.append( DiscoveredMachineBlockDevice( model="QEMU HARDDISK", serial=serial, id_path= f"/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_{serial}", size=size, tags=tags, storage_pool=storage_pool, )) # Discover interfaces. interfaces = [] boot = True config_mac_address = {} for configuration in expanded_config: if configuration.endswith("hwaddr"): mac = expanded_config[configuration] name = configuration.split(".")[1] config_mac_address[name] = mac for name, device in expanded_devices.items(): if device["type"] != "nic": continue device = expanded_devices[name] if "network" in device: # Try finding the nictype from the networks. # XXX: This should work for "bridge" networks, # but will most likely produce weird results for the # other types. network = await deferToThread(client.networks.get, device["network"]) attach_type = network.type attach_name = network.name else: attach_name = device["parent"] nictype = device["nictype"] attach_type = (InterfaceAttachType.BRIDGE if nictype == "bridged" else nictype) mac = device.get("hwaddr") if mac is None: mac = config_mac_address.get(name) interfaces.append( DiscoveredMachineInterface( mac_address=mac, vid=int(device.get("vlan", get_vid_from_ifname(name))), boot=boot, attach_type=attach_type, attach_name=attach_name, )) boot = False # LXD uses different suffixes to store memory so make # sure we convert to MiB, which is what MAAS uses. memory = expanded_config.get("limits.memory") if memory is not None: memory = convert_lxd_byte_suffixes(memory, divisor=1024**2) else: memory = 1024 hugepages_backed = _get_bool( expanded_config.get("limits.memory.hugepages")) cores, pinned_cores = _parse_cpu_cores( expanded_config.get("limits.cpu")) return DiscoveredMachine( hostname=machine.name, architecture=kernel_to_debian_architecture(machine.architecture), # 1 core and 1GiB of memory (we need it in MiB) is default for # LXD if not specified. cores=cores, memory=memory, cpu_speed=0, interfaces=interfaces, block_devices=block_devices, power_state=power_state, power_parameters={"instance_name": machine.name}, tags=[], hugepages_backed=hugepages_backed, pinned_cores=pinned_cores, # LXD VMs use only UEFI. bios_boot_method="uefi", ) def get_discovered_pod_storage_pool(self, storage_pool): """Get the Pod storage pool.""" storage_pool_config = storage_pool.config # Sometimes the config is empty, use get() method on the dictionary in case. storage_pool_path = storage_pool_config.get("source") storage_pool_resources = storage_pool.resources.get() total_storage = storage_pool_resources.space["total"] return DiscoveredPodStoragePool( # No ID's with LXD so we are just using the name as the ID. id=storage_pool.name, name=storage_pool.name, path=storage_pool_path, type=storage_pool.driver, storage=total_storage, ) @typed @asynchronous @inlineCallbacks def power_on(self, pod_id: str, context: dict): """Power on LXD VM.""" machine = yield self.get_machine(pod_id, context) if LXD_VM_POWER_STATE[machine.status_code] == "off": yield deferToThread(machine.start) @typed @asynchronous @inlineCallbacks def power_off(self, pod_id: str, context: dict): """Power off LXD VM.""" machine = yield self.get_machine(pod_id, context) if LXD_VM_POWER_STATE[machine.status_code] == "on": yield deferToThread(machine.stop) @typed @asynchronous @inlineCallbacks def power_query(self, pod_id: str, context: dict): """Power query LXD VM.""" machine = yield self.get_machine(pod_id, context) state = machine.status_code try: return LXD_VM_POWER_STATE[state] except KeyError: raise LXDPodError( f"Pod {pod_id}: Unknown power status code: {state}") async def discover(self, pod_id, context): """Discover all Pod host resources.""" # Connect to the Pod and make sure it is valid. client = await self.get_client(pod_id, context) if not client.has_api_extension("virtual-machines"): raise LXDPodError( "Please upgrade your LXD host to 3.19+ for virtual machine support." ) resources = await deferToThread(lambda: client.resources) mac_addresses = [] for card in resources["network"]["cards"]: for port in card["ports"]: mac_addresses.append(port["address"]) # After the region creates the Pod object it will sync LXD commissioning # data for all hardware information. discovered_pod = DiscoveredPod( # client.host_info["environment"]["architectures"] reports all the # architectures the host CPU supports, not the architectures LXD # supports. On x86_64 LXD reports [x86_64, i686] however LXD does # not currently support VMs on i686. The LXD API currently does not # have a way to query which architectures are usable for VMs. The # safest bet is to just use the kernel_architecture. architectures=[ kernel_to_debian_architecture( client.host_info["environment"]["kernel_architecture"]) ], name=client.host_info["environment"]["server_name"], mac_addresses=mac_addresses, capabilities=[ Capabilities.COMPOSABLE, Capabilities.DYNAMIC_LOCAL_STORAGE, Capabilities.OVER_COMMIT, Capabilities.STORAGE_POOLS, ], ) # Check that we have at least one storage pool. # If not, user should be warned that they need to create one. storage_pools = await deferToThread(client.storage_pools.all) if not storage_pools: raise LXDPodError( "No storage pools exists. Please create a storage pool in LXD." ) # Discover Storage Pools. pools = [] storage_pools = await deferToThread(client.storage_pools.all) local_storage = 0 for storage_pool in storage_pools: discovered_storage_pool = self.get_discovered_pod_storage_pool( storage_pool) local_storage += discovered_storage_pool.storage pools.append(discovered_storage_pool) discovered_pod.storage_pools = pools discovered_pod.local_storage = local_storage # Discover VMs. machines = [] virtual_machines = await deferToThread(client.virtual_machines.all) for virtual_machine in virtual_machines: discovered_machine = await self.get_discovered_machine( client, virtual_machine, storage_pools=discovered_pod.storage_pools, ) discovered_machine.cpu_speed = lxd_cpu_speed(resources) machines.append(discovered_machine) discovered_pod.machines = machines # Return the DiscoveredPod. return discovered_pod @asynchronous def get_commissioning_data(self, pod_id, context): """Retreive commissioning data from LXD.""" d = self.get_client(pod_id, context) # Replicate the LXD API in tree form, like machine-resources does. d.addCallback( lambda client: { # /1.0 **client.host_info, # /1.0/resources "resources": client.resources, # TODO - Add networking information. # /1.0/networks # 'networks': {'eth0': {...}, 'eth1': {...}, 'bond0': {...}}, }) d.addCallback(lambda resources: {LXD_OUTPUT_NAME: resources}) return d def get_usable_storage_pool(self, disk, storage_pools, default_storage_pool=None): """Return the storage pool and type that has enough space for `disk.size`.""" # Filter off of tags. filtered_storage_pools = [ storage_pool for storage_pool in storage_pools if storage_pool.name in disk.tags ] if filtered_storage_pools: for storage_pool in filtered_storage_pools: resources = storage_pool.resources.get() available = resources.space["total"] - resources.space["used"] if disk.size <= available: return storage_pool.name raise PodInvalidResources( "Not enough storage space on storage pools: %s" % (", ".join([ storage_pool.name for storage_pool in filtered_storage_pools ]))) # Filter off of default storage pool name. if default_storage_pool: filtered_storage_pools = [ storage_pool for storage_pool in storage_pools if storage_pool.name == default_storage_pool ] if filtered_storage_pools: default_storage_pool = filtered_storage_pools[0] resources = default_storage_pool.resources.get() available = resources.space["total"] - resources.space["used"] if disk.size <= available: return default_storage_pool.name raise PodInvalidResources( f"Not enough space in default storage pool: {default_storage_pool.name}" ) raise LXDPodError( f"Default storage pool '{default_storage_pool}' doesn't exist." ) # No filtering, just find a storage pool with enough space. for storage_pool in storage_pools: resources = storage_pool.resources.get() available = resources.space["total"] - resources.space["used"] if disk.size <= available: return storage_pool.name raise PodInvalidResources( "Not enough storage space on any storage pools: %s" % (", ".join([storage_pool.name for storage_pool in storage_pools]))) @inlineCallbacks def compose(self, pod_id: str, context: dict, request: RequestedMachine): """Compose a virtual machine.""" client = yield self.get_client(pod_id, context) # Check to see if there is a maas profile. If not, use the default. try: profile = yield deferToThread(client.profiles.get, "maas") except NotFound: # Fall back to default try: profile = yield deferToThread(client.profiles.get, "default") except NotFound: raise LXDPodError( f"Pod {pod_id}: MAAS needs LXD to have either a 'maas' " "profile or a 'default' profile, defined.") resources = yield deferToThread(lambda: client.resources) definition = get_lxd_machine_definition(request, profile.name) # Add disk to the definition. # XXX: LXD VMs currently only support one virtual block device. # Loop will need to be modified once LXD has multiple virtual # block device support. devices = {} storage_pools = yield deferToThread(client.storage_pools.all) default_storage_pool = context.get("default_storage_pool_id", context.get("default_storage_pool")) for idx, disk in enumerate(request.block_devices): usable_pool = self.get_usable_storage_pool(disk, storage_pools, default_storage_pool) devices["root"] = { "path": "/", "type": "disk", "pool": usable_pool, "size": str(disk.size), "boot.priority": "0", } # Create and attach interfaces to the machine. # The reason we are doing this after the machine is created # is because pylxd doesn't have a way to override the devices # that are defined in the profile. Since the profile is provided # by the user, we have no idea how many interfaces are defined. # # Currently, only the bridged type is supported with virtual machines. # https://lxd.readthedocs.io/en/latest/instances/#device-types nic_devices = {} profile_devices = profile.devices device_names = [] boot = True for interface in request.interfaces: if interface.ifname is None: # No interface constraints sent so use the best # nic device from the profile's devices. device_name, device = self.get_best_nic_device_from_profile( profile_devices) nic_devices[device_name] = device if "boot.priority" not in device and boot: nic_devices[device_name]["boot.priority"] = "1" boot = False device_names.append(device_name) else: nic_devices[interface.ifname] = get_lxd_nic_device(interface) # Set to boot from the first nic if boot: nic_devices[interface.ifname]["boot.priority"] = "1" boot = False device_names.append(interface.ifname) # Iterate over all of the profile's devices with type=nic # and set to type=none if not nic_device. This overrides # the device settings on the profile used by the machine. for dk, dv in profile_devices.items(): if dk not in device_names and dv["type"] == "nic": nic_devices[dk] = {"type": "none"} # Merge the devices and attach the devices to the defintion. for k, v in nic_devices.items(): devices[k] = v definition["devices"] = devices # Create the machine. machine = yield deferToThread(client.virtual_machines.create, definition, wait=True) # Pod hints are updated on the region after the machine # is composed. discovered_machine = yield ensureDeferred( self.get_discovered_machine(client, machine, storage_pools, request=request)) # Update the machine cpu speed. discovered_machine.cpu_speed = lxd_cpu_speed(resources) return discovered_machine, DiscoveredPodHints() def get_best_nic_device_from_profile(self, devices): """Return the nic name and device that is most likely to be on a MAAS DHCP enabled subnet. This is used when no interface constraints are in the request.""" nic_devices = {k: v for k, v in devices.items() if v["type"] == "nic"} # Check for boot.priority flag by sorting. # If the boot.priority flag is set, this will # most likely be an interface that is expected # to boot off the network. boot_priorities = sorted( {k: v for k, v in nic_devices.items() if "boot.priority" in v}, key=lambda i: nic_devices[i]["boot.priority"], reverse=True, ) if boot_priorities: return boot_priorities[0], nic_devices[boot_priorities[0]] # Since we couldn't find a nic device with boot.priority set # just choose the first nic device. device_name = list(nic_devices.keys())[0] return device_name, nic_devices[device_name] @inlineCallbacks def decompose(self, pod_id, context): """Decompose a virtual machine.""" client = yield self.get_client(pod_id, context) machine = yield deferToThread(client.virtual_machines.get, context["instance_name"]) # Stop the machine. yield deferToThread(machine.stop) yield deferToThread(machine.delete, wait=True) # Hints are updated on the region for LXDPodDriver. return DiscoveredPodHints()
class RECSPowerDriver(PowerDriver): name = "recs_box" chassis = True description = "Christmann RECS|Box Power Driver" settings = [ make_setting_field( "node_id", "Node ID", scope=SETTING_SCOPE.NODE, required=True ), make_setting_field("power_address", "Power address", required=True), make_setting_field("power_port", "Power port"), make_setting_field("power_user", "Power user"), make_setting_field( "power_pass", "Power password", field_type="password" ), ] ip_extractor = make_ip_extractor("power_address") def power_control_recs( self, ip, port, username, password, node_id, power_change ): """Control the power state for the given node.""" port = 8000 if port is None or port == 0 else port api = RECSAPI(ip, port, username, password) if power_change == "on": api.set_power_on_node(node_id) elif power_change == "off": api.set_power_off_node(node_id) else: raise RECSError("Unexpected MAAS power mode: %s" % power_change) def power_state_recs(self, ip, port, username, password, node_id): """Return the power state for the given node.""" port = 8000 if port is None or port == 0 else port api = RECSAPI(ip, port, username, password) try: power_state = api.get_node_power_state(node_id) except urllib.error.HTTPError as e: raise RECSError( "Failed to retrieve power state. HTTP error code: %s" % e.code ) except urllib.error.URLError as e: raise RECSError( "Failed to retrieve power state. Server not reachable: %s" % e.reason ) if power_state == "1": return "on" return "off" def set_boot_source_recs( self, ip, port, username, password, node_id, source, persistent ): """Control the boot source for the given node.""" port = 8000 if port is None or port == 0 else port api = RECSAPI(ip, port, username, password) api.set_boot_source(node_id, source, persistent) def detect_missing_packages(self): # uses urllib http client - nothing to look for! return [] def power_on(self, system_id, context): """Power on RECS node.""" power_change = "on" ip, port, username, password, node_id = extract_recs_parameters( context ) # Set default (persistent) boot to HDD self.set_boot_source_recs( ip, port, username, password, node_id, "HDD", True ) # Set next boot to PXE self.set_boot_source_recs( ip, port, username, password, node_id, "PXE", False ) self.power_control_recs( ip, port, username, password, node_id, power_change ) def power_off(self, system_id, context): """Power off RECS node.""" power_change = "off" ip, port, username, password, node_id = extract_recs_parameters( context ) self.power_control_recs( ip, port, username, password, node_id, power_change ) def power_query(self, system_id, context): """Power query RECS node.""" ip, port, username, password, node_id = extract_recs_parameters( context ) return self.power_state_recs(ip, port, username, password, node_id)
class HMCZPowerDriver(PowerDriver): name = "hmcz" chassis = True can_probe = True can_set_boot_order = True description = "IBM Hardware Management Console (HMC) for Z" settings = [ make_setting_field("power_address", "HMC Address", required=True), make_setting_field("power_user", "HMC username", required=True), make_setting_field("power_pass", "HMC password", field_type="password", required=True), make_setting_field( "power_partition_name", "HMC partition name", scope=SETTING_SCOPE.NODE, required=True, ), ] ip_extractor = make_ip_extractor("power_address") def detect_missing_packages(self): if no_zhmcclient: return ["python3-zhmcclient"] else: return [] @typed def _get_partition(self, context: dict): session = Session( context["power_address"], context["power_user"], context["power_pass"], ) partition_name = context["power_partition_name"] client = Client(session) # Each HMC manages one or more CPCs(Central Processor Complex). To find # a partition MAAS must iterate over all CPCs. for cpc in client.cpcs.list(): if not cpc.dpm_enabled: maaslog.warning( f"DPM is not enabled on '{cpc.get_property('name')}', " "skipping") continue with contextlib.suppress(NotFound): return cpc.partitions.find(name=partition_name) raise PowerActionError(f"Unable to find '{partition_name}' on HMC!") # IBM Z partitions can take awhile to start/stop. Don't wait for completion # so power actions don't consume a thread. @typed @asynchronous @threadDeferred def power_on(self, system_id: str, context: dict): """Power on IBM Z DPM.""" partition = self._get_partition(context) status = partition.get_property("status") if status in {"paused", "terminated"}: # A "paused" or "terminated" partition can only be started if # it is stopped first. MAAS can't execute the start action until # the stop action completes. This holds the thread in MAAS for ~30s. # IBM is aware this isn't optimal for us so they are looking into # modifying IBM Z to go into a stopped state. partition.stop(wait_for_completion=True) partition.start(wait_for_completion=False) @typed @asynchronous @threadDeferred def power_off(self, system_id: str, context: dict): """Power off IBM Z DPM.""" partition = self._get_partition(context) partition.stop(wait_for_completion=False) @typed @asynchronous @threadDeferred def power_query(self, system_id: str, context: dict): """Power on IBM Z DPM.""" partition = self._get_partition(context) status = partition.get_property("status") # IBM Z takes time to start or stop a partition. It returns a # transitional state during this time. Associate the transitional # state with on or off so MAAS doesn't repeatedly issue a power # on or off command. if status in {"starting", "active", "degraded"}: return "on" elif status in {"stopping", "stopped", "paused", "terminated"}: # A "paused" state isn't on or off, it just means the partition # isn't currently executing instructions. A partition can go into # a "paused" state if `shutdown -h now` is executed in the # partition. "paused" also happens when transitioning between # "starting" and "active". Consider it off so MAAS can start # it again when needed. IBM is aware this is weird and is working # on a solution. return "off" else: return "unknown" @typed @asynchronous @threadDeferred def set_boot_order(self, system_id: str, context: dict, order: list): """Set the specified boot order. :param system_id: `Node.system_id` :param context: Power settings for the node. :param order: An ordered list of network or storage devices. """ partition = self._get_partition(context) # You can only specify one boot device on IBM Z boot_device = order[0] if boot_device.get("mac_address"): nic = partition.nics.find( **{"mac-address": boot_device["mac_address"]}) partition.update_properties({ "boot-device": "network-adapter", "boot-network-device": nic.uri, }) else: for storage_group in partition.list_attached_storage_groups(): # MAAS/LXD detects the storage volume UUID as its serial. try: storage_volume = storage_group.storage_volumes.find( uuid=boot_device["serial"].upper()) except NotFound: pass else: break partition.update_properties({ "boot-device": "storage-volume", "boot-storage-volume": storage_volume.uri, })
class MicrosoftOCSPowerDriver(PowerDriver): name = "msftocs" chassis = True can_probe = True description = "Microsoft OCS - Chassis Manager" settings = [ make_setting_field("power_address", "Power address", required=True), make_setting_field("power_port", "Power port"), make_setting_field("power_user", "Power user"), make_setting_field("power_pass", "Power password", field_type="password"), make_setting_field( "blade_id", "Blade ID (Typically 1-24)", scope=SETTING_SCOPE.NODE, required=True, ), ] ip_extractor = make_ip_extractor("power_address") def detect_missing_packages(self): # uses urllib2 http client - nothing to look for! return [] def extract_from_response(self, response, element_tag): """Extract text from first element with element_tag in response.""" root = fromstring(response) return root.findtext(".//ns:%s" % element_tag, namespaces={"ns": root.nsmap[None]}) def get(self, command, context, params=None): """Dispatch a GET request to a Microsoft OCS chassis.""" if params is None: params = [] else: params = [param for param in params if bool(param)] url_base = "http://{power_address}:{power_port}/".format(**context) url = urllib.parse.urljoin(url_base, command) + "?" + "&".join(params) authinfo = urllib.request.HTTPPasswordMgrWithDefaultRealm() authinfo.add_password(None, url, context["power_user"], context["power_pass"]) proxy_handler = urllib.request.ProxyHandler({}) auth_handler = urllib.request.HTTPBasicAuthHandler(authinfo) opener = urllib.request.build_opener(proxy_handler, auth_handler) urllib.request.install_opener(opener) try: response = urllib.request.urlopen(url) except urllib.error.HTTPError as e: raise PowerConnError( "Could not make proper connection to Microsoft OCS Chassis." " HTTP error code: %s" % e.code) except urllib.error.URLError as e: raise PowerConnError( "Could not make proper connection to Microsoft OCS Chassis." " Server could not be reached: %s" % e.reason) else: return response.read() def set_next_boot_device(self, context, pxe=False, uefi=False, persistent=False): """Set Next Boot Device.""" boot_pxe = "2" if pxe else "3" boot_uefi = "true" if uefi else "false" boot_persistent = "true" if persistent else "false" params = [ "bladeid=%s" % context["blade_id"], "bootType=%s" % boot_pxe, "uefi=%s" % boot_uefi, "persistent=%s" % boot_persistent, ] self.get("SetNextBoot", context, params) def get_blades(self, context): """Gets available blades. Returns dictionary of blade numbers and their corresponding MAC Addresses. """ blades = {} root = fromstring(self.get("GetChassisInfo", context)) namespace = {"ns": root.nsmap[None]} blade_collections = root.find(".//ns:bladeCollections", namespaces=namespace) # Iterate over all BladeInfo Elements for blade_info in blade_collections: blade_mac_address = blade_info.find(".//ns:bladeMacAddress", namespaces=namespace) macs = [] # Iterate over all NicInfo Elements and add MAC Addresses for nic_info in blade_mac_address: macs.append( nic_info.findtext(".//ns:macAddress", namespaces=namespace)) macs = [mac for mac in macs if bool(mac)] if macs: # Retrive blade id number bladeid = blade_info.findtext(".//ns:bladeNumber", namespaces=namespace) # Add MAC Addresses for blade blades[bladeid] = macs return blades def power_on(self, system_id, context): """Power on MicrosoftOCS blade.""" if self.power_query(system_id, context) == "on": self.power_off(system_id, context) try: # Set default (persistent) boot to HDD self.set_next_boot_device(context, persistent=True) # Set next boot to PXE self.set_next_boot_device(context, pxe=True) # Power on blade self.get("SetBladeOn", context, ["bladeid=%s" % context["blade_id"]]) except PowerConnError as e: raise PowerActionError( "MicrosoftOCS Power Driver unable to power on blade_id %s: %s" % (context["blade_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 power_query(self, system_id, context): """Power query MicrosoftOCS blade.""" try: power_state = self.extract_from_response( self.get( "GetBladeState", context, ["bladeid=%s" % context["blade_id"]], ), "bladeState", ) except PowerConnError as e: raise PowerActionError( "MicrosoftOCS Power Driver unable to power query blade_id %s:" " %r" % (context["blade_id"], e)) else: if power_state == MicrosoftOCSState.OFF: return "off" elif power_state == MicrosoftOCSState.ON: return "on" else: raise PowerFatalError( "MicrosoftOCS Power Driver retrieved unknown power state" " %s for blade_id %s" % (power_state, context["blade_id"]))
class HMCPowerDriver(PowerDriver): name = 'hmc' chassis = True description = "IBM Hardware Management Console (HMC)" settings = [ make_setting_field('power_address', "IP for HMC", required=True), make_setting_field('power_user', "HMC username"), make_setting_field('power_pass', "HMC password", field_type='password'), make_setting_field('server_name', "HMC Managed System server name", scope=SETTING_SCOPE.NODE, required=True), make_setting_field('lpar', "HMC logical partition", scope=SETTING_SCOPE.NODE, required=True), ] ip_extractor = make_ip_extractor('power_address') def detect_missing_packages(self): # uses pure-python paramiko ssh client - nothing to look for! return [] def run_hmc_command(self, command, power_address=None, power_user=None, power_pass=None, **extra): """Run a single command on HMC via SSH and return output.""" try: ssh_client = SSHClient() ssh_client.set_missing_host_key_policy(AutoAddPolicy()) ssh_client.connect(power_address, username=power_user, password=power_pass) _, stdout, _ = ssh_client.exec_command(command) output = stdout.read().decode('utf-8').strip() except (SSHException, EOFError, SOCKETError) as e: raise PowerConnError("Could not make SSH connection to HMC for " "%s on %s - %s" % (power_user, power_address, e)) finally: ssh_client.close() return output 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_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 power_query(self, system_id, context): """Power query HMC lpar.""" try: # Power query lpar power_state = self.run_hmc_command( "lssyscfg -m %s -r lpar -F state --filter lpar_names=%s" % (context['server_name'], context['lpar']), **context) except PowerConnError as e: raise PowerActionError( "HMC Power Driver unable to power query lpar %s: %s" % (context['lpar'], e)) else: if power_state in HMCState.OFF: return 'off' elif power_state in HMCState.ON: return 'on' else: raise PowerFatalError( "HMC Power Driver retrieved unknown power state %s" " for lpar %s" % (power_state, context['lpar']))
class LXDPodDriver(PodDriver): name = "lxd" chassis = True can_probe = False can_set_boot_order = False description = "LXD (virtual systems)" settings = [ make_setting_field("power_address", "LXD address", required=True), make_setting_field( "instance_name", "Instance name", scope=SETTING_SCOPE.NODE, required=True, ), make_setting_field( "project", "LXD project", required=True, default="default", ), make_setting_field( "password", "LXD password (optional)", required=False, field_type="password", ), make_setting_field( "certificate", "LXD certificate (optional)", required=False, ), make_setting_field( "key", "LXD private key (optional)", required=False, field_type="password", ), ] ip_extractor = make_ip_extractor("power_address", IP_EXTRACTOR_PATTERNS.URL) _pylxd_client_class = Client def detect_missing_packages(self): # python3-pylxd is a required package # for maas and is installed by default. return [] @typed def get_url(self, context: dict): """Return url for the LXD host.""" power_address = context.get("power_address") url = urlparse(power_address) if not url.scheme: # When the scheme is not included in the power address # urlparse puts the url into path. url = url._replace(scheme="https", netloc="%s" % url.path, path="") if not url.port: if url.netloc: url = url._replace(netloc="%s:8443" % url.netloc) else: # Similar to above, we need to swap netloc and path. url = url._replace(netloc="%s:8443" % url.path, path="") return url.geturl() @typed @asynchronous @threadDeferred def power_on(self, pod_id: int, context: dict): """Power on LXD VM.""" with self._get_machine(pod_id, context) as machine: if LXD_VM_POWER_STATE[machine.status_code] == "off": machine.start() @typed @asynchronous @threadDeferred def power_off(self, pod_id: int, context: dict): """Power off LXD VM.""" with self._get_machine(pod_id, context) as machine: if LXD_VM_POWER_STATE[machine.status_code] == "on": machine.stop() @typed @asynchronous @threadDeferred def power_query(self, pod_id: int, context: dict): """Power query LXD VM.""" with self._get_machine(pod_id, context) as machine: state = machine.status_code try: return LXD_VM_POWER_STATE[state] except KeyError: raise LXDPodError( f"Pod {pod_id}: Unknown power status code: {state}") @threadDeferred def discover_projects(self, pod_id: int, context: dict): """Discover the list of projects in a pod.""" with self._get_client(pod_id, context) as client: self._check_required_extensions(client) return [{ "name": project.name, "description": project.description } for project in client.projects.all()] @threadDeferred def discover(self, pod_id: int, context: dict): """Discover all Pod host resources.""" # Allow the certificate not to be trusted (in which case an empty # DiscoveredPod is returned) when creating a new VM host, at which # point certs might not yet be trusted. allow_untrusted = pod_id is None with self._get_client(pod_id, context, allow_untrusted=allow_untrusted) as client: return self._discover(client, pod_id, context) def _discover(self, client: Client, pod_id: int, context: dict): self._check_required_extensions(client) if not client.trusted: # return empty information as the client is not authenticated and # gathering other info requires auth. return DiscoveredPod() self._ensure_project(client) environment = client.host_info["environment"] # After the region creates the Pod object it will sync LXD commissioning # data for all hardware information. discovered_pod = DiscoveredPod( # client.host_info["environment"]["architectures"] reports all the # architectures the host CPU supports, not the architectures LXD # supports. On x86_64 LXD reports [x86_64, i686] however LXD does # not currently support VMs on i686. The LXD API currently does not # have a way to query which architectures are usable for VMs. The # safest bet is to just use the kernel_architecture. architectures=[ kernel_to_debian_architecture( environment["kernel_architecture"]) ], name=environment["server_name"], version=environment["server_version"], capabilities=[ Capabilities.COMPOSABLE, Capabilities.DYNAMIC_LOCAL_STORAGE, Capabilities.OVER_COMMIT, Capabilities.STORAGE_POOLS, ], ) # Discover networks. "unknown" interfaces are considered too to match # ethernets in containers. networks_state = [ net.state() for net in client.networks.all() if net.type in ("unknown", "physical") ] discovered_pod.mac_addresses = list( {state.hwaddr for state in networks_state if state.hwaddr}) # Discover storage pools. storage_pools = client.storage_pools.all() if not storage_pools: raise LXDPodError( "No storage pools exists. Please create a storage pool in LXD." ) pools = [] local_storage = 0 for storage_pool in storage_pools: discovered_storage_pool = self._get_discovered_pod_storage_pool( storage_pool) local_storage += discovered_storage_pool.storage pools.append(discovered_storage_pool) discovered_pod.storage_pools = pools discovered_pod.local_storage = local_storage # Discover VMs. host_cpu_speed = lxd_cpu_speed(client.resources) projects = [project.name for project in client.projects.all()] machines = [] for project in projects: with self._get_client(pod_id, context, project=project) as project_cli: for virtual_machine in project_cli.virtual_machines.all(): discovered_machine = self._get_discovered_machine( project_cli, virtual_machine, storage_pools=discovered_pod.storage_pools, ) discovered_machine.cpu_speed = host_cpu_speed machines.append(discovered_machine) discovered_pod.machines = machines return discovered_pod @threadDeferred def get_commissioning_data(self, pod_id: int, context: dict): """Retreive commissioning data from LXD.""" with self._get_client(pod_id, context) as client: resources = { # /1.0 **client.host_info, # /1.0/resources "resources": client.resources, # /1.0/networks/<network>/state "networks": {net.name: dict(net.state()) for net in client.networks.all()}, } return {LXD_OUTPUT_NAME: resources} @threadDeferred def compose(self, pod_id: int, context: dict, request: RequestedMachine): """Compose a virtual machine.""" with self._get_client(pod_id, context) as client: storage_pools = client.storage_pools.all() default_storage_pool = context.get( "default_storage_pool_id", context.get("default_storage_pool")) include_profile = client.profiles.exists(LXD_MAAS_PROFILE) definition = get_lxd_machine_definition( request, include_profile=include_profile) definition["devices"] = { **self._get_machine_disks(request.block_devices, storage_pools, default_storage_pool), **self._get_machine_nics(request), } # Create the machine. machine = client.virtual_machines.create(definition, wait=True) # Pod hints are updated on the region after the machine is composed. discovered_machine = self._get_discovered_machine(client, machine, storage_pools, request=request) # Update the machine cpu speed. discovered_machine.cpu_speed = lxd_cpu_speed(client.resources) return discovered_machine, DiscoveredPodHints() @threadDeferred def decompose(self, pod_id: int, context: dict): """Decompose a virtual machine.""" with self._get_machine(pod_id, context) as machine: if not machine: maaslog.warning( f"Pod {pod_id}: machine {context['instance_name']} not found" ) return DiscoveredPodHints() if machine.status_code != 102: # 102 - Stopped machine.stop(force=True, wait=True) # collect machine attributes before removing it devices = machine.devices client = machine.client machine.delete(wait=True) self._delete_machine_volumes(client, pod_id, devices) # Hints are updated on the region for LXDPodDriver. return DiscoveredPodHints() def _check_required_extensions(self, client): """Raise an error if the LXD server doesn't support all required features.""" all_extensions = set(client.host_info["api_extensions"]) missing_extensions = sorted(LXD_REQUIRED_EXTENSIONS - all_extensions) if missing_extensions: raise LXDPodError( f"Please upgrade your LXD host to {LXD_MIN_VERSION} or higher " f"to support the following extensions: {','.join(missing_extensions)}" ) def _get_machine_disks(self, requested_disks, storage_pools, default_storage_pool): """Return definitions for machine disks, after creating needed volumes.""" disks = {} for idx, disk in enumerate(requested_disks): pool = self._get_usable_storage_pool(disk, storage_pools, default_storage_pool) size = str(disk.size) if idx == 0: label = "root" path = "/" extra_conf = { "boot.priority": "0", "size": size, } else: label = f"disk{idx}" path = "" volume = self._create_volume(pool, size) extra_conf = {"source": volume.name} disks[label] = { "path": path, "type": "disk", "pool": pool.name, **extra_conf, } return disks def _get_machine_nics(self, request): default_parent = self.get_default_interface_parent( request.known_host_interfaces) if default_parent is None: raise LXDPodError("No host network to attach VM interfaces to") nics = {} ifnames = set(iface.ifname for iface in request.interfaces if iface.ifname) ifindex = 0 for idx, interface in enumerate(request.interfaces): ifname = interface.ifname if ifname is None: # get the next available interface name ifname = f"eth{ifindex}" while ifname in ifnames: ifindex += 1 ifname = f"eth{ifindex}" ifnames.add(ifname) nic = get_lxd_nic_device(ifname, interface, default_parent) # Set to boot from the first nic if idx == 0: nic["boot.priority"] = "1" nics[ifname] = nic return nics def _create_volume(self, pool, size): """Create a storage volume.""" name = f"maas-{uuid.uuid4()}" return pool.volumes.create( "custom", { "name": name, "content_type": "block", "config": { "size": size } }, ) def _delete_machine_volumes(self, client, pod_id: int, devices: dict): """Delete machine volumes. The VM root volume is not removed as it's handled automatically by LXD. """ for device in devices.values(): source = device.get("source") if device["type"] != "disk" or not source: continue pool_name = device["pool"] try: pool = client.storage_pools.get(pool_name) pool.volumes.get("custom", source).delete() except Exception: maaslog.warning( f"Pod {pod_id}: failed to delete volume {source} in pool {pool_name}" ) def _ensure_project(self, client): """Ensure the project that the client is configured with exists.""" if client.projects.exists(client.project): return client.projects.create( name=client.project, **LXD_MAAS_PROJECT_CONFIG, ) def _get_usable_storage_pool(self, disk, storage_pools, default_storage_pool=None): """Return the storage pool and type that has enough space for `disk.size`.""" # Filter off of tags. filtered_storage_pools = [ storage_pool for storage_pool in storage_pools if storage_pool.name in disk.tags ] if filtered_storage_pools: for storage_pool in filtered_storage_pools: resources = storage_pool.resources.get() available = resources.space["total"] - resources.space["used"] if disk.size <= available: return storage_pool raise PodInvalidResources( "Not enough storage space on storage pools: %s" % (", ".join([ storage_pool.name for storage_pool in filtered_storage_pools ]))) # Filter off of default storage pool name. if default_storage_pool: filtered_storage_pools = [ storage_pool for storage_pool in storage_pools if storage_pool.name == default_storage_pool ] if filtered_storage_pools: default_storage_pool = filtered_storage_pools[0] resources = default_storage_pool.resources.get() available = resources.space["total"] - resources.space["used"] if disk.size <= available: return default_storage_pool raise PodInvalidResources( f"Not enough space in default storage pool: {default_storage_pool.name}" ) raise LXDPodError( f"Default storage pool '{default_storage_pool}' doesn't exist." ) # No filtering, just find a storage pool with enough space. for storage_pool in storage_pools: resources = storage_pool.resources.get() available = resources.space["total"] - resources.space["used"] if disk.size <= available: return storage_pool raise PodInvalidResources( "Not enough storage space on any storage pools: %s" % (", ".join([storage_pool.name for storage_pool in storage_pools]))) def _get_discovered_machine(self, client, machine, storage_pools, request=None): """Get the discovered machine.""" # Check the power state first. state = machine.status_code try: power_state = LXD_VM_POWER_STATE[state] except KeyError: maaslog.error( f"{machine.name}: Unknown power status code: {state}") power_state = "unknown" def _get_discovered_block_device(name, device, requested_device=None): tags = requested_device.tags if requested_device else [] # When LXD creates a QEMU disk the serial is always lxd_{device # name}. The device name is commonly "root" for the first disk. The # model and serial must be correctly defined here otherwise MAAS # will delete the disk created during composition which results in # losing the storage pool link. Without the storage pool link MAAS # can't determine how much of the storage pool has been used. serial = f"lxd_{name}" source = device.get("source") if source: pool = client.storage_pools.get(device["pool"]) volume = pool.volumes.get("custom", source) size = volume.config.get("size") else: size = device.get("size") # Default disk size is 10GB in LXD size = convert_lxd_byte_suffixes(size or "10GB") return DiscoveredMachineBlockDevice( model="QEMU HARDDISK", serial=serial, id_path=f"/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_{serial}", size=size, tags=tags, storage_pool=device.get("pool"), ) expanded_config = machine.expanded_config iface_to_mac = { key.split(".")[1]: value for key, value in expanded_config.items() if key.endswith("hwaddr") } def _get_discovered_interface(name, device, boot): if "network" in device: # Try finding the nictype from the networks. # XXX: This should work for "bridge" networks, # but will most likely produce weird results for the # other types. network = client.networks.get(device["network"]) attach_type = network.type attach_name = network.name else: attach_name = device["parent"] nictype = device["nictype"] attach_type = (InterfaceAttachType.BRIDGE if nictype == "bridged" else nictype) mac = device.get("hwaddr") if mac is None: mac = iface_to_mac.get(name) return DiscoveredMachineInterface( mac_address=mac, vid=int(device.get("vlan", 0)), boot=boot, attach_type=attach_type, attach_name=attach_name, ) extra_block_devices = 0 block_devices = [] interfaces = [] for name, device in machine.expanded_devices.items(): if device["type"] == "disk": requested_device = None if request: # for composed VMs, the root disk is always the first # one. Adjust the index so that it matches the requested # device if name == "root": index = 0 else: extra_block_devices += 1 index = extra_block_devices requested_device = request.block_devices[index] block_devices.append( _get_discovered_block_device( name, device, requested_device=requested_device)) elif device["type"] == "nic": interfaces.append( _get_discovered_interface(name, device, not interfaces)) # LXD uses different suffixes to store memory so make # sure we convert to MiB, which is what MAAS uses. memory = expanded_config.get("limits.memory") if memory is not None: memory = convert_lxd_byte_suffixes(memory, divisor=1024**2) else: memory = 1024 hugepages_backed = _get_bool( expanded_config.get("limits.memory.hugepages")) cores, pinned_cores = _parse_cpu_cores( expanded_config.get("limits.cpu")) return DiscoveredMachine( hostname=machine.name, architecture=kernel_to_debian_architecture(machine.architecture), # 1 core and 1GiB of memory (we need it in MiB) is default for # LXD if not specified. cores=cores, memory=memory, cpu_speed=0, interfaces=interfaces, block_devices=block_devices, power_state=power_state, power_parameters={ "instance_name": machine.name, "project": client.project, }, tags=[], hugepages_backed=hugepages_backed, pinned_cores=pinned_cores, # LXD VMs use only UEFI. bios_boot_method="uefi", ) def _get_discovered_pod_storage_pool(self, storage_pool): """Get the Pod storage pool.""" storage_pool_config = storage_pool.config # Sometimes the config is empty, use get() method on the dictionary in case. storage_pool_path = storage_pool_config.get("source") storage_pool_resources = storage_pool.resources.get() total_storage = storage_pool_resources.space["total"] return DiscoveredPodStoragePool( # No ID's with LXD so we are just using the name as the ID. id=storage_pool.name, name=storage_pool.name, path=storage_pool_path, type=storage_pool.driver, storage=total_storage, ) @typed @contextmanager def _get_machine(self, pod_id: int, context: dict, fail: bool = True): """Retrieve LXD VM. If "fail" is False, return None instead of raising an exception. """ instance_name = context.get("instance_name") with self._get_client(pod_id, context) as client: try: yield client.virtual_machines.get(instance_name) except NotFound: if fail: raise LXDPodError( f"Pod {pod_id}: LXD VM {instance_name} not found.") yield None @typed @contextmanager def _get_client( self, pod_id: int, context: dict, project: Optional[str] = None, allow_untrusted: bool = False, ): """Return a context manager with a PyLXD client.""" def Error(message): return LXDPodError(f"VM Host {pod_id}: {message}") endpoint = self.get_url(context) if not project: project = context.get("project", "default") password = context.get("password") cert_paths = self._get_cert_paths(context) maas_certs = get_maas_cert_tuple() if not cert_paths and not maas_certs: raise Error("No certificates available") def client_with_certs(cert): client = self._pylxd_client_class( endpoint=endpoint, project=project, cert=cert, verify=False, ) if not client.trusted and password: try: client.authenticate(password) except LXDAPIException as e: raise Error(f"Password authentication failed: {e}") return client try: if cert_paths: client = client_with_certs(cert_paths) if not client.trusted and maas_certs: with suppress(LXDAPIException): # Try to trust the certificate using the controller # certs. If this fails, ignore the error as the trusted # status for the original client is checked later. client_with_certs(maas_certs).certificates.create( "", Path(cert_paths[0]).read_bytes()) # create a new client since the certs are now trusted client = client_with_certs(cert_paths) else: client = client_with_certs(maas_certs) if not client.trusted and not allow_untrusted: raise Error( "Certificate is not trusted and no password was given") except ClientConnectionFailed: raise LXDPodError( f"Pod {pod_id}: Failed to connect to the LXD REST API.") yield client if cert_paths: for path in cert_paths: os.unlink(path) def _get_cert_paths(self, context: dict) -> Optional[Tuple[str]]: """Return a 2-tuple with paths for temporary files containing cert and key. If no certificate or key are provided, None is returned. """ cert = context.get("certificate") key = context.get("key") if not cert or not key: return None def write_temp(content) -> str: fileno, path = mkstemp() os.write(fileno, bytes(content, "ascii")) os.close(fileno) return path return write_temp(cert), write_temp(key)
class MSCMPowerDriver(PowerDriver): name = "mscm" chassis = True description = "HP Moonshot - iLO Chassis Manager" settings = [ make_setting_field("power_address", "IP for MSCM CLI API", required=True), make_setting_field("power_user", "MSCM CLI API user"), make_setting_field("power_pass", "MSCM CLI API password", field_type="password"), make_setting_field( "node_id", "Node ID - Must adhere to cXnY format " "(X=cartridge number, Y=node number).", scope=SETTING_SCOPE.NODE, required=True, ), ] ip_extractor = make_ip_extractor("power_address") def detect_missing_packages(self): # uses pure-python paramiko ssh client - nothing to look for! return [] def run_mscm_command(self, command, power_address=None, power_user=None, power_pass=None, **extra): """Run a single command on MSCM via SSH and return output.""" try: ssh_client = SSHClient() ssh_client.set_missing_host_key_policy(AutoAddPolicy()) ssh_client.connect(power_address, username=power_user, password=power_pass) _, stdout, _ = ssh_client.exec_command(command) output = stdout.read().decode("utf-8") except (SSHException, EOFError, SOCKETError) as e: raise PowerConnError("Could not make SSH connection to MSCM for " "%s on %s - %s" % (power_user, power_address, e)) finally: ssh_client.close() return output def power_on(self, system_id, context): """Power on MSCM node.""" node_id = context["node_id"] # If node is on, power off first if self.power_query(system_id, context) == "on": self.power_off(system_id, context) try: # Configure node to boot once from PXE self.run_mscm_command("set node bootonce pxe %s" % node_id, **context) # Power node on self.run_mscm_command("set node power on %s" % node_id, **context) except PowerConnError as e: raise PowerActionError( "MSCM Power Driver unable to power on node %s: %s" % (context["node_id"], e)) 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_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"
class MoonshotIPMIPowerDriver(PowerDriver): name = "moonshot" chassis = True description = "HP Moonshot - iLO4 (IPMI)" settings = [ make_setting_field("power_address", "Power address", required=True), make_setting_field("power_user", "Power user"), make_setting_field("power_pass", "Power password", field_type="password"), make_setting_field( "power_hwaddress", "Power hardware address", scope=SETTING_SCOPE.NODE, required=True, ), ] ip_extractor = make_ip_extractor("power_address") def detect_missing_packages(self): if not shell.has_command_available("ipmitool"): return ["ipmitool"] return [] 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=get_env_with_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) def power_on(self, system_id, context): self._issue_ipmitool_command("pxe", **context) self._issue_ipmitool_command("on", **context) def power_off(self, system_id, context): self._issue_ipmitool_command("off", **context) def power_query(self, system_id, context): return self._issue_ipmitool_command("status", **context)
class OpenBMCPowerDriver(PowerDriver): chassis = False can_probe = False can_set_boot_order = False name = "openbmc" description = "OpenBMC Power Driver" settings = [ make_setting_field("power_address", "OpenBMC address", required=True), make_setting_field("power_user", "OpenBMC user", required=True), make_setting_field( "power_pass", "OpenBMC password", field_type="password", required=True, ), ] ip_extractor = make_ip_extractor("power_address") cookie_jar = compat.cookielib.CookieJar() agent = CookieAgent( Agent(reactor, contextFactory=WebClientContextFactory()), cookie_jar ) def detect_missing_packages(self): # no required packages return [] @asynchronous def openbmc_request(self, method, uri, data=None): """Send the RESTful request and return the response.""" d = self.agent.request( method, uri, Headers({b"Content-Type": [b"application/json"]}), data, ) def cb_request(response): """Render the response received.""" def decode_data(data): data = data.decode("utf-8") return json.loads(data) # Error out if the response has a status code of 400 or above. if response.code >= int(HTTPStatus.BAD_REQUEST): raise PowerActionError( "OpenBMC request failed with response status code:" " %s." % response.code ) f = readBody(response) f.addCallback(decode_data) return f d.addCallback(cb_request) return d def get_uri(self, context, path=None): """Return url for the host.""" uri = context.get("power_address") if path is not None: uri = uri + path if "https" not in uri and "http" not in uri: uri = join("https://", uri) return uri.encode("utf-8") @inlineCallbacks def command(self, context, method, uri, data=None): """Current deployments of OpenBMC in the field do not support header based authentication. To issue RESTful commands, we need to login, issue RESTful command and logout. """ # login to BMC login_uri = self.get_uri(context, "/login") login_creds = { "data": [context.get("power_user"), context.get("power_pass")] } login_data = FileBodyProducer( BytesIO(json.dumps(login_creds).encode("utf-8")) ) login = yield self.openbmc_request(b"POST", login_uri, login_data) login_status = login.get("status") if login_status.lower() != "ok": raise PowerFatalError( "OpenBMC power driver received unexpected response" " to login command" ) # issue command cmd_out = yield self.openbmc_request(method, uri, data) # logout of BMC logout_uri = self.get_uri(context, "/logout") logout_creds = {"data": []} logout_data = FileBodyProducer( BytesIO(json.dumps(logout_creds).encode("utf-8")) ) logout = yield self.openbmc_request(b"POST", logout_uri, logout_data) logout_status = logout.get("status") if logout_status.lower() != "ok": raise PowerFatalError( "OpenBMC power driver received unexpected response" " to logout command" ) return cmd_out @inlineCallbacks def set_pxe_boot(self, context): """Set the host to PXE boot.""" # set boot mode to one-time boot. uri = self.get_uri(context, HOST_CONTROL + "one_time/attr/BootMode") data = FileBodyProducer(BytesIO(json.dumps(REG_MODE).encode("utf-8"))) yield self.command(context, b"PUT", uri, data) # set one-time boot source to network. uri = self.get_uri(context, HOST_CONTROL + "one_time/attr/BootSource") data = FileBodyProducer(BytesIO(json.dumps(SRC_NET).encode("utf-8"))) yield self.command(context, b"PUT", uri, data) @asynchronous @inlineCallbacks def power_query(self, system_id, context): """Power query host.""" uri = self.get_uri(context, HOST_STATE + "CurrentHostState") power_state = yield self.command(context, b"GET", uri, None) status = power_state.get("data").split(".")[-1].lower() if all(status not in state for state in ("running", "off")): raise PowerFatalError( "OpenBMC power driver received unexpected response" "to power query command" ) return {"running": "on", "off": "off"}.get(status) @asynchronous @inlineCallbacks def power_on(self, system_id, context): """Power on host.""" cur_state = yield self.power_query(system_id, context) uri = self.get_uri(context, HOST_STATE + "RequestedHostTransition") # power off host if it is currently on. if cur_state == "on": data = FileBodyProducer( BytesIO(json.dumps(HOST_OFF).encode("utf-8")) ) off_state = yield self.command(context, b"PUT", uri, data) status = off_state.get("status") if status.lower() != "ok": raise PowerFatalError( "OpenBMC power driver received unexpected response" " to power off command" ) # set one-time boot to PXE boot. yield self.set_pxe_boot(context) # power on host. data = FileBodyProducer(BytesIO(json.dumps(HOST_ON).encode("utf-8"))) on_state = yield self.command(context, b"PUT", uri, data) status = on_state.get("status") if status.lower() != "ok": raise PowerFatalError( "OpenBMC power driver received unexpected response" " to power on command" ) @asynchronous @inlineCallbacks def power_off(self, system_id, context): """Power off host.""" uri = self.get_uri(context, HOST_STATE + "RequestedHostTransition") data = FileBodyProducer(BytesIO(json.dumps(HOST_OFF).encode("utf-8"))) # set next one-time boot to PXE boot. yield self.set_pxe_boot(context) # power off host. power_state = yield self.command(context, b"PUT", uri, data) status = power_state.get("status") if status.lower() != "ok": raise PowerFatalError( "OpenBMC power driver received unexpected response" " to power off command" )
class FenceCDUPowerDriver(PowerDriver): name = 'fence_cdu' description = "Sentry Switch CDU" settings = [ make_setting_field('power_address', "Power address", required=True), make_setting_field( 'power_id', "Power ID", scope=SETTING_SCOPE.NODE, required=True), make_setting_field('power_user', "Power user"), make_setting_field( 'power_pass', "Power password", field_type='password'), ] ip_extractor = make_ip_extractor('power_address') queryable = False def detect_missing_packages(self): if not shell.has_command_available('fence_cdu'): return ['fence-agents'] return [] def _issue_fence_cdu_command( self, command, power_address=None, power_id=None, power_user=None, power_pass=None, **extra): """Issue fence_cdu command for the given power change.""" try: stdout = call_and_check([ 'fence_cdu', '-a', power_address, '-n', power_id, '-l', power_user, '-p', power_pass, '-o', command], env=select_c_utf8_locale()) except ExternalProcessError as e: # XXX 2016-01-08 newell-jensen, bug=1532310: # fence-agents fence_action method returns an exit code # of 2, by default, for querying power status while machine # is OFF. if e.returncode == 2 and command == 'status': return "Status: OFF\n" else: raise PowerError( "Fence CDU failed issuing command %s for Power ID %s: %s" % (command, power_id, e.output_as_unicode)) else: return stdout.decode("utf-8") def power_on(self, system_id, context): """Power ON Fence CDU power_id.""" if self.power_query(system_id, context) == 'on': self.power_off(system_id, context) sleep(1) if self.power_query(system_id, context) != 'off': raise PowerError( "Fence CDU unable to power off Power ID %s." % context['power_id']) self._issue_fence_cdu_command('on', **context) def power_off(self, system_id, context): """Power OFF Fence CDU power_id.""" self._issue_fence_cdu_command('off', **context) def power_query(self, system_id, context): """Power QUERY Fence CDU power_id.""" re_status = re.compile( r"Status: \s* \b(ON|OFF)\b", re.VERBOSE | re.IGNORECASE) query_output = self._issue_fence_cdu_command('status', **context) # Power query output is `Status: OFF\n` or `Status: ON\n` match = re_status.match(query_output) if match is None: raise PowerError( "Fence CDU obtained unexpected response to query of " "Power ID %s: %r" % (context['power_id'], query_output)) else: return match.group(1).lower()
class DLIPowerDriver(PowerDriver): name = "dli" chassis = True can_probe = False description = "Digital Loggers, Inc. PDU" settings = [ make_setting_field("outlet_id", "Outlet ID", scope=SETTING_SCOPE.NODE, required=True), make_setting_field("power_address", "Power address", required=True), make_setting_field("power_user", "Power user"), make_setting_field("power_pass", "Power password", field_type="password"), ] ip_extractor = make_ip_extractor("power_address") queryable = False def detect_missing_packages(self): if not shell.has_command_available("wget"): return ["wget"] return [] 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 _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 power_on(self, system_id, context): """Power on DLI outlet.""" # Power off the outlet if it is currently on if self._query_outlet_state(**context) == "on": self._set_outlet_state("OFF", **context) sleep(1) if self._query_outlet_state(**context) != "off": raise PowerError( "Unable to power off outlet %s that is already on." % context["outlet_id"]) self._set_outlet_state("ON", **context) def power_off(self, system_id, context): """Power off DLI outlet.""" self._set_outlet_state("OFF", **context) def power_query(self, system_id, context): """Power query DLI outlet.""" return self._query_outlet_state(**context)
class APCPowerDriver(PowerDriver): name = 'apc' description = "American Power Conversion (APC) PDU" settings = [ make_setting_field('power_address', "IP for APC PDU", required=True), make_setting_field('node_outlet', "APC PDU node outlet number (1-16)", scope=SETTING_SCOPE.NODE, required=True), make_setting_field('power_on_delay', "Power ON outlet delay (seconds)", default='5'), ] ip_extractor = make_ip_extractor('power_address') queryable = False def detect_missing_packages(self): binary, package = ['snmpset', 'snmp'] if not shell.has_command_available(binary): return [package] return [] def run_process(self, command): """Run SNMP command in subprocess.""" proc = Popen(command.split(), stdout=PIPE, stderr=PIPE, env=select_c_utf8_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_on(self, system_id, context): """Power on Apc outlet.""" if self.power_query(system_id, context) == 'on': self.power_off(system_id, context) sleep(float(context['power_on_delay'])) self.run_process('snmpset ' + COMMON_ARGS % (context['power_address'], context['node_outlet']) + ' i 1') def power_off(self, system_id, context): """Power off APC outlet.""" self.run_process('snmpset ' + COMMON_ARGS % (context['power_address'], context['node_outlet']) + ' i 2') 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)
class APCPowerDriver(PowerDriver): name = "apc" chassis = True description = "American Power Conversion (APC) PDU" settings = [ make_setting_field("power_address", "IP for APC PDU", required=True), make_setting_field( "node_outlet", "APC PDU node outlet number (1-16)", scope=SETTING_SCOPE.NODE, required=True, ), make_setting_field("power_on_delay", "Power ON outlet delay (seconds)", default="5"), ] ip_extractor = make_ip_extractor("power_address") queryable = False def detect_missing_packages(self): binary, package = ["snmpset", "snmp"] if not shell.has_command_available(binary): return [package] return [] 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_on(self, system_id, context): """Power on Apc outlet.""" if self.power_query(system_id, context) == "on": self.power_off(system_id, context) sleep(float(context["power_on_delay"])) self.run_process( "snmpset", *_get_common_args(context["power_address"], context["node_outlet"]), "i", "1", ) def power_off(self, system_id, context): """Power off APC outlet.""" self.run_process( "snmpset", *_get_common_args(context["power_address"], context["node_outlet"]), "i", "2", ) 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)
class RedfishPowerDriver(RedfishPowerDriverBase): chassis = True # Redfish API endpoints can be probed and enlisted. can_probe = False can_set_boot_order = False name = "redfish" description = "Redfish" settings = [ make_setting_field("power_address", "Redfish address", required=True), make_setting_field("power_user", "Redfish user", required=True), make_setting_field( "power_pass", "Redfish password", field_type="password", required=True, ), make_setting_field("node_id", "Node ID", scope=SETTING_SCOPE.NODE), ] ip_extractor = make_ip_extractor("power_address") def detect_missing_packages(self): # no required packages return [] @inlineCallbacks def process_redfish_context(self, context): """Process Redfish power driver context. Returns the basename of the first member found in the Redfish Systems: "Members": [ { "@odata.id": "/redfish/v1/Systems/1" } """ url = self.get_url(context) headers = self.make_auth_headers(**context) node_id = context.get("node_id") if node_id: node_id = node_id.encode("utf-8") else: node_id = yield self.get_node_id(url, headers) return url, node_id, headers @inlineCallbacks def get_node_id(self, url, headers): uri = join(url, REDFISH_SYSTEMS_ENDPOINT) systems, _ = yield self.redfish_request(b"GET", uri, headers) members = systems.get("Members") # remove trailing slashes. basename('...Systems/1/) = '' member = members[0].get("@odata.id").rstrip("/") return basename(member).encode("utf-8") @inlineCallbacks def set_pxe_boot(self, url, node_id, headers): """Set the machine with node_id to PXE boot.""" endpoint = join(REDFISH_SYSTEMS_ENDPOINT, b"%s" % node_id) payload = FileBodyProducer( BytesIO( json.dumps( { "Boot": { "BootSourceOverrideEnabled": "Once", "BootSourceOverrideTarget": "Pxe", } } ).encode("utf-8") ) ) yield self.redfish_request( b"PATCH", join(url, endpoint), headers, payload ) @inlineCallbacks def power(self, power_change, url, node_id, headers): """Issue `power` command.""" endpoint = REDFISH_POWER_CONTROL_ENDPOINT % node_id payload = FileBodyProducer( BytesIO(json.dumps({"ResetType": power_change}).encode("utf-8")) ) yield self.redfish_request( b"POST", join(url, endpoint), headers, payload ) @asynchronous @inlineCallbacks def power_on(self, node_id, context): """Power on machine.""" url, node_id, headers = yield self.process_redfish_context(context) power_state = yield self.power_query(node_id, context) # Power off the machine if currently on. if power_state == "on": yield self.power("ForceOff", url, node_id, headers) # Set to PXE boot. yield self.set_pxe_boot(url, node_id, headers) # Power on the machine. yield self.power("On", url, node_id, headers) @asynchronous @inlineCallbacks def power_off(self, node_id, context): """Power off machine.""" url, node_id, headers = yield self.process_redfish_context(context) # Power off the machine if it is not already off power_state = yield self.power_query(node_id, context) if power_state != "off": yield self.power("ForceOff", url, node_id, headers) # Set to PXE boot. yield self.set_pxe_boot(url, node_id, headers) @asynchronous @inlineCallbacks def power_query(self, node_id, context): """Power query machine.""" url, node_id, headers = yield self.process_redfish_context(context) uri = join(url, REDFISH_SYSTEMS_ENDPOINT, b"%s" % node_id) node_data, _ = yield self.redfish_request(b"GET", uri, headers) return node_data.get("PowerState").lower()
class VMwarePowerDriver(PowerDriver): name = "vmware" chassis = True can_probe = True description = "VMware" settings = [ make_setting_field( "power_vm_name", "VM Name (if UUID unknown)", required=False, scope=SETTING_SCOPE.NODE, ), make_setting_field( "power_uuid", "VM UUID (if known)", required=False, scope=SETTING_SCOPE.NODE, ), make_setting_field("power_address", "VMware IP", required=True), make_setting_field("power_user", "VMware username", required=True), make_setting_field( "power_pass", "VMware password", field_type="password", required=True, ), make_setting_field("power_port", "VMware API port (optional)", required=False), make_setting_field("power_protocol", "VMware API protocol (optional)", required=False), ] ip_extractor = make_ip_extractor("power_address") def detect_missing_packages(self): if not vmware.try_pyvmomi_import(): return ["python3-pyvmomi"] return [] def power_on(self, system_id, context): """Power on VMware node.""" power_change = "on" ( host, username, password, vm_name, uuid, port, protocol, ) = extract_vmware_parameters(context) power_control_vmware( host, username, password, vm_name, uuid, power_change, port, protocol, ) def power_off(self, system_id, context): """Power off VMware node.""" power_change = "off" ( host, username, password, vm_name, uuid, port, protocol, ) = extract_vmware_parameters(context) power_control_vmware( host, username, password, vm_name, uuid, power_change, port, protocol, ) def power_query(self, system_id, context): """Power query VMware node.""" ( host, username, password, vm_name, uuid, port, protocol, ) = extract_vmware_parameters(context) return power_query_vmware(host, username, password, vm_name, uuid, port, protocol)