Skip to content

Commit 752247a

Browse files
Merge pull request #26 from amd/alex_filemodel
Enable binary file copy
2 parents 679605d + 966d389 commit 752247a

File tree

10 files changed

+214
-46
lines changed

10 files changed

+214
-46
lines changed

nodescraper/base/inbandcollectortask.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from typing import Generic, Optional
2828

2929
from nodescraper.connection.inband import InBandConnection
30-
from nodescraper.connection.inband.inband import CommandArtifact, FileArtifact
30+
from nodescraper.connection.inband.inband import BaseFileArtifact, CommandArtifact
3131
from nodescraper.enums import EventPriority, OSFamily, SystemInteractionLevel
3232
from nodescraper.generictypes import TCollectArg, TDataModel
3333
from nodescraper.interfaces import DataCollector, TaskResultHook
@@ -99,7 +99,7 @@ def _run_sut_cmd(
9999

100100
def _read_sut_file(
101101
self, filename: str, encoding="utf-8", strip: bool = True, log_artifact=True
102-
) -> FileArtifact:
102+
) -> BaseFileArtifact:
103103
"""
104104
Read a file from the SUT and return its content.
105105
@@ -110,7 +110,7 @@ def _read_sut_file(
110110
log_artifact (bool, optional): whether we should log the contents of the file. Defaults to True.
111111
112112
Returns:
113-
FileArtifact: The content of the file read from the SUT, which includes the file name and content
113+
BaseFileArtifact: The content of the file read from the SUT, which includes the file name and content
114114
"""
115115
file_res = self.connection.read_file(filename=filename, encoding=encoding, strip=strip)
116116
if log_artifact:

nodescraper/connection/inband/__init__.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@
2323
# SOFTWARE.
2424
#
2525
###############################################################################
26-
from .inband import CommandArtifact, FileArtifact, InBandConnection
26+
from .inband import (
27+
BaseFileArtifact,
28+
BinaryFileArtifact,
29+
CommandArtifact,
30+
InBandConnection,
31+
TextFileArtifact,
32+
)
2733
from .inbandlocal import LocalShell
2834
from .inbandmanager import InBandConnectionManager
2935
from .sshparams import SSHConnectionParams
@@ -33,6 +39,8 @@
3339
"LocalShell",
3440
"InBandConnectionManager",
3541
"InBandConnection",
36-
"FileArtifact",
42+
"BaseFileArtifact",
43+
"TextFileArtifact",
44+
"BinaryFileArtifact",
3745
"CommandArtifact",
3846
]

nodescraper/connection/inband/inband.py

Lines changed: 100 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
#
2525
###############################################################################
2626
import abc
27+
import os
28+
from typing import Optional
2729

2830
from pydantic import BaseModel
2931

@@ -37,12 +39,103 @@ class CommandArtifact(BaseModel):
3739
exit_code: int
3840

3941

40-
class FileArtifact(BaseModel):
41-
"""Artifact to contains contents of file read into memory"""
42+
class BaseFileArtifact(BaseModel, abc.ABC):
43+
"""Base class for files"""
4244

4345
filename: str
46+
47+
@abc.abstractmethod
48+
def log_model(self, log_path: str) -> None:
49+
"""Write file to path
50+
51+
Args:
52+
log_path (str): Path for file
53+
"""
54+
pass
55+
56+
@abc.abstractmethod
57+
def contents_str(self) -> str:
58+
pass
59+
60+
@classmethod
61+
def from_bytes(
62+
cls,
63+
filename: str,
64+
raw_contents: bytes,
65+
encoding: Optional[str] = "utf-8",
66+
strip: bool = True,
67+
) -> "BaseFileArtifact":
68+
"""factory method
69+
70+
Args:
71+
filename (str): name of file to be read
72+
raw_contents (bytes): Raw file content
73+
encoding (Optional[str], optional): Optional encoding. Defaults to "utf-8".
74+
strip (bool, optional): Remove padding. Defaults to True.
75+
76+
Returns:
77+
BaseFileArtifact: _Returns instance of Artifact file
78+
"""
79+
if encoding is None:
80+
return BinaryFileArtifact(filename=filename, contents=raw_contents)
81+
82+
try:
83+
text = raw_contents.decode(encoding)
84+
return TextFileArtifact(filename=filename, contents=text.strip() if strip else text)
85+
except UnicodeDecodeError:
86+
return BinaryFileArtifact(filename=filename, contents=raw_contents)
87+
88+
89+
class TextFileArtifact(BaseFileArtifact):
90+
"""Class for text file artifacts"""
91+
4492
contents: str
4593

94+
def log_model(self, log_path: str) -> None:
95+
"""Write file to disk
96+
97+
Args:
98+
log_path (str): Path for file
99+
"""
100+
path = os.path.join(log_path, self.filename)
101+
with open(path, "w", encoding="utf-8") as f:
102+
f.write(self.contents)
103+
104+
def contents_str(self) -> str:
105+
"""Get content as str
106+
107+
Returns:
108+
str: Str instance of file content
109+
"""
110+
return self.contents
111+
112+
113+
class BinaryFileArtifact(BaseFileArtifact):
114+
"""Class for binary file artifacts"""
115+
116+
contents: bytes
117+
118+
def log_model(self, log_path: str) -> None:
119+
"""Write file to disk
120+
121+
Args:
122+
log_path (str): Path for file
123+
"""
124+
log_name = os.path.join(log_path, self.filename)
125+
with open(log_name, "wb") as f:
126+
f.write(self.contents)
127+
128+
def contents_str(self) -> str:
129+
"""File content
130+
131+
Returns:
132+
str: Str instance of file content
133+
"""
134+
try:
135+
return self.contents.decode("utf-8")
136+
except UnicodeDecodeError:
137+
return f"<binary data: {len(self.contents)} bytes>"
138+
46139

47140
class InBandConnection(abc.ABC):
48141

@@ -63,14 +156,16 @@ def run_command(
63156
"""
64157

65158
@abc.abstractmethod
66-
def read_file(self, filename: str, encoding: str = "utf-8", strip: bool = True) -> FileArtifact:
67-
"""Read a file into a FileArtifact
159+
def read_file(
160+
self, filename: str, encoding: str = "utf-8", strip: bool = True
161+
) -> BaseFileArtifact:
162+
"""Read a file into a BaseFileArtifact
68163
69164
Args:
70165
filename (str): filename
71166
encoding (str, optional): encoding to use when opening file. Defaults to "utf-8".
72167
strip (bool): automatically strip file contents
73168
74169
Returns:
75-
FileArtifact: file artifact
170+
BaseFileArtifact: file artifact
76171
"""

nodescraper/connection/inband/inbandlocal.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@
2626
import os
2727
import subprocess
2828

29-
from .inband import CommandArtifact, FileArtifact, InBandConnection
29+
from .inband import (
30+
BaseFileArtifact,
31+
CommandArtifact,
32+
InBandConnection,
33+
)
3034

3135

3236
class LocalShell(InBandConnection):
@@ -64,22 +68,25 @@ def run_command(
6468
exit_code=res.returncode,
6569
)
6670

67-
def read_file(self, filename: str, encoding: str = "utf-8", strip: bool = True) -> FileArtifact:
68-
"""Read a local file into a FileArtifact
71+
def read_file(
72+
self, filename: str, encoding: str = "utf-8", strip: bool = True
73+
) -> BaseFileArtifact:
74+
"""Read a local file into a BaseFileArtifact
6975
7076
Args:
7177
filename (str): filename
7278
encoding (str, optional): encoding to use when opening file. Defaults to "utf-8".
7379
strip (bool): automatically strip file contents
7480
7581
Returns:
76-
FileArtifact: file artifact
82+
BaseFileArtifact: file artifact
7783
"""
78-
contents = ""
79-
with open(filename, "r", encoding=encoding) as local_file:
80-
contents = local_file.read().strip()
84+
with open(filename, "rb") as f:
85+
raw_contents = f.read()
8186

82-
return FileArtifact(
87+
return BaseFileArtifact.from_bytes(
8388
filename=os.path.basename(filename),
84-
contents=contents.strip() if strip else contents,
89+
raw_contents=raw_contents,
90+
encoding=encoding,
91+
strip=strip,
8592
)

nodescraper/connection/inband/inbandremote.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@
3434
SSHException,
3535
)
3636

37-
from .inband import CommandArtifact, FileArtifact, InBandConnection
37+
from .inband import (
38+
BaseFileArtifact,
39+
CommandArtifact,
40+
InBandConnection,
41+
)
3842
from .sshparams import SSHConnectionParams
3943

4044

@@ -94,27 +98,26 @@ def connect_ssh(self):
9498
def read_file(
9599
self,
96100
filename: str,
97-
encoding="utf-8",
101+
encoding: str | None = "utf-8",
98102
strip: bool = True,
99-
) -> FileArtifact:
100-
"""Read a remote file into a file artifact
103+
) -> BaseFileArtifact:
104+
"""Read a remote file into a BaseFileArtifact.
101105
102106
Args:
103-
filename (str): filename
104-
encoding (str, optional): remote file encoding. Defaults to "utf-8".
105-
strip (bool): automatically strip file contents
107+
filename (str): Path to file on remote host
108+
encoding (str | None, optional): If None, file is read as binary. If str, decode using that encoding. Defaults to "utf-8".
109+
strip (bool): Strip whitespace for text files. Ignored for binary.
106110
107111
Returns:
108-
FileArtifact: file artifact
112+
BaseFileArtifact: Object representing file contents
109113
"""
110-
contents = ""
111-
112-
with self.client.open_sftp().open(filename) as remote_file:
113-
contents = remote_file.read().decode(encoding=encoding, errors="ignore")
114-
115-
return FileArtifact(
114+
with self.client.open_sftp().open(filename, "rb") as remote_file:
115+
raw_contents = remote_file.read()
116+
return BaseFileArtifact.from_bytes(
116117
filename=os.path.basename(filename),
117-
contents=contents.strip() if strip else contents,
118+
raw_contents=raw_contents,
119+
encoding=encoding,
120+
strip=strip,
118121
)
119122

120123
def run_command(

nodescraper/plugins/inband/dmesg/dmesg_analyzer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from typing import Optional
2929

3030
from nodescraper.base.regexanalyzer import ErrorRegex, RegexAnalyzer
31-
from nodescraper.connection.inband import FileArtifact
31+
from nodescraper.connection.inband import TextFileArtifact
3232
from nodescraper.enums import EventCategory, EventPriority
3333
from nodescraper.models import Event, TaskResult
3434

@@ -386,7 +386,7 @@ def analyze_data(
386386
args.analysis_range_end,
387387
)
388388
self.result.artifacts.append(
389-
FileArtifact(filename="filtered_dmesg.log", contents=dmesg_content)
389+
TextFileArtifact(filename="filtered_dmesg.log", contents=dmesg_content)
390390
)
391391
else:
392392
dmesg_content = data.dmesg_content

nodescraper/plugins/inband/nvme/nvme_collector.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def collect_data(
7171
return self.result, None
7272

7373
all_device_data = {}
74+
f_name = "telemetry_log.bin"
7475

7576
for dev in nvme_devices:
7677
device_data = {}
@@ -82,10 +83,13 @@ def collect_data(
8283
"fw_log": f"nvme fw-log {dev}",
8384
"self_test_log": f"nvme self-test-log {dev}",
8485
"get_log": f"nvme get-log {dev} --log-id=6 --log-len=512",
86+
"telemetry_log": f"nvme telemetry-log {dev} --output-file={dev}_{f_name}",
8587
}
8688

8789
for key, cmd in commands.items():
8890
res = self._run_sut_cmd(cmd, sudo=True)
91+
if "--output-file" in cmd:
92+
_ = self._read_sut_file(filename=f"{dev}_{f_name}", encoding=None)
8993
if res.exit_code == 0:
9094
device_data[key] = res.stdout
9195
else:

nodescraper/taskresulthooks/filesystemloghook.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
import os
2828
from typing import Optional
2929

30-
from nodescraper.connection.inband import FileArtifact
30+
from nodescraper.connection.inband import BaseFileArtifact
3131
from nodescraper.interfaces.taskresulthook import TaskResultHook
3232
from nodescraper.models import DataModel, TaskResult
3333
from nodescraper.utils import get_unique_filename, pascal_to_snake
@@ -60,10 +60,10 @@ def process_result(self, task_result: TaskResult, data: Optional[DataModel] = No
6060

6161
artifact_map = {}
6262
for artifact in task_result.artifacts:
63-
if isinstance(artifact, FileArtifact):
63+
if isinstance(artifact, BaseFileArtifact):
6464
log_name = get_unique_filename(log_path, artifact.filename)
65-
with open(os.path.join(log_path, log_name), "w", encoding="utf-8") as log_file:
66-
log_file.write(artifact.contents)
65+
artifact.log_model(log_path)
66+
6767
else:
6868
name = f"{pascal_to_snake(artifact.__class__.__name__)}s"
6969
if name in artifact_map:
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from pathlib import Path
2+
3+
from nodescraper.connection.inband.inband import (
4+
BaseFileArtifact,
5+
BinaryFileArtifact,
6+
TextFileArtifact,
7+
)
8+
9+
10+
def test_textfileartifact_contents_str():
11+
artifact = TextFileArtifact(filename="text.txt", contents="hello")
12+
assert artifact.contents_str() == "hello"
13+
14+
15+
def test_binaryfileartifact_contents_str():
16+
artifact = BinaryFileArtifact(filename="blob.bin", contents=b"\xff\x00\xab")
17+
result = artifact.contents_str()
18+
assert result.startswith("<binary data:")
19+
assert "bytes>" in result
20+
21+
22+
def test_from_bytes_text():
23+
artifact = BaseFileArtifact.from_bytes("test.txt", b"simple text", encoding="utf-8")
24+
assert isinstance(artifact, TextFileArtifact)
25+
assert artifact.contents == "simple text"
26+
27+
28+
def test_from_bytes_binary():
29+
artifact = BaseFileArtifact.from_bytes("data.bin", b"\xff\x00\xab", encoding="utf-8")
30+
assert isinstance(artifact, BinaryFileArtifact)
31+
assert artifact.contents == b"\xff\x00\xab"
32+
33+
34+
def test_log_model_text(tmp_path: Path):
35+
artifact = TextFileArtifact(filename="log.txt", contents="some text")
36+
artifact.log_model(str(tmp_path))
37+
output_path = tmp_path / "log.txt"
38+
assert output_path.exists()
39+
assert output_path.read_text(encoding="utf-8") == "some text"
40+
41+
42+
def test_log_model_binary(tmp_path: Path):
43+
binary_data = b"\x01\x02\xffDATA"
44+
artifact = BinaryFileArtifact(filename="binary.bin", contents=binary_data)
45+
artifact.log_model(str(tmp_path))
46+
output_path = tmp_path / "binary.bin"
47+
assert output_path.exists()
48+
assert output_path.read_bytes() == binary_data

0 commit comments

Comments
 (0)