예제 #1
0
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)
예제 #2
0
파일: vmware.py 프로젝트: zhangrb/maas
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)
예제 #3
0
파일: hs300.py 프로젝트: temnok/hs300-maas
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"
예제 #4
0
파일: virsh.py 프로젝트: ocni-dtu/maas
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)
예제 #5
0
파일: ucsm.py 프로젝트: sempervictus/maas
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)
예제 #6
0
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)
예제 #7
0
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"
예제 #8
0
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
예제 #9
0
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)
예제 #10
0
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)
예제 #11
0
파일: wedge.py 프로젝트: zhangrb/maas
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)
예제 #12
0
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
예제 #13
0
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)
예제 #14
0
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)
예제 #15
0
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'
예제 #16
0
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()
예제 #17
0
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)
예제 #18
0
파일: hmcz.py 프로젝트: sempervictus/maas
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,
            })
예제 #19
0
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"]))
예제 #20
0
파일: hmc.py 프로젝트: zhangrb/maas
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']))
예제 #21
0
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)
예제 #22
0
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"
예제 #23
0
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)
예제 #24
0
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"
            )
예제 #25
0
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()
예제 #26
0
파일: dli.py 프로젝트: shawnallen85/maas
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)
예제 #27
0
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)
예제 #28
0
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)
예제 #29
0
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()
예제 #30
0
파일: vmware.py 프로젝트: th3architect/maas
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)