2323# SOFTWARE.
2424#
2525###############################################################################
26+ import re
27+
2628from nodescraper .base import InBandDataCollector
29+ from nodescraper .connection .inband import FileArtifact
2730from nodescraper .enums import EventCategory , EventPriority , OSFamily
2831from nodescraper .models import TaskResult
2932
@@ -39,6 +42,84 @@ class DmesgCollector(InBandDataCollector[DmesgData, None]):
3942
4043 DMESG_CMD = "dmesg --time-format iso -x"
4144
45+ DMESG_LIST_CMD = (
46+ r"ls -1 /var/log/dmesg /var/log/dmesg.1 /var/log/dmesg.[0-9]*.gz 2>/dev/null || true"
47+ )
48+
49+ def _shell_quote (self , s : str ) -> str :
50+ """POSIX single-quote."""
51+ return "'" + s .replace ("'" , "'\" '\" '" ) + "'"
52+
53+ def _nice_dmesg_name (self , path : str ) -> str :
54+ """Map path to filename"""
55+ if path .endswith ("/dmesg" ):
56+ return "dmesg.log"
57+ if path .endswith ("/dmesg.1" ):
58+ return "dmesg.1.log"
59+ m = re .search (r"/dmesg\.(\d+)\.gz$" , path )
60+ if m :
61+ return f"dmesg.{ m .group (1 )} .log"
62+ base = path .rsplit ("/" , 1 )[- 1 ]
63+ return base .replace (".gz" , "" ) + ".log"
64+
65+ def _collect_dmesg_rotations (self ) -> int :
66+ """
67+ Collect /var/log/dmesg, /var/log/dmesg.1, and /var/log/dmesg.N.gz (decompressed).
68+ Attaches each as a text artifact.
69+
70+ Returns:
71+ list: list of logs collected.
72+
73+ """
74+ list_res = self ._run_sut_cmd (self .DMESG_LIST_CMD )
75+ paths = [p .strip () for p in (list_res .stdout or "" ).splitlines () if p .strip ()]
76+ if not paths :
77+ self ._log_event (
78+ category = EventCategory .OS ,
79+ description = "No /var/log/dmesg files found (including rotations)." ,
80+ data = {"list_exit_code" : list_res .exit_code },
81+ priority = EventPriority .WARNING ,
82+ )
83+ return 0
84+
85+ collected , failed = [], []
86+ for p in paths :
87+ qp = self ._shell_quote (p )
88+ if p .endswith (".gz" ):
89+ cmd = f"(command -v gzip >/dev/null && gzip -dc { qp } ) || (command -v zcat >/dev/null && zcat { qp } ) || cat { qp } "
90+ else :
91+ cmd = f"cat { qp } "
92+
93+ res = self ._run_sut_cmd (cmd )
94+ if res .exit_code == 0 and res .stdout is not None :
95+ fname = self ._nice_dmesg_name (p )
96+ self .result .artifacts .append (FileArtifact (filename = fname , contents = res .stdout ))
97+ collected .append (
98+ {"path" : p , "as" : fname , "bytes" : len (res .stdout .encode ("utf-8" , "ignore" ))}
99+ )
100+ else :
101+ failed .append ({"path" : p , "exit_code" : res .exit_code , "stderr" : res .stderr })
102+
103+ if collected :
104+ self ._log_event (
105+ category = EventCategory .OS ,
106+ description = "Collected dmesg rotated files" ,
107+ data = {"collected" : collected },
108+ priority = EventPriority .INFO ,
109+ )
110+ # self.result.status = self.result.status or ExecutionStatus.OK
111+ self .result .message = self .result .message or "dmesg rotated files collected"
112+
113+ if failed :
114+ self ._log_event (
115+ category = EventCategory .OS ,
116+ description = "Some dmesg files could not be collected." ,
117+ data = {"failed" : failed },
118+ priority = EventPriority .WARNING ,
119+ )
120+
121+ return collected
122+
42123 def _get_dmesg_content (self ) -> str :
43124 """run dmesg command on system and return output
44125
@@ -68,6 +149,7 @@ def collect_data(
68149 tuple[TaskResult, DmesgData | None]: tuple containing the result of the task and the dmesg data if available
69150 """
70151 dmesg_content = self ._get_dmesg_content ()
152+ _ = self ._collect_dmesg_rotations ()
71153
72154 if dmesg_content :
73155 dmesg_data = DmesgData (dmesg_content = dmesg_content )
0 commit comments