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:
1 parent
0e38e18e26
commit
6c60f7ac60
8 files changed
+137
-25
No files matched your search
+67
-6
@@ -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):
|
||||
|
||||
Reference in new issue
Block a user