From c2ec97b52900ead60bd9cd26d3bdf8cfc682cfac Mon Sep 17 00:00:00 2001 From: Alexandra Bara Date: Wed, 3 Dec 2025 09:30:20 -0600 Subject: [PATCH 1/2] added /user/bin/lsmem call to MemoryPlugin + utest --- .../plugins/inband/memory/memory_collector.py | 73 ++++++++- .../plugins/inband/memory/memorydata.py | 3 + test/unit/plugin/test_memory_collector.py | 138 +++++++++++++++--- 3 files changed, 194 insertions(+), 20 deletions(-) diff --git a/nodescraper/plugins/inband/memory/memory_collector.py b/nodescraper/plugins/inband/memory/memory_collector.py index 7f768c65..4e12028b 100644 --- a/nodescraper/plugins/inband/memory/memory_collector.py +++ b/nodescraper/plugins/inband/memory/memory_collector.py @@ -42,6 +42,7 @@ class MemoryCollector(InBandDataCollector[MemoryDataModel, None]): "wmic OS get FreePhysicalMemory /Value; wmic ComputerSystem get TotalPhysicalMemory /Value" ) CMD = "free -b" + CMD_LSMEM = "/usr/bin/lsmem" def collect_data(self, args=None) -> tuple[TaskResult, Optional[MemoryDataModel]]: """ @@ -78,8 +79,35 @@ def collect_data(self, args=None) -> tuple[TaskResult, Optional[MemoryDataModel] console_log=True, ) + # Collect lsmem data (Linux only) + lsmem_data = None + if self.system_info.os_family != OSFamily.WINDOWS: + lsmem_cmd = self._run_sut_cmd(self.CMD_LSMEM) + if lsmem_cmd.exit_code == 0: + lsmem_data = self._parse_lsmem_output(lsmem_cmd.stdout) + self._log_event( + category=EventCategory.OS, + description="lsmem output collected", + data=lsmem_data, + priority=EventPriority.INFO, + ) + else: + self._log_event( + category=EventCategory.OS, + description="Error running lsmem command", + data={ + "command": lsmem_cmd.command, + "exit_code": lsmem_cmd.exit_code, + "stderr": lsmem_cmd.stderr, + }, + priority=EventPriority.WARNING, + console_log=True, + ) + if mem_free and mem_total: - mem_data = MemoryDataModel(mem_free=mem_free, mem_total=mem_total) + mem_data = MemoryDataModel( + mem_free=mem_free, mem_total=mem_total, lsmem_output=lsmem_data + ) self._log_event( category=EventCategory.OS, description="Free and total memory read", @@ -94,3 +122,46 @@ def collect_data(self, args=None) -> tuple[TaskResult, Optional[MemoryDataModel] self.result.status = ExecutionStatus.ERROR return self.result, mem_data + + def _parse_lsmem_output(self, output: str) -> dict: + """ + Parse lsmem command output into a structured dictionary. + + Args: + output: Raw stdout from lsmem command + + Returns: + dict: Parsed lsmem data with memory blocks and summary information + """ + lines = output.strip().split("\n") + memory_blocks = [] + summary = {} + + for line in lines: + line = line.strip() + if not line: + continue + + # Parse memory range lines (e.g., "0x0000000000000000-0x000000007fffffff 2G online yes 0-15") + if line.startswith("0x"): + parts = line.split() + if len(parts) >= 4: + memory_blocks.append( + { + "range": parts[0], + "size": parts[1], + "state": parts[2], + "removable": parts[3] if len(parts) > 3 else None, + "block": parts[4] if len(parts) > 4 else None, + } + ) + # Parse summary lines + elif ":" in line: + key, value = line.split(":", 1) + summary[key.strip().lower().replace(" ", "_")] = value.strip() + + return { + "raw_output": output, + "memory_blocks": memory_blocks, + "summary": summary, + } diff --git a/nodescraper/plugins/inband/memory/memorydata.py b/nodescraper/plugins/inband/memory/memorydata.py index f500ee2e..4d0142e9 100644 --- a/nodescraper/plugins/inband/memory/memorydata.py +++ b/nodescraper/plugins/inband/memory/memorydata.py @@ -23,9 +23,12 @@ # SOFTWARE. # ############################################################################### +from typing import Optional + from nodescraper.models import DataModel class MemoryDataModel(DataModel): mem_free: str mem_total: str + lsmem_output: Optional[dict] = None diff --git a/test/unit/plugin/test_memory_collector.py b/test/unit/plugin/test_memory_collector.py index 7b194bb4..973243fe 100644 --- a/test/unit/plugin/test_memory_collector.py +++ b/test/unit/plugin/test_memory_collector.py @@ -31,7 +31,6 @@ from nodescraper.enums.systeminteraction import SystemInteractionLevel from nodescraper.models.systeminfo import OSFamily from nodescraper.plugins.inband.memory.memory_collector import MemoryCollector -from nodescraper.plugins.inband.memory.memorydata import MemoryDataModel @pytest.fixture @@ -44,24 +43,52 @@ def collector(system_info, conn_mock): def test_run_linux(collector, conn_mock): - conn_mock.run_command.return_value = CommandArtifact( - exit_code=0, - stdout=( - " total used free shared buff/cache available\n" - "Mem: 2164113772544 31750934528 2097459761152 893313024 34903076864 2122320150528\n" - "Swap: 8589930496 0 8589930496" - ), - stderr="", - command="free -h", - ) + def mock_run_command(command, **kwargs): + if "free" in command: + return CommandArtifact( + exit_code=0, + stdout=( + " total used free shared buff/cache available\n" + "Mem: 2164113772544 31750934528 2097459761152 893313024 34903076864 2122320150528\n" + "Swap: 8589930496 0 8589930496" + ), + stderr="", + command="free -b", + ) + elif "lsmem" in command: + return CommandArtifact( + exit_code=0, + stdout=( + "RANGE SIZE STATE REMOVABLE BLOCK\n" + "0x0000000000000000-0x000000007fffffff 2G online yes 0-15\n" + "0x0000000100000000-0x000000207fffffff 126G online yes 32-2047\n" + "\n" + "Memory block size: 128M\n" + "Total online memory: 128G\n" + "Total offline memory: 0B\n" + ), + stderr="", + command="/usr/bin/lsmem", + ) + return CommandArtifact(exit_code=1, stdout="", stderr="", command=command) + + conn_mock.run_command.side_effect = mock_run_command result, data = collector.collect_data() assert result.status == ExecutionStatus.OK - assert data == MemoryDataModel( - mem_free="2097459761152", - mem_total="2164113772544", - ) + assert data.mem_free == "2097459761152" + assert data.mem_total == "2164113772544" + assert data.lsmem_output is not None + assert "memory_blocks" in data.lsmem_output + assert "summary" in data.lsmem_output + assert "raw_output" in data.lsmem_output + assert len(data.lsmem_output["memory_blocks"]) == 2 + assert data.lsmem_output["memory_blocks"][0]["range"] == "0x0000000000000000-0x000000007fffffff" + assert data.lsmem_output["memory_blocks"][0]["size"] == "2G" + assert data.lsmem_output["memory_blocks"][0]["state"] == "online" + assert data.lsmem_output["summary"]["memory_block_size"] == "128M" + assert data.lsmem_output["summary"]["total_online_memory"] == "128G" def test_run_windows(collector, conn_mock): @@ -76,10 +103,44 @@ def test_run_windows(collector, conn_mock): result, data = collector.collect_data() assert result.status == ExecutionStatus.OK - assert data == MemoryDataModel( - mem_free="12345678", - mem_total="123412341234", - ) + assert data.mem_free == "12345678" + assert data.mem_total == "123412341234" + assert data.lsmem_output is None + assert conn_mock.run_command.call_count == 1 + + +def test_run_linux_lsmem_fails(collector, conn_mock): + def mock_run_command(command, **kwargs): + if "free" in command: + return CommandArtifact( + exit_code=0, + stdout=( + " total used free shared buff/cache available\n" + "Mem: 2164113772544 31750934528 2097459761152 893313024 34903076864 2122320150528\n" + "Swap: 8589930496 0 8589930496" + ), + stderr="", + command="free -b", + ) + elif "lsmem" in command: + return CommandArtifact( + exit_code=127, + stdout="", + stderr="lsmem: command not found", + command="/usr/bin/lsmem", + ) + return CommandArtifact(exit_code=1, stdout="", stderr="", command=command) + + conn_mock.run_command.side_effect = mock_run_command + + result, data = collector.collect_data() + + assert result.status == ExecutionStatus.OK + assert data.mem_free == "2097459761152" + assert data.mem_total == "2164113772544" + assert data.lsmem_output is None + lsmem_events = [e for e in result.events if "lsmem" in e.description] + assert len(lsmem_events) > 0 def test_run_error(collector, conn_mock): @@ -101,3 +162,42 @@ def test_run_error(collector, conn_mock): assert data is None assert result.events[0].category == EventCategory.OS.value assert result.events[0].description == "Error checking available and total memory" + + +def test_parse_lsmem_output(collector): + """Test parsing of lsmem command output.""" + lsmem_output = ( + "RANGE SIZE STATE REMOVABLE BLOCK\n" + "0x0000000000000000-0x000000007fffffff 2G online yes 0-15\n" + "0x0000000100000000-0x000000207fffffff 126G online yes 32-2047\n" + "0x0000002080000000-0x000000407fffffff 126G online no 2048-4095\n" + "\n" + "Memory block size: 128M\n" + "Total online memory: 254G\n" + "Total offline memory: 0B\n" + ) + + result = collector._parse_lsmem_output(lsmem_output) + + assert "raw_output" in result + assert "memory_blocks" in result + assert "summary" in result + assert result["raw_output"] == lsmem_output + assert len(result["memory_blocks"]) == 3 + + assert result["memory_blocks"][0]["range"] == "0x0000000000000000-0x000000007fffffff" + assert result["memory_blocks"][0]["size"] == "2G" + assert result["memory_blocks"][0]["state"] == "online" + assert result["memory_blocks"][0]["removable"] == "yes" + assert result["memory_blocks"][0]["block"] == "0-15" + + assert result["memory_blocks"][1]["range"] == "0x0000000100000000-0x000000207fffffff" + assert result["memory_blocks"][1]["size"] == "126G" + assert result["memory_blocks"][1]["state"] == "online" + + assert result["memory_blocks"][2]["removable"] == "no" + assert result["memory_blocks"][2]["block"] == "2048-4095" + + assert result["summary"]["memory_block_size"] == "128M" + assert result["summary"]["total_online_memory"] == "254G" + assert result["summary"]["total_offline_memory"] == "0B" From 47a273f351795635c90d8a79c17a8d5019830cec Mon Sep 17 00:00:00 2001 From: Alexandra Bara Date: Wed, 3 Dec 2025 09:45:11 -0600 Subject: [PATCH 2/2] removed extraneous printout --- nodescraper/plugins/inband/memory/memory_collector.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/nodescraper/plugins/inband/memory/memory_collector.py b/nodescraper/plugins/inband/memory/memory_collector.py index 4e12028b..0ca25605 100644 --- a/nodescraper/plugins/inband/memory/memory_collector.py +++ b/nodescraper/plugins/inband/memory/memory_collector.py @@ -79,7 +79,6 @@ def collect_data(self, args=None) -> tuple[TaskResult, Optional[MemoryDataModel] console_log=True, ) - # Collect lsmem data (Linux only) lsmem_data = None if self.system_info.os_family != OSFamily.WINDOWS: lsmem_cmd = self._run_sut_cmd(self.CMD_LSMEM) @@ -101,7 +100,7 @@ def collect_data(self, args=None) -> tuple[TaskResult, Optional[MemoryDataModel] "stderr": lsmem_cmd.stderr, }, priority=EventPriority.WARNING, - console_log=True, + console_log=False, ) if mem_free and mem_total: @@ -114,7 +113,7 @@ def collect_data(self, args=None) -> tuple[TaskResult, Optional[MemoryDataModel] data=mem_data.model_dump(), priority=EventPriority.INFO, ) - self.result.message = f"Memory: {mem_data.model_dump()}" + self.result.message = f"Memory: mem_free={mem_free}, mem_total={mem_total}" self.result.status = ExecutionStatus.OK else: mem_data = None @@ -142,7 +141,7 @@ def _parse_lsmem_output(self, output: str) -> dict: if not line: continue - # Parse memory range lines (e.g., "0x0000000000000000-0x000000007fffffff 2G online yes 0-15") + # Parse mem range lines (sample: "0x0000000000000000-0x000000007fffffff 2G online yes 0-15") if line.startswith("0x"): parts = line.split() if len(parts) >= 4: