Skip to content

Add powerdrivers for AMT and IPMI powered PC's #1603

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion debian/control
Original file line number Diff line number Diff line change
Expand Up @@ -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, 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
Expand Down
39 changes: 39 additions & 0 deletions doc/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,45 @@ 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`_

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
Expand Down
2 changes: 1 addition & 1 deletion labgrid/driver/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from .powerdriver import ManualPowerDriver, ExternalPowerDriver, \
DigitalOutputPowerDriver, YKUSHPowerDriver, \
USBPowerDriver, SiSPMPowerDriver, NetworkPowerDriver, \
PDUDaemonDriver
PDUDaemonDriver, AMTPowerDriver, IPMIPowerDriver
from .usbloader import MXSUSBDriver, IMXUSBDriver, BDIMXUSBDriver, RKUSBDriver, UUUDriver
from .usbsdmuxdriver import USBSDMuxDriver
from .usbsdwiredriver import USBSDWireDriver
Expand Down
99 changes: 99 additions & 0 deletions labgrid/driver/powerdriver.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import shlex
import subprocess
import time
import math
from importlib import import_module
Expand Down Expand Up @@ -440,3 +441,101 @@ 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}")


@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}")
7 changes: 5 additions & 2 deletions labgrid/remote/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, IPMIPowerPort
drv = None
try:
drv = target.get_driver("PowerProtocol", name=name)
Expand All @@ -888,6 +887,10 @@ 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)
elif isinstance(resource, IPMIPowerPort):
drv = self._get_driver_or_new(target, "IPMIPowerDriver", name=name)
if drv:
break

Expand Down
2 changes: 1 addition & 1 deletion labgrid/resource/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, IPMIPowerPort
from .remote import RemotePlace
from .udev import (
AlteraUSBBlaster,
Expand Down
34 changes: 34 additions & 0 deletions labgrid/resource/power.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,37 @@ 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))


@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))