Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 72 additions & 2 deletions nodescraper/plugins/inband/memory/memory_collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]]:
"""
Expand Down Expand Up @@ -78,19 +79,88 @@ def collect_data(self, args=None) -> tuple[TaskResult, Optional[MemoryDataModel]
console_log=True,
)

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=False,
)

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",
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
self.result.message = "Memory usage data not available"
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 mem range lines (sample: "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,
}
3 changes: 3 additions & 0 deletions nodescraper/plugins/inband/memory/memorydata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
138 changes: 119 additions & 19 deletions test/unit/plugin/test_memory_collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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"