Exemplo n.º 1
0
class WebhookPowerDriver(PowerDriver):

    name = "webhook"
    chassis = False
    can_probe = False
    can_set_boot_order = False
    description = "Webhook"
    settings = [
        make_setting_field("power_on_uri",
                           "URI to power on the node",
                           required=True),
        make_setting_field("power_off_uri",
                           "URI to power off the node",
                           required=True),
        make_setting_field(
            "power_query_uri",
            "URI to query the nodes power status",
            required=True,
        ),
        make_setting_field(
            "power_on_regex",
            "Regex to confirm the node is on",
            default=r"status.*\:.*running",
            required=True,
        ),
        make_setting_field(
            "power_off_regex",
            "Regex to confirm the node is off",
            default=r"status.*\:.*stopped",
            required=True,
        ),
        make_setting_field("power_user", "Power user"),
        make_setting_field("power_pass",
                           "Power password",
                           field_type="password"),
        make_setting_field(
            "power_token",
            "Power token, will be used in place of power_user and power_pass",
            field_type="password",
        ),
        make_setting_field(
            "power_verify_ssl",
            "Verify SSL connections with system CA certificates",
            field_type="choice",
            required=True,
            choices=SSL_INSECURE_CHOICES,
            default=SSL_INSECURE_NO,
        ),
    ]

    # Use the power_query_uri as that is the URL MAAS uses the most.
    ip_extractor = make_ip_extractor("power_query_uri",
                                     IP_EXTRACTOR_PATTERNS.URL)

    def _make_auth_headers(self, system_id, context, extra_headers=None):
        """Return authentication headers."""
        power_user = context.get("power_user")
        power_pass = context.get("power_pass")
        power_token = context.get("power_token")
        if extra_headers is None:
            extra_headers = {}

        # Don't include a Content-Type here, some services will reject the
        # request unless content is expected.
        headers = {
            b"User-Agent": [f"MAAS {get_running_version()}".encode()],
            b"Accept": [b"application/json"],
            b"System_Id": [system_id.encode()],
            **extra_headers,
        }
        if power_token:
            headers[b"Authorization"] = [f"Bearer {power_token}".encode()]
        elif power_user and power_pass:
            # Base64 encoded per RFC7617
            headers[b"Authorization"] = [
                b"Basic " +
                base64.b64encode(f"{power_user}:{power_pass}".encode())
            ]
        return Headers(headers)

    def _webhook_request(self,
                         method,
                         uri,
                         headers,
                         verify_ssl=False,
                         bodyProducer=None):
        """Send the webhook request and return the response."""

        agent = RedirectAgent(
            Agent(
                reactor,
                contextFactory=WebClientContextFactory(verify=verify_ssl),
            ))
        d = agent.request(
            method,
            uri,
            headers=headers,
            bodyProducer=bodyProducer,
        )

        def render_response(response):
            """Render the HTTPS response received."""
            def eb_catch_partial(failure):
                # Twisted is raising PartialDownloadError because the responses
                # do not contain a Content-Length header. Since every response
                # holds the whole body we just take the result.
                failure.trap(PartialDownloadError)
                if int(failure.value.status) == HTTPStatus.OK:
                    return failure.value.response
                else:
                    return failure

            # Error out if the response has a status code of 400 or above.
            if response.code >= int(HTTPStatus.BAD_REQUEST):
                # if there was no trailing slash, retry with a trailing slash
                # because of varying requirements of BMC manufacturers
                if response.code == HTTPStatus.NOT_FOUND and uri[-1] != b"/":
                    d = agent.request(
                        method,
                        uri + b"/",
                        headers=headers,
                        bodyProducer=bodyProducer,
                    )
                else:
                    raise PowerActionError(
                        "Request failed with response status code: "
                        "%s." % response.code)

            d = readBody(response)
            d.addErrback(eb_catch_partial)
            return d

        d.addCallback(render_response)
        return d

    def detect_missing_packages(self):
        # uses Twisted http client - nothing to look for!
        return []

    @asynchronous
    @inlineCallbacks
    def power_on(self, system_id, context):
        """Power on webhook."""
        yield self._webhook_request(
            b"POST",
            context["power_on_uri"].encode(),
            self._make_auth_headers(system_id, context),
            context.get("power_verify_ssl") == SSL_INSECURE_YES,
        )

    @asynchronous
    @inlineCallbacks
    def power_off(self, system_id, context):
        """Power off webhook."""
        yield self._webhook_request(
            b"POST",
            context["power_off_uri"].encode(),
            self._make_auth_headers(system_id, context),
            context.get("power_verify_ssl") == SSL_INSECURE_YES,
        )

    @asynchronous
    @inlineCallbacks
    def power_query(self, system_id, context):
        """Power query webhook."""
        power_on_regex = context.get("power_on_regex")
        power_off_regex = context.get("power_off_regex")

        node_data = yield self._webhook_request(
            b"GET",
            context["power_query_uri"].encode(),
            self._make_auth_headers(system_id, context),
            context.get("power_verify_ssl") == SSL_INSECURE_YES,
        )
        node_data = node_data.decode()
        if power_on_regex and re.search(power_on_regex, node_data) is not None:
            return "on"
        elif (power_off_regex
              and re.search(power_off_regex, node_data) is not None):
            return "off"
        else:
            return "unknown"
Exemplo n.º 2
0
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")

    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)
Exemplo n.º 3
0
class AMTPowerDriver(PowerDriver):

    name = 'amt'
    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 _get_amt_environment(self, power_pass):
        """Set and return environment for AMT."""
        env = shell.select_c_utf8_locale()
        env['AMT_PASSWORD'] = power_pass
        return env

    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:
                command = 'wsman', 'invoke', '--method', method, schema_uri
                command += wsman_opts + ('--input', '-')
                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."""
        env = self._get_amt_environment(power_pass)
        process = Popen(command, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env)
        stdout, stderr = process.communicate(stdin)
        if process.returncode != 0:
            raise PowerActionError(
                "Failed to run command: %s with error: %s" %
                (command, stderr.decode("utf-8", "replace")))
        return stdout

    @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)
            command = ('wsman', 'invoke', '--method',
                       'RequestPowerStateChange',
                       wsman_power_schema_uri) + wsman_opts + ('--input', '-')
        elif power_change == 'query':
            stdin = None  # No input for query
            command = ('wsman', 'enumerate', wsman_query_schema_uri
                       ) + wsman_opts + ('--optimize', '--encoding', 'utf-8')
        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
        output = None
        for _ in range(10):
            output = self._issue_amttool_command('info', ip_address,
                                                 power_pass)
            if output is not None and len(output) > 0:
                break
            # Wait 1 second between retries.  AMT controllers are generally
            # very light and may not be comfortable with more frequent
            # queries.
            sleep(1)

        if output is None:
            raise PowerActionError("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.
        output = None
        for _ in range(10):
            output = self._issue_wsman_command('query', ip_address, power_pass)
            if output is not None and len(output) > 0:
                break
            # Wait 1 second between retries.  AMT controllers are generally
            # very light and may not be comfortable with more frequent
            # queries.
            sleep(1)

        if output is None:
            raise PowerActionError("wsman power querying failed.")
        else:
            state = self.get_power_state(output)
            # There are a LOT of possible power states
            # 1: Other                    9: Power Cycle (Off-Hard)
            # 2: On                       10: Master Bus Reset
            # 3: Sleep - Light            11: Diagnostic Interrupt (NMI)
            # 4: Sleep - Deep             12: Off - Soft Graceful
            # 5: Power Cycle (Off - Soft) 13: Off - Hard Graceful
            # 6: Off - Hard               14: Master Bus Reset Graceful
            # 7: Hibernate (Off - Soft)   15: Power Cycle (Off-Soft Graceful)
            # 8: Off - Soft               16: Power Cycle (Off-Hard Graceful)
            #                             17: Diagnostic Interrupt (INIT)

            # These are all power states that indicate that the system is
            # either ON or will resume function in an ON or Powered Up
            # state (e.g. being power cycled currently)
            if state in ('2', '3', '4', '5', '7', '9', '10', '14', '15', '16'):
                return 'on'
            elif state in ('6', '8', '12', '13'):
                return 'off'
            else:
                raise PowerActionError(
                    "Got unknown power state from node: %s" % state)

    def 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
        env = self._get_amt_environment(power_pass)
        process = Popen(('amttool', ip_address, 'info'),
                        stdout=PIPE,
                        stderr=PIPE,
                        env=env)
        stdout, stderr = process.communicate()
        stdout = stdout.decode("utf-8")
        stderr = stderr.decode("utf-8")
        if stdout == "" or stdout.isspace():
            for error, error_info in AMT_ERRORS.items():
                if error in stderr:
                    raise error_info.get('exception')(
                        error_info.get('message'))
            raise PowerConnError("Unable to retrieve AMT version: %s" % stderr)
        else:
            match = re.search("AMT version:\s*([0-9]+)", stdout)
            if match is None:
                raise PowerActionError("Unable to extract AMT version from "
                                       "amttool output: %s" % stdout)
            else:
                version = match.group(1)
                if int(version) > 8:
                    return 'wsman'
                else:
                    return 'amttool'

    def _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)
Exemplo n.º 4
0
class RECSPowerDriver(PowerDriver):

    name = "recs_box"
    chassis = True
    can_probe = 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)
Exemplo n.º 5
0
class LXDPowerDriver(PowerDriver):

    name = "lxd"
    chassis = True
    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
    def get_client(self, system_id: str, context: dict):
        """Connect and return PyLXD client."""
        endpoint = self.get_url(context)
        password = context.get("password")
        try:
            client = Client(
                endpoint=endpoint,
                cert=(MAAS_CERTIFICATE, MAAS_PRIVATE_KEY),
                verify=False,
            )
            if not client.has_api_extension("virtual-machines"):
                raise LXDError(
                    "Please upgrade your LXD host to 3.19+ for virtual machine support."
                )
            if not client.trusted:
                if password:
                    client.authenticate(password)
                else:
                    raise LXDError(
                        f"{system_id}: Certificate is not trusted and no password was given."
                    )
            return client
        except ClientConnectionFailed:
            raise LXDError(
                f"{system_id}: Failed to connect to the LXD REST API.")

    @typed
    def get_machine(self, system_id: str, context: dict):
        """Retrieve LXD VM."""
        client = self.get_client(system_id, context)
        instance_name = context.get("instance_name")
        try:
            machine = client.virtual_machines.get(instance_name)
        except NotFound:
            raise LXDError(f"{system_id}: LXD VM {instance_name} not found.")
        return machine

    @typed
    def power_on(self, system_id: str, context: dict):
        """Power on LXD VM."""
        machine = self.get_machine(system_id, context)
        if LXD_VM_POWER_STATE[machine.status_code] == "off":
            machine.start()

    @typed
    def power_off(self, system_id: str, context: dict):
        """Power off LXD VM."""
        machine = self.get_machine(system_id, context)
        if LXD_VM_POWER_STATE[machine.status_code] == "on":
            machine.stop()

    @typed
    def power_query(self, system_id: str, context: dict):
        """Power query LXD VM."""
        machine = self.get_machine(system_id, context)
        state = machine.status_code
        try:
            return LXD_VM_POWER_STATE[state]
        except KeyError:
            raise LXDError(f"{system_id}: Unknown power status code: {state}")
Exemplo n.º 6
0
Arquivo: apc.py Projeto: zeronewb/maas
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=get_env_with_locale())
        stdout, stderr = proc.communicate()
        stdout = stdout.decode("utf-8")
        stderr = stderr.decode("utf-8")
        if proc.returncode != 0:
            raise PowerActionError(
                "APC Power Driver external process error for command %s: %s"
                % (command, stderr))
        match = re.search("INTEGER:\s*([1-2])", stdout)
        if match is None:
            raise PowerActionError(
                "APC Power Driver unable to extract outlet power state"
                " from: %s" % stdout)
        else:
            return match.group(1)

    def power_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)
Exemplo n.º 7
0
class LXDPodDriver(PodDriver):

    name = "lxd"
    chassis = True
    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, system_id: str, context: dict):
        """Connect and return PyLXD client."""
        endpoint = self.get_url(context)
        password = context.get("password")
        try:
            client = yield deferToThread(
                Client,
                endpoint=endpoint,
                cert=(MAAS_CERTIFICATE, MAAS_PRIVATE_KEY),
                verify=False,
            )
            if not client.trusted:
                if password:
                    yield deferToThread(client.authenticate, password)
                else:
                    raise LXDError(
                        f"{system_id}: Certificate is not trusted and no password was given."
                    )
            return client
        except ClientConnectionFailed:
            raise LXDError(
                f"{system_id}: Failed to connect to the LXD REST API.")

    @typed
    @inlineCallbacks
    def get_machine(self, system_id: str, context: dict):
        """Retrieve LXD VM."""
        client = yield self.get_client(system_id, context)
        instance_name = context.get("instance_name")
        try:
            machine = yield deferToThread(client.virtual_machines.get,
                                          instance_name)
        except NotFound:
            raise LXDError(f"{system_id}: LXD VM {instance_name} not found.")
        return machine

    @typed
    @inlineCallbacks
    def power_on(self, system_id: str, context: dict):
        """Power on LXD VM."""
        machine = yield deferToThread(self.get_machine, system_id, context)
        if LXD_VM_POWER_STATE[machine.status_code] == "off":
            yield deferToThread(machine.start)

    @typed
    @inlineCallbacks
    def power_off(self, system_id: str, context: dict):
        """Power off LXD VM."""
        machine = yield deferToThread(self.get_machine, system_id, context)
        if LXD_VM_POWER_STATE[machine.status_code] == "on":
            yield deferToThread(machine.stop)

    @typed
    @inlineCallbacks
    def power_query(self, system_id: str, context: dict):
        """Power query LXD VM."""
        machine = yield deferToThread(self.get_machine, system_id, context)
        state = machine.status_code
        try:
            return LXD_VM_POWER_STATE[state]
        except KeyError:
            raise LXDError(f"{system_id}: Unknown power status code: {state}")

    @inlineCallbacks
    def discover(self, pod_id, context):
        """Discover all Pod host resources."""
        client = yield self.get_client(pod_id, context)
        if not client.has_api_extension("virtual-machines"):
            raise LXDError(
                "Please upgrade your LXD host to 3.19+ for virtual machine support."
            )
        resources = yield 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.
        return DiscoveredPod(
            architectures=[
                kernel_to_debian_architecture(arch)
                for arch in client.host_info["environment"]["architectures"]
            ],
            name=client.host_info["environment"]["server_name"],
            mac_addresses=mac_addresses,
            capabilities=[
                Capabilities.COMPOSABLE,
                Capabilities.DYNAMIC_LOCAL_STORAGE,
                Capabilities.OVER_COMMIT,
                Capabilities.STORAGE_POOLS,
            ],
        )

    @inlineCallbacks
    def compose(self, system_id, context, request):
        """Compose a virtual machine."""
        # abstract method, will update in subsequent branch.
        pass

    @inlineCallbacks
    def decompose(self, system_id, context):
        """Decompose a virtual machine machine."""
        # abstract method, will update in subsequent branch.
        pass
Exemplo n.º 8
0
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)
        elif status == "stopping":
            # The HMC does not allow a machine to be powered on if its
            # currently stopping. Wait 120s for it which should be more
            # than enough time.
            try:
                partition.wait_for_status("stopped", 120)
            except StatusTimeout:
                # If 120s isn't enough time raise a PowerError() which will
                # trigger the builtin retry code in the base PowerDriver()
                # class.
                raise PowerError("Partition is stuck in a "
                                 f"{partition.get_property('status')} state!")

        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)
        status = partition.get_property("status")
        if status == "starting":
            # The HMC does not allow a machine to be powered off if its
            # currently starting. Wait 120s for it which should be more
            # than enough time.
            try:
                partition.wait_for_status("active", 120)
            except StatusTimeout:
                # If 120s isn't enough time raise a PowerError() which will
                # trigger the builtin retry code in the base PowerDriver()
                # class.
                raise PowerError("Partition is stuck in a "
                                 f"{partition.get_property('status')} state!")
        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)
        status = partition.get_property("status")

        if status in {"starting", "stopping"}:
            # The HMC does not allow a machine's boot order to be reconfigured
            # while in a transitional state. Wait for it to complete. If this
            # times out allow it to be raised so the region can log it.
            partition.wait_for_status(["stopped", "active"], 120)

        # 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,
            })
Exemplo n.º 9
0
Arquivo: ipmi.py Projeto: zhangrb/maas
class IPMIPowerDriver(PowerDriver):

    name = 'ipmi'
    chassis = 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('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):
        env = shell.get_env_with_locale()
        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)
            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.get_env_with_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,
                            mac_address=None,
                            power_boot_type=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 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)
Exemplo n.º 10
0
 def test_returns_json_verifiable_dict(self):
     json_field = make_setting_field("some_field", "Some Label")
     jsonschema.validate(json_field, SETTING_PARAMETER_FIELD_SCHEMA)
Exemplo n.º 11
0
 def make_field(self):
     return make_setting_field(self.getUniqueString(),
                               self.getUniqueString())
Exemplo n.º 12
0
class MoonshotIPMIPowerDriver(PowerDriver):

    name = "moonshot"
    chassis = True
    can_probe = False
    can_set_boot_order = False
    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,
            "-L",
            "OPERATOR",
        ) + 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)
Exemplo n.º 13
0
class OpenBMCPowerDriver(PowerDriver):

    chassis = 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"
            )
Exemplo n.º 14
0
class LXDPodDriver(PodDriver):

    name = "lxd"
    chassis = True
    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=(MAAS_CERTIFICATE, MAAS_PRIVATE_KEY),
                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

    @inlineCallbacks
    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
        for configuration in expanded_config:
            if configuration.endswith("hwaddr"):
                mac = expanded_config[configuration]
                name = configuration.split(".")[1]
                nictype = expanded_devices[name].get("nictype")
                if nictype is None and "network" in expanded_devices[name]:
                    # Try finding the nictype from the networks.
                    network = yield client.networks.get(
                        expanded_devices[name]["network"])
                    nictype = network.type
                interfaces.append(
                    DiscoveredMachineInterface(
                        mac_address=mac,
                        vid=get_vid_from_ifname(name),
                        boot=boot,
                        attach_type=nictype,
                        attach_name=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

        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=int(expanded_config.get("limits.cpu", 1)),
            memory=memory,
            cpu_speed=0,
            interfaces=interfaces,
            block_devices=block_devices,
            power_state=power_state,
            power_parameters={"instance_name": machine.name},
            tags=[],
        )

    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}")

    @inlineCallbacks
    def discover(self, pod_id, context):
        """Discover all Pod host resources."""
        # Connect to the Pod and make sure it is valid.
        client = yield 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 = yield 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 = yield 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 = yield 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 = yield deferToThread(client.virtual_machines.all)
        for virtual_machine in virtual_machines:
            discovered_machine = yield 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 = {
            "name": request.hostname,
            "architecture":
            debian_to_kernel_architecture(request.architecture),
            "config": {
                "limits.cpu": str(request.cores),
                "limits.memory": str(request.memory * 1024**2),
                # LP: 1867387 - Disable secure boot until its fixed in MAAS
                "security.secureboot": "false",
            },
            "profiles": [profile.name],
            # Image source is empty as we get images
            # from MAAS when netbooting.
            "source": {
                "type": "none"
            },
        }

        # 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:
                # Interface constraints sent so build the nic devices.
                # LXD uses 'bridged' while MAAS uses 'bridge' so convert
                # the nictype.
                nictype = ("".join([interface.attach_type, "d"])
                           if interface.attach_type == "bridge" else
                           interface.attach_type)
                nic_devices[interface.ifname] = {
                    "name": interface.ifname,
                    "parent": interface.attach_name,
                    "nictype": nictype,
                    "type": "nic",
                }
                # 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 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()
Exemplo n.º 15
0
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"]))
Exemplo n.º 16
0
 def test_returns_valid_schema(self):
     setting = make_setting_field(
         factory.make_name('name'), factory.make_name('label'))
     #: doesn't raise ValidationError
     validate(setting, SETTING_PARAMETER_FIELD_SCHEMA)
Exemplo n.º 17
0
class EatonPowerDriver(PowerDriver):

    name = "eaton"
    chassis = True
    can_probe = False
    description = "Eaton PDU"
    settings = [
        make_setting_field("power_address", "IP for Eaton PDU", required=True),
        make_setting_field(
            "node_outlet",
            "Eaton PDU node outlet number (1-24)",
            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 = True

    def detect_missing_packages(self):
        if not shell.has_command_available("snmpget"):
            return ["snmp"]
        return []

    def run_process(self, *command):
        """Run SNMP command in subprocess."""
        result = shell.run_command(*command)
        if result.returncode != 0:
            raise PowerActionError(
                "Eaton Power Driver external process error for command %s: %s"
                % ("".join(command), result.stderr))
        match = re.search(r"INTEGER:\s*([0-1])", result.stdout)
        if match is None:
            raise PowerActionError(
                "Eaton 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 Eaton 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"],
                EatonFunction.ON,
                context["node_outlet"],
            ),
            "i",
            "0",
        )

    def power_off(self, system_id, context):
        """Power off Eaton outlet."""
        self.run_process(
            "snmpset",
            *_get_common_args(
                context["power_address"],
                EatonFunction.OFF,
                context["node_outlet"],
            ),
            "i",
            "0",
        )

    def power_query(self, system_id, context):
        """Power query for Eaton outlet."""
        power_state = self.run_process(
            "snmpget",
            *_get_common_args(
                context["power_address"],
                EatonFunction.QUERY,
                context["node_outlet"],
            ),
        )
        if power_state == EatonState.OFF:
            return "off"
        elif power_state == EatonState.ON:
            return "on"
        else:
            raise PowerActionError(
                "Eaton Power Driver retrieved unknown power state: %r" %
                power_state)
Exemplo n.º 18
0
 def test_defaults_field_type_to_string(self):
     setting = make_setting_field(
         factory.make_name('name'), factory.make_name('label'))
     self.assertEqual('string', setting['field_type'])
Exemplo n.º 19
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),
    ]
    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)
Exemplo n.º 20
0
 def test_defaults_choices_to_empty_list(self):
     setting = make_setting_field(
         factory.make_name('name'), factory.make_name('label'))
     self.assertEqual([], setting['choices'])
Exemplo n.º 21
0
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
        )
Exemplo n.º 22
0
 def test_defaults_default_to_empty_string(self):
     setting = make_setting_field(
         factory.make_name('name'), factory.make_name('label'))
     self.assertEqual("", setting['default'])
Exemplo n.º 23
0
class MicrosoftOCSPowerDriver(PowerDriver):

    name = "msftocs"
    chassis = 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"]))
Exemplo n.º 24
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",
            "--input",
            "-",
            "invoke",
            "--method",
        )
        # 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:
                command = self._get_wsman_command(*wsman_opts, method,
                                                  schema_uri)
                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")
        command_args = (
            "--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)
            command_args += (
                "--input",
                "-",
                "invoke",
                "--method",
                "RequestPowerStateChange",
                wsman_power_schema_uri,
            )
        elif power_change == "query":
            stdin = None  # No input for query
            command_args += (
                "--optimize",
                "--encoding",
                "utf-8",
                "enumerate",
                wsman_query_schema_uri,
            )
        command = self._get_wsman_command(*command_args)
        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
        command = self._get_wsman_command(
            "identify",
            "--port",
            "16992",
            "--hostname",
            ip_address,
            "--username",
            "admin",
            "--password",
            power_pass,
        )
        result = shell.run_command(*command)
        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_wsman_command(self, *args):
        base_path = snappy.get_snap_path() or "/"
        return (
            "wsman",
            "-C",
            join(base_path, "etc/openwsman/openwsman_client.conf"),
        ) + args

    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)
Exemplo n.º 25
0
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)
Exemplo n.º 26
0
class RedfishPowerDriver(RedfishPowerDriverBase):

    chassis = True  # Redfish API endpoints can be probed and enlisted.

    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')
        member = members[0].get('@odata.id')
        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 = 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({
                    'Action': "Reset",
                    'ResetType': "%s" % 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)
        # Set to PXE boot.
        yield self.set_pxe_boot(url, node_id, headers)
        # Power off the machine.
        yield self.power("ForceOff", 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()
Exemplo n.º 27
0
class SeaMicroPowerDriver(PowerDriver):

    name = "sm15k"
    chassis = True
    can_probe = 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"
Exemplo n.º 28
0
class IPMIPowerDriver(PowerDriver):

    name = "ipmi"
    chassis = False
    can_probe = False
    can_set_boot_order = 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):
        try:
            self._issue_ipmi_command("on", **context)
        except PowerAuthError as e:
            if (context.get("k_g")
                    and str(e) == IPMI_ERRORS["k_g invalid"]["message"]):
                send_node_event(
                    EVENT_TYPES.NODE_POWER_ON_FAILED,
                    system_id,
                    None,
                    "Incorrect K_g key, trying again without K_g key",
                )
                context.pop("k_g")
                self._issue_ipmi_command("on", **context)
            else:
                raise e

    def power_off(self, system_id, context):
        try:
            self._issue_ipmi_command("off", **context)
        except PowerAuthError as e:
            if (context.get("k_g")
                    and str(e) == IPMI_ERRORS["k_g invalid"]["message"]):
                send_node_event(
                    EVENT_TYPES.NODE_POWER_OFF_FAILED,
                    system_id,
                    None,
                    "Incorrect K_g key, trying again without K_g key",
                )
                context.pop("k_g")
                self._issue_ipmi_command("off", **context)
            else:
                raise e

    def power_query(self, system_id, context):
        try:
            return self._issue_ipmi_command("query", **context)
        except PowerAuthError as e:
            if (context.get("k_g")
                    and str(e) == IPMI_ERRORS["k_g invalid"]["message"]):
                send_node_event(
                    EVENT_TYPES.NODE_POWER_QUERY_FAILED,
                    system_id,
                    None,
                    "Incorrect K_g key, trying again without K_g key",
                )
                context.pop("k_g")
                return self._issue_ipmi_command("query", **context)
            else:
                raise e
Exemplo n.º 29
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)

        # Check that we have at least one storage pool.  If not, create it.
        pools = yield deferToThread(conn.list_pools)
        if not len(pools):
            yield deferToThread(conn.create_storage_pool)

        # 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,
                storage_pools=discovered_pod.storage_pools)
            if discovered_machine is not None:
                discovered_machine.cpu_speed = discovered_pod.cpu_speed
                machines.append(discovered_machine)
        discovered_pod.machines = machines

        # Set KVM Pod tags to 'virtual'.
        discovered_pod.tags = ['virtual']

        # Return the DiscoveredPod
        return discovered_pod

    @inlineCallbacks
    def compose(self, system_id, context, request):
        """Compose machine."""
        conn = yield self.get_virsh_connection(context)
        default_pool = context.get('default_storage_pool_id',
                                   context.get('default_storage_pool'))
        created_machine = yield deferToThread(conn.create_domain, request,
                                              default_pool)
        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
Exemplo n.º 30
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",
        ),
    ]
    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
    @asynchronous
    @threadDeferred
    def power_on(self, pod_id: int, context: dict):
        """Power on LXD VM."""
        machine = self._get_machine(pod_id, context)
        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."""
        machine = self._get_machine(pod_id, context)
        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."""
        machine = 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}")

    @threadDeferred
    def discover_projects(self, pod_id: int, context: dict):
        """Discover the list of projects in a pod."""
        client = self._get_client(pod_id, context)
        if not client.has_api_extension("projects"):
            raise LXDPodError(
                "Please upgrade your LXD host to 3.6+ for projects support.")
        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."""
        # Connect to the Pod and make sure it is valid.
        client = 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."
            )

        self._ensure_project(client)

        # get MACs for host interfaces. "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")
        ]
        mac_addresses = list(
            {state.hwaddr
             for state in networks_state if state.hwaddr})

        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"],
            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 = 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 = []
        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

        host_cpu_speed = lxd_cpu_speed(client.resources)

        # Discover VMs.
        projects = [project.name for project in client.projects.all()]
        machines = []
        for project in projects:
            project_cli = self._get_client(pod_id, context, project=project)
            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 the DiscoveredPod.
        return discovered_pod

    @threadDeferred
    def get_commissioning_data(self, pod_id: int, context: dict):
        """Retreive commissioning data from LXD."""
        client = self._get_client(pod_id, context)
        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."""
        client = self._get_client(pod_id, context)

        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."""
        machine = self._get_machine(pod_id, context)
        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 _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):
        usable_parents = sorted(
            (iface
             for iface in request.known_host_interfaces if iface.dhcp_enabled),
            # sort bridges first
            key=lambda iface: iface.attach_type != InterfaceAttachType.BRIDGE,
        )
        try:
            default_parent = usable_parents[0]
        except IndexError:
            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

        # When creating a project, by default don't enable per-project images
        # and storage. Enabling those setting up at least one storage pool so
        # that VMs can be created. Users can change those settings later
        # without affecting MAAS functionality.
        client.projects.create(
            name=client.project,
            description="Project managed by MAAS",
            config={
                "features.images": "false",
                "features.profiles": "true",
                "features.storage.volumes": "false",
            },
        )

    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", get_vid_from_ifname(name))),
                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
    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.
        """
        client = self._get_client(pod_id, context)
        instance_name = context.get("instance_name")
        try:
            return client.virtual_machines.get(instance_name)
        except NotFound:
            if fail:
                raise LXDPodError(
                    f"Pod {pod_id}: LXD VM {instance_name} not found.")
            return None

    @typed
    def _get_client(self,
                    pod_id: int,
                    context: dict,
                    project: Optional[str] = None):
        """Connect PyLXD client."""
        if not project:
            project = context.get("project", "default")
        endpoint = self.get_url(context)
        try:
            client = Client(
                endpoint=endpoint,
                project=project,
                cert=get_maas_cert_tuple(),
                verify=False,
            )
            if not client.trusted:
                password = context.get("password")
                if password:
                    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