Skip to content

Commit 4d124c2

Browse files
committed
Add BIOS scraping from Redfish
Signed-off-by: Pavel Abramov <uncle.decart@gmail.com>
1 parent a682aa2 commit 4d124c2

3 files changed

Lines changed: 282 additions & 0 deletions

File tree

conf/config.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
11
benchmark_output_path: "${hydra:run.dir}/output.csv"
22
sysinfo_collector_file: "${hydra:run.dir}/sysinfo.json"
33

4+
# --- Configuration for getting BIOS data from redfish
5+
6+
bios:
7+
enable: true
8+
redfish:
9+
host: "0.0.0.0"
10+
username: "root"
11+
password: "SECRET_PASSWORD"
12+
verify_ssl: false
13+
timeout: 15
14+
15+
output:
16+
format: "json"
17+
file: "${hydra:run.dir}/bios.json"
18+
pretty: true
19+
420
# --- Intel CAT specific configuration ---
521

622
pqos:

main.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
from omegaconf import DictConfig, OmegaConf
88

9+
from src.bios_settings import process_bios_settings
10+
911
from src.metrics import (
1012
CPUmonitor,
1113
InterruptMonitor,
@@ -91,6 +93,10 @@ def main(cfg: DictConfig):
9193
collector.gather_all(cfg)
9294
collector.dump_to_file(cfg.sysinfo_collector_file)
9395

96+
# Collect BIOS settings via redfish
97+
if cfg.bios.enable:
98+
process_bios_settings(cfg.bios)
99+
94100
runner = DockerTestRunner(cfg)
95101

96102
if cfg.run.command == "build":

src/bios_settings.py

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
import argparse
2+
import urllib3
3+
import json
4+
import re
5+
import sys
6+
7+
from omegaconf import DictConfig
8+
from pathlib import Path
9+
from typing import Dict, Optional, Tuple
10+
11+
12+
try:
13+
import requests
14+
from requests.auth import HTTPBasicAuth
15+
16+
REQUESTS_AVAILABLE = True
17+
except ImportError:
18+
print(
19+
"ERROR: requests library not found. Install with: pip install requests",
20+
file=sys.stderr,
21+
)
22+
sys.exit(1)
23+
24+
try:
25+
import yaml
26+
27+
YAML_AVAILABLE = True
28+
except ImportError:
29+
YAML_AVAILABLE = False
30+
31+
# Disable SSL warnings for self-signed iDRAC certificates
32+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
33+
34+
import urllib3
35+
36+
37+
def process_bios_settings(cfg: DictConfig):
38+
"""
39+
Fetches BIOS settings using parameters from a Hydra config object.
40+
41+
Expected Config Structure (example):
42+
redfish:
43+
enabled: true
44+
host: "1.2.3.4"
45+
username: "root"
46+
password: "password"
47+
verify_ssl: false
48+
timeout: 10
49+
output:
50+
format: "text" # json, yaml, text
51+
file: "bios.json" # or null for stdout
52+
pretty: true
53+
"""
54+
55+
# 1. Validation & Setup
56+
# Access keys safely. Using .get() allows for defaults if keys are missing in YAML.
57+
# We assume 'redfish' and 'output' are root keys in the passed cfg,
58+
# or you can pass cfg.bios_task to this function.
59+
rf_cfg = cfg.get("redfish", {})
60+
out_cfg = cfg.get("output", {})
61+
62+
if not rf_cfg.get("enabled", True):
63+
print("WARNING: Redfish task is disabled in config", file=sys.stderr)
64+
return
65+
66+
host = rf_cfg.get("host")
67+
username = rf_cfg.get("username", "root")
68+
password = rf_cfg.get("password")
69+
verify_ssl = rf_cfg.get("verify_ssl", False)
70+
timeout = rf_cfg.get("timeout", 10)
71+
72+
if not host:
73+
print("ERROR: No host specified in config (redfish.host)", file=sys.stderr)
74+
raise ValueError("Host Missing")
75+
76+
if not password:
77+
raise ValueError("ERROR: No password specified in config (redfish.password)")
78+
79+
session, host_url = connect_redfish(host, username, password, verify_ssl, timeout)
80+
if not session:
81+
raise ValueError("Session missing")
82+
83+
attributes = get_bios_attributes(session, host_url, timeout)
84+
if not attributes:
85+
raise ValueError("Attributes missing")
86+
87+
output_fmt = out_cfg.get("format", "text")
88+
is_pretty = out_cfg.get("pretty", True)
89+
90+
if output_fmt == "json":
91+
output_data = format_json(attributes, pretty=is_pretty)
92+
elif output_fmt == "yaml":
93+
output_data = format_yaml(attributes)
94+
else:
95+
output_data = format_text(attributes)
96+
97+
output_file = out_cfg.get("file")
98+
99+
if output_file:
100+
try:
101+
with open(output_file, "w") as f:
102+
f.write(output_data)
103+
print(f"✓ Saved to {output_file}", file=sys.stderr)
104+
except Exception as e:
105+
raise ValueError(f"ERROR: Failed to write to {output_file}: {e}")
106+
else:
107+
# Print to stdout
108+
print(output_data)
109+
110+
111+
def connect_redfish(
112+
host: str, username: str, password: str, verify_ssl: bool = False, timeout: int = 10
113+
) -> Tuple[Optional[requests.Session], str]:
114+
"""
115+
Create authenticated Redfish session to iDRAC.
116+
117+
Returns:
118+
Tuple of (authenticated session or None, normalized host URL)
119+
"""
120+
# Ensure host has https:// prefix
121+
if not host.startswith("http://") and not host.startswith("https://"):
122+
host = f"https://{host}"
123+
124+
session = requests.Session()
125+
session.auth = HTTPBasicAuth(username, password)
126+
session.verify = verify_ssl
127+
session.headers.update(
128+
{"Content-Type": "application/json", "Accept": "application/json"}
129+
)
130+
131+
# Test connection with root endpoint
132+
try:
133+
print(f"Connecting to {host}...", file=sys.stderr)
134+
response = session.get(f"{host}/redfish/v1/", timeout=timeout)
135+
response.raise_for_status()
136+
print(f"✓ Connected successfully", file=sys.stderr)
137+
return session, host
138+
except requests.exceptions.Timeout:
139+
print(f"ERROR: Connection timeout to {host}", file=sys.stderr)
140+
return None, host
141+
except requests.exceptions.ConnectionError as e:
142+
print(f"ERROR: Connection failed to {host}: {e}", file=sys.stderr)
143+
return None, host
144+
except requests.exceptions.HTTPError as e:
145+
print(f"ERROR: HTTP error from {host}: {e}", file=sys.stderr)
146+
return None, host
147+
except Exception as e:
148+
print(f"ERROR: Unexpected error connecting to {host}: {e}", file=sys.stderr)
149+
return None, host
150+
151+
152+
def get_bios_attributes(
153+
session: requests.Session, host: str, timeout: int = 10
154+
) -> Optional[Dict[str, any]]:
155+
"""
156+
Retrieve BIOS attributes from Dell iDRAC.
157+
158+
Returns:
159+
Dictionary of BIOS attributes or None on failure
160+
"""
161+
try:
162+
# Dell iDRAC standard endpoint for BIOS settings
163+
url = f"{host}/redfish/v1/Systems/System.Embedded.1/Bios"
164+
print(f"Fetching BIOS attributes from {url}...", file=sys.stderr)
165+
166+
response = session.get(url, timeout=timeout)
167+
response.raise_for_status()
168+
data = response.json()
169+
170+
# BIOS attributes are in the "Attributes" key
171+
attributes = data.get("Attributes", {})
172+
print(f"✓ Retrieved {len(attributes)} BIOS attributes", file=sys.stderr)
173+
return attributes
174+
except requests.exceptions.HTTPError as e:
175+
print(f"ERROR: Failed to fetch BIOS attributes: {e}", file=sys.stderr)
176+
print(
177+
f" Response: {e.response.text if e.response else 'No response'}",
178+
file=sys.stderr,
179+
)
180+
return None
181+
except Exception as e:
182+
print(f"ERROR: Unexpected error fetching BIOS: {e}", file=sys.stderr)
183+
return None
184+
185+
186+
def format_text(attributes: Dict[str, any]) -> str:
187+
"""
188+
Format BIOS attributes in human-readable text format.
189+
Groups attributes by prefix for better organization.
190+
"""
191+
if not attributes:
192+
return "No BIOS attributes available"
193+
194+
lines = []
195+
lines.append("=" * 80)
196+
lines.append(f"BIOS SETTINGS ({len(attributes)} total attributes)")
197+
lines.append("=" * 80)
198+
lines.append("")
199+
200+
# Group by prefix (first word before capital letter)
201+
groups = {}
202+
ungrouped = []
203+
204+
for key in sorted(attributes.keys()):
205+
value = attributes[key]
206+
# Try to extract prefix (e.g., "Proc" from "ProcTurboMode")
207+
match = re.match(r"^([A-Z][a-z]+)", key)
208+
if match:
209+
prefix = match.group(1)
210+
if prefix not in groups:
211+
groups[prefix] = []
212+
groups[prefix].append((key, value))
213+
else:
214+
ungrouped.append((key, value))
215+
216+
# Output grouped attributes
217+
for prefix in sorted(groups.keys()):
218+
lines.append(f"[{prefix}*] Settings ({len(groups[prefix])} attributes)")
219+
lines.append("-" * 80)
220+
for key, value in groups[prefix]:
221+
# Format value nicely
222+
if isinstance(value, bool):
223+
value_str = "Enabled" if value else "Disabled"
224+
elif isinstance(value, str) and len(value) > 60:
225+
value_str = value[:57] + "..."
226+
else:
227+
value_str = str(value)
228+
lines.append(f" {key:<40} = {value_str}")
229+
lines.append("")
230+
231+
# Output ungrouped attributes
232+
if ungrouped:
233+
lines.append(f"[Other] Settings ({len(ungrouped)} attributes)")
234+
lines.append("-" * 80)
235+
for key, value in ungrouped:
236+
if isinstance(value, bool):
237+
value_str = "Enabled" if value else "Disabled"
238+
elif isinstance(value, str) and len(value) > 60:
239+
value_str = value[:57] + "..."
240+
else:
241+
value_str = str(value)
242+
lines.append(f" {key:<40} = {value_str}")
243+
lines.append("")
244+
245+
return "\n".join(lines)
246+
247+
248+
def format_json(attributes: Dict[str, any], pretty: bool = True) -> str:
249+
"""Format BIOS attributes as JSON."""
250+
if pretty:
251+
return json.dumps(attributes, indent=2, sort_keys=True)
252+
else:
253+
return json.dumps(attributes, sort_keys=True)
254+
255+
256+
def format_yaml(attributes: Dict[str, any]) -> str:
257+
"""Format BIOS attributes as YAML."""
258+
if not YAML_AVAILABLE:
259+
raise ValueError("ERROR: PyYAML not installed. Cannot output YAML format.")
260+
return yaml.dump(attributes, default_flow_style=False, sort_keys=True)

0 commit comments

Comments
 (0)