def storage(self) -> Dict[str, Any]:
return {"storage": self.backend.get_storage()}
+ @cherrypy.expose
+ @cherrypy.tools.allow(methods=["GET"])
+ @cherrypy.tools.json_out()
+ def fcm(self) -> Dict[str, Any]:
+ return {"fcm": self.backend.get_fcm()}
+
@cherrypy.expose
@cherrypy.tools.allow(methods=["GET"])
@cherrypy.tools.json_out()
INVALID_SERIALS = ("unknown", "not available", "n/a", "")
-class AtollonSystem(BaseRedfishSystem):
+class AtollonRedfishProvider(BaseRedfishSystem):
+ """Redfish provider for Atollon BMC quirks."""
+
def __init__(self, **kw: Any) -> None:
super().__init__(**kw)
self.log = get_logger(__name__)
speed_gbps = controller.get("SpeedGbps")
if speed_gbps is not None:
drive_data["speed_gbps"] = speed_gbps
+
+
+# Backward-compatible alias for entry points and external imports.
+AtollonSystem = AtollonRedfishProvider
"refresh_interval", DEFAULTS["system"]["refresh_interval"]
)
+ def initialize_redfish_session(self) -> None:
+ self.client.login()
+ self.endpoints.init()
+
+ def run_update_cycle(self) -> None:
+ self._update_system()
+ self._update_sn()
+ with concurrent.futures.ThreadPoolExecutor() as executor:
+ executor.map(lambda f: f(), self.update_funcs)
+
def update(
self,
collection: str,
def main(self) -> None:
self.stop = False
- self.client.login()
- self.endpoints.init()
+ self.initialize_redfish_session()
while not self.stop:
self.log.debug("waiting for a lock in the update loop.")
if not self.pending_shutdown:
self.log.debug("lock acquired in the update loop.")
try:
- self._update_system()
- self._update_sn()
-
- with concurrent.futures.ThreadPoolExecutor() as executor:
- executor.map(lambda f: f(), self.update_funcs)
-
+ self.run_update_cycle()
self.data_ready = True
except RuntimeError as e:
self.stop = True
def get_storage(self) -> Dict[str, Dict[str, Dict]]:
raise NotImplementedError()
+ def get_fcm(self) -> Dict[str, Dict[str, Dict]]:
+ return {}
+
def get_firmware(self) -> Dict[str, Dict[str, Dict]]:
raise NotImplementedError()
--- /dev/null
+import ctypes
+import fcntl
+import glob
+import os
+import struct
+from typing import Optional, TypedDict
+
+from ceph_node_proxy.util import get_logger
+
+SYSFS_BLOCK = "/sys/block"
+
+NVME_ADMIN_OPC_GET_LOG_PAGE = 0x02
+NVME_NSID_ALL = 0xFFFFFFFF
+FCM_LOG_PAGE_ID = 0xCA
+FCM_LOG_PAGE_LEN = 32
+FCM_LOG_PAGE_OFFSET = 280
+FCM_RATIO_DISPLAY_MIN_LOG_UTIL_PERCENT = 0.05
+
+_IOC_NRBITS = 8
+_IOC_TYPEBITS = 8
+_IOC_SIZEBITS = 14
+_IOC_DIRBITS = 2
+_IOC_NRSHIFT = 0
+_IOC_TYPESHIFT = _IOC_NRSHIFT + _IOC_NRBITS
+_IOC_SIZESHIFT = _IOC_TYPESHIFT + _IOC_TYPEBITS
+_IOC_DIRSHIFT = _IOC_SIZESHIFT + _IOC_SIZEBITS
+
+
+def ioc(dir_: int, type_: str, nr: int, size: int) -> int:
+ return (
+ (dir_ << _IOC_DIRSHIFT)
+ | (ord(type_) << _IOC_TYPESHIFT)
+ | (nr << _IOC_NRSHIFT)
+ | (size << _IOC_SIZESHIFT)
+ )
+
+
+class _NvmePassthruCmd64(ctypes.Structure):
+ _fields_ = [
+ ("opcode", ctypes.c_uint8),
+ ("flags", ctypes.c_uint8),
+ ("rsvd1", ctypes.c_uint16),
+ ("nsid", ctypes.c_uint32),
+ ("cdw2", ctypes.c_uint32),
+ ("cdw3", ctypes.c_uint32),
+ ("metadata", ctypes.c_uint64),
+ ("addr", ctypes.c_uint64),
+ ("metadata_len", ctypes.c_uint32),
+ ("data_len", ctypes.c_uint32),
+ ("cdw10", ctypes.c_uint32),
+ ("cdw11", ctypes.c_uint32),
+ ("cdw12", ctypes.c_uint32),
+ ("cdw13", ctypes.c_uint32),
+ ("cdw14", ctypes.c_uint32),
+ ("cdw15", ctypes.c_uint32),
+ ("timeout_ms", ctypes.c_uint32),
+ ("rsvd2", ctypes.c_uint32),
+ ("result", ctypes.c_uint64),
+ ]
+
+
+NVME_IOCTL_ADMIN64_CMD = ioc(
+ 3, "N", 0x47, ctypes.sizeof(_NvmePassthruCmd64)
+)
+
+logger = get_logger(__name__)
+
+
+class FCMStatsData(TypedDict):
+ device: str
+ model: str
+ serial_number: str
+ valid: bool
+ phy_size_bytes: int
+ phy_util_bytes: int
+ log_size_bytes: int
+ log_util_bytes: int
+ phy_util_percent: float
+ log_util_percent: float
+ compression_ratio: float
+ compression_ratio_str: str
+ savings_bytes: int
+ compression_ratio_display: str
+ savings_display: str
+ phy_usage_display: str
+ log_usage_display: str
+ status: dict[str, str]
+
+
+def read_sysfs(path: str) -> str:
+ try:
+ with open(path, encoding="utf-8") as handle:
+ return handle.read().strip()
+ except OSError:
+ return ""
+
+
+def read_sysfs_block(device: str, attribute: str) -> str:
+ return read_sysfs(os.path.join(SYSFS_BLOCK, device, attribute))
+
+
+def list_nvme_namespace_names() -> list[str]:
+ return sorted(
+ os.path.basename(path)
+ for path in glob.glob(f"{SYSFS_BLOCK}/nvme*")
+ if "c" not in os.path.basename(path)
+ )
+
+
+def is_fcm_device(device: str) -> bool:
+ model = read_sysfs_block(device, "device/model")
+ return "FCM" in model
+
+
+def query_nvme_log_page(device: str) -> Optional[bytes]:
+ device_path = f"/dev/{device}"
+ numd = (FCM_LOG_PAGE_LEN // 4) - 1
+ numdl = numd & 0xFFFF
+ numdu = (numd >> 16) & 0xFFFF
+
+ buffer = (ctypes.c_uint8 * FCM_LOG_PAGE_LEN)()
+ command = _NvmePassthruCmd64()
+ command.opcode = NVME_ADMIN_OPC_GET_LOG_PAGE
+ command.nsid = NVME_NSID_ALL
+ command.addr = ctypes.addressof(buffer)
+ command.data_len = FCM_LOG_PAGE_LEN
+ command.cdw10 = FCM_LOG_PAGE_ID | (numdl << 16)
+ command.cdw11 = numdu
+ command.cdw12 = FCM_LOG_PAGE_OFFSET & 0xFFFFFFFF
+ command.cdw13 = (FCM_LOG_PAGE_OFFSET >> 32) & 0xFFFFFFFF
+
+ fd = os.open(device_path, os.O_RDONLY)
+ try:
+ fcntl.ioctl(fd, NVME_IOCTL_ADMIN64_CMD, command)
+ except OSError as exc:
+ logger.debug(
+ "NVMe log page 0xCA query failed for device %s: %s",
+ device,
+ exc,
+ )
+ return None
+ finally:
+ os.close(fd)
+
+ if command.result:
+ logger.debug(
+ "NVMe log page 0xCA returned non-zero status 0x%x for device %s",
+ command.result,
+ device,
+ )
+ return None
+
+ return bytes(buffer)
+
+
+def compression_ratio_str(ratio: float) -> str:
+ ratio_text = f"{ratio:.1f}"
+ if ratio_text.endswith("0"):
+ return f"{int(ratio)}:1"
+ return f"{ratio_text}:1"
+
+
+def human_readable(capacity: int, dec_places: int = 1) -> str:
+ suffixes = ["b", "KB", "MB", "GB", "TB", "PB"]
+ size = float(capacity)
+ unit = suffixes[0]
+ for unit in suffixes:
+ if size < 1000:
+ break
+ size /= 1000
+ return f"{size:.{dec_places}f} {unit}"
+
+
+def fcm_usage_display(util_bytes: int, util_percent: float) -> str:
+ return f"{human_readable(util_bytes)}({int(util_percent)}%)"
+
+
+def empty_fcm_display_fields() -> dict[str, str]:
+ return {
+ "compression_ratio_display": "",
+ "savings_display": "",
+ "phy_usage_display": "",
+ "log_usage_display": "",
+ }
+
+
+def apply_fcm_display_fields(stats: FCMStatsData) -> FCMStatsData:
+ display = empty_fcm_display_fields()
+ if not stats["valid"]:
+ return {**stats, **display}
+
+ display["savings_display"] = human_readable(stats["savings_bytes"])
+ display["phy_usage_display"] = fcm_usage_display(
+ stats["phy_util_bytes"], stats["phy_util_percent"]
+ )
+ display["log_usage_display"] = fcm_usage_display(
+ stats["log_util_bytes"], stats["log_util_percent"]
+ )
+ if stats["log_util_percent"] > FCM_RATIO_DISPLAY_MIN_LOG_UTIL_PERCENT:
+ display["compression_ratio_display"] = stats["compression_ratio_str"]
+ return {**stats, **display}
+
+
+def read_fcm_stats(device: str) -> FCMStatsData:
+ model = read_sysfs_block(device, "device/model")
+ serial_number = read_sysfs_block(device, "device/serial")
+ invalid = apply_fcm_display_fields({
+ "device": device,
+ "model": model,
+ "serial_number": serial_number,
+ "valid": False,
+ "phy_size_bytes": 0,
+ "phy_util_bytes": 0,
+ "log_size_bytes": 0,
+ "log_util_bytes": 0,
+ "phy_util_percent": 0.0,
+ "log_util_percent": 0.0,
+ "compression_ratio": 0.0,
+ "compression_ratio_str": "",
+ "savings_bytes": 0,
+ "status": {"health": "Unknown", "state": "Unavailable"},
+ "compression_ratio_display": "",
+ "savings_display": "",
+ "phy_usage_display": "",
+ "log_usage_display": "",
+ })
+
+ raw_bytes = query_nvme_log_page(device)
+ if raw_bytes is None:
+ return invalid
+
+ try:
+ phy_size_bytes = struct.unpack("<Q", raw_bytes[:8])[0]
+ phy_util_bytes = struct.unpack("<Q", raw_bytes[8:16])[0]
+ log_size_bytes = struct.unpack("<Q", raw_bytes[16:24])[0]
+ log_util_bytes = struct.unpack("<Q", raw_bytes[24:32])[0]
+ except struct.error as exc:
+ logger.error(
+ "Problem unpacking usage stats from log page 0xCA for device %s: %s",
+ device,
+ exc,
+ )
+ return invalid
+
+ phy_util_percent = (
+ (phy_util_bytes / phy_size_bytes) * 100 if phy_size_bytes else 0.0
+ )
+ log_util_percent = (
+ (log_util_bytes / log_size_bytes) * 100 if log_size_bytes else 0.0
+ )
+ compression_ratio = (
+ log_util_bytes / phy_util_bytes if phy_util_bytes else 0.0
+ )
+ savings_bytes = log_util_bytes - phy_util_bytes
+
+ return apply_fcm_display_fields({
+ "device": device,
+ "model": model,
+ "serial_number": serial_number,
+ "valid": True,
+ "phy_size_bytes": phy_size_bytes,
+ "phy_util_bytes": phy_util_bytes,
+ "log_size_bytes": log_size_bytes,
+ "log_util_bytes": log_util_bytes,
+ "phy_util_percent": phy_util_percent,
+ "log_util_percent": log_util_percent,
+ "compression_ratio": compression_ratio,
+ "compression_ratio_str": compression_ratio_str(compression_ratio),
+ "savings_bytes": savings_bytes,
+ "status": {"health": "OK", "state": "Enabled"},
+ "compression_ratio_display": "",
+ "savings_display": "",
+ "phy_usage_display": "",
+ "log_usage_display": "",
+ })
+
+
+def collect_fcm_stats() -> dict[str, FCMStatsData]:
+ stats: dict[str, FCMStatsData] = {}
+ for device in list_nvme_namespace_names():
+ if not is_fcm_device(device):
+ continue
+ stats[device] = read_fcm_stats(device)
+ return stats
--- /dev/null
+from abc import ABC, abstractmethod
+from typing import Any, Dict
+
+from ceph_node_proxy.fcm_stats import collect_fcm_stats
+
+
+class LocalCollector(ABC):
+ """Collects node-local hardware metrics outside of Redfish."""
+
+ @property
+ @abstractmethod
+ def name(self) -> str:
+ """Category name exposed in node-proxy reports."""
+
+ @abstractmethod
+ def update(self) -> None:
+ """Refresh collector data from the local OS."""
+
+ @abstractmethod
+ def get_data(self) -> Dict[str, Any]:
+ """Return the latest collected data."""
+
+ def flush(self) -> None:
+ """Clear cached collector data."""
+
+
+class LocalCollectorRunner:
+ def __init__(self, collectors: list[LocalCollector]) -> None:
+ self._collectors = collectors
+
+ def update(self) -> None:
+ for collector in self._collectors:
+ collector.update()
+
+ def flush(self) -> None:
+ for collector in self._collectors:
+ collector.flush()
+
+ def categories(self) -> list[str]:
+ return [collector.name for collector in self._collectors]
+
+ def get_category(self, name: str) -> Dict[str, Any]:
+ for collector in self._collectors:
+ if collector.name == name:
+ return collector.get_data()
+ return {}
+
+
+class FCMCollector(LocalCollector):
+ """Collect FCM stats via NVMe ioctls."""
+
+ def __init__(self) -> None:
+ self._data: Dict[str, Any] = {}
+
+ @property
+ def name(self) -> str:
+ return "fcm"
+
+ def update(self) -> None:
+ self._data = {"local": collect_fcm_stats()}
+
+ def get_data(self) -> Dict[str, Any]:
+ return dict(self._data)
+
+ def flush(self) -> None:
+ self._data = {}
from ceph_node_proxy.api import NodeProxyApi
from ceph_node_proxy.bootstrap import create_node_proxy_manager
from ceph_node_proxy.config import load_cephadm_config
-from ceph_node_proxy.registry import get_system_class
+from ceph_node_proxy.registry import create_node_backend
from ceph_node_proxy.reporter import Reporter
from ceph_node_proxy.util import DEFAULTS, Config, get_logger, http_req
try:
vendor = self.config.get("system", {}).get("vendor", "generic")
self.log.info("Using Redfish vendor: %s", vendor)
- system_cls = get_system_class(vendor)
- self.system = system_cls(
+ self.system = create_node_backend(
+ vendor,
host=oob_details["host"],
port=oob_details["port"],
username=oob_details["username"],
--- /dev/null
+from time import sleep
+from typing import Any, Dict
+
+from ceph_node_proxy.baseredfishsystem import BaseRedfishSystem
+from ceph_node_proxy.basesystem import BaseSystem
+from ceph_node_proxy.local_collectors import LocalCollectorRunner
+from ceph_node_proxy.util import get_logger
+
+
+class NodeBackend(BaseSystem):
+ def __init__(
+ self,
+ redfish: BaseRedfishSystem,
+ local_collectors: LocalCollectorRunner,
+ ) -> None:
+ super().__init__(config=redfish.config)
+ self.redfish = redfish
+ self._local = local_collectors
+ self.data_ready = False
+ self.previous_data: Dict[str, Any] = {}
+ self.log = get_logger(__name__)
+
+ @property
+ def client(self) -> Any:
+ return self.redfish.client
+
+ def main(self) -> None:
+ self.stop = False
+ self.redfish.stop = False
+ self.redfish.initialize_redfish_session()
+
+ while not self.stop:
+ self.log.debug("waiting for a lock in the update loop.")
+ with self.lock:
+ if not self.pending_shutdown:
+ self.log.debug("lock acquired in the update loop.")
+ try:
+ self.redfish.run_update_cycle()
+ self._local.update()
+ self.data_ready = True
+ except RuntimeError as exc:
+ self.stop = True
+ self.redfish.stop = True
+ self.log.error(
+ "Error detected, trying to gracefully log out from "
+ "redfish api.\n%s",
+ exc,
+ )
+ self.redfish.client.logout()
+ raise
+ sleep(self.redfish.refresh_interval)
+ self.log.debug("lock released in the update loop.")
+ self.log.debug("exiting update loop.")
+ raise SystemExit(0)
+
+ def flush(self) -> None:
+ self.log.debug("Acquiring lock to flush data.")
+ self.lock.acquire()
+ self.log.debug("Lock acquired, flushing data.")
+ self.redfish.flush()
+ self._local.flush()
+ self.previous_data = {}
+ self.data_ready = False
+ self.log.info("Data flushed.")
+ self.lock.release()
+
+ def get_system(self) -> Dict[str, Any]:
+ result = self.redfish.get_system()
+ for category in self._local.categories():
+ result["status"][category] = self._local.get_category(category)
+ return result
+
+ def get_local_category(self, name: str) -> Dict[str, Any]:
+ return self._local.get_category(name)
+
+ def get_fcm(self) -> Dict[str, Any]:
+ return self.get_local_category("fcm")
+
+ def get_status(self) -> Dict[str, Dict[str, Dict]]:
+ return dict(self.redfish.get_status())
+
+ def get_memory(self) -> Dict[str, Dict[str, Dict]]:
+ return self.redfish.get_memory()
+
+ def get_processors(self) -> Dict[str, Dict[str, Dict]]:
+ return self.redfish.get_processors()
+
+ def get_network(self) -> Dict[str, Dict[str, Dict]]:
+ return self.redfish.get_network()
+
+ def get_storage(self) -> Dict[str, Dict[str, Dict]]:
+ return self.redfish.get_storage()
+
+ def get_firmware(self) -> Dict[str, Dict[str, Dict]]:
+ return self.redfish.get_firmware()
+
+ def get_power(self) -> Dict[str, Dict[str, Dict]]:
+ return self.redfish.get_power()
+
+ def get_fans(self) -> Dict[str, Dict[str, Dict]]:
+ return self.redfish.get_fans()
+
+ def get_temperatures(self) -> Dict[str, Dict[str, Dict]]:
+ return self.redfish.get_temperatures()
+
+ def get_sn(self) -> str:
+ return self.redfish.get_sn()
+
+ def get_led(self) -> Dict[str, Any]:
+ return self.redfish.get_led()
+
+ def set_led(self, data: Dict[str, str]) -> int:
+ return self.redfish.set_led(data)
+
+ def get_chassis_led(self) -> Dict[str, Any]:
+ return self.redfish.get_chassis_led()
+
+ def set_chassis_led(self, data: Dict[str, str]) -> int:
+ return self.redfish.set_chassis_led(data)
+
+ def device_led_on(self, device: str) -> int:
+ return self.redfish.device_led_on(device)
+
+ def device_led_off(self, device: str) -> int:
+ return self.redfish.device_led_off(device)
+
+ def get_device_led(self, device: str) -> Dict[str, Any]:
+ return self.redfish.get_device_led(device)
+
+ def set_device_led(self, device: str, data: Dict[str, bool]) -> int:
+ return self.redfish.set_device_led(device, data)
+
+ def chassis_led_on(self) -> int:
+ return self.redfish.chassis_led_on()
+
+ def chassis_led_off(self) -> int:
+ return self.redfish.chassis_led_off()
+
+ def shutdown_host(self, force: bool = False) -> int:
+ return self.redfish.shutdown_host(force=force)
+
+ def powercycle(self) -> int:
+ return self.redfish.powercycle()
def get_storage(self) -> Dict[str, Any]: ...
+ def get_fcm(self) -> Dict[str, Any]: ...
+
def get_processors(self) -> Dict[str, Any]: ...
def get_power(self) -> Dict[str, Any]: ...
-from typing import Dict, Type
+from typing import Dict, List, Type
-# Built-in implementations
-from ceph_node_proxy.atollon import AtollonSystem
+from ceph_node_proxy.atollon import AtollonRedfishProvider
from ceph_node_proxy.baseredfishsystem import BaseRedfishSystem
+from ceph_node_proxy.local_collectors import FCMCollector, LocalCollector, LocalCollectorRunner
+from ceph_node_proxy.node_backend import NodeBackend
from ceph_node_proxy.redfishdellsystem import RedfishDellSystem
-from ceph_node_proxy.util import get_logger
+from ceph_node_proxy.util import Config, get_logger
-REDFISH_SYSTEM_CLASSES: Dict[str, Type[BaseRedfishSystem]] = {
+REDFISH_PROVIDER_CLASSES: Dict[str, Type[BaseRedfishSystem]] = {
"generic": BaseRedfishSystem,
"dell": RedfishDellSystem,
- "atollon": AtollonSystem,
+ "atollon": AtollonRedfishProvider,
+}
+
+LOCAL_COLLECTOR_CLASSES_BY_VENDOR: Dict[str, List[Type[LocalCollector]]] = {
+ "atollon": [FCMCollector],
}
logger = get_logger(__name__)
-def _load_entry_point_systems() -> None:
+def _load_entry_point_redfish_providers() -> None:
try:
import pkg_resources # type: ignore[import-not-found]
except ImportError:
logger.debug(
- "pkg_resources not available; only built-in Redfish systems will be used."
+ "pkg_resources not available; only built-in Redfish providers will be used."
)
return
for ep in pkg_resources.iter_entry_points("ceph_node_proxy.systems"):
try:
- REDFISH_SYSTEM_CLASSES[ep.name] = ep.load()
- except (ImportError, AttributeError, ModuleNotFoundError) as e:
+ REDFISH_PROVIDER_CLASSES[ep.name] = ep.load()
+ except (ImportError, AttributeError, ModuleNotFoundError) as exc:
logger.warning(
- "Failed to load Redfish system entry point %s: %s", ep.name, e
+ "Failed to load Redfish provider entry point %s: %s", ep.name, exc
)
+def get_redfish_provider_class(vendor: str) -> Type[BaseRedfishSystem]:
+ """Return the Redfish provider class for the given vendor."""
+ return REDFISH_PROVIDER_CLASSES.get(vendor, BaseRedfishSystem)
+
+
def get_system_class(vendor: str) -> Type[BaseRedfishSystem]:
- """Return the Redfish system class for the given vendor.
- Falls back to generic."""
- return REDFISH_SYSTEM_CLASSES.get(vendor, BaseRedfishSystem)
+ """Deprecated alias for get_redfish_provider_class."""
+ return get_redfish_provider_class(vendor)
+
+
+def create_local_collector_runner(vendor: str) -> LocalCollectorRunner:
+ """Build the local collector runner configured for a vendor."""
+ collector_classes = LOCAL_COLLECTOR_CLASSES_BY_VENDOR.get(vendor, [])
+ collectors = [collector_cls() for collector_cls in collector_classes]
+ return LocalCollectorRunner(collectors)
+
+
+def create_node_backend(
+ vendor: str,
+ *,
+ host: str,
+ port: str,
+ username: str,
+ password: str,
+ config: Config,
+) -> NodeBackend:
+ """Create the node backend facade for a vendor."""
+ provider_cls = get_redfish_provider_class(vendor)
+ redfish = provider_cls(
+ host=host,
+ port=port,
+ username=username,
+ password=password,
+ config=config,
+ )
+ local_collectors = create_local_collector_runner(vendor)
+ return NodeBackend(redfish, local_collectors)
+
+_load_entry_point_redfish_providers()
-_load_entry_point_systems()
+# Backward-compatible alias used by older imports and entry point docs.
+REDFISH_SYSTEM_CLASSES = REDFISH_PROVIDER_CLASSES
import pytest
-from ceph_node_proxy.atollon import AtollonSystem
+from ceph_node_proxy.atollon import AtollonRedfishProvider
from ceph_node_proxy.baseredfishsystem import BaseRedfishSystem
@pytest.fixture
-def atollon_system():
+def atollon_redfish():
with (
patch("ceph_node_proxy.baseredfishsystem.RedFishClient", return_value=MagicMock()),
patch("ceph_node_proxy.baseredfishsystem.EndpointMgr", return_value=MagicMock()),
):
- return AtollonSystem(
+ return AtollonRedfishProvider(
host="testhost",
port="443",
username="user",
)
-class TestAtollonSystemMemoryOverrides:
- def test_get_specs_memory_uses_id_instead_of_description(self, atollon_system):
- specs = atollon_system.get_specs("memory")
+class TestAtollonRedfishProviderMemoryOverrides:
+ def test_get_specs_memory_uses_id_instead_of_description(self, atollon_redfish):
+ specs = atollon_redfish.get_specs("memory")
assert len(specs) == 1
assert "Id" in specs[0].fields
assert "Description" not in specs[0].fields
assert "MemoryDeviceType" in specs[0].fields
- def test_update_memory_maps_id_to_description(self, atollon_system):
- atollon_system._sys["memory"] = {
+ def test_update_memory_maps_id_to_description(self, atollon_redfish):
+ atollon_redfish._sys["memory"] = {
"1": {
"dimm0": {
"id": "dimm0",
with patch.object(BaseRedfishSystem, "_update_memory") as mock_super:
mock_super.side_effect = lambda: None
- atollon_system._update_memory()
+ atollon_redfish._update_memory()
- assert atollon_system._sys["memory"]["1"]["dimm0"]["description"] == "dimm0"
+ assert atollon_redfish._sys["memory"]["1"]["dimm0"]["description"] == "dimm0"
-class TestAtollonSystemStorageOverrides:
- def test_update_storage_replaces_unknown_description(self, atollon_system):
- atollon_system._sys["storage"] = {
+class TestAtollonRedfishProviderStorageOverrides:
+ def test_update_storage_replaces_unknown_description(self, atollon_redfish):
+ atollon_redfish._sys["storage"] = {
"Self": {
"nvme_device0_nsid1": {
"description": "unknown",
with patch.object(BaseRedfishSystem, "_update_storage") as mock_super:
mock_super.side_effect = lambda: None
- atollon_system.fix_storage_descriptions()
+ atollon_redfish.fix_storage_descriptions()
- drive = atollon_system._sys["storage"]["Self"]["nvme_device0_nsid1"]
+ drive = atollon_redfish._sys["storage"]["Self"]["nvme_device0_nsid1"]
assert drive["description"] == "NVMe_Device0_NSID1"
- def test_enrich_storage_from_controllers_by_serial(self, atollon_system):
- atollon_system._sys["storage"] = {
+ def test_enrich_storage_from_controllers_by_serial(self, atollon_redfish):
+ atollon_redfish._sys["storage"] = {
"Self": {
"nvme_device0_nsid1": {
"description": "NVMe_Device0_NSID1",
systems_endpoint.__getitem__ = MagicMock(
side_effect=lambda key: member_endpoint if key == "Self" else MagicMock()
)
- atollon_system.endpoints = MagicMock()
- atollon_system.endpoints.__getitem__ = MagicMock(
+ atollon_redfish.endpoints = MagicMock()
+ atollon_redfish.endpoints.__getitem__ = MagicMock(
side_effect=lambda key: systems_endpoint if key == "systems" else MagicMock()
)
- atollon_system.enrich_storage_from_controllers()
+ atollon_redfish.enrich_storage_from_controllers()
- drive = atollon_system._sys["storage"]["Self"]["nvme_device0_nsid1"]
+ drive = atollon_redfish._sys["storage"]["Self"]["nvme_device0_nsid1"]
assert drive["firmware_version"] == "V6MA001"
assert drive["slot"] == "0"
assert drive["speed_gbps"] == 63.02
assert drive["physical_location"]["partlocation"]["locationordinalvalue"] == 0
- def test_enrich_storage_from_controllers_by_device_index(self, atollon_system):
- atollon_system._sys["storage"] = {
+ def test_enrich_storage_from_controllers_by_device_index(self, atollon_redfish):
+ atollon_redfish._sys["storage"] = {
"Self": {
"nvme_device2_nsid1": {
"description": "NVMe_Device2_NSID1",
systems_endpoint.__getitem__ = MagicMock(
side_effect=lambda key: member_endpoint if key == "Self" else MagicMock()
)
- atollon_system.endpoints = MagicMock()
- atollon_system.endpoints.__getitem__ = MagicMock(
+ atollon_redfish.endpoints = MagicMock()
+ atollon_redfish.endpoints.__getitem__ = MagicMock(
side_effect=lambda key: systems_endpoint if key == "systems" else MagicMock()
)
- atollon_system.enrich_storage_from_controllers()
+ atollon_redfish.enrich_storage_from_controllers()
- drive = atollon_system._sys["storage"]["Self"]["nvme_device2_nsid1"]
+ drive = atollon_redfish._sys["storage"]["Self"]["nvme_device2_nsid1"]
assert drive["firmware_version"] == "000A5305"
assert drive["slot"] == "2"
assert result["status"]["power"] == {}
assert result["status"]["fans"] == {}
assert result["status"]["temperatures"] == {}
+ assert "fcm" not in result["status"]
assert "firmware" in result
+ def test_get_fcm_empty(self, system):
+ assert system.get_fcm() == {}
+
class TestBaseRedfishSystemGetSpecs:
--- /dev/null
+import struct
+from unittest.mock import patch
+
+from ceph_node_proxy.fcm_stats import (
+ apply_fcm_display_fields,
+ collect_fcm_stats,
+ fcm_usage_display,
+ is_fcm_device,
+ read_fcm_stats,
+)
+
+
+class TestFCMStatsHelpers:
+ def test_is_fcm_device_true(self):
+ with patch(
+ "ceph_node_proxy.fcm_stats.read_sysfs_block",
+ return_value="IBM FCM5 3.2TB",
+ ):
+ assert is_fcm_device("nvme2n1") is True
+
+ def test_is_fcm_device_false(self):
+ with patch(
+ "ceph_node_proxy.fcm_stats.read_sysfs_block",
+ return_value="Micron_2550_MTFDKBK512TGE",
+ ):
+ assert is_fcm_device("nvme0n1") is False
+
+
+class TestReadFCMStats:
+ def _sample_log_page(self) -> bytes:
+ return struct.pack(
+ "<QQQQ",
+ 3_200_000_000_000,
+ 1_600_000_000_000,
+ 6_400_000_000_000,
+ 3_200_000_000_000,
+ )
+
+ def test_read_fcm_stats_success(self):
+ with (
+ patch(
+ "ceph_node_proxy.fcm_stats.read_sysfs_block",
+ side_effect=lambda device, attr: {
+ "device/model": "IBM FCM5 3.2TB",
+ "device/serial": "03NK797YS344D57S056",
+ }[attr],
+ ),
+ patch(
+ "ceph_node_proxy.fcm_stats.query_nvme_log_page",
+ return_value=self._sample_log_page(),
+ ),
+ ):
+ stats = read_fcm_stats("nvme2n1")
+
+ assert stats["valid"] is True
+ assert stats["device"] == "nvme2n1"
+ assert stats["compression_ratio"] == 2.0
+ assert stats["compression_ratio_str"] == "2:1"
+ assert stats["savings_bytes"] == 1_600_000_000_000
+ assert stats["compression_ratio_display"] == "2:1"
+ assert stats["savings_display"] == "1.6 TB"
+ assert stats["phy_usage_display"].endswith("(50%)")
+ assert stats["log_usage_display"].endswith("(50%)")
+ assert stats["status"]["health"] == "OK"
+
+ def test_read_fcm_stats_ioctl_failure(self):
+ with (
+ patch(
+ "ceph_node_proxy.fcm_stats.read_sysfs_block",
+ side_effect=lambda device, attr: {
+ "device/model": "IBM FCM5 3.2TB",
+ "device/serial": "03NK797YS344D57S056",
+ }[attr],
+ ),
+ patch(
+ "ceph_node_proxy.fcm_stats.query_nvme_log_page",
+ return_value=None,
+ ),
+ ):
+ stats = read_fcm_stats("nvme2n1")
+
+ assert stats["valid"] is False
+ assert stats["compression_ratio_str"] == ""
+ assert stats["compression_ratio_display"] == ""
+ assert stats["phy_usage_display"] == ""
+ assert stats["status"]["health"] == "Unknown"
+
+
+class TestFCMDisplayFields:
+ def test_fcm_usage_display_shows_human_bytes_and_int_percent(self):
+ usage = fcm_usage_display(15833446, 0.00027025391332477435)
+ assert usage.endswith("(0%)")
+ assert "MB" in usage
+
+ def test_apply_fcm_display_fields_hides_ratio_when_logical_usage_is_low(self):
+ stats = apply_fcm_display_fields({
+ "device": "nvme2n1",
+ "model": "FCM5",
+ "serial_number": "SN1",
+ "valid": True,
+ "phy_size_bytes": 0,
+ "phy_util_bytes": 0,
+ "log_size_bytes": 0,
+ "log_util_bytes": 0,
+ "phy_util_percent": 0.0,
+ "log_util_percent": 0.0,
+ "compression_ratio": 0.0,
+ "compression_ratio_str": "0:1",
+ "savings_bytes": 0,
+ "compression_ratio_display": "",
+ "savings_display": "",
+ "phy_usage_display": "",
+ "log_usage_display": "",
+ "status": {"health": "OK", "state": "Enabled"},
+ })
+ assert stats["compression_ratio_display"] == ""
+ assert stats["log_usage_display"].endswith("(0%)")
+
+ def test_apply_fcm_display_fields_shows_ratio_and_formatted_values(self):
+ stats = apply_fcm_display_fields({
+ "device": "nvme5n1",
+ "model": "FCM5",
+ "serial_number": "SN2",
+ "valid": True,
+ "phy_size_bytes": 1,
+ "phy_util_bytes": 15833446,
+ "log_size_bytes": 1,
+ "log_util_bytes": 16032506,
+ "phy_util_percent": 0.00027025391332477435,
+ "log_util_percent": 1.0,
+ "compression_ratio": 1.0,
+ "compression_ratio_str": "1:1",
+ "savings_bytes": 199060,
+ "compression_ratio_display": "",
+ "savings_display": "",
+ "phy_usage_display": "",
+ "log_usage_display": "",
+ "status": {"health": "OK", "state": "Enabled"},
+ })
+ assert stats["compression_ratio_display"] == "1:1"
+ assert stats["savings_display"] == "199.1 KB"
+ assert stats["phy_usage_display"].endswith("(0%)")
+ assert stats["log_usage_display"].endswith("(1%)")
+
+ def test_apply_fcm_display_fields_clears_metrics_when_invalid(self):
+ stats = apply_fcm_display_fields({
+ "device": "nvme0n1",
+ "model": "FCM5",
+ "serial_number": "SN3",
+ "valid": False,
+ "phy_size_bytes": 0,
+ "phy_util_bytes": 0,
+ "log_size_bytes": 0,
+ "log_util_bytes": 0,
+ "phy_util_percent": 0.0,
+ "log_util_percent": 0.0,
+ "compression_ratio": 0.0,
+ "compression_ratio_str": "",
+ "savings_bytes": 0,
+ "compression_ratio_display": "",
+ "savings_display": "",
+ "phy_usage_display": "",
+ "log_usage_display": "",
+ "status": {"health": "Unknown", "state": "Unavailable"},
+ })
+ assert stats["compression_ratio_display"] == ""
+ assert stats["savings_display"] == ""
+ assert stats["phy_usage_display"] == ""
+ assert stats["log_usage_display"] == ""
+
+
+class TestCollectFCMStats:
+ def test_collect_fcm_stats_only_fcm_devices(self):
+ with (
+ patch(
+ "ceph_node_proxy.fcm_stats.list_nvme_namespace_names",
+ return_value=["nvme0n1", "nvme2n1"],
+ ),
+ patch(
+ "ceph_node_proxy.fcm_stats.is_fcm_device",
+ side_effect=lambda device: device == "nvme2n1",
+ ),
+ patch(
+ "ceph_node_proxy.fcm_stats.read_fcm_stats",
+ side_effect=lambda device: {
+ "device": device,
+ "valid": True,
+ "compression_ratio": 2.0,
+ "compression_ratio_str": "2:1",
+ },
+ ),
+ ):
+ stats = collect_fcm_stats()
+
+ assert set(stats.keys()) == {"nvme2n1"}
+ assert stats["nvme2n1"]["compression_ratio_str"] == "2:1"
--- /dev/null
+from unittest.mock import patch
+
+from ceph_node_proxy.local_collectors import FCMCollector, LocalCollectorRunner
+
+
+class TestFCMCollector:
+ def test_name(self):
+ assert FCMCollector().name == "fcm"
+
+ def test_update_and_get_data(self):
+ sample_stats = {
+ "nvme2n1": {
+ "device": "nvme2n1",
+ "valid": True,
+ "compression_ratio": 2.5,
+ "compression_ratio_str": "2.5:1",
+ },
+ }
+ collector = FCMCollector()
+ with patch(
+ "ceph_node_proxy.local_collectors.collect_fcm_stats",
+ return_value=sample_stats,
+ ):
+ collector.update()
+
+ assert collector.get_data() == {"local": sample_stats}
+
+ def test_flush_clears_data(self):
+ collector = FCMCollector()
+ collector._data = {"local": {"nvme2n1": {"valid": True}}}
+ collector.flush()
+ assert collector.get_data() == {}
+
+
+class TestLocalCollectorRunner:
+ def test_runner_update_and_get_category(self):
+ sample_stats = {"nvme2n1": {"valid": True}}
+ runner = LocalCollectorRunner([FCMCollector()])
+ with patch(
+ "ceph_node_proxy.local_collectors.collect_fcm_stats",
+ return_value=sample_stats,
+ ):
+ runner.update()
+
+ assert runner.get_category("fcm") == {"local": sample_stats}
+ assert runner.get_category("missing") == {}
+
+ def test_runner_flush(self):
+ runner = LocalCollectorRunner([FCMCollector()])
+ runner._collectors[0]._data = {"local": {"nvme2n1": {"valid": True}}}
+ runner.flush()
+ assert runner.get_category("fcm") == {}
--- /dev/null
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+from ceph_node_proxy.local_collectors import FCMCollector, LocalCollectorRunner
+from ceph_node_proxy.node_backend import NodeBackend
+from ceph_node_proxy.registry import create_local_collector_runner, create_node_backend
+
+
+@pytest.fixture
+def mock_redfish():
+ redfish = MagicMock()
+ redfish.config = {}
+ redfish.refresh_interval = 1
+ redfish.get_system.return_value = {
+ "host": "node01",
+ "sn": "SN123",
+ "status": {
+ "storage": {},
+ "processors": {},
+ "network": {},
+ "memory": {},
+ "power": {},
+ "fans": {},
+ "temperatures": {},
+ },
+ "firmware": {},
+ }
+ return redfish
+
+
+class TestLocalCollectorRunnerRegistry:
+ def test_atollon_vendor_includes_fcm_collector(self):
+ runner = create_local_collector_runner("atollon")
+ assert runner.categories() == ["fcm"]
+
+ def test_generic_vendor_has_no_collectors(self):
+ runner = create_local_collector_runner("generic")
+ assert runner.categories() == []
+
+
+class TestNodeBackend:
+ def test_get_fcm_delegates_to_local_runner(self, mock_redfish):
+ runner = LocalCollectorRunner([FCMCollector()])
+ backend = NodeBackend(mock_redfish, runner)
+ sample_stats = {
+ "nvme2n1": {
+ "device": "nvme2n1",
+ "valid": True,
+ "compression_ratio": 2.5,
+ "compression_ratio_str": "2.5:1",
+ },
+ }
+ with patch(
+ "ceph_node_proxy.local_collectors.collect_fcm_stats",
+ return_value=sample_stats,
+ ):
+ runner.update()
+
+ assert backend.get_fcm() == {"local": sample_stats}
+
+ def test_get_system_merges_redfish_and_local_categories(self, mock_redfish):
+ runner = LocalCollectorRunner([FCMCollector()])
+ backend = NodeBackend(mock_redfish, runner)
+ sample_stats = {"nvme2n1": {"valid": True, "compression_ratio": 2.0}}
+ with patch(
+ "ceph_node_proxy.local_collectors.collect_fcm_stats",
+ return_value=sample_stats,
+ ):
+ runner.update()
+
+ result = backend.get_system()
+ assert result["status"]["fcm"] == {"local": sample_stats}
+ assert "storage" in result["status"]
+ mock_redfish.get_system.assert_called_once()
+
+ def test_flush_clears_local_collectors(self, mock_redfish):
+ runner = LocalCollectorRunner([FCMCollector()])
+ backend = NodeBackend(mock_redfish, runner)
+ runner.update()
+ backend.flush()
+ assert backend.get_fcm() == {}
+ mock_redfish.flush.assert_called_once()
+
+ def test_delegates_storage_to_redfish(self, mock_redfish):
+ mock_redfish.get_storage.return_value = {"Self": {"drive1": {}}}
+ backend = NodeBackend(mock_redfish, LocalCollectorRunner([]))
+ assert backend.get_storage() == {"Self": {"drive1": {}}}
+
+
+class TestCreateNodeBackend:
+ def test_create_node_backend_atollon(self):
+ with patch("ceph_node_proxy.registry.get_redfish_provider_class") as mock_provider:
+ mock_redfish = MagicMock()
+ mock_provider.return_value.return_value = mock_redfish
+ backend = create_node_backend(
+ "atollon",
+ host="bmc",
+ port="443",
+ username="user",
+ password="secret",
+ config=MagicMock(),
+ )
+
+ assert isinstance(backend, NodeBackend)
+ assert backend.redfish is mock_redfish
+ assert backend._local.categories() == ["fcm"]
raise cherrypy.HTTPError(404, f"{kw.get('hostname')} not found.")
return results
+ @cherrypy.expose
+ @cherrypy.tools.allow(methods=['GET'])
+ @cherrypy.tools.json_out()
+ def fcm(self, **kw: Any) -> Dict[str, Any]:
+ try:
+ results = self.mgr.node_proxy_cache.common('fcm', **kw)
+ except (KeyError, OrchestratorError):
+ raise cherrypy.HTTPError(404, f"{kw.get('hostname')} not found.")
+ return results
+
@cherrypy.expose
@cherrypy.tools.allow(methods=['GET'])
@cherrypy.tools.json_out()
full_set_with_critical = {'host': 'host01', 'sn': '12345', 'status': {'storage': {'1': {'disk.bay.0:enclosure.internal.0-1:raid.integrated.1-1': {'description': 'Solid State Disk 0:1:0', 'entity': 'RAID.Integrated.1-1', 'capacity_bytes': 959656755200, 'model': 'KPM5XVUG960G', 'protocol': 'SAS', 'serial_number': '8080A1CRTP5F', 'status': {'health': 'Critical', 'healthrollup': 'OK', 'state': 'Enabled'}, 'physical_location': {'partlocation': {'locationordinalvalue': 0, 'locationtype': 'Slot'}}}, 'disk.bay.9:enclosure.internal.0-1': {'description': 'PCIe SSD in Slot 9 in Bay 1', 'entity': 'CPU.1', 'capacity_bytes': 1600321314816, 'model': 'Dell Express Flash NVMe P4610 1.6TB SFF', 'protocol': 'PCIe', 'serial_number': 'PHLN035305MN1P6AGN', 'status': {'health': 'Critical', 'healthrollup': 'OK', 'state': 'Enabled'}, 'physical_location': {'partlocation': {'locationordinalvalue': 9, 'locationtype': 'Slot'}}}}}, 'processors': {'1': {'cpu.socket.2': {'description': 'Represents the properties of a Processor attached to this System', 'total_cores': 20, 'total_threads': 40, 'processor_type': 'CPU', 'model': 'Intel(R) Xeon(R) Gold 6230 CPU @ 2.10GHz', 'status': {'health': 'OK', 'state': 'Enabled'}, 'manufacturer': 'Intel'}}}, 'network': {'1': {'nic.slot.1-1-1': {'description': 'NIC in Slot 1 Port 1 Partition 1', 'name': 'System Ethernet Interface', 'speed_mbps': 0, 'status': {'health': 'OK', 'state': 'StandbyOffline'}}}}, 'memory': {'1': {'dimm.socket.a1': {'description': 'DIMM A1', 'memory_device_type': 'DDR4', 'capacity_mi_b': 31237, 'status': {'health': 'Critical', 'state': 'Enabled'}}}}}, 'firmware': {}}
mgr_inventory_cache = {'host01': {'hostname': 'host01', 'addr': '10.10.10.11', 'labels': ['_admin'], 'status': '', 'oob': {'hostname': '10.10.10.11', 'username': 'root', 'password': 'ceph123'}}, 'host02': {'hostname': 'host02', 'addr': '10.10.10.12', 'labels': [], 'status': '', 'oob': {'hostname': '10.10.10.12', 'username': 'root', 'password': 'ceph123'}}}
-full_set = {'host01': {'host': 'host01', 'sn': 'FR8Y5X3', 'status': {'storage': {'1': {'disk.bay.8:enclosure.internal.0-1:nonraid.slot.2-1': {'description': 'Disk 8 in Backplane 1 of Storage Controller in Slot 2', 'entity': 'NonRAID.Slot.2-1', 'capacity_bytes': 20000588955136, 'model': 'ST20000NM008D-3D', 'protocol': 'SATA', 'serial_number': 'ZVT99QLL', 'status': {'health': 'OK', 'healthrollup': 'OK', 'state': 'Enabled'}, 'physical_location': {'partlocation': {'locationordinalvalue': 8, 'locationtype': 'Slot'}}}}}, 'processors': {'1': {'cpu.socket.2': {'description': 'Represents the properties of a Processor attached to this System', 'total_cores': 16, 'total_threads': 32, 'processor_type': 'CPU', 'model': 'Intel(R) Xeon(R) Silver 4314 CPU @ 2.40GHz', 'status': {'health': 'OK', 'state': 'Enabled'}, 'manufacturer': 'Intel'}, 'cpu.socket.1': {'description': 'Represents the properties of a Processor attached to this System', 'total_cores': 16, 'total_threads': 32, 'processor_type': 'CPU', 'model': 'Intel(R) Xeon(R) Silver 4314 CPU @ 2.40GHz', 'status': {'health': 'OK', 'state': 'Enabled'}, 'manufacturer': 'Intel'}}}, 'network': {'1': {'oslogicalnetwork.2': {'description': 'eno8303', 'name': 'eno8303', 'speed_mbps': 0, 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'memory': {'1': {'dimm.socket.a1': {'description': 'DIMM A1', 'memory_device_type': 'DDR4', 'capacity_mi_b': 16384, 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'power': {'1': {'0': {'name': 'PS1 Status', 'model': 'PWR SPLY,800W,RDNT,LTON', 'manufacturer': 'DELL', 'status': {'health': 'OK', 'state': 'Enabled'}}, '1': {'name': 'PS2 Status', 'model': 'PWR SPLY,800W,RDNT,LTON', 'manufacturer': 'DELL', 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'fans': {'1': {'0': {'name': 'System Board Fan1A', 'physical_context': 'SystemBoard', 'reading': 4800, 'reading_units': 'RPM', 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'temperatures': {'1': {'0': {'name': 'System Board Inlet Temp', 'physical_context': 'Intake', 'reading': 24, 'reading_units': 'Cel', 'status': {'health': 'OK', 'state': 'Enabled'}}}}}, 'firmware': {'installed-28897-6.10.30.20__usc.embedded.1:lc.embedded.1': {'name': 'Lifecycle Controller', 'description': 'Represents Firmware Inventory', 'release_date': '00:00:00Z', 'version': '6.10.30.20', 'updateable': True, 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'host02': {'host': 'host02', 'sn': 'FR8Y5X4', 'status': {'storage': {'1': {'disk.bay.8:enclosure.internal.0-1:nonraid.slot.2-1': {'description': 'Disk 8 in Backplane 1 of Storage Controller in Slot 2', 'entity': 'NonRAID.Slot.2-1', 'capacity_bytes': 20000588955136, 'model': 'ST20000NM008D-3D', 'protocol': 'SATA', 'serial_number': 'ZVT99QLL', 'status': {'health': 'OK', 'healthrollup': 'OK', 'state': 'Enabled'}, 'physical_location': {'partlocation': {'locationordinalvalue': 8, 'locationtype': 'Slot'}}}}}, 'processors': {'1': {'cpu.socket.2': {'description': 'Represents the properties of a Processor attached to this System', 'total_cores': 16, 'total_threads': 32, 'processor_type': 'CPU', 'model': 'Intel(R) Xeon(R) Silver 4314 CPU @ 2.40GHz', 'status': {'health': 'OK', 'state': 'Enabled'}, 'manufacturer': 'Intel'}, 'cpu.socket.1': {'description': 'Represents the properties of a Processor attached to this System', 'total_cores': 16, 'total_threads': 32, 'processor_type': 'CPU', 'model': 'Intel(R) Xeon(R) Silver 4314 CPU @ 2.40GHz', 'status': {'health': 'OK', 'state': 'Enabled'}, 'manufacturer': 'Intel'}}}, 'network': {'1': {'oslogicalnetwork.2': {'description': 'eno8303', 'name': 'eno8303', 'speed_mbps': 0, 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'memory': {'1': {'dimm.socket.a1': {'description': 'DIMM A1', 'memory_device_type': 'DDR4', 'capacity_mi_b': 16384, 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'power': {'1': {'0': {'name': 'PS1 Status', 'model': 'PWR SPLY,800W,RDNT,LTON', 'manufacturer': 'DELL', 'status': {'health': 'OK', 'state': 'Enabled'}}, '1': {'name': 'PS2 Status', 'model': 'PWR SPLY,800W,RDNT,LTON', 'manufacturer': 'DELL', 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'fans': {'1': {'0': {'name': 'System Board Fan1A', 'physical_context': 'SystemBoard', 'reading': 4800, 'reading_units': 'RPM', 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'temperatures': {'1': {'0': {'name': 'System Board Inlet Temp', 'physical_context': 'Intake', 'reading': 24, 'reading_units': 'Cel', 'status': {'health': 'OK', 'state': 'Enabled'}}}}}, 'firmware': {'installed-28897-6.10.30.20__usc.embedded.1:lc.embedded.1': {'name': 'Lifecycle Controller', 'description': 'Represents Firmware Inventory', 'release_date': '00:00:00Z', 'version': '6.10.30.20', 'updateable': True, 'status': {'health': 'OK', 'state': 'Enabled'}}}}}
+full_set = {'host01': {'host': 'host01', 'sn': 'FR8Y5X3', 'status': {'storage': {'1': {'disk.bay.8:enclosure.internal.0-1:nonraid.slot.2-1': {'description': 'Disk 8 in Backplane 1 of Storage Controller in Slot 2', 'entity': 'NonRAID.Slot.2-1', 'capacity_bytes': 20000588955136, 'model': 'ST20000NM008D-3D', 'protocol': 'SATA', 'serial_number': 'ZVT99QLL', 'status': {'health': 'OK', 'healthrollup': 'OK', 'state': 'Enabled'}, 'physical_location': {'partlocation': {'locationordinalvalue': 8, 'locationtype': 'Slot'}}}}}, 'processors': {'1': {'cpu.socket.2': {'description': 'Represents the properties of a Processor attached to this System', 'total_cores': 16, 'total_threads': 32, 'processor_type': 'CPU', 'model': 'Intel(R) Xeon(R) Silver 4314 CPU @ 2.40GHz', 'status': {'health': 'OK', 'state': 'Enabled'}, 'manufacturer': 'Intel'}, 'cpu.socket.1': {'description': 'Represents the properties of a Processor attached to this System', 'total_cores': 16, 'total_threads': 32, 'processor_type': 'CPU', 'model': 'Intel(R) Xeon(R) Silver 4314 CPU @ 2.40GHz', 'status': {'health': 'OK', 'state': 'Enabled'}, 'manufacturer': 'Intel'}}}, 'network': {'1': {'oslogicalnetwork.2': {'description': 'eno8303', 'name': 'eno8303', 'speed_mbps': 0, 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'memory': {'1': {'dimm.socket.a1': {'description': 'DIMM A1', 'memory_device_type': 'DDR4', 'capacity_mi_b': 16384, 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'power': {'1': {'0': {'name': 'PS1 Status', 'model': 'PWR SPLY,800W,RDNT,LTON', 'manufacturer': 'DELL', 'status': {'health': 'OK', 'state': 'Enabled'}}, '1': {'name': 'PS2 Status', 'model': 'PWR SPLY,800W,RDNT,LTON', 'manufacturer': 'DELL', 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'fans': {'1': {'0': {'name': 'System Board Fan1A', 'physical_context': 'SystemBoard', 'reading': 4800, 'reading_units': 'RPM', 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'temperatures': {'1': {'0': {'name': 'System Board Inlet Temp', 'physical_context': 'Intake', 'reading': 24, 'reading_units': 'Cel', 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'fcm': {'local': {'nvme2n1': {'device': 'nvme2n1', 'model': 'IBM FCM5 3.2TB', 'serial_number': '03NK797YS344D57S056', 'valid': True, 'compression_ratio': 2.5, 'compression_ratio_str': '2.5:1', 'savings_bytes': 1000000, 'phy_util_percent': 50.0, 'log_util_percent': 25.0, 'status': {'health': 'OK', 'state': 'Enabled'}}}}}, 'firmware': {'installed-28897-6.10.30.20__usc.embedded.1:lc.embedded.1': {'name': 'Lifecycle Controller', 'description': 'Represents Firmware Inventory', 'release_date': '00:00:00Z', 'version': '6.10.30.20', 'updateable': True, 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'host02': {'host': 'host02', 'sn': 'FR8Y5X4', 'status': {'storage': {'1': {'disk.bay.8:enclosure.internal.0-1:nonraid.slot.2-1': {'description': 'Disk 8 in Backplane 1 of Storage Controller in Slot 2', 'entity': 'NonRAID.Slot.2-1', 'capacity_bytes': 20000588955136, 'model': 'ST20000NM008D-3D', 'protocol': 'SATA', 'serial_number': 'ZVT99QLL', 'status': {'health': 'OK', 'healthrollup': 'OK', 'state': 'Enabled'}, 'physical_location': {'partlocation': {'locationordinalvalue': 8, 'locationtype': 'Slot'}}}}}, 'processors': {'1': {'cpu.socket.2': {'description': 'Represents the properties of a Processor attached to this System', 'total_cores': 16, 'total_threads': 32, 'processor_type': 'CPU', 'model': 'Intel(R) Xeon(R) Silver 4314 CPU @ 2.40GHz', 'status': {'health': 'OK', 'state': 'Enabled'}, 'manufacturer': 'Intel'}, 'cpu.socket.1': {'description': 'Represents the properties of a Processor attached to this System', 'total_cores': 16, 'total_threads': 32, 'processor_type': 'CPU', 'model': 'Intel(R) Xeon(R) Silver 4314 CPU @ 2.40GHz', 'status': {'health': 'OK', 'state': 'Enabled'}, 'manufacturer': 'Intel'}}}, 'network': {'1': {'oslogicalnetwork.2': {'description': 'eno8303', 'name': 'eno8303', 'speed_mbps': 0, 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'memory': {'1': {'dimm.socket.a1': {'description': 'DIMM A1', 'memory_device_type': 'DDR4', 'capacity_mi_b': 16384, 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'power': {'1': {'0': {'name': 'PS1 Status', 'model': 'PWR SPLY,800W,RDNT,LTON', 'manufacturer': 'DELL', 'status': {'health': 'OK', 'state': 'Enabled'}}, '1': {'name': 'PS2 Status', 'model': 'PWR SPLY,800W,RDNT,LTON', 'manufacturer': 'DELL', 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'fans': {'1': {'0': {'name': 'System Board Fan1A', 'physical_context': 'SystemBoard', 'reading': 4800, 'reading_units': 'RPM', 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'temperatures': {'1': {'0': {'name': 'System Board Inlet Temp', 'physical_context': 'Intake', 'reading': 24, 'reading_units': 'Cel', 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'fcm': {'local': {'nvme2n1': {'device': 'nvme2n1', 'model': 'IBM FCM5 3.2TB', 'serial_number': '03NK797YS344D57S056', 'valid': True, 'compression_ratio': 2.5, 'compression_ratio_str': '2.5:1', 'savings_bytes': 1000000, 'phy_util_percent': 50.0, 'log_util_percent': 25.0, 'status': {'health': 'OK', 'state': 'Enabled'}}}}}, 'firmware': {'installed-28897-6.10.30.20__usc.embedded.1:lc.embedded.1': {'name': 'Lifecycle Controller', 'description': 'Represents Firmware Inventory', 'release_date': '00:00:00Z', 'version': '6.10.30.20', 'updateable': True, 'status': {'health': 'OK', 'state': 'Enabled'}}}}}
self.getPage("/host03/temperatures", method="GET")
self.assertStatus('404 Not Found')
+ def test_fcm_with_valid_hostname(self):
+ self.getPage("/host02/fcm", method="GET")
+ self.assertStatus('200 OK')
+
+ def test_fcm_no_hostname(self):
+ self.getPage("/fcm", method="GET")
+ self.assertStatus('200 OK')
+
+ def test_fcm_with_invalid_hostname(self):
+ self.getPage("/host03/fcm", method="GET")
+ self.assertStatus('404 Not Found')
+
def test_firmware_with_valid_hostname(self):
self.getPage("/host02/firmware", method="GET")
self.assertStatus('200 OK')
'power': ['HOST', 'CHASSIS_ID', 'ID', 'NAME', 'MODEL', 'MANUFACTURER', 'STATUS', 'STATE'],
'fans': ['HOST', 'CHASSIS_ID', 'ID', 'NAME', 'READING', 'UNITS', 'STATUS', 'STATE'],
'temperatures': ['HOST', 'CHASSIS_ID', 'ID', 'NAME', 'READING', 'UNITS', 'STATUS', 'STATE'],
+ 'fcm': ['HOST', 'SOURCE', 'DEVICE', 'MODEL', 'SN', 'RATIO', 'SAVINGS',
+ 'PHYS USED', 'LOG USED', 'VALID', 'STATUS', 'STATE'],
}
if category == 'firmwares':
'power': ('name', 'model', 'manufacturer', 'health', 'state'),
'fans': ('name', 'reading', 'reading_units', 'health', 'state'),
'temperatures': ('name', 'reading', 'reading_units', 'health', 'state'),
+ 'fcm': ('device', 'model', 'serial_number', 'compression_ratio_display',
+ 'savings_display', 'phy_usage_display', 'log_usage_display',
+ 'valid', 'health', 'state'),
}
fields = mapping.get(category, ())