From adfacf8082e54900b9ed4172fcf87f127dcb7e26 Mon Sep 17 00:00:00 2001 From: Tobias Binkowski Date: Tue, 3 Dec 2024 17:16:19 +0100 Subject: [PATCH 1/2] add AMTPowerPort and AMTPowerDriver to control AMT powered devices This currently uses amtctrl as the backend as it is simpler then importing the library of the same tool. If more flexibility is needed the python amt library could be used. Signed-off-by: Tobias Binkowski --- debian/control | 2 +- doc/configuration.rst | 18 +++++++++++++ labgrid/driver/__init__.py | 2 +- labgrid/driver/powerdriver.py | 48 +++++++++++++++++++++++++++++++++++ labgrid/remote/client.py | 5 ++-- labgrid/resource/__init__.py | 2 +- labgrid/resource/power.py | 15 +++++++++++ 7 files changed, 87 insertions(+), 5 deletions(-) diff --git a/debian/control b/debian/control index b647d4715..068a45fc0 100644 --- a/debian/control +++ b/debian/control @@ -10,7 +10,7 @@ Package: labgrid Architecture: any Pre-Depends: dpkg (>= 1.16.1), python3, ${misc:Pre-Depends} Depends: ${python3:Depends}, ${misc:Depends}, ${shlibs:Depends} -Recommends: openssh-client, microcom, socat, sshfs, rsync, bash-completion +Recommends: openssh-client, microcom, socat, sshfs, rsync, bash-completion, python3-amt Description: embedded board control python library Labgrid is an embedded board control python library with a focus on testing, development and general automation. It includes a remote control layer to diff --git a/doc/configuration.rst b/doc/configuration.rst index fef3bfbba..026094232 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -310,6 +310,24 @@ NetworkYKUSHPowerPort A :any:`NetworkYKUSHPowerPort` describes a `YKUSHPowerPort`_ available on a remote computer. +AMTPowerPort +++++++++++++ +A :any:`AMTPowerPort` describes a AMT port accessible via `amtctrl`. + +.. code-block:: yaml + + AMTPowerPort: + host: 'amt-hostname' + password: 'secret-password' + +Arguments: + - host (str): hostname or ip the AMT interface of the PC is reachable + - password (str): password to use for AMT login + - timeout (int): timeout to use when polling the resource + +Used by: + - `AMTPowerDriver`_ + USBPowerPort ++++++++++++ A :any:`USBPowerPort` describes a generic switchable USB hub as supported by diff --git a/labgrid/driver/__init__.py b/labgrid/driver/__init__.py index 721256bbf..689468152 100644 --- a/labgrid/driver/__init__.py +++ b/labgrid/driver/__init__.py @@ -15,7 +15,7 @@ from .powerdriver import ManualPowerDriver, ExternalPowerDriver, \ DigitalOutputPowerDriver, YKUSHPowerDriver, \ USBPowerDriver, SiSPMPowerDriver, NetworkPowerDriver, \ - PDUDaemonDriver + PDUDaemonDriver, AMTPowerDriver from .usbloader import MXSUSBDriver, IMXUSBDriver, BDIMXUSBDriver, RKUSBDriver, UUUDriver from .usbsdmuxdriver import USBSDMuxDriver from .usbsdwiredriver import USBSDWireDriver diff --git a/labgrid/driver/powerdriver.py b/labgrid/driver/powerdriver.py index 80c8377fb..c0520856a 100644 --- a/labgrid/driver/powerdriver.py +++ b/labgrid/driver/powerdriver.py @@ -440,3 +440,51 @@ def cycle(self): @Driver.check_active def get(self): raise NotImplementedError("pdudaemon does not support retrieving the port's state") + + +@target_factory.reg_driver +@attr.s(eq=False) +class AMTPowerDriver(Driver, PowerResetMixin, PowerProtocol): + """AMTPowerDriver - Driver using an Intel AMT PowerPort""" + bindings = {"port": "AMTPowerPort", } + delay = attr.ib(default=5.0, validator=attr.validators.instance_of(float)) + + def __attrs_post_init__(self): + super().__attrs_post_init__() + + def _amt_power(self, cmd): + runstr = f"amtctrl -p {self.port.host} {cmd}" + p = subprocess.Popen(runstr.split(' '), text=True, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + stdin=subprocess.PIPE) + p.stdin.write(self.port.password) + p.stdin.close() + p.wait(self.port.timeout) + out = p.stdout.read() + p.stderr.read() + assert p.returncode == 0, "Failed to execute AMT command {cmd}, ret: {p.returncode}, {out}" + return out.strip() + + @Driver.check_active + @step() + def on(self): + self._amt_power("on") + + @Driver.check_active + @step() + def off(self): + self._amt_power("off") + + @Driver.check_active + @step() + def cycle(self): + self._amt_power("reboot") + + @Driver.check_active + def get(self): + power = self._amt_power("status") + if power == "on": + return True + elif power == "off": + return False + else: + raise ExecutionError(f"got unexpected power status {power}") diff --git a/labgrid/remote/client.py b/labgrid/remote/client.py index a55113f67..0890d281a 100755 --- a/labgrid/remote/client.py +++ b/labgrid/remote/client.py @@ -867,8 +867,7 @@ def power(self): target = self._get_target(place) from ..resource.power import NetworkPowerPort, PDUDaemonPort from ..resource.remote import NetworkUSBPowerPort, NetworkSiSPMPowerPort - from ..resource import TasmotaPowerPort, NetworkYKUSHPowerPort - + from ..resource import TasmotaPowerPort, NetworkYKUSHPowerPort, AMTPowerPort drv = None try: drv = target.get_driver("PowerProtocol", name=name) @@ -888,6 +887,8 @@ def power(self): drv = self._get_driver_or_new(target, "TasmotaPowerDriver", name=name) elif isinstance(resource, NetworkYKUSHPowerPort): drv = self._get_driver_or_new(target, "YKUSHPowerDriver", name=name) + elif isinstance(resource, AMTPowerPort): + drv = self._get_driver_or_new(target, "AMTPowerDriver", name=name) if drv: break diff --git a/labgrid/resource/__init__.py b/labgrid/resource/__init__.py index dd7554dff..1afe8ed77 100644 --- a/labgrid/resource/__init__.py +++ b/labgrid/resource/__init__.py @@ -5,7 +5,7 @@ from .modbusrtu import ModbusRTU from .networkservice import NetworkService from .onewireport import OneWirePIO -from .power import NetworkPowerPort, PDUDaemonPort +from .power import NetworkPowerPort, PDUDaemonPort, AMTPowerPort from .remote import RemotePlace from .udev import ( AlteraUSBBlaster, diff --git a/labgrid/resource/power.py b/labgrid/resource/power.py index c64861106..6f0fb9773 100644 --- a/labgrid/resource/power.py +++ b/labgrid/resource/power.py @@ -33,3 +33,18 @@ class PDUDaemonPort(Resource): host = attr.ib(validator=attr.validators.instance_of(str)) pdu = attr.ib(validator=attr.validators.instance_of(str)) index = attr.ib(validator=attr.validators.instance_of(int), converter=int) + + +@target_factory.reg_resource +@attr.s(eq=False) +class AMTPowerPort(Resource): + """The AMTPowerPort describes an Intel AMT power controllable PC with BMC + + Args: + host (str): hostname or ip the AMT interface of the PC is reachable + password (str): password to use for AMT login + timeout (int): timeout to use when polling the resource + """ + host = attr.ib(validator=attr.validators.instance_of(str)) + password = attr.ib(validator=attr.validators.instance_of(str)) + timeout = attr.ib(default=30, validator=attr.validators.instance_of(int)) From 0c9c3662b46b53a1ced5806e8bd93d4b733fbe81 Mon Sep 17 00:00:00 2001 From: Tobias Binkowski Date: Mon, 9 Dec 2024 10:05:20 +0100 Subject: [PATCH 2/2] add IPMIPowerPort and IPMIPowerDriver to control IPMI powered devices this currently uses ipmi-power tool from freeipmi-tools Signed-off-by: Tobias Binkowski --- debian/control | 2 +- doc/configuration.rst | 21 +++++++++++++++ labgrid/driver/__init__.py | 2 +- labgrid/driver/powerdriver.py | 51 +++++++++++++++++++++++++++++++++++ labgrid/remote/client.py | 4 ++- labgrid/resource/__init__.py | 2 +- labgrid/resource/power.py | 19 +++++++++++++ 7 files changed, 97 insertions(+), 4 deletions(-) diff --git a/debian/control b/debian/control index 068a45fc0..c9d35b908 100644 --- a/debian/control +++ b/debian/control @@ -10,7 +10,7 @@ Package: labgrid Architecture: any Pre-Depends: dpkg (>= 1.16.1), python3, ${misc:Pre-Depends} Depends: ${python3:Depends}, ${misc:Depends}, ${shlibs:Depends} -Recommends: openssh-client, microcom, socat, sshfs, rsync, bash-completion, python3-amt +Recommends: openssh-client, microcom, socat, sshfs, rsync, bash-completion, python3-amt, freeipmi-tools Description: embedded board control python library Labgrid is an embedded board control python library with a focus on testing, development and general automation. It includes a remote control layer to diff --git a/doc/configuration.rst b/doc/configuration.rst index 026094232..c1412ce39 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -328,6 +328,27 @@ Arguments: Used by: - `AMTPowerDriver`_ +IPMIPowerPort ++++++++++++++ +A :any:`IPMIPowerPort` describes a IPMI port accessible via `ipmi-power`. + +.. code-block:: yaml + + IPMIPowerPort: + host: 'ipmi-hostname' + username: 'admin' + password: 'secret-password' + +Arguments: + - host (str): hostname or ip the IPMI interface of the PC is reachable + - username (str): username to use for IPMI login + - password (str): password to use for IPMI login + - timeout (int): timeout to use when polling the resource + - args (str): extra args to prepend the command with + +Used by: + - `IPMIPowerDriver`_ + USBPowerPort ++++++++++++ A :any:`USBPowerPort` describes a generic switchable USB hub as supported by diff --git a/labgrid/driver/__init__.py b/labgrid/driver/__init__.py index 689468152..24454e6c3 100644 --- a/labgrid/driver/__init__.py +++ b/labgrid/driver/__init__.py @@ -15,7 +15,7 @@ from .powerdriver import ManualPowerDriver, ExternalPowerDriver, \ DigitalOutputPowerDriver, YKUSHPowerDriver, \ USBPowerDriver, SiSPMPowerDriver, NetworkPowerDriver, \ - PDUDaemonDriver, AMTPowerDriver + PDUDaemonDriver, AMTPowerDriver, IPMIPowerDriver from .usbloader import MXSUSBDriver, IMXUSBDriver, BDIMXUSBDriver, RKUSBDriver, UUUDriver from .usbsdmuxdriver import USBSDMuxDriver from .usbsdwiredriver import USBSDWireDriver diff --git a/labgrid/driver/powerdriver.py b/labgrid/driver/powerdriver.py index c0520856a..5ef7ba9b3 100644 --- a/labgrid/driver/powerdriver.py +++ b/labgrid/driver/powerdriver.py @@ -1,4 +1,5 @@ import shlex +import subprocess import time import math from importlib import import_module @@ -488,3 +489,53 @@ def get(self): return False else: raise ExecutionError(f"got unexpected power status {power}") + + +@target_factory.reg_driver +@attr.s(eq=False) +class IPMIPowerDriver(Driver, PowerResetMixin, PowerProtocol): + """IPMIPowerDriver - Driver using an IPMI PowerPort""" + bindings = {"port": "IPMIPowerPort", } + delay = attr.ib(default=5.0, validator=attr.validators.instance_of(float)) + + def _ipmi_power(self, cmd): + runstr = f"ipmi-power -h {self.port.host} -u {self.port.username} " + runstr += f"--session-timeout={self.port.timeout*1000} " + runstr += f"-p {self.port.password} {cmd}" + runstr = runstr.split(' ') + if len(self.port.args) > 0: + runstr.extend(self.port.args.split(' ')) + return subprocess.run(runstr, capture_output=True) + + def _ipmi_cmd(self, cmd): + ret = self._ipmi_power(cmd) + stdout = ret.stdout.decode('utf-8').split(' ') + assert self.port.host == stdout[0][:-1] + assert "ok" == stdout[1][:-1] + + @Driver.check_active + @step() + def on(self): + self._ipmi_cmd('--on') + + @Driver.check_active + @step() + def off(self): + self._ipmi_cmd('--off') + + @Driver.check_active + @step() + def cycle(self): + self._ipmi_cmd('--cycle') + + @Driver.check_active + def get(self): + ret = self._ipmi_power('--stat') + stdout = ret.stdout.decode('utf-8').split(' ') + assert self.port.host == stdout[0][:-1] + power = stdout[1][:-1] + if power == 'off': + return False + elif power == 'on': + return True + raise ExecutionError(f"got unexpected power status {power}") diff --git a/labgrid/remote/client.py b/labgrid/remote/client.py index 0890d281a..33b77d337 100755 --- a/labgrid/remote/client.py +++ b/labgrid/remote/client.py @@ -867,7 +867,7 @@ def power(self): target = self._get_target(place) from ..resource.power import NetworkPowerPort, PDUDaemonPort from ..resource.remote import NetworkUSBPowerPort, NetworkSiSPMPowerPort - from ..resource import TasmotaPowerPort, NetworkYKUSHPowerPort, AMTPowerPort + from ..resource import TasmotaPowerPort, NetworkYKUSHPowerPort, AMTPowerPort, IPMIPowerPort drv = None try: drv = target.get_driver("PowerProtocol", name=name) @@ -889,6 +889,8 @@ def power(self): drv = self._get_driver_or_new(target, "YKUSHPowerDriver", name=name) elif isinstance(resource, AMTPowerPort): drv = self._get_driver_or_new(target, "AMTPowerDriver", name=name) + elif isinstance(resource, IPMIPowerPort): + drv = self._get_driver_or_new(target, "IPMIPowerDriver", name=name) if drv: break diff --git a/labgrid/resource/__init__.py b/labgrid/resource/__init__.py index 1afe8ed77..af9e355a4 100644 --- a/labgrid/resource/__init__.py +++ b/labgrid/resource/__init__.py @@ -5,7 +5,7 @@ from .modbusrtu import ModbusRTU from .networkservice import NetworkService from .onewireport import OneWirePIO -from .power import NetworkPowerPort, PDUDaemonPort, AMTPowerPort +from .power import NetworkPowerPort, PDUDaemonPort, AMTPowerPort, IPMIPowerPort from .remote import RemotePlace from .udev import ( AlteraUSBBlaster, diff --git a/labgrid/resource/power.py b/labgrid/resource/power.py index 6f0fb9773..253335a18 100644 --- a/labgrid/resource/power.py +++ b/labgrid/resource/power.py @@ -48,3 +48,22 @@ class AMTPowerPort(Resource): host = attr.ib(validator=attr.validators.instance_of(str)) password = attr.ib(validator=attr.validators.instance_of(str)) timeout = attr.ib(default=30, validator=attr.validators.instance_of(int)) + + +@target_factory.reg_resource +@attr.s(eq=False) +class IPMIPowerPort(Resource): + """The IPMIPowerPrt describes an IPMI controllable PC with BMC + + Args: + host (str): hostname or ip the IPMI interface of the PC is reachable + username (str): username to use for IPMI login + password (str): password to use for IPMI login + timeout (int): timeout to use when polling the resource + args (str): extra args to prepend the command with + """ + host = attr.ib(validator=attr.validators.instance_of(str)) + username = attr.ib(validator=attr.validators.instance_of(str)) + password = attr.ib(validator=attr.validators.instance_of(str)) + timeout = attr.ib(default=30, validator=attr.validators.instance_of(int)) + args = attr.ib(default="", validator=attr.validators.instance_of(str))