Skip to content

Commit 1cddc3e

Browse files
Merge branch 'development' into alex_doc_update3
2 parents 5a9eb86 + 74d00cd commit 1cddc3e

File tree

3 files changed

+194
-21
lines changed

3 files changed

+194
-21
lines changed

nodescraper/plugins/inband/memory/memory_collector.py

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class MemoryCollector(InBandDataCollector[MemoryDataModel, None]):
4242
"wmic OS get FreePhysicalMemory /Value; wmic ComputerSystem get TotalPhysicalMemory /Value"
4343
)
4444
CMD = "free -b"
45+
CMD_LSMEM = "/usr/bin/lsmem"
4546

4647
def collect_data(self, args=None) -> tuple[TaskResult, Optional[MemoryDataModel]]:
4748
"""
@@ -78,19 +79,88 @@ def collect_data(self, args=None) -> tuple[TaskResult, Optional[MemoryDataModel]
7879
console_log=True,
7980
)
8081

82+
lsmem_data = None
83+
if self.system_info.os_family != OSFamily.WINDOWS:
84+
lsmem_cmd = self._run_sut_cmd(self.CMD_LSMEM)
85+
if lsmem_cmd.exit_code == 0:
86+
lsmem_data = self._parse_lsmem_output(lsmem_cmd.stdout)
87+
self._log_event(
88+
category=EventCategory.OS,
89+
description="lsmem output collected",
90+
data=lsmem_data,
91+
priority=EventPriority.INFO,
92+
)
93+
else:
94+
self._log_event(
95+
category=EventCategory.OS,
96+
description="Error running lsmem command",
97+
data={
98+
"command": lsmem_cmd.command,
99+
"exit_code": lsmem_cmd.exit_code,
100+
"stderr": lsmem_cmd.stderr,
101+
},
102+
priority=EventPriority.WARNING,
103+
console_log=False,
104+
)
105+
81106
if mem_free and mem_total:
82-
mem_data = MemoryDataModel(mem_free=mem_free, mem_total=mem_total)
107+
mem_data = MemoryDataModel(
108+
mem_free=mem_free, mem_total=mem_total, lsmem_output=lsmem_data
109+
)
83110
self._log_event(
84111
category=EventCategory.OS,
85112
description="Free and total memory read",
86113
data=mem_data.model_dump(),
87114
priority=EventPriority.INFO,
88115
)
89-
self.result.message = f"Memory: {mem_data.model_dump()}"
116+
self.result.message = f"Memory: mem_free={mem_free}, mem_total={mem_total}"
90117
self.result.status = ExecutionStatus.OK
91118
else:
92119
mem_data = None
93120
self.result.message = "Memory usage data not available"
94121
self.result.status = ExecutionStatus.ERROR
95122

96123
return self.result, mem_data
124+
125+
def _parse_lsmem_output(self, output: str) -> dict:
126+
"""
127+
Parse lsmem command output into a structured dictionary.
128+
129+
Args:
130+
output: Raw stdout from lsmem command
131+
132+
Returns:
133+
dict: Parsed lsmem data with memory blocks and summary information
134+
"""
135+
lines = output.strip().split("\n")
136+
memory_blocks = []
137+
summary = {}
138+
139+
for line in lines:
140+
line = line.strip()
141+
if not line:
142+
continue
143+
144+
# Parse mem range lines (sample: "0x0000000000000000-0x000000007fffffff 2G online yes 0-15")
145+
if line.startswith("0x"):
146+
parts = line.split()
147+
if len(parts) >= 4:
148+
memory_blocks.append(
149+
{
150+
"range": parts[0],
151+
"size": parts[1],
152+
"state": parts[2],
153+
"removable": parts[3] if len(parts) > 3 else None,
154+
"block": parts[4] if len(parts) > 4 else None,
155+
}
156+
)
157+
# Parse summary lines
158+
elif ":" in line:
159+
key, value = line.split(":", 1)
160+
summary[key.strip().lower().replace(" ", "_")] = value.strip()
161+
162+
return {
163+
"raw_output": output,
164+
"memory_blocks": memory_blocks,
165+
"summary": summary,
166+
}

nodescraper/plugins/inband/memory/memorydata.py

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

2830

2931
class MemoryDataModel(DataModel):
3032
mem_free: str
3133
mem_total: str
34+
lsmem_output: Optional[dict] = None

test/unit/plugin/test_memory_collector.py

Lines changed: 119 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
from nodescraper.enums.systeminteraction import SystemInteractionLevel
3232
from nodescraper.models.systeminfo import OSFamily
3333
from nodescraper.plugins.inband.memory.memory_collector import MemoryCollector
34-
from nodescraper.plugins.inband.memory.memorydata import MemoryDataModel
3534

3635

3736
@pytest.fixture
@@ -44,24 +43,52 @@ def collector(system_info, conn_mock):
4443

4544

4645
def test_run_linux(collector, conn_mock):
47-
conn_mock.run_command.return_value = CommandArtifact(
48-
exit_code=0,
49-
stdout=(
50-
" total used free shared buff/cache available\n"
51-
"Mem: 2164113772544 31750934528 2097459761152 893313024 34903076864 2122320150528\n"
52-
"Swap: 8589930496 0 8589930496"
53-
),
54-
stderr="",
55-
command="free -h",
56-
)
46+
def mock_run_command(command, **kwargs):
47+
if "free" in command:
48+
return CommandArtifact(
49+
exit_code=0,
50+
stdout=(
51+
" total used free shared buff/cache available\n"
52+
"Mem: 2164113772544 31750934528 2097459761152 893313024 34903076864 2122320150528\n"
53+
"Swap: 8589930496 0 8589930496"
54+
),
55+
stderr="",
56+
command="free -b",
57+
)
58+
elif "lsmem" in command:
59+
return CommandArtifact(
60+
exit_code=0,
61+
stdout=(
62+
"RANGE SIZE STATE REMOVABLE BLOCK\n"
63+
"0x0000000000000000-0x000000007fffffff 2G online yes 0-15\n"
64+
"0x0000000100000000-0x000000207fffffff 126G online yes 32-2047\n"
65+
"\n"
66+
"Memory block size: 128M\n"
67+
"Total online memory: 128G\n"
68+
"Total offline memory: 0B\n"
69+
),
70+
stderr="",
71+
command="/usr/bin/lsmem",
72+
)
73+
return CommandArtifact(exit_code=1, stdout="", stderr="", command=command)
74+
75+
conn_mock.run_command.side_effect = mock_run_command
5776

5877
result, data = collector.collect_data()
5978

6079
assert result.status == ExecutionStatus.OK
61-
assert data == MemoryDataModel(
62-
mem_free="2097459761152",
63-
mem_total="2164113772544",
64-
)
80+
assert data.mem_free == "2097459761152"
81+
assert data.mem_total == "2164113772544"
82+
assert data.lsmem_output is not None
83+
assert "memory_blocks" in data.lsmem_output
84+
assert "summary" in data.lsmem_output
85+
assert "raw_output" in data.lsmem_output
86+
assert len(data.lsmem_output["memory_blocks"]) == 2
87+
assert data.lsmem_output["memory_blocks"][0]["range"] == "0x0000000000000000-0x000000007fffffff"
88+
assert data.lsmem_output["memory_blocks"][0]["size"] == "2G"
89+
assert data.lsmem_output["memory_blocks"][0]["state"] == "online"
90+
assert data.lsmem_output["summary"]["memory_block_size"] == "128M"
91+
assert data.lsmem_output["summary"]["total_online_memory"] == "128G"
6592

6693

6794
def test_run_windows(collector, conn_mock):
@@ -76,10 +103,44 @@ def test_run_windows(collector, conn_mock):
76103
result, data = collector.collect_data()
77104

78105
assert result.status == ExecutionStatus.OK
79-
assert data == MemoryDataModel(
80-
mem_free="12345678",
81-
mem_total="123412341234",
82-
)
106+
assert data.mem_free == "12345678"
107+
assert data.mem_total == "123412341234"
108+
assert data.lsmem_output is None
109+
assert conn_mock.run_command.call_count == 1
110+
111+
112+
def test_run_linux_lsmem_fails(collector, conn_mock):
113+
def mock_run_command(command, **kwargs):
114+
if "free" in command:
115+
return CommandArtifact(
116+
exit_code=0,
117+
stdout=(
118+
" total used free shared buff/cache available\n"
119+
"Mem: 2164113772544 31750934528 2097459761152 893313024 34903076864 2122320150528\n"
120+
"Swap: 8589930496 0 8589930496"
121+
),
122+
stderr="",
123+
command="free -b",
124+
)
125+
elif "lsmem" in command:
126+
return CommandArtifact(
127+
exit_code=127,
128+
stdout="",
129+
stderr="lsmem: command not found",
130+
command="/usr/bin/lsmem",
131+
)
132+
return CommandArtifact(exit_code=1, stdout="", stderr="", command=command)
133+
134+
conn_mock.run_command.side_effect = mock_run_command
135+
136+
result, data = collector.collect_data()
137+
138+
assert result.status == ExecutionStatus.OK
139+
assert data.mem_free == "2097459761152"
140+
assert data.mem_total == "2164113772544"
141+
assert data.lsmem_output is None
142+
lsmem_events = [e for e in result.events if "lsmem" in e.description]
143+
assert len(lsmem_events) > 0
83144

84145

85146
def test_run_error(collector, conn_mock):
@@ -101,3 +162,42 @@ def test_run_error(collector, conn_mock):
101162
assert data is None
102163
assert result.events[0].category == EventCategory.OS.value
103164
assert result.events[0].description == "Error checking available and total memory"
165+
166+
167+
def test_parse_lsmem_output(collector):
168+
"""Test parsing of lsmem command output."""
169+
lsmem_output = (
170+
"RANGE SIZE STATE REMOVABLE BLOCK\n"
171+
"0x0000000000000000-0x000000007fffffff 2G online yes 0-15\n"
172+
"0x0000000100000000-0x000000207fffffff 126G online yes 32-2047\n"
173+
"0x0000002080000000-0x000000407fffffff 126G online no 2048-4095\n"
174+
"\n"
175+
"Memory block size: 128M\n"
176+
"Total online memory: 254G\n"
177+
"Total offline memory: 0B\n"
178+
)
179+
180+
result = collector._parse_lsmem_output(lsmem_output)
181+
182+
assert "raw_output" in result
183+
assert "memory_blocks" in result
184+
assert "summary" in result
185+
assert result["raw_output"] == lsmem_output
186+
assert len(result["memory_blocks"]) == 3
187+
188+
assert result["memory_blocks"][0]["range"] == "0x0000000000000000-0x000000007fffffff"
189+
assert result["memory_blocks"][0]["size"] == "2G"
190+
assert result["memory_blocks"][0]["state"] == "online"
191+
assert result["memory_blocks"][0]["removable"] == "yes"
192+
assert result["memory_blocks"][0]["block"] == "0-15"
193+
194+
assert result["memory_blocks"][1]["range"] == "0x0000000100000000-0x000000207fffffff"
195+
assert result["memory_blocks"][1]["size"] == "126G"
196+
assert result["memory_blocks"][1]["state"] == "online"
197+
198+
assert result["memory_blocks"][2]["removable"] == "no"
199+
assert result["memory_blocks"][2]["block"] == "2048-4095"
200+
201+
assert result["summary"]["memory_block_size"] == "128M"
202+
assert result["summary"]["total_online_memory"] == "254G"
203+
assert result["summary"]["total_offline_memory"] == "0B"

0 commit comments

Comments
 (0)