"""
Execution module for BMC (Baseboard Management Controller) hardware.
Supports two protocol backends — Redfish and IPMI (via ``pyghmi``) —
selected per profile by a ``backend`` key. The default is ``auto``,
which probes Redfish first and falls back to IPMI. See
:mod:`saltext.bmc.utils.backend` for backend selection details.
Configuration via Pillar::
saltext.bmc:
profiles:
bmc-host-01:
host: 10.0.0.5
username: root
password: calvin
verify_ssl: false
# backend defaults to 'auto'; set explicitly to skip the probe:
# backend: redfish # or 'ipmi'
legacy-host:
host: 10.0.0.7
username: ADMIN
password: ADMIN
backend: ipmi
port: 623 # IPMI-only
CLI examples::
salt-call --local bmc.power_status bmc-host-01
salt-call --local bmc.set_boot_device bmc-host-01 device=http persistent=False
salt-call --local bmc.power_cycle bmc-host-01
salt-call --local bmc.get_system_info bmc-host-01
salt-call --local bmc.get_sensor_data bmc-host-01
# Bypass pillar with explicit connection kwargs:
salt-call --local bmc.power_status host=10.0.0.5 username=root password=calvin verify_ssl=False
salt-call --local bmc.power_status host=10.0.0.7 username=ADMIN password=ADMIN backend=ipmi
"""
from __future__ import annotations
import logging
from saltext.bmc.utils import backend as bk
from saltext.bmc.utils import boot as boot_canon
log = logging.getLogger(__name__)
__virtualname__ = "bmc"
def __virtual__():
return __virtualname__
# ----------------------------------------------------------------------
# Power operations
# ----------------------------------------------------------------------
[docs]
def power_status(name: str | None = None, **conn) -> str:
"""
Return the current power state: ``'on'``, ``'off'``, or ``'unknown'``.
:param name: BMC profile name (key under ``saltext.bmc:profiles``).
Omit to use the top-level ``saltext.bmc`` config.
CLI Example:
.. code-block:: bash
salt-call --local bmc.power_status bmc-host-01
"""
with bk.open_backend(__opts__, name=name, **conn) as backend:
return backend.power_status()
[docs]
def power_on(name: str | None = None, **conn) -> dict:
"""
Power on the host.
CLI Example:
.. code-block:: bash
salt-call --local bmc.power_on bmc-host-01
"""
with bk.open_backend(__opts__, name=name, **conn) as backend:
return backend.do_reset("On")
[docs]
def power_off(name: str | None = None, force: bool = False, **conn) -> dict:
"""
Power off the host.
:param force: If True, issue ``ForceOff`` (hard cut). Otherwise
``GracefulShutdown`` is attempted.
CLI Example:
.. code-block:: bash
salt-call --local bmc.power_off bmc-host-01
salt-call --local bmc.power_off bmc-host-01 force=True
"""
with bk.open_backend(__opts__, name=name, **conn) as backend:
return backend.do_reset("ForceOff" if force else "GracefulShutdown")
[docs]
def power_cycle(name: str | None = None, **conn) -> dict:
"""
Power-cycle the host (off then on).
CLI Example:
.. code-block:: bash
salt-call --local bmc.power_cycle bmc-host-01
"""
with bk.open_backend(__opts__, name=name, **conn) as backend:
return backend.do_reset("PowerCycle")
[docs]
def power_reset(name: str | None = None, force: bool = False, **conn) -> dict:
"""
Reset the host. Prefers ``GracefulRestart``; pass ``force=True`` for ``ForceRestart``.
CLI Example:
.. code-block:: bash
salt-call --local bmc.power_reset bmc-host-01
"""
with bk.open_backend(__opts__, name=name, **conn) as backend:
return backend.do_reset("ForceRestart" if force else "GracefulRestart")
# ----------------------------------------------------------------------
# Boot-device operations
# ----------------------------------------------------------------------
[docs]
def get_boot_device(name: str | None = None, **conn) -> dict:
"""
Return the current one-shot/persistent boot override.
Returns a dict with keys ``device`` (friendly name), ``redfish_target``,
``native_target``, and ``enabled`` (``Once``/``Continuous``/``Disabled``).
On IPMI backends ``redfish_target`` is ``None``.
CLI Example:
.. code-block:: bash
salt-call --local bmc.get_boot_device bmc-host-01
"""
with bk.open_backend(__opts__, name=name, **conn) as backend:
return backend.get_boot()
[docs]
def set_boot_device(
name: str | None = None,
device: str = "none",
persistent: bool = False,
**conn,
) -> dict:
"""
Set the boot override.
:param str device: One of ``disk``, ``pxe``, ``http``, ``bios``, ``cd``,
``usb``, ``none``. ``http`` and ``usb`` are not
supported by the IPMI backend.
:param bool persistent: If True, override persists across reboots
(Redfish ``Continuous``). Otherwise it is
consumed on the next boot (``Once``).
CLI Example:
.. code-block:: bash
salt-call --local bmc.set_boot_device bmc-host-01 device=http
salt-call --local bmc.set_boot_device bmc-host-01 device=pxe persistent=True
"""
# Validate the device name up-front so we fail fast without opening a
# connection on a typo.
boot_canon.canonicalize(device)
with bk.open_backend(__opts__, name=name, **conn) as backend:
return backend.set_boot(device=device, persistent=persistent)
# ----------------------------------------------------------------------
# Inventory and sensors
# ----------------------------------------------------------------------
[docs]
def get_system_info(name: str | None = None, **conn) -> dict:
"""
Return a normalised inventory dict.
Keys: ``manufacturer``, ``model``, ``serial_number``, ``uuid``, ``sku``,
``host_name``, ``bios_version``, ``firmware_version``, ``power_state``.
Unknown fields are returned as ``None``.
CLI Example:
.. code-block:: bash
salt-call --local bmc.get_system_info bmc-host-01
"""
with bk.open_backend(__opts__, name=name, **conn) as backend:
return backend.get_system_info()
[docs]
def get_sensor_data(name: str | None = None, **conn) -> dict:
"""
Return ``{"temperatures": [...], "fans": [...], "voltages": [...]}``.
Each sensor entry has ``name``, ``reading``, ``unit``, ``status``.
A backend may return an empty list for a sensor class it does not
expose.
CLI Example:
.. code-block:: bash
salt-call --local bmc.get_sensor_data bmc-host-01
"""
with bk.open_backend(__opts__, name=name, **conn) as backend:
return backend.get_sensors()