Skip to content

Commit 3a0b75c

Browse files
Merge pull request #35 from amd/alex_dmesg_logs
Dmesg log collection
2 parents cc410e4 + 91fa616 commit 3a0b75c

File tree

3 files changed

+253
-0
lines changed

3 files changed

+253
-0
lines changed

nodescraper/plugins/inband/dmesg/collector_args.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,11 @@
2828

2929

3030
class DmesgCollectorArgs(CollectorArgs):
31+
"""Collector args
32+
33+
Args:
34+
CollectorArgs (CollectorArgs): specific dmesg collector args
35+
"""
36+
37+
collect_rotated_logs: bool = False
3138
skip_sudo: bool = False

nodescraper/plugins/inband/dmesg/dmesg_collector.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@
2323
# SOFTWARE.
2424
#
2525
###############################################################################
26+
import re
2627
from typing import Optional
2728

2829
from nodescraper.base import InBandDataCollector
30+
from nodescraper.connection.inband import TextFileArtifact
2931
from nodescraper.enums import EventCategory, EventPriority, ExecutionStatus, OSFamily
3032
from nodescraper.models import TaskResult
3133

@@ -42,6 +44,113 @@ class DmesgCollector(InBandDataCollector[DmesgData, DmesgCollectorArgs]):
4244

4345
DMESG_CMD = "dmesg --time-format iso -x"
4446

47+
DMESG_LOGS_CMD = (
48+
r"ls -1 /var/log/dmesg* 2>/dev/null | grep -E '^/var/log/dmesg(\.[0-9]+(\.gz)?)?$' || true"
49+
)
50+
51+
def _shell_quote(self, s: str) -> str:
52+
"""Single quote fix
53+
54+
Args:
55+
s (str): path to be converted
56+
57+
Returns:
58+
str: path to be returned
59+
"""
60+
return "'" + s.replace("'", "'\"'\"'") + "'"
61+
62+
def _nice_dmesg_name(self, path: str) -> str:
63+
"""Map path to filename
64+
65+
Args:
66+
path (str): file path
67+
68+
Returns:
69+
str: new local filename
70+
"""
71+
prefix = "rotated_"
72+
base = path.rstrip("/").rsplit("/", 1)[-1]
73+
74+
if base == "dmesg":
75+
return f"{prefix}dmesg_log.log"
76+
77+
m = re.fullmatch(r"dmesg\.(\d+)\.gz", base)
78+
if m:
79+
return f"{prefix}dmesg.{m.group(1)}.gz.log"
80+
81+
m = re.fullmatch(r"dmesg\.(\d+)", base)
82+
if m:
83+
return f"{prefix}dmesg.{m.group(1)}.log"
84+
85+
middle = base[:-3] if base.endswith(".gz") else base
86+
return f"{prefix}{middle}.log"
87+
88+
def _collect_dmesg_rotations(self):
89+
"""Collect dmesg logs"""
90+
list_res = self._run_sut_cmd(self.DMESG_LOGS_CMD, sudo=True)
91+
paths = [p.strip() for p in (list_res.stdout or "").splitlines() if p.strip()]
92+
if not paths:
93+
self._log_event(
94+
category=EventCategory.OS,
95+
description="No /var/log/dmesg files found (including rotations).",
96+
data={"list_exit_code": list_res.exit_code},
97+
priority=EventPriority.WARNING,
98+
)
99+
return 0
100+
101+
collected_logs, failed_logs = [], []
102+
for p in paths:
103+
qp = self._shell_quote(p)
104+
if p.endswith(".gz"):
105+
cmd = f"gzip -dc {qp} 2>/dev/null || zcat {qp} 2>/dev/null"
106+
res = self._run_sut_cmd(cmd, sudo=True, log_artifact=False)
107+
if res.exit_code == 0 and res.stdout is not None:
108+
fname = self._nice_dmesg_name(p)
109+
self.logger.info("Collected dmesg log: %s", fname)
110+
self.result.artifacts.append(
111+
TextFileArtifact(filename=fname, contents=res.stdout)
112+
)
113+
collected_logs.append(
114+
{"path": p, "as": fname, "bytes": len(res.stdout.encode("utf-8", "ignore"))}
115+
)
116+
else:
117+
failed_logs.append(
118+
{"path": p, "exit_code": res.exit_code, "stderr": res.stderr, "cmd": cmd}
119+
)
120+
else:
121+
cmd = f"cat {qp}"
122+
res = self._run_sut_cmd(cmd, sudo=True, log_artifact=False)
123+
if res.exit_code == 0 and res.stdout is not None:
124+
fname = self._nice_dmesg_name(p)
125+
self.logger.info("Collected dmesg log: %s", fname)
126+
self.result.artifacts.append(
127+
TextFileArtifact(filename=fname, contents=res.stdout)
128+
)
129+
collected_logs.append(
130+
{"path": p, "as": fname, "bytes": len(res.stdout.encode("utf-8", "ignore"))}
131+
)
132+
else:
133+
failed_logs.append(
134+
{"path": p, "exit_code": res.exit_code, "stderr": res.stderr, "cmd": cmd}
135+
)
136+
137+
if collected_logs:
138+
self._log_event(
139+
category=EventCategory.OS,
140+
description="Collected dmesg rotated files",
141+
data={"collected": collected_logs},
142+
priority=EventPriority.INFO,
143+
)
144+
self.result.message = self.result.message or "dmesg rotated files collected"
145+
146+
if failed_logs:
147+
self._log_event(
148+
category=EventCategory.OS,
149+
description="Some dmesg files could not be collected.",
150+
data={"failed": failed_logs},
151+
priority=EventPriority.WARNING,
152+
)
153+
45154
def _get_dmesg_content(self) -> str:
46155
"""run dmesg command on system and return output
47156
@@ -79,6 +188,8 @@ def collect_data(
79188
return self.result, None
80189

81190
dmesg_content = self._get_dmesg_content()
191+
if args.collect_rotated_logs:
192+
self._collect_dmesg_rotations()
82193

83194
if dmesg_content:
84195
dmesg_data = DmesgData(dmesg_content=dmesg_content)

test/unit/plugin/test_dmesg_collector.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
# SOFTWARE.
2424
#
2525
###############################################################################
26+
import types
27+
2628
import pytest
2729

2830
from nodescraper.connection.inband.inband import CommandArtifact
@@ -141,3 +143,136 @@ def test_data_model():
141143
assert dmesg_data2.dmesg_content == (
142144
"2023-06-01T01:00:00,685236-05:00 test message1\n2023-06-01T02:30:00,685106-05:00 test message2"
143145
)
146+
147+
148+
class DummyRes:
149+
def __init__(self, command="", stdout="", exit_code=0, stderr=""):
150+
self.command = command
151+
self.stdout = stdout
152+
self.exit_code = exit_code
153+
self.stderr = stderr
154+
155+
156+
def get_collector(monkeypatch, run_map, system_info, conn_mock):
157+
c = DmesgCollector(
158+
system_info=system_info,
159+
system_interaction_level=SystemInteractionLevel.INTERACTIVE,
160+
connection=conn_mock,
161+
)
162+
c.result = types.SimpleNamespace(artifacts=[], message=None)
163+
c._events = []
164+
165+
def _log_event(**kw):
166+
c._events.append(kw)
167+
168+
def _run_sut_cmd(cmd, *args, **kwargs):
169+
return run_map(cmd, *args, **kwargs)
170+
171+
monkeypatch.setattr(c, "_log_event", _log_event, raising=True)
172+
monkeypatch.setattr(c, "_run_sut_cmd", _run_sut_cmd, raising=True)
173+
return c
174+
175+
176+
def test_collect_rotations_good_path(monkeypatch, system_info, conn_mock):
177+
ls_out = (
178+
"\n".join(
179+
[
180+
"/var/log/dmesg_log",
181+
"/var/log/dmesg.1",
182+
"/var/log/dmesg.2.gz",
183+
"/var/log/dmesg.10.gz",
184+
]
185+
)
186+
+ "\n"
187+
)
188+
189+
def run_map(cmd, **kwargs):
190+
if cmd.startswith("ls -1 /var/log/dmesg"):
191+
return DummyRes(command=cmd, stdout=ls_out, exit_code=0)
192+
if cmd.startswith("cat '"):
193+
if "/var/log/dmesg.1'" in cmd:
194+
return DummyRes(command=cmd, stdout="dmesg.1 content\n", exit_code=0)
195+
if "/var/log/dmesg_log'" in cmd:
196+
return DummyRes(command=cmd, stdout="dmesg content\n", exit_code=0)
197+
if "gzip -dc" in cmd and "/var/log/dmesg.2.gz" in cmd:
198+
return DummyRes(command=cmd, stdout="gz2 content\n", exit_code=0)
199+
if "gzip -dc" in cmd and "/var/log/dmesg.10.gz" in cmd:
200+
return DummyRes(command=cmd, stdout="gz10 content\n", exit_code=0)
201+
return DummyRes(command=cmd, stdout="", exit_code=1, stderr="unexpected")
202+
203+
c = get_collector(monkeypatch, run_map, system_info, conn_mock)
204+
205+
c._collect_dmesg_rotations()
206+
207+
names = {a.filename for a in c.result.artifacts}
208+
assert names == {
209+
"rotated_dmesg_log.log",
210+
"rotated_dmesg.1.log",
211+
"rotated_dmesg.2.gz.log",
212+
"rotated_dmesg.10.gz.log",
213+
}
214+
215+
descs = [e["description"] for e in c._events]
216+
assert "Collected dmesg rotated files" in descs
217+
218+
219+
def test_collect_rotations_no_files(monkeypatch, system_info, conn_mock):
220+
def run_map(cmd, **kwargs):
221+
if cmd.startswith("ls -1 /var/log/dmesg"):
222+
return DummyRes(command=cmd, stdout="", exit_code=0)
223+
return DummyRes(command=cmd, stdout="", exit_code=1)
224+
225+
c = get_collector(monkeypatch, run_map, system_info, conn_mock)
226+
227+
c._collect_dmesg_rotations()
228+
229+
assert c.result.artifacts == []
230+
231+
events = c._events
232+
assert any(
233+
e["description"].startswith("No /var/log/dmesg files found")
234+
and e["priority"].name == "WARNING"
235+
for e in events
236+
)
237+
238+
239+
def test_collect_rotations_gz_failure(monkeypatch, system_info, conn_mock):
240+
ls_out = "/var/log/dmesg.2.gz\n"
241+
242+
def run_map(cmd, **kwargs):
243+
if cmd.startswith("ls -1 /var/log/dmesg"):
244+
return DummyRes(command=cmd, stdout=ls_out, exit_code=0)
245+
if "gzip -dc" in cmd and "/var/log/dmesg.2.gz" in cmd:
246+
return DummyRes(command=cmd, stdout="", exit_code=1, stderr="gzip: not found")
247+
return DummyRes(command=cmd, stdout="", exit_code=1)
248+
249+
c = get_collector(monkeypatch, run_map, system_info, conn_mock)
250+
251+
c._collect_dmesg_rotations()
252+
253+
assert c.result.artifacts == []
254+
255+
fail_events = [
256+
e for e in c._events if e["description"] == "Some dmesg files could not be collected."
257+
]
258+
assert fail_events, "Expected a failure event"
259+
failed = fail_events[-1]["data"]["failed"]
260+
assert any(item["path"].endswith("/var/log/dmesg.2.gz") for item in failed)
261+
262+
263+
def test_collect_data_integration(monkeypatch, system_info, conn_mock):
264+
def run_map(cmd, **kwargs):
265+
if cmd == DmesgCollector.DMESG_CMD:
266+
return DummyRes(command=cmd, stdout="DMESG OUTPUT\n", exit_code=0)
267+
if cmd.startswith("ls -1 /var/log/dmesg"):
268+
return DummyRes(command=cmd, stdout="/var/log/dmesg\n", exit_code=0)
269+
if cmd.startswith("cat '") and "/var/log/dmesg'" in cmd:
270+
return DummyRes(command=cmd, stdout="dmesg file content\n", exit_code=0)
271+
return DummyRes(command=cmd, stdout="", exit_code=1)
272+
273+
c = get_collector(monkeypatch, run_map, system_info, conn_mock)
274+
275+
result, data = c.collect_data()
276+
277+
assert isinstance(data, DmesgData)
278+
assert data.dmesg_content == "DMESG OUTPUT\n"

0 commit comments

Comments
 (0)