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
@@ -7,5 +7,5 @@ takes the first driver class of this package. ``IpmiDriver`` lives in
|
||||
|
||||
from napalm_redfish.driver import RedfishDriver
|
||||
|
||||
__version__ = "0.2.1"
|
||||
__version__ = "0.3.0"
|
||||
__all__ = ["RedfishDriver"]
|
||||
+42
-7
@@ -1,9 +1,9 @@
|
||||
"""Read-only NAPALM driver for BMCs over IPMI 2.0, for boards that refuse or lack Redfish.
|
||||
|
||||
IPMI over LAN gives a password hash to anyone who names a valid user (CVE-2013-4786), so
|
||||
this is the fallback, not the main path. It only reads: facts and inventory from the FRU,
|
||||
sensors, the power state and the system event log. It neither switches the server's power
|
||||
nor resets the BMC; those go through Redfish.
|
||||
this is the fallback, not the main path. It reads facts and inventory from the FRU,
|
||||
sensors, the power state and the system event log, and switches the server's power
|
||||
(chassis control). It does not reset the BMC; that goes through Redfish.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -12,6 +12,8 @@ from typing import Any
|
||||
|
||||
from napalm.base.exceptions import ConnectionException
|
||||
from napalm_device_types import HealthMetricsMixin, ManagementControllerDriver
|
||||
from napalm_device_types.models import POWER_ACTIONS
|
||||
from pyghmi.exceptions import IpmiException
|
||||
from pyghmi.ipmi.command import Command
|
||||
from pyghmi.ipmi.private.session import Session
|
||||
|
||||
@@ -21,6 +23,14 @@ _JsonDict = dict[str, Any]
|
||||
_WARNING, _CRITICAL, _FAILED = 1, 2, 4
|
||||
#: The only interface a BMC reports over IPMI: its own LAN channel.
|
||||
_BMC = "bmc"
|
||||
#: Role power actions -> pyghmi's chassis control. IPMI has no graceful restart, and pyghmi
|
||||
#: offers no power cycle; neither is offered.
|
||||
_CHASSIS_CONTROL = {
|
||||
"On": "on",
|
||||
"GracefulShutdown": "shutdown",
|
||||
"ForceOff": "off",
|
||||
"ForceRestart": "reset",
|
||||
}
|
||||
|
||||
|
||||
def _severity(health: int) -> str:
|
||||
@@ -36,7 +46,7 @@ def _supply_present(reading: Any) -> bool:
|
||||
|
||||
|
||||
class IpmiDriver(HealthMetricsMixin, ManagementControllerDriver):
|
||||
"""NAPALM driver for BMCs over IPMI 2.0 (lanplus), read-only.
|
||||
"""NAPALM driver for BMCs over IPMI 2.0 (lanplus): reads, and the server's power.
|
||||
|
||||
``HealthMetricsMixin``: the BMC's own load, memory and interfaces over SNMP (UCD-MIB),
|
||||
where the BMC runs net-snmp. The managed server's sensors come from ``get_environment``.
|
||||
@@ -185,10 +195,35 @@ class IpmiDriver(HealthMetricsMixin, ManagementControllerDriver):
|
||||
|
||||
# --- role methods --------------------------------------------------------------------
|
||||
|
||||
def _power_state(self) -> str:
|
||||
return str(self._cmd().get_power().get("powerstate") or "").capitalize()
|
||||
|
||||
@staticmethod
|
||||
def _accepted_actions(state: str) -> list[str]:
|
||||
"""The power actions that make sense in ``state``; IPMI advertises none itself."""
|
||||
if state == "Off":
|
||||
return ["On"]
|
||||
if state == "On":
|
||||
return [a for a in POWER_ACTIONS if a in _CHASSIS_CONTROL and a != "On"]
|
||||
return []
|
||||
|
||||
def get_power_state(self) -> _JsonDict:
|
||||
state = str(self._cmd().get_power().get("powerstate") or "")
|
||||
# Read-only: no action is offered.
|
||||
return {"state": state.capitalize(), "actions": []}
|
||||
state = self._power_state()
|
||||
return {"state": state, "actions": self._accepted_actions(state)}
|
||||
|
||||
def set_power_state(self, action: str) -> None:
|
||||
if action not in POWER_ACTIONS:
|
||||
raise ValueError(f"{action!r} is not one of {', '.join(POWER_ACTIONS)}")
|
||||
state = self._power_state()
|
||||
accepted = self._accepted_actions(state)
|
||||
if action not in accepted:
|
||||
raise RuntimeError(
|
||||
f"{self.hostname}: {action} not accepted while the server is "
|
||||
f"{state or 'in an unknown state'} (accepted: {', '.join(accepted) or 'none'})")
|
||||
try:
|
||||
self._cmd().set_power(_CHASSIS_CONTROL[action], wait=False)
|
||||
except IpmiException as exc:
|
||||
raise RuntimeError(f"{self.hostname}: {action} refused: {exc}") from exc
|
||||
|
||||
def get_hardware_inventory(self) -> _JsonDict:
|
||||
"""What the FRU and the sensor records tell; IPMI lists no CPUs, DIMMs or drives."""
|
||||
|
||||
Reference in new issue
Block a user