feat: add ManagementControllerDriver for server BMCs
A role base for baseboard management controllers (Supermicro, Dell iDRAC, HPE iLO, Lenovo XCC), reached over Redfish or IPMI, not SSH. It declares, under TYPE_CHECKING only: get_power_state, set_power_state (Redfish ResetType actions), get_hardware_inventory (CPUs, DIMMs, drives, PSUs, NICs, and the server's serial, system UUID and NIC MACs), get_event_log, get_firmware_inventory and get_management_protocols. reboot_host restarts the BMC itself (Manager.Reset), never the managed server, which goes through set_power_state. No HealthMetricsMixin and no command channel; sensors come through NAPALM's get_environment.
This commit is contained in:
1 parent
dd8977750d
commit
e509445572
7 files changed
+364
-2
No files matched your search
@@ -36,6 +36,16 @@ class QnapQtsDriver(StorageDriver, HypervisorDriver, LinuxDriver):
|
||||
`roles_of(cls)`, `role_keys_of(cls)` and `primary_role_of(cls)` read that back. Nothing
|
||||
restates the ranking: there is no precedence table and no attribute to override.
|
||||
|
||||
A server's baseboard management controller (Supermicro BMC, Dell iDRAC, HPE iLO,
|
||||
Lenovo XCC) is a role of its own, `ManagementControllerDriver` (`management_controller`),
|
||||
reached over Redfish or IPMI (`USES_SSH = False`). It is not the server it manages:
|
||||
`reboot_host` restarts the BMC (Redfish `Manager.Reset`), while `set_power_state`
|
||||
(`On`, `GracefulShutdown`, `ForceOff`, `GracefulRestart`, `ForceRestart`, `PowerCycle`)
|
||||
acts on the server. `get_hardware_inventory` carries the server's serial number, system
|
||||
UUID and NIC MACs -- what ties the BMC to the host it manages. Its sensors come through
|
||||
NAPALM's `get_environment`, with `cpu` and `memory` empty, since a BMC does not know the
|
||||
host OS's load; it has no `HealthMetricsMixin` (SNMP) and no command channel.
|
||||
|
||||
### Role bases declare; they never implement
|
||||
|
||||
**A role base must not contain a single runtime method — not even a
|
||||
@@ -258,6 +268,7 @@ Requires Python ≥ 3.10 (tested up to 3.13) and NAPALM ≥ 4.0.
|
||||
| `ResidentialGatewayDriver` | Router + firewall + AP in one box | OpenWrt, FritzBox |
|
||||
| `PhoneDriver` | IP telephony endpoints | Yealink |
|
||||
| `MediaDriver` | Speakers, streamers, media renderers | Sonos, Chromecast |
|
||||
| `ManagementControllerDriver` | Server baseboard management controllers | Supermicro BMC, Dell iDRAC, HPE iLO, Lenovo XCC |
|
||||
|
||||
Mixins that come with the classes above: `PingSweepMixin` (subnet sweeps, in
|
||||
every driver through `DeviceTypeDriver`) and `DhcpServerMixin` (static DHCP
|
||||
|
||||
@@ -29,6 +29,7 @@ Role bases -- what a device *is*:
|
||||
* :class:`~napalm_device_types.storage.StorageDriver`
|
||||
* :class:`~napalm_device_types.phone.PhoneDriver`
|
||||
* :class:`~napalm_device_types.media.MediaDriver`
|
||||
* :class:`~napalm_device_types.management_controller.ManagementControllerDriver`
|
||||
* :class:`~napalm_device_types.residential_gateway.ResidentialGatewayDriver`
|
||||
|
||||
Function classes -- what a device *can do*. Shared behaviour lives here once
|
||||
@@ -92,7 +93,17 @@ from napalm_device_types.listening import (
|
||||
parse_listening_sockets,
|
||||
)
|
||||
from napalm_device_types.mac_acl import MacAclMixin
|
||||
from napalm_device_types.management_controller import ManagementControllerDriver
|
||||
from napalm_device_types.media import MediaDriver
|
||||
from napalm_device_types.models import (
|
||||
POWER_ACTIONS,
|
||||
EventLogEntryDict,
|
||||
FirmwareDict,
|
||||
HardwareInventoryDict,
|
||||
ManagementProtocolsDict,
|
||||
PowerAction,
|
||||
PowerStateDict,
|
||||
)
|
||||
from napalm_device_types.nat_vpn import NatVpnMixin
|
||||
from napalm_device_types.packages import PackageManagementMixin
|
||||
from napalm_device_types.phone import PhoneDriver
|
||||
@@ -154,6 +165,14 @@ __all__ = [
|
||||
"HypervisorDriver",
|
||||
"InterfaceFilterMixin",
|
||||
"MacAclMixin",
|
||||
"ManagementControllerDriver",
|
||||
"POWER_ACTIONS",
|
||||
"PowerAction",
|
||||
"PowerStateDict",
|
||||
"HardwareInventoryDict",
|
||||
"EventLogEntryDict",
|
||||
"FirmwareDict",
|
||||
"ManagementProtocolsDict",
|
||||
"MediaDriver",
|
||||
"NatVpnMixin",
|
||||
"OSDriver",
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Abstract base class for server baseboard management controllers (BMCs).
|
||||
|
||||
Usage::
|
||||
|
||||
from napalm_device_types import ManagementControllerDriver
|
||||
|
||||
class IdracDriver(ManagementControllerDriver):
|
||||
...
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING, List
|
||||
|
||||
from napalm_device_types.base import DeviceTypeDriver
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover - for type checkers only
|
||||
from napalm_device_types.models import (
|
||||
EventLogEntryDict,
|
||||
FirmwareDict,
|
||||
HardwareInventoryDict,
|
||||
ManagementProtocolsDict,
|
||||
PowerAction,
|
||||
PowerStateDict,
|
||||
)
|
||||
|
||||
|
||||
class ManagementControllerDriver(DeviceTypeDriver):
|
||||
"""
|
||||
Abstract intermediate driver for the management controller of a server
|
||||
(e.g. Supermicro BMC, Dell iDRAC, HPE iLO, Lenovo XCC), reached over
|
||||
Redfish or IPMI rather than SSH.
|
||||
|
||||
A BMC is not the server it manages. ``reboot_host`` restarts the BMC
|
||||
itself (Redfish ``Manager.Reset``), never the managed server; the server is
|
||||
switched on, off and restarted through :meth:`set_power_state`.
|
||||
|
||||
The BMC's sensors (temperatures, fans, PSU power) come through NAPALM's
|
||||
``get_environment``, with ``cpu`` and ``memory`` empty: a BMC does not know
|
||||
the load of the operating system on its server. There is no
|
||||
``HealthMetricsMixin`` here -- that is SNMP's -- and
|
||||
``CommandChannelMixin`` stays unimplemented: a BMC offers no shell to run a
|
||||
command in.
|
||||
"""
|
||||
|
||||
#: Stable key netOrk exposes as ``device_class``. The order in which a
|
||||
#: driver lists its role bases is the ranking; see
|
||||
#: :func:`napalm_device_types.roles.primary_role_of`.
|
||||
ROLE: str = "management_controller"
|
||||
TYPE_LABEL: str = "Management Controller"
|
||||
USES_SSH: bool = False
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
def get_power_state(self) -> PowerStateDict:
|
||||
"""
|
||||
Returns the managed server's power state.
|
||||
|
||||
* state (string) - ``"On"``, ``"Off"``, ``"PoweringOn"`` or ``"PoweringOff"``
|
||||
* actions (list) - the :data:`~napalm_device_types.models.PowerAction`
|
||||
values the BMC accepts right now
|
||||
|
||||
Example::
|
||||
|
||||
{"state": "On", "actions": ["GracefulShutdown", "ForceOff", "ForceRestart"]}
|
||||
"""
|
||||
...
|
||||
|
||||
def set_power_state(self, action: PowerAction) -> None:
|
||||
"""
|
||||
Switches, shuts down or restarts the managed server.
|
||||
|
||||
Returns once the BMC has accepted the request; the server takes its
|
||||
time. ``GracefulShutdown`` and ``GracefulRestart`` ask the operating
|
||||
system (ACPI) and may be ignored by it.
|
||||
|
||||
:param action: One of ``On``, ``GracefulShutdown``, ``ForceOff``,
|
||||
``GracefulRestart``, ``ForceRestart`` or ``PowerCycle``.
|
||||
:raises ValueError: If *action* is not one of them.
|
||||
:raises RuntimeError: If the BMC refuses it, e.g. ``On`` for a
|
||||
server that is already on.
|
||||
"""
|
||||
...
|
||||
|
||||
def get_hardware_inventory(self) -> HardwareInventoryDict:
|
||||
"""
|
||||
Returns the managed server's hardware: CPUs, DIMMs, drives, power
|
||||
supplies and NICs, with the server's serial number, its SMBIOS
|
||||
system UUID and the MAC address of each NIC -- what ties the BMC to
|
||||
the operating system running on the server.
|
||||
|
||||
Example::
|
||||
|
||||
{
|
||||
"manufacturer": "Dell Inc.",
|
||||
"model": "PowerEdge R650",
|
||||
"serial_number": "7XK2Q93",
|
||||
"system_uuid": "4c4c4544-0058-4b10-8032-b7c04f513933",
|
||||
"cpus": [{"socket": "CPU.Socket.1", "model": "Intel(R) Xeon(R) Silver 4314",
|
||||
"cores": 16, "threads": 32, "status": "OK"}],
|
||||
"memory": [...],
|
||||
"drives": [...],
|
||||
"power_supplies": [...],
|
||||
"nics": [{"name": "NIC.Integrated.1-1", "mac_address": "b0:7b:25:de:1a:40",
|
||||
"speed_mbps": 10000, "link_up": True}],
|
||||
}
|
||||
"""
|
||||
...
|
||||
|
||||
def get_event_log(self, limit: int = 500) -> List[EventLogEntryDict]:
|
||||
"""
|
||||
Returns the BMC's system event log (SEL, Lifecycle Log, IML),
|
||||
newest first.
|
||||
|
||||
:param limit: At most this many entries.
|
||||
"""
|
||||
...
|
||||
|
||||
def get_firmware_inventory(self) -> List[FirmwareDict]:
|
||||
"""
|
||||
Returns every firmware component the BMC reports: its own, the
|
||||
BIOS, storage controllers, NICs, power supplies.
|
||||
|
||||
Example::
|
||||
|
||||
[
|
||||
{"name": "Integrated Dell Remote Access Controller",
|
||||
"version": "7.00.00.172", "updateable": True},
|
||||
{"name": "BIOS", "version": "1.13.2", "updateable": True},
|
||||
]
|
||||
"""
|
||||
...
|
||||
|
||||
def get_management_protocols(self) -> ManagementProtocolsDict:
|
||||
"""
|
||||
Returns what the BMC itself listens on: IPMI, SSH, HTTP, HTTPS,
|
||||
SNMP, the KVM console and virtual media, each with whether it is
|
||||
enabled and its port.
|
||||
|
||||
Example::
|
||||
|
||||
{
|
||||
"ipmi": {"enabled": False, "port": 623},
|
||||
"ssh": {"enabled": True, "port": 22},
|
||||
"http": {"enabled": False, "port": 80},
|
||||
"https": {"enabled": True, "port": 443},
|
||||
"snmp": {"enabled": False, "port": 161},
|
||||
"kvm": {"enabled": True, "port": 5900},
|
||||
"virtual_media": {"enabled": True, "port": None},
|
||||
}
|
||||
"""
|
||||
...
|
||||
@@ -5,7 +5,7 @@ These supplement the types defined in napalm.base.models and are used by the
|
||||
abstract device-type driver classes in this package.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional
|
||||
from typing import Dict, List, Literal, Optional, Tuple, get_args
|
||||
from typing_extensions import NotRequired, TypedDict
|
||||
|
||||
|
||||
@@ -1035,3 +1035,119 @@ class PingSweepResultDict(TypedDict):
|
||||
scanned: int # destinations actually probed
|
||||
alive_count: int # entries with alive=True
|
||||
truncated: bool # True if targets were dropped at the sweep cap
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Management controller (BMC: Supermicro, iDRAC, iLO, XCC)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
#: What ``set_power_state`` does to the managed server; Redfish's ResetType names.
|
||||
PowerAction = Literal[
|
||||
"On",
|
||||
"GracefulShutdown",
|
||||
"ForceOff",
|
||||
"GracefulRestart",
|
||||
"ForceRestart",
|
||||
"PowerCycle",
|
||||
]
|
||||
POWER_ACTIONS: Tuple[str, ...] = get_args(PowerAction)
|
||||
|
||||
|
||||
class PowerStateDict(TypedDict):
|
||||
"""The managed server's power, as its BMC reports it."""
|
||||
|
||||
state: str # "On", "Off", "PoweringOn" or "PoweringOff"
|
||||
actions: List[str] # the PowerAction values this BMC accepts now
|
||||
|
||||
|
||||
class BmcCpuDict(TypedDict):
|
||||
socket: str # "CPU1", "Proc 1"
|
||||
model: str # "Intel(R) Xeon(R) Silver 4314 CPU @ 2.40GHz"
|
||||
cores: Optional[int]
|
||||
threads: Optional[int]
|
||||
status: str # "OK", "Warning", "Critical", "Absent"
|
||||
|
||||
|
||||
class BmcDimmDict(TypedDict):
|
||||
slot: str # "DIMMA1", "PROC 1 DIMM 3"
|
||||
size_bytes: Optional[int]
|
||||
type: str # "DDR4", "DDR5"; empty when unknown
|
||||
speed_mts: Optional[int] # MT/s
|
||||
part_number: str
|
||||
serial_number: str
|
||||
status: str
|
||||
|
||||
|
||||
class BmcDriveDict(TypedDict):
|
||||
slot: str
|
||||
model: str
|
||||
serial_number: str
|
||||
size_bytes: Optional[int]
|
||||
media: str # "HDD", "SSD"; empty when unknown
|
||||
protocol: str # "SATA", "SAS", "NVMe"; empty when unknown
|
||||
status: str
|
||||
|
||||
|
||||
class BmcPsuDict(TypedDict):
|
||||
slot: str
|
||||
model: str
|
||||
serial_number: str
|
||||
capacity_watts: Optional[int]
|
||||
status: str
|
||||
|
||||
|
||||
class BmcNicDict(TypedDict):
|
||||
"""A network port of the managed server, not of the BMC."""
|
||||
|
||||
name: str # "NIC.Integrated.1-1", "Port 1"
|
||||
mac_address: str # lowercase, colon-separated; empty when unknown
|
||||
speed_mbps: Optional[int]
|
||||
link_up: Optional[bool]
|
||||
|
||||
|
||||
class HardwareInventoryDict(TypedDict):
|
||||
"""The managed server's hardware, as its BMC sees it."""
|
||||
|
||||
manufacturer: str
|
||||
model: str
|
||||
serial_number: str # the server's, not the BMC's
|
||||
system_uuid: str # SMBIOS system UUID, lowercase
|
||||
cpus: List[BmcCpuDict]
|
||||
memory: List[BmcDimmDict]
|
||||
drives: List[BmcDriveDict]
|
||||
power_supplies: List[BmcPsuDict]
|
||||
nics: List[BmcNicDict]
|
||||
|
||||
|
||||
class EventLogEntryDict(TypedDict):
|
||||
"""One entry of the BMC's system event log (SEL / Lifecycle / IML)."""
|
||||
|
||||
id: str
|
||||
timestamp: Optional[float] # Unix time; None when the BMC gave none
|
||||
severity: str # "OK", "Warning" or "Critical"
|
||||
message: str
|
||||
|
||||
|
||||
class FirmwareDict(TypedDict):
|
||||
"""One firmware component: the BMC's own, BIOS, a controller, a NIC."""
|
||||
|
||||
name: str # "BMC", "BIOS", "PERC H755 Front"
|
||||
version: str
|
||||
updateable: Optional[bool]
|
||||
|
||||
|
||||
class ProtocolStateDict(TypedDict):
|
||||
enabled: bool
|
||||
port: Optional[int] # None for a protocol without a port of its own
|
||||
|
||||
|
||||
class ManagementProtocolsDict(TypedDict):
|
||||
"""What the BMC itself listens on."""
|
||||
|
||||
ipmi: ProtocolStateDict
|
||||
ssh: ProtocolStateDict
|
||||
http: ProtocolStateDict
|
||||
https: ProtocolStateDict
|
||||
snmp: ProtocolStateDict
|
||||
kvm: ProtocolStateDict
|
||||
virtual_media: ProtocolStateDict
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "napalm-device-types"
|
||||
version = "4.5.1"
|
||||
version = "4.6.0"
|
||||
description = "Abstract device-type base classes for NAPALM drivers"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -22,6 +22,7 @@ from napalm_device_types import (
|
||||
DeviceTypeDriver,
|
||||
FirewallDriver,
|
||||
HypervisorDriver,
|
||||
ManagementControllerDriver,
|
||||
MediaDriver,
|
||||
OSDriver,
|
||||
PhoneDriver,
|
||||
@@ -35,6 +36,7 @@ ROLE_BASES = [
|
||||
AccessPointDriver,
|
||||
FirewallDriver,
|
||||
HypervisorDriver,
|
||||
ManagementControllerDriver,
|
||||
MediaDriver,
|
||||
OSDriver,
|
||||
PhoneDriver,
|
||||
@@ -84,6 +86,12 @@ class TestDeclaredMethodsDoNotExistAtRuntime:
|
||||
(ResidentialGatewayDriver, "get_wan_status"),
|
||||
(PhoneDriver, "get_sip_accounts"),
|
||||
(MediaDriver, "get_playback_state"),
|
||||
(ManagementControllerDriver, "get_power_state"),
|
||||
(ManagementControllerDriver, "set_power_state"),
|
||||
(ManagementControllerDriver, "get_hardware_inventory"),
|
||||
(ManagementControllerDriver, "get_event_log"),
|
||||
(ManagementControllerDriver, "get_firmware_inventory"),
|
||||
(ManagementControllerDriver, "get_management_protocols"),
|
||||
],
|
||||
)
|
||||
def test_absent_until_a_driver_implements_it(self, base, method):
|
||||
@@ -139,3 +147,57 @@ class TestRoleIntrospection:
|
||||
|
||||
assert roles_of(Bare) == []
|
||||
assert primary_role_of(Bare) is None
|
||||
|
||||
|
||||
class TestManagementController:
|
||||
"""A BMC is reached over Redfish or IPMI, and it is not the server it manages."""
|
||||
|
||||
def test_role_key_and_transport(self):
|
||||
assert ManagementControllerDriver.ROLE == "management_controller"
|
||||
assert ManagementControllerDriver.USES_SSH is False
|
||||
|
||||
def test_power_actions_are_redfish_reset_types(self):
|
||||
from napalm_device_types import POWER_ACTIONS
|
||||
|
||||
assert POWER_ACTIONS == (
|
||||
"On",
|
||||
"GracefulShutdown",
|
||||
"ForceOff",
|
||||
"GracefulRestart",
|
||||
"ForceRestart",
|
||||
"PowerCycle",
|
||||
)
|
||||
|
||||
def test_carries_neither_snmp_health_metrics_nor_a_command_channel(self):
|
||||
"""Health metrics are SNMP; the BMC's sensors come through get_environment."""
|
||||
from napalm_device_types import HealthMetricsMixin
|
||||
|
||||
assert not issubclass(ManagementControllerDriver, HealthMetricsMixin)
|
||||
assert not hasattr(ManagementControllerDriver, "get_health_metrics")
|
||||
assert not hasattr(ManagementControllerDriver, "run_command")
|
||||
assert not hasattr(ManagementControllerDriver, "open_stream")
|
||||
|
||||
def test_docstring_says_reboot_host_restarts_the_bmc_not_the_server(self):
|
||||
doc = ManagementControllerDriver.__doc__ or ""
|
||||
assert "reboot_host" in doc and "Manager.Reset" in doc
|
||||
assert "set_power_state" in doc
|
||||
assert "get_environment" in doc
|
||||
|
||||
def test_its_return_types_are_exported(self):
|
||||
import napalm_device_types as package
|
||||
|
||||
for name in (
|
||||
"PowerAction",
|
||||
"PowerStateDict",
|
||||
"HardwareInventoryDict",
|
||||
"EventLogEntryDict",
|
||||
"FirmwareDict",
|
||||
"ManagementProtocolsDict",
|
||||
):
|
||||
assert name in package.__all__ and hasattr(package, name), name
|
||||
|
||||
def test_a_server_driver_can_rank_it_first(self):
|
||||
class Idrac(ManagementControllerDriver):
|
||||
pass
|
||||
|
||||
assert primary_role_of(Idrac) == "management_controller"
|
||||
@@ -5,6 +5,7 @@ from napalm_device_types import (
|
||||
AccessPointDriver,
|
||||
FirewallDriver,
|
||||
HypervisorDriver,
|
||||
ManagementControllerDriver,
|
||||
OSDriver,
|
||||
ResidentialGatewayDriver,
|
||||
StorageDriver,
|
||||
@@ -16,6 +17,7 @@ BASE_CLASSES = [
|
||||
(AccessPointDriver, "Access Point"),
|
||||
(FirewallDriver, "Firewall"),
|
||||
(HypervisorDriver, "Hypervisor"),
|
||||
(ManagementControllerDriver, "Management Controller"),
|
||||
(OSDriver, "OS"),
|
||||
(ResidentialGatewayDriver, "Gateway"),
|
||||
(StorageDriver, "Storage"),
|
||||
|
||||
Reference in new issue
Block a user