Created IAC reverse generator

This commit is contained in:
p2913020
2026-05-22 00:19:30 -04:00
parent d04c2c6e4b
commit 1a11244fff
161 changed files with 26806 additions and 51 deletions

View File

@@ -0,0 +1,497 @@
"""Bare Metal provider plugin using Redfish/IPMI API.
Discovers hardware inventory, BMC configurations, network interfaces,
and RAID configurations from physical servers via the Redfish REST API
(standard BMC management interface).
"""
import logging
from typing import Callable
from urllib.parse import urljoin
import requests
from iac_reverse.models import (
CpuArchitecture,
DiscoveredResource,
PlatformCategory,
ProviderType,
ScanProgress,
ScanResult,
)
from iac_reverse.plugin_base import ProviderPlugin
from iac_reverse.scanner.scanner import AuthenticationError
logger = logging.getLogger(__name__)
class BareMetalPlugin(ProviderPlugin):
"""Provider plugin for bare metal servers using Redfish/IPMI API.
Connects to a server's BMC (Baseboard Management Controller) via the
Redfish REST API to discover hardware inventory, BMC configuration,
network interfaces, and RAID configurations.
Expected credentials dict keys:
host: BMC hostname or IP address (required)
username: BMC username (required)
password: BMC password (required)
port: BMC port (optional, default 443)
use_ssl: Whether to use HTTPS (optional, default "true")
"""
SUPPORTED_RESOURCE_TYPES = [
"bare_metal_hardware",
"bare_metal_bmc_config",
"bare_metal_network_interface",
"bare_metal_raid_config",
]
def __init__(self) -> None:
self._session: requests.Session | None = None
self._base_url: str = ""
self._host: str = ""
def authenticate(self, credentials: dict[str, str]) -> None:
"""Authenticate with the BMC via Redfish session creation.
Args:
credentials: Dict with keys: host, username, password,
and optionally port (default 443) and use_ssl (default "true").
Raises:
AuthenticationError: If connection or login fails.
"""
host = credentials.get("host", "")
username = credentials.get("username", "")
password = credentials.get("password", "")
port = credentials.get("port", "443")
use_ssl = credentials.get("use_ssl", "true").lower() == "true"
if not host or not username or not password:
raise AuthenticationError(
provider_name="bare_metal",
reason="Missing required credentials: host, username, and password are required",
)
scheme = "https" if use_ssl else "http"
self._base_url = f"{scheme}://{host}:{port}"
self._host = host
session = requests.Session()
session.verify = False # BMC certs are typically self-signed
session.headers.update({
"Content-Type": "application/json",
"Accept": "application/json",
})
# Attempt Redfish session-based authentication
session_url = f"{self._base_url}/redfish/v1/SessionService/Sessions"
payload = {"UserName": username, "Password": password}
try:
response = session.post(session_url, json=payload, timeout=30)
if response.status_code in (200, 201):
# Extract session token from response headers
token = response.headers.get("X-Auth-Token", "")
if token:
session.headers["X-Auth-Token"] = token
elif response.status_code == 401:
raise AuthenticationError(
provider_name="bare_metal",
reason="Invalid credentials (HTTP 401)",
)
else:
raise AuthenticationError(
provider_name="bare_metal",
reason=f"Unexpected response status {response.status_code}",
)
except requests.exceptions.ConnectionError as exc:
raise AuthenticationError(
provider_name="bare_metal",
reason=f"Cannot connect to BMC at {self._base_url}: {exc}",
) from exc
except requests.exceptions.Timeout as exc:
raise AuthenticationError(
provider_name="bare_metal",
reason=f"Connection to BMC timed out: {exc}",
) from exc
except AuthenticationError:
raise
except Exception as exc:
raise AuthenticationError(
provider_name="bare_metal",
reason=f"Unexpected error during authentication: {exc}",
) from exc
self._session = session
def get_platform_category(self) -> PlatformCategory:
"""Return PlatformCategory.BARE_METAL."""
return PlatformCategory.BARE_METAL
def list_endpoints(self) -> list[str]:
"""Return the BMC host as the single endpoint."""
return [self._host] if self._host else []
def list_supported_resource_types(self) -> list[str]:
"""Return supported bare metal resource types."""
return list(self.SUPPORTED_RESOURCE_TYPES)
def detect_architecture(self, endpoint: str) -> CpuArchitecture:
"""Detect CPU architecture from Redfish system hardware info.
Queries /redfish/v1/Systems/1/Processors to determine the
processor architecture.
Args:
endpoint: The BMC host address.
Returns:
CpuArchitecture enum value based on processor info.
"""
if self._session is None:
return CpuArchitecture.AMD64
processors_url = f"{self._base_url}/redfish/v1/Systems/1/Processors"
try:
response = self._session.get(processors_url, timeout=30)
if response.status_code == 200:
data = response.json()
members = data.get("Members", [])
if members:
# Query first processor for architecture details
proc_uri = members[0].get("@odata.id", "")
if proc_uri:
proc_url = f"{self._base_url}{proc_uri}"
proc_response = self._session.get(proc_url, timeout=30)
if proc_response.status_code == 200:
proc_data = proc_response.json()
return self._parse_architecture(proc_data)
except Exception as exc:
logger.warning("Failed to detect architecture: %s", exc)
return CpuArchitecture.AMD64
def discover_resources(
self,
endpoints: list[str],
resource_types: list[str],
progress_callback: Callable[[ScanProgress], None],
) -> ScanResult:
"""Discover bare metal resources via Redfish API.
Args:
endpoints: List of BMC host addresses to scan.
resource_types: Resource types to discover.
progress_callback: Progress reporting callback.
Returns:
ScanResult with discovered resources.
"""
resources: list[DiscoveredResource] = []
warnings: list[str] = []
errors: list[str] = []
total_types = len(resource_types)
types_completed = 0
for endpoint in endpoints:
architecture = self.detect_architecture(endpoint)
for resource_type in resource_types:
try:
discovered = self._discover_resource_type(
endpoint, resource_type, architecture
)
resources.extend(discovered)
except Exception as exc:
error_msg = (
f"Error discovering {resource_type} on {endpoint}: {exc}"
)
errors.append(error_msg)
logger.error(error_msg)
types_completed += 1
progress_callback(
ScanProgress(
current_resource_type=resource_type,
resources_discovered=len(resources),
resource_types_completed=types_completed,
total_resource_types=total_types,
)
)
return ScanResult(
resources=resources,
warnings=warnings,
errors=errors,
scan_timestamp="",
profile_hash="",
)
# -----------------------------------------------------------------------
# Private helpers
# -----------------------------------------------------------------------
def _discover_resource_type(
self,
endpoint: str,
resource_type: str,
architecture: CpuArchitecture,
) -> list[DiscoveredResource]:
"""Dispatch discovery to the appropriate handler."""
handlers = {
"bare_metal_hardware": self._discover_hardware,
"bare_metal_bmc_config": self._discover_bmc_config,
"bare_metal_network_interface": self._discover_network_interfaces,
"bare_metal_raid_config": self._discover_raid_config,
}
handler = handlers.get(resource_type)
if handler is None:
return []
return handler(endpoint, architecture)
def _discover_hardware(
self, endpoint: str, architecture: CpuArchitecture
) -> list[DiscoveredResource]:
"""Discover hardware inventory via /redfish/v1/Systems/1."""
if self._session is None:
return []
url = f"{self._base_url}/redfish/v1/Systems/1"
try:
response = self._session.get(url, timeout=30)
if response.status_code != 200:
return []
data = response.json()
except Exception as exc:
logger.warning("Failed to discover hardware: %s", exc)
return []
system_id = data.get("Id", "System.1")
return [
DiscoveredResource(
resource_type="bare_metal_hardware",
unique_id=f"{endpoint}:{system_id}",
name=data.get("Name", f"System {system_id}"),
provider=ProviderType.BARE_METAL,
platform_category=PlatformCategory.BARE_METAL,
architecture=architecture,
endpoint=endpoint,
attributes={
"manufacturer": data.get("Manufacturer", ""),
"model": data.get("Model", ""),
"serial_number": data.get("SerialNumber", ""),
"sku": data.get("SKU", ""),
"bios_version": data.get("BiosVersion", ""),
"total_memory_gib": data.get("MemorySummary", {}).get(
"TotalSystemMemoryGiB", 0
),
"processor_count": data.get("ProcessorSummary", {}).get(
"Count", 0
),
"processor_model": data.get("ProcessorSummary", {}).get(
"Model", ""
),
"power_state": data.get("PowerState", ""),
"status": data.get("Status", {}),
},
)
]
def _discover_bmc_config(
self, endpoint: str, architecture: CpuArchitecture
) -> list[DiscoveredResource]:
"""Discover BMC configuration via /redfish/v1/Managers/1."""
if self._session is None:
return []
url = f"{self._base_url}/redfish/v1/Managers/1"
try:
response = self._session.get(url, timeout=30)
if response.status_code != 200:
return []
data = response.json()
except Exception as exc:
logger.warning("Failed to discover BMC config: %s", exc)
return []
manager_id = data.get("Id", "BMC.1")
return [
DiscoveredResource(
resource_type="bare_metal_bmc_config",
unique_id=f"{endpoint}:{manager_id}",
name=data.get("Name", f"BMC {manager_id}"),
provider=ProviderType.BARE_METAL,
platform_category=PlatformCategory.BARE_METAL,
architecture=architecture,
endpoint=endpoint,
attributes={
"manager_type": data.get("ManagerType", ""),
"firmware_version": data.get("FirmwareVersion", ""),
"model": data.get("Model", ""),
"status": data.get("Status", {}),
"uuid": data.get("UUID", ""),
},
)
]
def _discover_network_interfaces(
self, endpoint: str, architecture: CpuArchitecture
) -> list[DiscoveredResource]:
"""Discover network interfaces via /redfish/v1/Systems/1/EthernetInterfaces."""
if self._session is None:
return []
url = f"{self._base_url}/redfish/v1/Systems/1/EthernetInterfaces"
try:
response = self._session.get(url, timeout=30)
if response.status_code != 200:
return []
data = response.json()
except Exception as exc:
logger.warning("Failed to discover network interfaces: %s", exc)
return []
resources: list[DiscoveredResource] = []
for member in data.get("Members", []):
nic_uri = member.get("@odata.id", "")
if not nic_uri:
continue
try:
nic_url = f"{self._base_url}{nic_uri}"
nic_response = self._session.get(nic_url, timeout=30)
if nic_response.status_code != 200:
continue
nic_data = nic_response.json()
except Exception as exc:
logger.warning("Failed to get NIC details at %s: %s", nic_uri, exc)
continue
nic_id = nic_data.get("Id", "")
resources.append(
DiscoveredResource(
resource_type="bare_metal_network_interface",
unique_id=f"{endpoint}:{nic_id}",
name=nic_data.get("Name", f"NIC {nic_id}"),
provider=ProviderType.BARE_METAL,
platform_category=PlatformCategory.BARE_METAL,
architecture=architecture,
endpoint=endpoint,
attributes={
"mac_address": nic_data.get("MACAddress", ""),
"speed_mbps": nic_data.get("SpeedMbps", 0),
"status": nic_data.get("Status", {}),
"ipv4_addresses": nic_data.get("IPv4Addresses", []),
"ipv6_addresses": nic_data.get("IPv6Addresses", []),
"vlan": nic_data.get("VLAN", {}),
"link_status": nic_data.get("LinkStatus", ""),
"auto_neg": nic_data.get("AutoNeg", False),
},
)
)
return resources
def _discover_raid_config(
self, endpoint: str, architecture: CpuArchitecture
) -> list[DiscoveredResource]:
"""Discover RAID configuration via /redfish/v1/Systems/1/Storage."""
if self._session is None:
return []
url = f"{self._base_url}/redfish/v1/Systems/1/Storage"
try:
response = self._session.get(url, timeout=30)
if response.status_code != 200:
return []
data = response.json()
except Exception as exc:
logger.warning("Failed to discover RAID config: %s", exc)
return []
resources: list[DiscoveredResource] = []
for member in data.get("Members", []):
storage_uri = member.get("@odata.id", "")
if not storage_uri:
continue
try:
storage_url = f"{self._base_url}{storage_uri}"
storage_response = self._session.get(storage_url, timeout=30)
if storage_response.status_code != 200:
continue
storage_data = storage_response.json()
except Exception as exc:
logger.warning(
"Failed to get storage details at %s: %s", storage_uri, exc
)
continue
storage_id = storage_data.get("Id", "")
drives = []
for drive in storage_data.get("Drives", []):
drive_uri = drive.get("@odata.id", "")
if drive_uri:
drives.append(drive_uri)
volumes = []
volumes_link = storage_data.get("Volumes", {}).get("@odata.id", "")
if volumes_link:
try:
vol_url = f"{self._base_url}{volumes_link}"
vol_response = self._session.get(vol_url, timeout=30)
if vol_response.status_code == 200:
vol_data = vol_response.json()
for vol_member in vol_data.get("Members", []):
vol_uri = vol_member.get("@odata.id", "")
if vol_uri:
volumes.append(vol_uri)
except Exception as exc:
logger.warning("Failed to get volumes: %s", exc)
resources.append(
DiscoveredResource(
resource_type="bare_metal_raid_config",
unique_id=f"{endpoint}:{storage_id}",
name=storage_data.get("Name", f"Storage {storage_id}"),
provider=ProviderType.BARE_METAL,
platform_category=PlatformCategory.BARE_METAL,
architecture=architecture,
endpoint=endpoint,
attributes={
"storage_controllers": [
ctrl.get("Name", "")
for ctrl in storage_data.get(
"StorageControllers", []
)
],
"drive_count": len(drives),
"drives": drives,
"volumes": volumes,
"status": storage_data.get("Status", {}),
},
)
)
return resources
@staticmethod
def _parse_architecture(proc_data: dict) -> CpuArchitecture:
"""Parse CPU architecture from Redfish processor data.
Examines InstructionSet and Model fields to determine architecture.
"""
instruction_set = proc_data.get("InstructionSet", "").lower()
model = proc_data.get("Model", "").lower()
if "aarch64" in instruction_set or "arm" in instruction_set:
return CpuArchitecture.AARCH64
if "arm" in model:
if "64" in model or "aarch64" in model or "v8" in model:
return CpuArchitecture.AARCH64
return CpuArchitecture.ARM
# Default to AMD64 for x86/x86_64/IA-32e
return CpuArchitecture.AMD64