sts-sponsors team mailing list archive
-
sts-sponsors team
-
Mailing list archive
-
Message #04817
[Merge] ~igor-brovtsin/maas:power-drivers-wrapper into maas:master
Igor Brovtsin has proposed merging ~igor-brovtsin/maas:power-drivers-wrapper into maas:master.
Commit message:
Initial version of extracted power drivers layout
- Common wrapper for MAAS power drivers
- Minimal example of a concrete power driver (see `powerdrivers/abstract`)
- Tested `pex` with it, works fine
TBD:
- Integrate it into our Makefile
- Move some power drivers there
- Unit testing configuration for the drivers
- Actual unit tests for the wrapper
Requested reviews:
MAAS Maintainers (maas-maintainers)
For more details, see:
https://code.launchpad.net/~igor-brovtsin/maas/+git/maas/+merge/436340
I'm asking for a review of:
- the `powerdrivers` folder structure
- wrapper code in `powerdrivers/common/maaspd`
- the layout of an example power driver project in `powerdrivers/abstract`
--
Your team MAAS Committers is subscribed to branch maas:master.
diff --git a/powerdrivers/abstract/README.md b/powerdrivers/abstract/README.md
new file mode 100644
index 0000000..ddc26a4
--- /dev/null
+++ b/powerdrivers/abstract/README.md
@@ -0,0 +1,6 @@
+Build it with
+`pex --inherit-path=fallback -f ../common/dist/ -c maas-powerdriver-abstract -o maas-powerdriver-abstract .`
+
+Run it with `./maas-powerdriver-abstract <action> empty_driver_params.py`
+
+These commands are not final, but will do for now.
\ No newline at end of file
diff --git a/powerdrivers/abstract/empty_driver_params.json b/powerdrivers/abstract/empty_driver_params.json
new file mode 100644
index 0000000..978b7a4
--- /dev/null
+++ b/powerdrivers/abstract/empty_driver_params.json
@@ -0,0 +1,5 @@
+{
+ "driverParameters": {
+
+ }
+}
diff --git a/powerdrivers/abstract/pd_abstract/__init__.py b/powerdrivers/abstract/pd_abstract/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/powerdrivers/abstract/pd_abstract/__init__.py
diff --git a/powerdrivers/abstract/pd_abstract/driver.py b/powerdrivers/abstract/pd_abstract/driver.py
new file mode 100644
index 0000000..7a95453
--- /dev/null
+++ b/powerdrivers/abstract/pd_abstract/driver.py
@@ -0,0 +1,39 @@
+import sys
+
+from maaspd import pd_main
+from maaspd.classes import PowerDriver
+
+
+class ConcreteAbstractPowerDriver(PowerDriver):
+ @staticmethod
+ def get_description() -> str:
+ return "Concrete Abstract power driver"
+
+ @staticmethod
+ def get_schema() -> str:
+ return "{}"
+
+ @staticmethod
+ def supports_probe() -> bool:
+ return False
+
+ @staticmethod
+ def supports_boot_ordering() -> bool:
+ return False
+
+ def power_on(self) -> None:
+ pass
+
+ def power_off(self) -> None:
+ pass
+
+ def power_query(self) -> bool:
+ pass
+
+
+def run():
+ sys.exit(pd_main(ConcreteAbstractPowerDriver))
+
+
+if __name__ == "__main__":
+ run()
diff --git a/powerdrivers/abstract/pyproject.toml b/powerdrivers/abstract/pyproject.toml
new file mode 100644
index 0000000..9fb59cc
--- /dev/null
+++ b/powerdrivers/abstract/pyproject.toml
@@ -0,0 +1,28 @@
+[build-system]
+# With setuptools 50.0.0, 'make .ve' fails.
+requires = ["setuptools < 50.0.0", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[tool.black]
+line-length = 79
+exclude = """
+/.egg
+/.git
+/.mypy_cache
+"""
+
+[tool.isort]
+from_first = false
+force_sort_within_sections = true
+profile = "black"
+line_length = 79
+known_first_party = """
+
+"""
+order_by_type = false
+
+[tool.pytest.ini_options]
+filterwarnings = "error::BytesWarning"
+testpaths = [
+ "src",
+]
diff --git a/powerdrivers/abstract/setup.cfg b/powerdrivers/abstract/setup.cfg
new file mode 100644
index 0000000..dfc96a5
--- /dev/null
+++ b/powerdrivers/abstract/setup.cfg
@@ -0,0 +1,63 @@
+[metadata]
+name = maas-powerdriver-abstract
+version = 1.0.0
+description = Concrete Abstract power driver of MAAS (Metal As A Service)
+url = https://maas.io/
+license = AGPLv3
+author = MAAS Developers
+author_email = maas-devel@xxxxxxxxxxxxxxxxxxx
+classifiers =
+ Development Status :: 5 - Production/Stable
+ Intended Audience :: Information Technology
+ Intended Audience :: System Administrators
+ License :: OSI Approved :: GNU Affero General Public License v3
+ Operating System :: POSIX :: Linux
+ Programming Language :: Python :: 3
+ Topic :: System :: Systems Administration
+
+[options]
+packages = find:
+install_requires =
+ maaspd
+
+[options.entry_points]
+console_scripts =
+ maas-powerdriver-abstract = pd_abstract.driver:run
+
+[globals]
+lint_files =
+ setup.py
+ src/
+
+cog_files =
+ pyproject.toml
+
+deps_lint =
+ black == 22.6.0
+ flake8 == 5.0.4
+ isort == 5.7.0
+ cogapp == 3.3.0
+ click == 8.1.3
+
+
+[flake8]
+ignore = E203, E266, E501, W503, W504
+
+[tox:tox]
+skipsdist = True
+envlist = format,lint
+
+[testenv:format]
+deps = {[globals]deps_lint}
+commands =
+ isort {[globals]lint_files}
+ black -q {[globals]lint_files}
+ cog -r --verbosity=1 {[globals]cog_files}
+
+[testenv:lint]
+deps = {[globals]deps_lint}
+commands =
+ isort --check-only --diff {[globals]lint_files}
+ black --check {[globals]lint_files}
+ flake8 {[globals]lint_files}
+ cog --check --verbosity=1 {[globals]cog_files}
diff --git a/powerdrivers/abstract/setup.py b/powerdrivers/abstract/setup.py
new file mode 100644
index 0000000..0bde62d
--- /dev/null
+++ b/powerdrivers/abstract/setup.py
@@ -0,0 +1,5 @@
+import os
+
+from setuptools import setup
+
+setup()
diff --git a/powerdrivers/common/README.md b/powerdrivers/common/README.md
new file mode 100644
index 0000000..c806dac
--- /dev/null
+++ b/powerdrivers/common/README.md
@@ -0,0 +1,53 @@
+# MAAS Power driver wrapper
+
+This package contains generic classes and functions useful for any power driver implementation.
+
+Specifically, it has a `PowerDriver` class that outlines the actions MAAS expects from a power driver. It also provides
+`pd_main` method that handles the argument parsing and result formatting according to power driver schema. Finally, it
+includes `PowerDriverException` child classes that can be easily serialised to a dict that conforms to the MAAS schema.
+
+### Minimal example of a power driver
+```python
+import sys
+
+from maaspd import pd_main
+from maaspd.classes import PowerDriver
+
+
+class ConcreteAbstractPowerDriver(PowerDriver):
+ @staticmethod
+ def get_description() -> str:
+ return "Concrete Abstract power driver"
+
+ @staticmethod
+ def get_schema() -> str:
+ return "{}"
+
+ @staticmethod
+ def supports_probe() -> bool:
+ return False
+
+ @staticmethod
+ def supports_boot_ordering() -> bool:
+ return False
+
+ def power_on(self) -> None:
+ pass
+
+ def power_off(self) -> None:
+ pass
+
+ def power_query(self) -> bool:
+ pass
+
+
+def run():
+ sys.exit(pd_main(ConcreteAbstractPowerDriver))
+
+
+if __name__ == "__main__":
+ run()
+
+```
+
+In this example, `run` is an entry point for `maas-powerdriver-abstract` command.
\ No newline at end of file
diff --git a/powerdrivers/common/maaspd/__init__.py b/powerdrivers/common/maaspd/__init__.py
new file mode 100644
index 0000000..947c57f
--- /dev/null
+++ b/powerdrivers/common/maaspd/__init__.py
@@ -0,0 +1,3 @@
+from .main import pd_main
+
+__all__ = ["pd_main"]
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):
+ """
+ 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"
+ 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..8961364
--- /dev/null
+++ b/powerdrivers/common/maaspd/main.py
@@ -0,0 +1,203 @@
+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
+ 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 _input_subparser(subparsers, action, help):
+ """Helper function to register a subparser with 'input' argument"""
+ result = subparsers.add_parser(
+ action,
+ help=help,
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+ )
+ result.add_argument(
+ "input",
+ type=argparse.FileType("r"),
+ nargs='?',
+ default='-',
+ help="File to read parameters from",
+ )
+ return result
+
+
+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,
+ )
+
+ subparsers = parser.add_subparsers(dest="action")
+ subparsers.required = True
+
+ subparsers.add_parser("schema", help="Prints schema")
+
+ _input_subparser(
+ subparsers,
+ _PowerAction.POWER_ON.value,
+ help="Send power on command to the machine",
+ )
+ _input_subparser(
+ subparsers,
+ _PowerAction.POWER_OFF.value,
+ help="Send power off command to the machine",
+ )
+ _input_subparser(
+ subparsers,
+ _PowerAction.POWER_QUERY.value,
+ help="Query machine power state",
+ )
+ _input_subparser(
+ subparsers,
+ _PowerAction.PROBE.value,
+ help=_mark_unsupported(
+ "Probe chassis for a list of controlled machines",
+ not power_driver_cls.supports_probe(),
+ ),
+ )
+ _input_subparser(
+ subparsers,
+ _PowerAction.SET_BOOT_ORDER.value,
+ help=_mark_unsupported(
+ "Reorder boot sources",
+ not power_driver_cls.supports_boot_ordering(),
+ ),
+ )
+
+ 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
diff --git a/powerdrivers/common/pyproject.toml b/powerdrivers/common/pyproject.toml
new file mode 100644
index 0000000..9fb59cc
--- /dev/null
+++ b/powerdrivers/common/pyproject.toml
@@ -0,0 +1,28 @@
+[build-system]
+# With setuptools 50.0.0, 'make .ve' fails.
+requires = ["setuptools < 50.0.0", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[tool.black]
+line-length = 79
+exclude = """
+/.egg
+/.git
+/.mypy_cache
+"""
+
+[tool.isort]
+from_first = false
+force_sort_within_sections = true
+profile = "black"
+line_length = 79
+known_first_party = """
+
+"""
+order_by_type = false
+
+[tool.pytest.ini_options]
+filterwarnings = "error::BytesWarning"
+testpaths = [
+ "src",
+]
diff --git a/powerdrivers/common/setup.cfg b/powerdrivers/common/setup.cfg
new file mode 100644
index 0000000..6fb9237
--- /dev/null
+++ b/powerdrivers/common/setup.cfg
@@ -0,0 +1,61 @@
+[metadata]
+name = maaspd
+version = 1.0.0
+description = Wrapper for power drivers of MAAS (Metal As A Service)
+url = https://maas.io/
+license = AGPLv3
+author = MAAS Developers
+author_email = maas-devel@xxxxxxxxxxxxxxxxxxx
+classifiers =
+ Development Status :: 5 - Production/Stable
+ Intended Audience :: Information Technology
+ Intended Audience :: System Administrators
+ License :: OSI Approved :: GNU Affero General Public License v3
+ Operating System :: POSIX :: Linux
+ Programming Language :: Python :: 3
+ Topic :: System :: Systems Administration
+
+[options]
+packages = find:
+
+[options.packages.find]
+exclude =
+ *.tests
+
+[globals]
+lint_files =
+ setup.py
+ maaspd/
+
+cog_files =
+ pyproject.toml
+
+deps_lint =
+ black == 22.6.0
+ flake8 == 5.0.4
+ isort == 5.7.0
+ cogapp == 3.3.0
+ click == 8.1.3
+
+
+[flake8]
+ignore = E203, E266, E501, W503, W504
+
+[tox:tox]
+skipsdist = True
+envlist = format,lint
+
+[testenv:format]
+deps = {[globals]deps_lint}
+commands =
+ isort {[globals]lint_files}
+ black -q {[globals]lint_files}
+ cog -r --verbosity=1 {[globals]cog_files}
+
+[testenv:lint]
+deps = {[globals]deps_lint}
+commands =
+ isort --check-only --diff {[globals]lint_files}
+ black --check {[globals]lint_files}
+ flake8 {[globals]lint_files}
+ cog --check --verbosity=1 {[globals]cog_files}
diff --git a/powerdrivers/common/setup.py b/powerdrivers/common/setup.py
new file mode 100644
index 0000000..6068493
--- /dev/null
+++ b/powerdrivers/common/setup.py
@@ -0,0 +1,3 @@
+from setuptools import setup
+
+setup()
Follow ups
-
[Merge] ~igor-brovtsin/maas:power-drivers-wrapper into maas:master
From: Igor Brovtsin, 2023-04-18
-
Re: [UNITTESTS] -b power-drivers-wrapper lp:~igor-brovtsin/maas/+git/maas into -b master lp:~maas-committers/maas - TESTS PASS
From: MAAS Lander, 2023-03-13
-
Re: [Merge] ~igor-brovtsin/maas:power-drivers-wrapper into maas:master
From: Adam Collard, 2023-03-13
-
Re: [UNITTESTS] -b power-drivers-wrapper lp:~igor-brovtsin/maas/+git/maas into -b master lp:~maas-committers/maas - TESTS FAILED
From: MAAS Lander, 2023-01-27
-
Re: [UNITTESTS] -b power-drivers-wrapper lp:~igor-brovtsin/maas/+git/maas into -b master lp:~maas-committers/maas - TESTS FAILED
From: MAAS Lander, 2023-01-27
-
Re: [UNITTESTS] -b power-drivers-wrapper lp:~igor-brovtsin/maas/+git/maas into -b master lp:~maas-committers/maas - TESTS FAILED
From: MAAS Lander, 2023-01-26
-
Re: [Merge] ~igor-brovtsin/maas:power-drivers-wrapper into maas:master
From: Igor Brovtsin, 2023-01-26
-
Re: [Merge] ~igor-brovtsin/maas:power-drivers-wrapper into maas:master
From: Anton Troyanov, 2023-01-26
-
Re: [UNITTESTS] -b power-drivers-wrapper lp:~igor-brovtsin/maas/+git/maas into -b master lp:~maas-committers/maas - TESTS PASS
From: MAAS Lander, 2023-01-26
-
Re: [UNITTESTS] -b power-drivers-wrapper lp:~igor-brovtsin/maas/+git/maas into -b master lp:~maas-committers/maas - TESTS PASS
From: MAAS Lander, 2023-01-26
-
Re: [UNITTESTS] -b power-drivers-wrapper lp:~igor-brovtsin/maas/+git/maas into -b master lp:~maas-committers/maas - TESTS PASS
From: MAAS Lander, 2023-01-25