saltext.bmc 0.0.1__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,17 @@
1
+ # pylint: disable=missing-module-docstring
2
+ import pathlib
3
+
4
+ PACKAGE_ROOT = pathlib.Path(__file__).resolve().parent
5
+ try:
6
+ from .version import __version__
7
+ except ImportError: # pragma: no cover
8
+ __version__ = "0.0.0.not-installed"
9
+ try:
10
+ from importlib.metadata import version, PackageNotFoundError
11
+
12
+ try:
13
+ __version__ = version(__name__)
14
+ except PackageNotFoundError:
15
+ pass
16
+ except ImportError:
17
+ pass
File without changes
@@ -0,0 +1,229 @@
1
+ """
2
+ Execution module for BMC (Baseboard Management Controller) hardware.
3
+
4
+ Supports two protocol backends — Redfish and IPMI (via ``pyghmi``) —
5
+ selected per profile by a ``backend`` key. The default is ``auto``,
6
+ which probes Redfish first and falls back to IPMI. See
7
+ :mod:`saltext.bmc.utils.backend` for backend selection details.
8
+
9
+ Configuration via Pillar::
10
+
11
+ saltext.bmc:
12
+ profiles:
13
+ bmc-host-01:
14
+ host: 10.0.0.5
15
+ username: root
16
+ password: calvin
17
+ verify_ssl: false
18
+ # backend defaults to 'auto'; set explicitly to skip the probe:
19
+ # backend: redfish # or 'ipmi'
20
+ legacy-host:
21
+ host: 10.0.0.7
22
+ username: ADMIN
23
+ password: ADMIN
24
+ backend: ipmi
25
+ port: 623 # IPMI-only
26
+
27
+ CLI examples::
28
+
29
+ salt-call --local bmc.power_status bmc-host-01
30
+ salt-call --local bmc.set_boot_device bmc-host-01 device=http persistent=False
31
+ salt-call --local bmc.power_cycle bmc-host-01
32
+ salt-call --local bmc.get_system_info bmc-host-01
33
+ salt-call --local bmc.get_sensor_data bmc-host-01
34
+
35
+ # Bypass pillar with explicit connection kwargs:
36
+ salt-call --local bmc.power_status host=10.0.0.5 username=root password=calvin verify_ssl=False
37
+ salt-call --local bmc.power_status host=10.0.0.7 username=ADMIN password=ADMIN backend=ipmi
38
+ """
39
+
40
+ from __future__ import annotations
41
+
42
+ import logging
43
+
44
+ from saltext.bmc.utils import backend as bk
45
+ from saltext.bmc.utils import boot as boot_canon
46
+
47
+ log = logging.getLogger(__name__)
48
+
49
+ __virtualname__ = "bmc"
50
+
51
+
52
+ def __virtual__():
53
+ return __virtualname__
54
+
55
+
56
+ # ----------------------------------------------------------------------
57
+ # Power operations
58
+ # ----------------------------------------------------------------------
59
+
60
+
61
+ def power_status(name: str | None = None, **conn) -> str:
62
+ """
63
+ Return the current power state: ``'on'``, ``'off'``, or ``'unknown'``.
64
+
65
+ :param name: BMC profile name (key under ``saltext.bmc:profiles``).
66
+ Omit to use the top-level ``saltext.bmc`` config.
67
+
68
+ CLI Example:
69
+
70
+ .. code-block:: bash
71
+
72
+ salt-call --local bmc.power_status bmc-host-01
73
+ """
74
+ with bk.open_backend(__opts__, name=name, **conn) as backend:
75
+ return backend.power_status()
76
+
77
+
78
+ def power_on(name: str | None = None, **conn) -> dict:
79
+ """
80
+ Power on the host.
81
+
82
+ CLI Example:
83
+
84
+ .. code-block:: bash
85
+
86
+ salt-call --local bmc.power_on bmc-host-01
87
+ """
88
+ with bk.open_backend(__opts__, name=name, **conn) as backend:
89
+ return backend.do_reset("On")
90
+
91
+
92
+ def power_off(name: str | None = None, force: bool = False, **conn) -> dict:
93
+ """
94
+ Power off the host.
95
+
96
+ :param force: If True, issue ``ForceOff`` (hard cut). Otherwise
97
+ ``GracefulShutdown`` is attempted.
98
+
99
+ CLI Example:
100
+
101
+ .. code-block:: bash
102
+
103
+ salt-call --local bmc.power_off bmc-host-01
104
+ salt-call --local bmc.power_off bmc-host-01 force=True
105
+ """
106
+ with bk.open_backend(__opts__, name=name, **conn) as backend:
107
+ return backend.do_reset("ForceOff" if force else "GracefulShutdown")
108
+
109
+
110
+ def power_cycle(name: str | None = None, **conn) -> dict:
111
+ """
112
+ Power-cycle the host (off then on).
113
+
114
+ CLI Example:
115
+
116
+ .. code-block:: bash
117
+
118
+ salt-call --local bmc.power_cycle bmc-host-01
119
+ """
120
+ with bk.open_backend(__opts__, name=name, **conn) as backend:
121
+ return backend.do_reset("PowerCycle")
122
+
123
+
124
+ def power_reset(name: str | None = None, force: bool = False, **conn) -> dict:
125
+ """
126
+ Reset the host. Prefers ``GracefulRestart``; pass ``force=True`` for ``ForceRestart``.
127
+
128
+ CLI Example:
129
+
130
+ .. code-block:: bash
131
+
132
+ salt-call --local bmc.power_reset bmc-host-01
133
+ """
134
+ with bk.open_backend(__opts__, name=name, **conn) as backend:
135
+ return backend.do_reset("ForceRestart" if force else "GracefulRestart")
136
+
137
+
138
+ # ----------------------------------------------------------------------
139
+ # Boot-device operations
140
+ # ----------------------------------------------------------------------
141
+
142
+
143
+ def get_boot_device(name: str | None = None, **conn) -> dict:
144
+ """
145
+ Return the current one-shot/persistent boot override.
146
+
147
+ Returns a dict with keys ``device`` (friendly name), ``redfish_target``,
148
+ ``native_target``, and ``enabled`` (``Once``/``Continuous``/``Disabled``).
149
+ On IPMI backends ``redfish_target`` is ``None``.
150
+
151
+ CLI Example:
152
+
153
+ .. code-block:: bash
154
+
155
+ salt-call --local bmc.get_boot_device bmc-host-01
156
+ """
157
+ with bk.open_backend(__opts__, name=name, **conn) as backend:
158
+ return backend.get_boot()
159
+
160
+
161
+ def set_boot_device(
162
+ name: str | None = None,
163
+ device: str = "none",
164
+ persistent: bool = False,
165
+ **conn,
166
+ ) -> dict:
167
+ """
168
+ Set the boot override.
169
+
170
+ :param str device: One of ``disk``, ``pxe``, ``http``, ``bios``, ``cd``,
171
+ ``usb``, ``none``. ``http`` and ``usb`` are not
172
+ supported by the IPMI backend.
173
+ :param bool persistent: If True, override persists across reboots
174
+ (Redfish ``Continuous``). Otherwise it is
175
+ consumed on the next boot (``Once``).
176
+
177
+ CLI Example:
178
+
179
+ .. code-block:: bash
180
+
181
+ salt-call --local bmc.set_boot_device bmc-host-01 device=http
182
+ salt-call --local bmc.set_boot_device bmc-host-01 device=pxe persistent=True
183
+ """
184
+ # Validate the device name up-front so we fail fast without opening a
185
+ # connection on a typo.
186
+ boot_canon.canonicalize(device)
187
+ with bk.open_backend(__opts__, name=name, **conn) as backend:
188
+ return backend.set_boot(device=device, persistent=persistent)
189
+
190
+
191
+ # ----------------------------------------------------------------------
192
+ # Inventory and sensors
193
+ # ----------------------------------------------------------------------
194
+
195
+
196
+ def get_system_info(name: str | None = None, **conn) -> dict:
197
+ """
198
+ Return a normalised inventory dict.
199
+
200
+ Keys: ``manufacturer``, ``model``, ``serial_number``, ``uuid``, ``sku``,
201
+ ``host_name``, ``bios_version``, ``firmware_version``, ``power_state``.
202
+ Unknown fields are returned as ``None``.
203
+
204
+ CLI Example:
205
+
206
+ .. code-block:: bash
207
+
208
+ salt-call --local bmc.get_system_info bmc-host-01
209
+ """
210
+ with bk.open_backend(__opts__, name=name, **conn) as backend:
211
+ return backend.get_system_info()
212
+
213
+
214
+ def get_sensor_data(name: str | None = None, **conn) -> dict:
215
+ """
216
+ Return ``{"temperatures": [...], "fans": [...], "voltages": [...]}``.
217
+
218
+ Each sensor entry has ``name``, ``reading``, ``unit``, ``status``.
219
+ A backend may return an empty list for a sensor class it does not
220
+ expose.
221
+
222
+ CLI Example:
223
+
224
+ .. code-block:: bash
225
+
226
+ salt-call --local bmc.get_sensor_data bmc-host-01
227
+ """
228
+ with bk.open_backend(__opts__, name=name, **conn) as backend:
229
+ return backend.get_sensors()
@@ -0,0 +1,126 @@
1
+ """
2
+ Low-level Redfish HTTP passthrough.
3
+
4
+ Bypasses the backend abstraction in :mod:`saltext.bmc.modules.bmc` to let
5
+ operators read or write any Redfish endpoint directly. Useful for
6
+ ad-hoc inspection, vendor-specific OEM extensions, and one-off
7
+ operations that the high-level module does not cover.
8
+
9
+ This module is Redfish-only. If the profile's ``backend`` is set to
10
+ ``ipmi``, calls raise :class:`~saltext.bmc.utils.redfish.RedfishError`
11
+ immediately.
12
+
13
+ CLI examples (``path`` is the first positional arg; pass the profile via
14
+ ``name=``)::
15
+
16
+ salt-call --local bmc_redfish.get /redfish/v1/ name=bmc-host-01
17
+ salt-call --local bmc_redfish.get /redfish/v1/Systems name=bmc-host-01
18
+ salt-call --local bmc_redfish.patch /redfish/v1/Systems/1 \\
19
+ body='{"AssetTag": "rack-7-slot-3"}' name=bmc-host-01
20
+ salt-call --local bmc_redfish.post \\
21
+ /redfish/v1/Systems/1/Actions/ComputerSystem.Reset \\
22
+ body='{"ResetType": "On"}' name=bmc-host-01
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import logging
28
+
29
+ from saltext.bmc.utils import redfish as rf
30
+
31
+ log = logging.getLogger(__name__)
32
+
33
+ __virtualname__ = "bmc_redfish"
34
+
35
+
36
+ def __virtual__():
37
+ return __virtualname__
38
+
39
+
40
+ def _require_redfish(name: str | None, conn: dict) -> None:
41
+ """Raise if the resolved profile uses a non-Redfish backend."""
42
+ cfg = rf.resolve_conn(__opts__, name=name, **conn)
43
+ backend = (cfg.get("backend") or "redfish").lower()
44
+ if backend == "ipmi":
45
+ raise rf.RedfishError(
46
+ f"Profile {name!r} uses backend={backend!r}; bmc_redfish.* requires Redfish."
47
+ )
48
+
49
+
50
+ def _client(name: str | None, conn: dict) -> rf.RedfishClient:
51
+ _require_redfish(name, conn)
52
+ return rf.open_client(__opts__, name=name, **conn)
53
+
54
+
55
+ def get(path: str, name: str | None = None, **conn) -> dict:
56
+ """
57
+ Raw Redfish GET.
58
+
59
+ :param str path: Redfish path (must start with ``/redfish/v1/``).
60
+ :param str name: Profile name; falls back to top-level config.
61
+
62
+ CLI Example:
63
+
64
+ .. code-block:: bash
65
+
66
+ salt-call --local bmc_redfish.get /redfish/v1/Systems name=bmc-host-01
67
+ """
68
+ with _client(name, conn) as client:
69
+ return client.get(path)
70
+
71
+
72
+ def post(path: str, name: str | None = None, body: dict | None = None, **conn) -> dict | None:
73
+ """
74
+ Raw Redfish POST.
75
+
76
+ :param str path: Redfish action path.
77
+ :param dict body: Request body (optional for some Redfish actions).
78
+ :param str name: Profile name.
79
+
80
+ CLI Example:
81
+
82
+ .. code-block:: bash
83
+
84
+ salt-call --local bmc_redfish.post \\
85
+ /redfish/v1/Systems/1/Actions/ComputerSystem.Reset \\
86
+ body='{"ResetType": "On"}' name=bmc-host-01
87
+ """
88
+ with _client(name, conn) as client:
89
+ return client.post(path, body)
90
+
91
+
92
+ def patch(path: str, body: dict, name: str | None = None, **conn) -> dict | None:
93
+ """
94
+ Raw Redfish PATCH.
95
+
96
+ :param str path: Redfish resource path.
97
+ :param dict body: Request body (required).
98
+ :param str name: Profile name.
99
+
100
+ CLI Example:
101
+
102
+ .. code-block:: bash
103
+
104
+ salt-call --local bmc_redfish.patch /redfish/v1/Systems/1 \\
105
+ body='{"AssetTag": "rack-7-slot-3"}' name=bmc-host-01
106
+ """
107
+ with _client(name, conn) as client:
108
+ return client.patch(path, body)
109
+
110
+
111
+ def delete(path: str, name: str | None = None, **conn) -> dict | None:
112
+ """
113
+ Raw Redfish DELETE.
114
+
115
+ :param str path: Redfish resource path.
116
+ :param str name: Profile name.
117
+
118
+ CLI Example:
119
+
120
+ .. code-block:: bash
121
+
122
+ salt-call --local bmc_redfish.delete \\
123
+ /redfish/v1/SessionService/Sessions/3 name=bmc-host-01
124
+ """
125
+ with _client(name, conn) as client:
126
+ return client.delete(path)
File without changes
@@ -0,0 +1,190 @@
1
+ """
2
+ ``bmc`` resource type — per-host BMC management over Redfish or IPMI.
3
+
4
+ Each ``bmc`` resource maps to one physical machine, addressed by its BMC
5
+ host/credentials. The resource ID is the human-friendly name used in
6
+ Pillar and targeting.
7
+
8
+ Configuration (via Pillar)::
9
+
10
+ resources:
11
+ bmc:
12
+ bmc-host-01:
13
+ host: 10.10.10.5
14
+ username: root
15
+ password: calvin
16
+ verify_ssl: false
17
+ # backend defaults to 'auto'; set explicitly to skip the probe:
18
+ # backend: redfish # or 'ipmi'
19
+ legacy-host:
20
+ host: 10.10.10.7
21
+ username: ADMIN
22
+ password: ADMIN
23
+ backend: ipmi
24
+ port: 623
25
+
26
+ Targeting::
27
+
28
+ salt bmc-host-01 bmc_host.power_status # by resource ID alone
29
+ salt -C 'T@bmc:bmc-host-01' bmc_host.power_status # by full SRN
30
+ salt -C 'T@bmc' bmc_host.power_status # all bmc resources
31
+ salt bmc-host-01 state.sls baremetal/boot_http
32
+
33
+ Per-host execution functions live in
34
+ :mod:`saltext.bmc.resources.bmc.modules.bmc_host`; the per-host states
35
+ in :mod:`saltext.bmc.resources.bmc.states.bmc_host`.
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ import logging
41
+
42
+ import salt.utils.resources # pylint: disable=import-error,no-name-in-module
43
+
44
+ from saltext.bmc.utils import backend as bk
45
+
46
+ log = logging.getLogger(__name__)
47
+
48
+ CONTEXT_KEY = "bmc_resource"
49
+
50
+
51
+ def __virtual__():
52
+ return True
53
+
54
+
55
+ # ---------------------------------------------------------------------------
56
+ # Internal helpers
57
+ # ---------------------------------------------------------------------------
58
+
59
+
60
+ def _resource_id() -> str:
61
+ return __resource__["id"] # pylint: disable=undefined-variable
62
+
63
+
64
+ def _ctx() -> dict:
65
+ return __context__.get(CONTEXT_KEY, {}) # pylint: disable=undefined-variable
66
+
67
+
68
+ def _host_cfg(resource_id: str) -> dict:
69
+ cfg = _ctx().get("hosts", {}).get(resource_id, {})
70
+ if not cfg:
71
+ raise ValueError(f"No bmc resource '{resource_id}' in pillar resources:bmc:")
72
+ return cfg
73
+
74
+
75
+ def _opts_for(resource_id: str) -> dict:
76
+ """
77
+ Build a Salt-opts shim that routes through :func:`backend.open_backend`.
78
+
79
+ ``open_backend`` reads its connection config from ``opts["pillar"]["saltext.bmc"]``;
80
+ we synthesise that from the per-resource pillar entry so we can reuse the same
81
+ factory.
82
+ """
83
+ cfg = _host_cfg(resource_id)
84
+ return {
85
+ "pillar": {
86
+ "saltext.bmc": {
87
+ "profiles": {resource_id: cfg},
88
+ }
89
+ }
90
+ }
91
+
92
+
93
+ def _open(resource_id: str | None = None):
94
+ rid = resource_id or _resource_id()
95
+ return bk.open_backend(_opts_for(rid), name=rid)
96
+
97
+
98
+ # ---------------------------------------------------------------------------
99
+ # Required resource interface
100
+ # ---------------------------------------------------------------------------
101
+
102
+
103
+ def init(opts: dict) -> None:
104
+ """
105
+ Initialise the ``bmc`` resource type.
106
+
107
+ Reads BMC host configs from Pillar and caches them in
108
+ ``__context__["bmc_resource"]``.
109
+ """
110
+ type_cfg = salt.utils.resources.pillar_resources_tree(opts).get("bmc", {}) or {}
111
+ __context__[CONTEXT_KEY] = { # pylint: disable=undefined-variable
112
+ "initialized": True,
113
+ "hosts": type_cfg,
114
+ }
115
+ log.debug("bmc init(), managing: %s", list(type_cfg))
116
+
117
+
118
+ def initialized() -> bool:
119
+ """Return ``True`` if :func:`init` has been called successfully."""
120
+ return _ctx().get("initialized", False)
121
+
122
+
123
+ def discover(opts: dict) -> list:
124
+ """
125
+ Return the list of BMC resource IDs declared in Pillar.
126
+
127
+ :param dict opts: The Salt opts dict.
128
+ :rtype: list[str]
129
+ """
130
+ type_cfg = salt.utils.resources.pillar_resources_tree(opts).get("bmc", {}) or {}
131
+ return list(type_cfg.keys())
132
+
133
+
134
+ # ---------------------------------------------------------------------------
135
+ # Per-resource operations
136
+ # ---------------------------------------------------------------------------
137
+
138
+
139
+ def power_status() -> str:
140
+ """Return ``'on'`` / ``'off'`` / ``'unknown'`` for this host."""
141
+ with _open() as backend:
142
+ return backend.power_status()
143
+
144
+
145
+ def power_on() -> dict:
146
+ """Power on this host."""
147
+ with _open() as backend:
148
+ return backend.do_reset("On")
149
+
150
+
151
+ def power_off(force: bool = False) -> dict:
152
+ """Power off this host (``GracefulShutdown`` by default, ``ForceOff`` if force=True)."""
153
+ with _open() as backend:
154
+ return backend.do_reset("ForceOff" if force else "GracefulShutdown")
155
+
156
+
157
+ def power_cycle() -> dict:
158
+ """Power-cycle this host."""
159
+ with _open() as backend:
160
+ return backend.do_reset("PowerCycle")
161
+
162
+
163
+ def power_reset(force: bool = False) -> dict:
164
+ """Reset this host (``GracefulRestart`` by default, ``ForceRestart`` if force=True)."""
165
+ with _open() as backend:
166
+ return backend.do_reset("ForceRestart" if force else "GracefulRestart")
167
+
168
+
169
+ def get_boot_device() -> dict:
170
+ """Return the current boot override (device/enabled/native_target)."""
171
+ with _open() as backend:
172
+ return backend.get_boot()
173
+
174
+
175
+ def set_boot_device(device: str = "none", persistent: bool = False) -> dict:
176
+ """Set the boot override on this host."""
177
+ with _open() as backend:
178
+ return backend.set_boot(device=device, persistent=persistent)
179
+
180
+
181
+ def get_system_info() -> dict:
182
+ """Return a normalised inventory dict for this host."""
183
+ with _open() as backend:
184
+ return backend.get_system_info()
185
+
186
+
187
+ def get_sensor_data() -> dict:
188
+ """Return temperatures/fans/voltages dict for this host."""
189
+ with _open() as backend:
190
+ return backend.get_sensors()
File without changes