Skip to content

Commit de1d824

Browse files
committed
utest + picking up info through lspci
1 parent 4d35e1f commit de1d824

File tree

8 files changed

+341
-50
lines changed

8 files changed

+341
-50
lines changed

nodescraper/plugins/inband/device_enumeration/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,7 @@
2323
# SOFTWARE.
2424
#
2525
###############################################################################
26+
from .analyzer_args import DeviceEnumerationAnalyzerArgs
27+
from .device_enumeration_plugin import DeviceEnumerationPlugin
28+
29+
__all__ = ["DeviceEnumerationPlugin", "DeviceEnumerationAnalyzerArgs"]

nodescraper/plugins/inband/device_enumeration/analyzer_args.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,17 @@
2323
# SOFTWARE.
2424
#
2525
###############################################################################
26+
from typing import Optional, Union
2627

2728
from nodescraper.models import AnalyzerArgs
28-
from nodescraper.plugins.inband.device_enumeration.deviceenumdata import (
29-
DeviceEnumerationDataModel,
30-
)
29+
30+
from .deviceenumdata import DeviceEnumerationDataModel
3131

3232

3333
class DeviceEnumerationAnalyzerArgs(AnalyzerArgs):
34-
cpu_count: list | int = (None,)
35-
gpu_count: list | int = (None,)
36-
vf_count: list | int = (None,)
34+
cpu_count: Optional[Union[list[int], int]] = None
35+
gpu_count: Optional[Union[list[int], int]] = None
36+
vf_count: Optional[Union[list[int], int]] = None
3737

3838
@classmethod
3939
def build_from_model(
@@ -47,4 +47,8 @@ def build_from_model(
4747
Returns:
4848
DeviceEnumerationAnalyzerArgs: instance of analyzer args class
4949
"""
50-
return cls(exp_bios_version=datamodel.bios_version)
50+
return cls(
51+
cpu_count=datamodel.cpu_count,
52+
gpu_count=datamodel.gpu_count,
53+
vf_count=datamodel.vf_count,
54+
)

nodescraper/plugins/inband/device_enumeration/device_enumeration_analyzer.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@
2222
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2323
# SOFTWARE.
2424
#
25-
###########################
26-
27-
from errorscraper.event import EventCategory, EventPriority
25+
###############################################################################
26+
from typing import Optional
2827

28+
from nodescraper.enums import EventCategory, EventPriority, ExecutionStatus
2929
from nodescraper.interfaces import DataAnalyzer
30-
from nodescraper.models import TaskResult, TaskStatus
31-
from nodescraper.plugins.inband.deviceenumdata import DeviceEnumerationDataModel
30+
from nodescraper.models import TaskResult
31+
32+
from .analyzer_args import DeviceEnumerationAnalyzerArgs
33+
from .deviceenumdata import DeviceEnumerationDataModel
3234

3335

3436
class DeviceEnumerationAnalyzer(
@@ -45,22 +47,20 @@ def analyze_data(
4547

4648
if not args:
4749
self.result.message = "Expected Device Enumeration expected data not provided"
48-
self.result.status = TaskStatus.NOT_RAN
50+
self.result.status = ExecutionStatus.NOT_RAN
4951
return self.result
5052

51-
if isinstance(args.cpu_count, int):
52-
cpu_count = [args.cpu_count]
53-
if isinstance(args.gpu_count, int):
54-
gpu_count = [args.gpu_count]
55-
if isinstance(args.vf_count, int):
56-
vf_count = [args.vf_count]
53+
# Convert to lists if integers, otherwise use as-is
54+
cpu_count = [args.cpu_count] if isinstance(args.cpu_count, int) else args.cpu_count
55+
gpu_count = [args.gpu_count] if isinstance(args.gpu_count, int) else args.gpu_count
56+
vf_count = [args.vf_count] if isinstance(args.vf_count, int) else args.vf_count
5757

5858
checks = {}
59-
if cpu_count not in [None, []]:
59+
if cpu_count is not None and cpu_count != []:
6060
checks["cpu_count"] = cpu_count
61-
if gpu_count not in [None, []]:
61+
if gpu_count is not None and gpu_count != []:
6262
checks["gpu_count"] = gpu_count
63-
if vf_count not in [None, []]:
63+
if vf_count is not None and vf_count != []:
6464
checks["vf_count"] = vf_count
6565

6666
self.result.message = ""
@@ -69,7 +69,7 @@ def analyze_data(
6969
if actual_count not in accepted_counts:
7070
message = f"Expected {check} in {accepted_counts}, but got {actual_count}. "
7171
self.result.message += message
72-
self.result.status = TaskStatus.ERRORS_DETECTED
72+
self.result.status = ExecutionStatus.ERROR
7373
self._log_event(
7474
category=EventCategory.PLATFORM,
7575
description=message,
@@ -78,7 +78,7 @@ def analyze_data(
7878
console_log=True,
7979
)
8080
if self.result.message == "":
81-
self.result.status = TaskStatus.OK
81+
self.result.status = ExecutionStatus.OK
8282
self.result.message = f"Device Enumeration validated on {checks.keys()}."
8383

8484
return self.result

nodescraper/plugins/inband/device_enumeration/device_enumeration_collector.py

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,35 @@
2323
# SOFTWARE.
2424
#
2525
###############################################################################
26-
26+
from typing import Optional
2727

2828
from nodescraper.base import InBandDataCollector
2929
from nodescraper.connection.inband.inband import CommandArtifact
30-
from nodescraper.enums import EventCategory, EventPriority, OSFamily
31-
from nodescraper.models import TaskResult, TaskStatus
32-
from nodescraper.plugins.inband.devenumdata import DeviceEnumerationDataModel
30+
from nodescraper.enums import EventCategory, EventPriority, ExecutionStatus, OSFamily
31+
from nodescraper.models import TaskResult
32+
33+
from .deviceenumdata import DeviceEnumerationDataModel
3334

3435

35-
class DeviceEnumerationCollector(InBandDataCollector[DeviceEnumerationDataModel]):
36+
class DeviceEnumerationCollector(InBandDataCollector[DeviceEnumerationDataModel, None]):
3637
"""Collect CPU and GPU count"""
3738

3839
DATA_MODEL = DeviceEnumerationDataModel
3940

41+
# Linux commands
42+
CMD_CPU_COUNT_LINUX = "lscpu | grep Socket | awk '{ print $2 }'"
43+
CMD_GPU_COUNT_LINUX = "lspci -d 1002: | grep -i 'VGA\\|Display\\|3D' | wc -l"
44+
CMD_VF_COUNT_LINUX = "lspci -d 1002: | grep -i 'Virtual Function' | wc -l"
45+
46+
# Windows commands
47+
CMD_CPU_COUNT_WINDOWS = (
48+
'powershell -Command "(Get-WmiObject -Class Win32_Processor | Measure-Object).Count"'
49+
)
50+
CMD_GPU_COUNT_WINDOWS = 'powershell -Command "(wmic path win32_VideoController get name | findstr AMD | Measure-Object).Count"'
51+
CMD_VF_COUNT_WINDOWS = (
52+
'powershell -Command "(Get-VMHostPartitionableGpu | Measure-Object).Count"'
53+
)
54+
4055
def _warning(
4156
self,
4257
description: str,
@@ -55,30 +70,26 @@ def _warning(
5570
priority=EventPriority.WARNING,
5671
)
5772

58-
def collect_data(self, args=None) -> tuple[TaskResult, DeviceEnumerationDataModel | None]:
73+
def collect_data(self, args=None) -> tuple[TaskResult, Optional[DeviceEnumerationDataModel]]:
5974
"""
6075
Read CPU and GPU count
6176
On Linux, use lscpu and lspci
6277
On Windows, use WMI and hyper-v cmdlets
6378
"""
6479
device_enum = None
6580
if self.system_info.os_family == OSFamily.LINUX:
66-
baremetal_device_id = self.system_info.devid_ep
67-
sriov_device_id = self.system_info.devid_ep_vf
81+
# Count CPU sockets
82+
cpu_count_res = self._run_sut_cmd(self.CMD_CPU_COUNT_LINUX)
6883

69-
cpu_count_res = self._run_system_command("lscpu | grep Socket | awk '{ print $2 }'")
70-
gpu_count_res = self._run_system_command(f"lspci -d :{baremetal_device_id} | wc -l")
71-
vf_count_res = self._run_system_command(f"lspci -d :{sriov_device_id} | wc -l")
84+
# Count all AMD GPUs (vendor ID 1002)
85+
gpu_count_res = self._run_sut_cmd(self.CMD_GPU_COUNT_LINUX)
86+
87+
# Count AMD Virtual Functions
88+
vf_count_res = self._run_sut_cmd(self.CMD_VF_COUNT_LINUX)
7289
else:
73-
cpu_count_res = self._run_system_command(
74-
'powershell -Command "(Get-WmiObject -Class Win32_Processor | Measure-Object).Count"'
75-
)
76-
gpu_count_res = self._run_system_command(
77-
'powershell -Command "(wmic path win32_VideoController get name | findstr AMD | Measure-Object).Count"'
78-
)
79-
vf_count_res = self._run_system_command(
80-
'powershell -Command "(Get-VMHostPartitionableGpu | Measure-Object).Count"'
81-
)
90+
cpu_count_res = self._run_sut_cmd(self.CMD_CPU_COUNT_WINDOWS)
91+
gpu_count_res = self._run_sut_cmd(self.CMD_GPU_COUNT_WINDOWS)
92+
vf_count_res = self._run_sut_cmd(self.CMD_VF_COUNT_WINDOWS)
8293
cpu_count, gpu_count, vf_count = [None, None, None]
8394

8495
if cpu_count_res.exit_code == 0:
@@ -111,10 +122,10 @@ def collect_data(self, args=None) -> tuple[TaskResult, DeviceEnumerationDataMode
111122
priority=EventPriority.INFO,
112123
)
113124
self.result.message = f"Device Enumeration: {device_enum.model_dump(exclude_none=True)}"
114-
self.result.status = TaskStatus.OK
125+
self.result.status = ExecutionStatus.OK
115126
else:
116127
self.result.message = "Device Enumeration info not found"
117-
self.result.status = TaskStatus.EXECUTION_FAILURE
128+
self.result.status = ExecutionStatus.EXECUTION_FAILURE
118129
self._log_event(
119130
category=EventCategory.SW_DRIVER,
120131
description=self.result.message,

nodescraper/plugins/inband/device_enumeration/device_enumeration_plugin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from .analyzer_args import DeviceEnumerationAnalyzerArgs
2929
from .device_enumeration_analyzer import DeviceEnumerationAnalyzer
3030
from .device_enumeration_collector import DeviceEnumerationCollector
31-
from .device_enumerationdata import DeviceEnumerationDataModel
31+
from .deviceenumdata import DeviceEnumerationDataModel
3232

3333

3434
class DeviceEnumerationPlugin(

nodescraper/plugins/inband/device_enumeration/deviceenumdata.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@
2323
# SOFTWARE.
2424
#
2525
###############################################################################
26+
from typing import Optional
27+
2628
from nodescraper.models import DataModel
2729

2830

2931
class DeviceEnumerationDataModel(DataModel):
30-
cpu_count: int | None = None
31-
gpu_count: int | None = None
32-
vf_count: int | None = None
32+
cpu_count: Optional[int] = None
33+
gpu_count: Optional[int] = None
34+
vf_count: Optional[int] = None
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
###############################################################################
2+
#
3+
# MIT License
4+
#
5+
# Copyright (c) 2025 Advanced Micro Devices, Inc.
6+
#
7+
# Permission is hereby granted, free of charge, to any person obtaining a copy
8+
# of this software and associated documentation files (the "Software"), to deal
9+
# in the Software without restriction, including without limitation the rights
10+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
# copies of the Software, and to permit persons to whom the Software is
12+
# furnished to do so, subject to the following conditions:
13+
#
14+
# The above copyright notice and this permission notice shall be included in all
15+
# copies or substantial portions of the Software.
16+
#
17+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
# SOFTWARE.
24+
#
25+
###############################################################################
26+
import pytest
27+
28+
from nodescraper.enums.eventcategory import EventCategory
29+
from nodescraper.enums.eventpriority import EventPriority
30+
from nodescraper.enums.executionstatus import ExecutionStatus
31+
from nodescraper.models.systeminfo import OSFamily
32+
from nodescraper.plugins.inband.device_enumeration.analyzer_args import (
33+
DeviceEnumerationAnalyzerArgs,
34+
)
35+
from nodescraper.plugins.inband.device_enumeration.device_enumeration_analyzer import (
36+
DeviceEnumerationAnalyzer,
37+
)
38+
from nodescraper.plugins.inband.device_enumeration.deviceenumdata import (
39+
DeviceEnumerationDataModel,
40+
)
41+
42+
43+
@pytest.fixture
44+
def device_enumeration_analyzer(system_info):
45+
return DeviceEnumerationAnalyzer(system_info=system_info)
46+
47+
48+
@pytest.fixture
49+
def device_enumeration_data():
50+
return DeviceEnumerationDataModel(cpu_count=4, gpu_count=4, vf_count=8)
51+
52+
53+
def test_analyze_passing_linux(system_info, device_enumeration_analyzer, device_enumeration_data):
54+
"""Test a normal passing case with matching config"""
55+
system_info.os_family = OSFamily.LINUX
56+
57+
args = DeviceEnumerationAnalyzerArgs(cpu_count=4, gpu_count=4, vf_count=8)
58+
59+
result = device_enumeration_analyzer.analyze_data(data=device_enumeration_data, args=args)
60+
61+
assert result.status == ExecutionStatus.OK
62+
assert len(result.events) == 0
63+
64+
65+
def test_analyze_passing_windows(system_info, device_enumeration_analyzer, device_enumeration_data):
66+
"""Test a normal passing case on Windows"""
67+
system_info.os_family = OSFamily.WINDOWS
68+
69+
args = DeviceEnumerationAnalyzerArgs(gpu_count=4, vf_count=8)
70+
71+
result = device_enumeration_analyzer.analyze_data(data=device_enumeration_data, args=args)
72+
73+
assert result.status == ExecutionStatus.OK
74+
assert len(result.events) == 0
75+
76+
77+
def test_analyze_no_args(device_enumeration_analyzer, device_enumeration_data):
78+
"""Test with no analyzer args provided"""
79+
80+
result = device_enumeration_analyzer.analyze_data(data=device_enumeration_data, args=None)
81+
82+
assert result.status == ExecutionStatus.NOT_RAN
83+
assert "Expected Device Enumeration expected data not provided" in result.message
84+
assert len(result.events) == 0
85+
86+
87+
def test_analyze_unexpected_counts(device_enumeration_analyzer, device_enumeration_data):
88+
"""Test with config specifying different device counts"""
89+
90+
args = DeviceEnumerationAnalyzerArgs(cpu_count=1, gpu_count=10)
91+
92+
result = device_enumeration_analyzer.analyze_data(data=device_enumeration_data, args=args)
93+
94+
assert result.status == ExecutionStatus.ERROR
95+
assert "but got" in result.message
96+
97+
for event in result.events:
98+
assert event.priority == EventPriority.CRITICAL
99+
assert event.category == EventCategory.PLATFORM.value
100+
101+
102+
def test_analyze_mismatched_cpu_count(device_enumeration_analyzer):
103+
"""Test with invalid device enumeration on SUT"""
104+
105+
data = DeviceEnumerationDataModel(cpu_count=5, gpu_count=4, vf_count=8)
106+
args = DeviceEnumerationAnalyzerArgs(cpu_count=4, gpu_count=4)
107+
108+
result = device_enumeration_analyzer.analyze_data(data=data, args=args)
109+
110+
assert result.status == ExecutionStatus.ERROR
111+
assert "but got" in result.message
112+
113+
for event in result.events:
114+
assert event.priority == EventPriority.CRITICAL
115+
assert event.category == EventCategory.PLATFORM.value
116+
117+
118+
def test_analyze_list_of_accepted_counts(device_enumeration_analyzer):
119+
"""Test with a list of acceptable counts"""
120+
121+
data = DeviceEnumerationDataModel(cpu_count=4, gpu_count=4, vf_count=8)
122+
args = DeviceEnumerationAnalyzerArgs(cpu_count=[2, 4, 8], gpu_count=[4, 8])
123+
124+
result = device_enumeration_analyzer.analyze_data(data=data, args=args)
125+
126+
assert result.status == ExecutionStatus.OK
127+
assert len(result.events) == 0

0 commit comments

Comments
 (0)