feat: switch the server's power over IPMI (0.3.0)

IpmiDriver.set_power_state sends an IPMI chassis control: On, GracefulShutdown,
ForceOff and ForceRestart. get_power_state now offers what fits the server's
state: On when it is off, the other three when it is on. IPMI has no graceful
restart and pyghmi offers no power cycle, so neither is offered. The driver
still does not reset the BMC.

For boards that offer nothing but IPMI, such as a Supermicro X10 without a
Redfish license.
This commit is contained in:
Christian Manivong committed 2026-10-17 01:04:43 +02:00
1 parent 0e38e18e26
commit 6c60f7ac60
8 files changed
+137 -25

No files matched your search

+67 -6
View File
@@ -1,7 +1,7 @@
"""IpmiDriver against recorded IPMI answers of a Supermicro X10DRL-i (BMC 3.91).
The board refuses Redfish without a license; IPMI answers without one. The driver
is read-only: it neither switches the server's power nor resets the BMC.
reads, and switches the server's power; it does not reset the BMC.
"""
import json
@@ -31,6 +31,8 @@ class FakeCommand:
def __init__(self, bmc=None, userid=None, password=None, port=623, **kw):
self.args = {"bmc": bmc, "userid": userid, "password": password, "port": port}
self.logged_out = False
self.powerstate = fixture("power")["powerstate"]
self.power_requests: list = []
self.ipmi_session = SimpleNamespace(logout=self._logout)
FakeCommand.instances.append(self)
@@ -38,7 +40,11 @@ class FakeCommand:
self.logged_out = True
def get_power(self):
return fixture("power")
return {"powerstate": self.powerstate}
def set_power(self, powerstate, wait=False):
self.power_requests.append((powerstate, wait))
return {"pendingpowerstate": powerstate}
def get_net_configuration(self):
return fixture("net_configuration")
@@ -78,13 +84,13 @@ def assert_shape(value, typed_dict):
# --- class and connection ------------------------------------------------------------------
def test_role_and_read_only():
def test_role_power_control_and_no_bmc_reset():
assert issubclass(IpmiDriver, ManagementControllerDriver)
assert primary_role_of(IpmiDriver) == "management_controller"
assert IpmiDriver.USES_SSH is False
assert IpmiDriver.DRIVER_NAME == "ipmi"
# Read-only: no power control, no BMC reset over IPMI.
assert not hasattr(IpmiDriver, "set_power_state")
# The server's power can be switched; the BMC is not reset over IPMI.
assert callable(getattr(IpmiDriver, "set_power_state", None))
assert not hasattr(IpmiDriver, "reboot_host")
@@ -195,7 +201,62 @@ def test_sensor_health_flags(driver, monkeypatch):
def test_get_power_state(driver):
state = driver.get_power_state()
assert_shape(state, models.PowerStateDict)
assert state == {"state": "On", "actions": []}
assert state == {"state": "On",
"actions": ["GracefulShutdown", "ForceOff", "ForceRestart"]}
def test_a_server_that_is_off_can_only_be_switched_on(driver):
FakeCommand.instances[0].powerstate = "off"
assert driver.get_power_state() == {"state": "Off", "actions": ["On"]}
def test_every_offered_action_is_a_role_power_action(driver):
for powerstate in ("on", "off"):
FakeCommand.instances[0].powerstate = powerstate
assert set(driver.get_power_state()["actions"]) <= set(models.POWER_ACTIONS)
@pytest.mark.parametrize("action, powerstate, expected", [
("GracefulShutdown", "on", "shutdown"),
("ForceOff", "on", "off"),
("ForceRestart", "on", "reset"),
("On", "off", "on"),
])
def test_set_power_state_sends_the_chassis_control(driver, action, powerstate, expected):
cmd = FakeCommand.instances[0]
cmd.powerstate = powerstate
driver.set_power_state(action)
# Returns once the BMC has accepted the request, not when the server is done.
assert cmd.power_requests == [(expected, False)]
def test_set_power_state_rejects_an_unknown_action(driver):
with pytest.raises(ValueError, match="Reboot"):
driver.set_power_state("Reboot")
assert FakeCommand.instances[0].power_requests == []
@pytest.mark.parametrize("action, powerstate", [
("On", "on"),
("ForceOff", "off"),
("GracefulRestart", "on"), # IPMI has no graceful restart
("PowerCycle", "on"),
])
def test_set_power_state_refuses_what_is_not_offered_now(driver, action, powerstate):
cmd = FakeCommand.instances[0]
cmd.powerstate = powerstate
with pytest.raises(RuntimeError, match=f"{action} not accepted"):
driver.set_power_state(action)
assert cmd.power_requests == []
def test_a_refused_power_request_is_a_runtime_error(driver):
def refuse(powerstate, wait=False):
raise ipmi.IpmiException("Insufficient privilege level")
FakeCommand.instances[0].set_power = refuse
with pytest.raises(RuntimeError, match="ForceOff refused: Insufficient privilege"):
driver.set_power_state("ForceOff")
def test_get_hardware_inventory(driver):