← Back to team overview

sts-sponsors team mailing list archive

Re: [Merge] ~igor-brovtsin/maas:power-drivers-wrapper into maas:master

 


Diff comments:

> diff --git a/powerdrivers/common/maaspd/classes.py b/powerdrivers/common/maaspd/classes.py
> new file mode 100644
> index 0000000..53b2ba0
> --- /dev/null
> +++ b/powerdrivers/common/maaspd/classes.py
> @@ -0,0 +1,112 @@
> +from abc import ABC, abstractmethod
> +from dataclasses import dataclass
> +from typing import Dict, Sequence
> +
> +
> +@dataclass
> +class PowerDriverProbedSystem:
> +    """Dataclass for `probe` action results"""
> +
> +    hostname: str
> +    mac: str
> +    arch: str
> +    driver_name: str
> +    driver_parameters: Dict
> +
> +
> +@dataclass
> +class PowerDriverNetworkBootDevice:
> +    """Network boot device object passed to `set_boot_order` action handler"""
> +
> +    # NIC MAC address
> +    mac: str
> +
> +
> +@dataclass
> +class PowerDriverStorageBootDevice:
> +    """Storage boot device object passed to `set_boot_order` action handler"""
> +
> +    # Storage serial number
> +    serial: str
> +
> +
> +class PowerDriver(ABC):

Yes, indeed. This is also the reason why we have it in a separate package and not in the `src` alongside other MAAS sources

> +    """
> +    Abstract base class for MAAS power drivers.
> +
> +    Please note that driver parameters will be passed as kwargs
> +    to the class initializer when using common `pd_main`.
> +    """
> +
> +    @staticmethod
> +    @abstractmethod
> +    def get_description() -> str:
> +        """Returns a description line `argparse` will use with a suffix"""
> +
> +    @staticmethod
> +    @abstractmethod
> +    def get_schema() -> str:
> +        """Returns a valid JSON-string with the MAAS power driver schema"""
> +
> +    @staticmethod
> +    @abstractmethod
> +    def supports_probe() -> bool:
> +        """Returns True when power driver supports `probe` action.
> +
> +        When using `pd_main`, the action will still be available,
> +        but `PowerDriverUnsupportedAction` will be raised and printed
> +        as a JSON
> +        """
> +        return False
> +
> +    @staticmethod
> +    @abstractmethod
> +    def supports_boot_ordering() -> bool:
> +        """Returns True when power driver supports `set_boot_order` action.
> +
> +        When using `pd_main`, the action will still be available,
> +        but `PowerDriverUnsupportedAction` will be raised and printed
> +        as a JSON"""
> +        return False
> +
> +    @abstractmethod
> +    def power_on(self) -> None:
> +        """A method that will be called for `power_on` action.
> +
> +        Should power machine on or throw PowerDriverError
> +        child exception that reflects the failure reason.
> +        """
> +
> +    @abstractmethod
> +    def power_off(self) -> None:
> +        """A method that will be called for `power_off` action.
> +
> +        Should power machine off or throw PowerDriverError
> +        child exception that reflects the failure reason."""
> +
> +    @abstractmethod
> +    def power_query(self) -> bool:
> +        """A method that will be called for `power_on` action.
> +
> +        Should return 'True' when machine is on, 'False' when machine is off
> +        or throw PowerDriverError child exception that reflects the failure reason.
> +        """
> +
> +    def probe(self) -> Sequence[PowerDriverProbedSystem]:
> +        """A method that will be called for `probe` action.
> +
> +        Should return an iterable (possibly empty) with PowerDriverProbedSystem
> +        or throw PowerDriverError child exception that reflects the failure reason.
> +        """
> +        return []
> +
> +    def set_boot_order(
> +        self,
> +        devices: Sequence[
> +            PowerDriverNetworkBootDevice | PowerDriverStorageBootDevice
> +        ],
> +    ) -> None:
> +        """A method that will be called for `set_boot_order` action.
> +
> +        Should update boot order or throw PowerDriverError
> +        child exception that reflects the failure reason."""
> diff --git a/powerdrivers/common/maaspd/exc.py b/powerdrivers/common/maaspd/exc.py
> new file mode 100644
> index 0000000..6661a2a
> --- /dev/null
> +++ b/powerdrivers/common/maaspd/exc.py
> @@ -0,0 +1,98 @@
> +import enum
> +
> +
> +class PowerDriverErrorCode(enum.Enum):
> +    FATAL_ERROR = "fatalError"

Sure, I'll update it.

> +    SETTINGS_ERROR = "settingsError"
> +    TOOL_ERROR = "toolError"
> +    AUTH_ERROR = "authError"
> +    CONNECTION_ERROR = "connError"
> +    ACTION_ERROR = "actionError"
> +    UNSUPPORTED_ACTION = "unsupportedActionError"
> +
> +
> +class PowerDriverError(Exception):
> +    """Base class for power driver exceptions"""
> +
> +    code: PowerDriverErrorCode = None
> +
> +    def __init__(self, message="No message provided"):
> +        self.message = message
> +        super().__init__(message)
> +
> +    def __json__(self) -> dict:
> +        """Helper that returns dict representation of an exception
> +        in a format described by PowerDriverError schema"""
> +        return {"code": self.code.value, "message": self.message}
> +
> +
> +class PowerDriverFatalError(PowerDriverError):
> +    """Error that is raised when the power action should not continue to
> +    retry at all.
> +
> +    This exception will cause the power action to fail instantly,
> +    without retrying.
> +    """
> +
> +    code = PowerDriverErrorCode.FATAL_ERROR
> +
> +
> +class PowerDriverSettingsError(PowerDriverError):
> +    """Error that is raised when the power type is missing argument
> +    that is required to control the BMC.
> +
> +    This exception will cause the power action to fail instantly,
> +    without retrying.
> +    """
> +
> +    code = PowerDriverErrorCode.SETTINGS_ERROR
> +
> +
> +class PowerDriverToolError(PowerDriverError):
> +    """Error that is raised when the power tool is missing completely
> +    for use.
> +
> +    This exception will cause the power action to fail instantly,
> +    without retrying.
> +    """
> +
> +    code = PowerDriverErrorCode.TOOL_ERROR
> +
> +
> +class PowerDriverAuthError(PowerDriverError):
> +    """Error raised when power driver fails to authenticate to BMC.
> +
> +    This exception will cause the power action to fail instantly,
> +    without retrying.
> +    """
> +
> +    code = PowerDriverErrorCode.AUTH_ERROR
> +
> +
> +class PowerDriverConnError(PowerDriverError):
> +    """Error raised when power driver fails to communicate to BMC.
> +
> +    This exception will cause a retry attempt later unless retries limit is reached.
> +    """
> +
> +    code = PowerDriverErrorCode.CONNECTION_ERROR
> +
> +
> +class PowerDriverActionError(PowerDriverError):
> +    """Error when actually performing an action on the BMC, like `on`
> +    or `off`.
> +
> +    This exception will cause a retry attempt later unless retries limit is reached.
> +    """
> +
> +    code = PowerDriverErrorCode.ACTION_ERROR
> +
> +
> +class PowerDriverUnsupportedActionError(PowerDriverError):
> +    """Error raised when requested power driver action is unsupported.
> +
> +    This exception will cause the power action to fail instantly,
> +    without retrying.
> +    """
> +
> +    code = PowerDriverErrorCode.UNSUPPORTED_ACTION
> diff --git a/powerdrivers/common/maaspd/main.py b/powerdrivers/common/maaspd/main.py
> new file mode 100644
> index 0000000..0095648
> --- /dev/null
> +++ b/powerdrivers/common/maaspd/main.py
> @@ -0,0 +1,196 @@
> +import argparse
> +import enum
> +import json
> +from typing import Type, Dict, List
> +
> +from .classes import (
> +    PowerDriverNetworkBootDevice,
> +    PowerDriverStorageBootDevice,
> +    PowerDriver,
> +)
> +from .exc import (
> +    PowerDriverError,
> +    PowerDriverUnsupportedActionError,
> +    PowerDriverFatalError,
> +)
> +
> +
> +class _PowerAction(enum.Enum):
> +    POWER_ON = "power_on"
> +    POWER_OFF = "power_off"
> +    POWER_QUERY = "power_query"
> +    PROBE = "probe"
> +    SET_BOOT_ORDER = "set_boot_order"
> +
> +
> +def _prepare_set_boot_order_devices(
> +    params: dict,
> +) -> List[PowerDriverNetworkBootDevice | PowerDriverStorageBootDevice]:
> +    """Converts the devices list of dicts to a matching list of dataclasses"""
> +    result = []
> +    for device in params["devices"]:
> +        device_type = device["deviceType"]
> +        if device_type == "NetworkInterface":
> +            clazz = PowerDriverNetworkBootDevice

Will change, thanks!

> +        elif device_type == "StorageDevice":
> +            clazz = PowerDriverStorageBootDevice
> +        else:
> +            raise PowerDriverFatalError(f'Unknown device type "{device_type}"')
> +        result.append(clazz(**device["properties"]))
> +    return result
> +
> +
> +def _dispatch_action(power_driver: PowerDriver, action: str, parameters: dict):
> +    """Calls the method of power_driver that performs the requested action"""
> +    if action == _PowerAction.POWER_ON.value:
> +        power_driver.power_on()
> +    elif action == _PowerAction.POWER_OFF.value:
> +        power_driver.power_off()
> +    elif action == _PowerAction.POWER_QUERY.value:
> +        return {"status": "on" if power_driver.power_query() else "off"}
> +    elif action == _PowerAction.PROBE.value:
> +        return {"hosts": power_driver.probe()}
> +    elif action == _PowerAction.SET_BOOT_ORDER.value:
> +        power_driver.set_boot_order(
> +            _prepare_set_boot_order_devices(parameters)
> +        )
> +    else:
> +        raise PowerDriverUnsupportedActionError(f"Unknown action {action}")
> +
> +
> +def _handle_action(power_driver_cls: Type[PowerDriver], args) -> Dict:
> +    """Instantiates a power driver from provided class and
> +    calls the method matching the requested action. Returns a dict
> +    that conforms to the MAAS power driver schema
> +    """
> +    action: str = args.action
> +    # With MAAS power drivers, we expect that stdin contains a valid JSON with parameters
> +    parameters: dict = json.load(args.input)
> +    assert isinstance(
> +        parameters, dict
> +    ), f"Expected stdin parameters to be a dict, `{type(parameters)}` deserialised instead"
> +
> +    response = {"action_type": action, "error": None}
> +
> +    try:
> +        # Since instantiating a power driver class
> +        if (
> +            action == _PowerAction.PROBE.value
> +            and not power_driver_cls.supports_probe()
> +        ) or (
> +            action == _PowerAction.SET_BOOT_ORDER.value
> +            and not power_driver_cls.supports_boot_ordering()
> +        ):
> +            raise PowerDriverUnsupportedActionError(
> +                f'"{action}" action is not supported for this power driver'
> +            )
> +
> +        instance = power_driver_cls(**parameters["driverParameters"])
> +        call_result = _dispatch_action(instance, action, parameters)
> +        if isinstance(call_result, dict):
> +            response["data"] = call_result
> +    except PowerDriverError as e:
> +        # We don't want to handle all errors since python interpreter is smart enough
> +        # to print unhandled exceptions tracebacks to stderr
> +        response["error"] = e.__json__()
> +
> +    return response
> +
> +
> +def _mark_unsupported(line: str, unsupported: bool):
> +    """Adds an '(Not supported by this driver)' suffix to an argparse help line"""
> +    suffix = " (Not supported by this driver)" if unsupported else ""
> +    return line + suffix
> +
> +
> +class _PowerDriverJSONEncoder(json.JSONEncoder):
> +    def default(self, obj):
> +        json_magic = obj.getattr("__json__", None)
> +        if json_magic and callable(json_magic):
> +            result = json_magic()
> +            return result
> +        return json.JSONEncoder.default(self, obj)
> +
> +
> +def pd_main(power_driver_cls: Type[PowerDriver]) -> int:
> +    """
> +    Common main() function for all MAAS power drivers.
> +
> +    :param power_driver_cls: Power driver class inherited from `PowerDriver`.
> +                             Note that the *class initializer must accept power
> +                             driver parameters values as kwargs*.
> +
> +    :return exit code
> +    :rtype: int
> +    """
> +    parser = argparse.ArgumentParser(
> +        description=f"{power_driver_cls.get_description()} — A power driver used in MAAS",
> +        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
> +    )
> +
> +    input_parent_parser = argparse.ArgumentParser(add_help=False)
> +    input_parent_parser.add_argument(
> +        "input",
> +        type=argparse.FileType("r"),
> +        nargs="?",
> +        default="-",
> +        help="File to read parameters from",
> +    )
> +
> +    subparsers = parser.add_subparsers(dest="action")
> +    subparsers.required = True
> +
> +    subparsers.add_parser("schema", help="Prints schema")
> +
> +    subparsers.add_parser(
> +        _PowerAction.POWER_ON.value,
> +        help="Send power on command to the machine",
> +        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
> +        parents=[input_parent_parser],
> +    )
> +    subparsers.add_parser(
> +        _PowerAction.POWER_OFF.value,
> +        help="Send power off command to the machine",
> +        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
> +        parents=[input_parent_parser],
> +    )
> +    subparsers.add_parser(
> +        _PowerAction.POWER_QUERY.value,
> +        help="Query machine power state",
> +        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
> +        parents=[input_parent_parser],
> +    )
> +    subparsers.add_parser(
> +        _PowerAction.PROBE.value,
> +        help=_mark_unsupported(
> +            "Probe chassis for a list of controlled machines",
> +            not power_driver_cls.supports_probe(),
> +        ),
> +        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
> +        parents=[input_parent_parser],
> +    )
> +    subparsers.add_parser(
> +        _PowerAction.SET_BOOT_ORDER.value,
> +        help=_mark_unsupported(
> +            "Reorder boot sources",
> +            not power_driver_cls.supports_boot_ordering(),
> +        ),
> +        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
> +        parents=[input_parent_parser],
> +    )
> +
> +    args = parser.parse_args()
> +
> +    if args.action == "schema":
> +        print(power_driver_cls.get_schema())
> +    elif args.action in [e.value for e in _PowerAction]:
> +        result = _handle_action(power_driver_cls, args)
> +        print(
> +            json.dumps(
> +                result, indent=4, sort_keys=True, cls=_PowerDriverJSONEncoder
> +            )
> +        )
> +    else:
> +        raise NotImplementedError()
> +
> +    return 0


-- 
https://code.launchpad.net/~igor-brovtsin/maas/+git/maas/+merge/436340
Your team MAAS Committers is subscribed to branch maas:master.



References