Source code for saltext.bmc.resources.bmc
"""
``bmc`` resource type — per-host BMC management over Redfish or IPMI.
Each ``bmc`` resource maps to one physical machine, addressed by its BMC
host/credentials. The resource ID is the human-friendly name used in
Pillar and targeting.
Configuration (via Pillar)::
resources:
bmc:
bmc-host-01:
host: 10.10.10.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.10.10.7
username: ADMIN
password: ADMIN
backend: ipmi
port: 623
Targeting::
salt bmc-host-01 bmc_host.power_status # by resource ID alone
salt -C 'T@bmc:bmc-host-01' bmc_host.power_status # by full SRN
salt -C 'T@bmc' bmc_host.power_status # all bmc resources
salt bmc-host-01 state.sls baremetal/boot_http
Per-host execution functions live in
:mod:`saltext.bmc.resources.bmc.modules.bmc_host`; the per-host states
in :mod:`saltext.bmc.resources.bmc.states.bmc_host`.
"""
from __future__ import annotations
import logging
import salt.utils.resources # pylint: disable=import-error,no-name-in-module
from saltext.bmc.utils import backend as bk
log = logging.getLogger(__name__)
CONTEXT_KEY = "bmc_resource"
def __virtual__():
return True
# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------
def _resource_id() -> str:
return __resource__["id"] # pylint: disable=undefined-variable
def _ctx() -> dict:
return __context__.get(CONTEXT_KEY, {}) # pylint: disable=undefined-variable
def _host_cfg(resource_id: str) -> dict:
cfg = _ctx().get("hosts", {}).get(resource_id, {})
if not cfg:
raise ValueError(f"No bmc resource '{resource_id}' in pillar resources:bmc:")
return cfg
def _opts_for(resource_id: str) -> dict:
"""
Build a Salt-opts shim that routes through :func:`backend.open_backend`.
``open_backend`` reads its connection config from ``opts["pillar"]["saltext.bmc"]``;
we synthesise that from the per-resource pillar entry so we can reuse the same
factory.
"""
cfg = _host_cfg(resource_id)
return {
"pillar": {
"saltext.bmc": {
"profiles": {resource_id: cfg},
}
}
}
def _open(resource_id: str | None = None):
rid = resource_id or _resource_id()
return bk.open_backend(_opts_for(rid), name=rid)
# ---------------------------------------------------------------------------
# Required resource interface
# ---------------------------------------------------------------------------
[docs]
def init(opts: dict) -> None:
"""
Initialise the ``bmc`` resource type.
Reads BMC host configs from Pillar and caches them in
``__context__["bmc_resource"]``.
"""
type_cfg = salt.utils.resources.pillar_resources_tree(opts).get("bmc", {}) or {}
__context__[CONTEXT_KEY] = { # pylint: disable=undefined-variable
"initialized": True,
"hosts": type_cfg,
}
log.debug("bmc init(), managing: %s", list(type_cfg))
[docs]
def initialized() -> bool:
"""Return ``True`` if :func:`init` has been called successfully."""
return _ctx().get("initialized", False)
[docs]
def discover(opts: dict) -> list:
"""
Return the list of BMC resource IDs declared in Pillar.
:param dict opts: The Salt opts dict.
:rtype: list[str]
"""
type_cfg = salt.utils.resources.pillar_resources_tree(opts).get("bmc", {}) or {}
return list(type_cfg.keys())
# ---------------------------------------------------------------------------
# Per-resource operations
# ---------------------------------------------------------------------------
[docs]
def power_status() -> str:
"""Return ``'on'`` / ``'off'`` / ``'unknown'`` for this host."""
with _open() as backend:
return backend.power_status()
[docs]
def power_on() -> dict:
"""Power on this host."""
with _open() as backend:
return backend.do_reset("On")
[docs]
def power_off(force: bool = False) -> dict:
"""Power off this host (``GracefulShutdown`` by default, ``ForceOff`` if force=True)."""
with _open() as backend:
return backend.do_reset("ForceOff" if force else "GracefulShutdown")
[docs]
def power_cycle() -> dict:
"""Power-cycle this host."""
with _open() as backend:
return backend.do_reset("PowerCycle")
[docs]
def power_reset(force: bool = False) -> dict:
"""Reset this host (``GracefulRestart`` by default, ``ForceRestart`` if force=True)."""
with _open() as backend:
return backend.do_reset("ForceRestart" if force else "GracefulRestart")
[docs]
def get_boot_device() -> dict:
"""Return the current boot override (device/enabled/native_target)."""
with _open() as backend:
return backend.get_boot()
[docs]
def set_boot_device(device: str = "none", persistent: bool = False) -> dict:
"""Set the boot override on this host."""
with _open() as backend:
return backend.set_boot(device=device, persistent=persistent)
[docs]
def get_system_info() -> dict:
"""Return a normalised inventory dict for this host."""
with _open() as backend:
return backend.get_system_info()
[docs]
def get_sensor_data() -> dict:
"""Return temperatures/fans/voltages dict for this host."""
with _open() as backend:
return backend.get_sensors()