Skip to content

Commit 4951d30

Browse files
Dix issues with getting id (#3556)
* fix issues with getting id * ignore linter * fix: resolve ruff linting issues in tracing utils --------- Co-authored-by: Greyson LaLonde <[email protected]>
1 parent 7426969 commit 4951d30

File tree

2 files changed

+272
-28
lines changed

2 files changed

+272
-28
lines changed

src/crewai/events/listeners/tracing/utils.py

Lines changed: 148 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -54,44 +54,164 @@ def _get_machine_id() -> str:
5454
[f"{(uuid.getnode() >> b) & 0xFF:02x}" for b in range(0, 12, 2)][::-1]
5555
)
5656
parts.append(mac)
57-
except Exception:
58-
logger.warning("Error getting machine id for fingerprinting")
57+
except Exception: # noqa: S110
58+
pass
5959

60-
sysname = platform.system()
61-
parts.append(sysname)
60+
try:
61+
sysname = platform.system()
62+
parts.append(sysname)
63+
except Exception:
64+
sysname = "unknown"
65+
parts.append(sysname)
6266

6367
try:
6468
if sysname == "Darwin":
65-
res = subprocess.run(
66-
["/usr/sbin/system_profiler", "SPHardwareDataType"],
67-
capture_output=True,
68-
text=True,
69-
timeout=2,
70-
)
71-
m = re.search(r"Hardware UUID:\s*([A-Fa-f0-9\-]+)", res.stdout)
72-
if m:
73-
parts.append(m.group(1))
74-
elif sysname == "Linux":
7569
try:
76-
parts.append(Path("/etc/machine-id").read_text().strip())
77-
except Exception:
78-
parts.append(Path("/sys/class/dmi/id/product_uuid").read_text().strip())
70+
res = subprocess.run(
71+
["/usr/sbin/system_profiler", "SPHardwareDataType"],
72+
capture_output=True,
73+
text=True,
74+
timeout=2,
75+
)
76+
m = re.search(r"Hardware UUID:\s*([A-Fa-f0-9\-]+)", res.stdout)
77+
if m:
78+
parts.append(m.group(1))
79+
except Exception: # noqa: S110
80+
pass
81+
82+
elif sysname == "Linux":
83+
linux_id = _get_linux_machine_id()
84+
if linux_id:
85+
parts.append(linux_id)
86+
7987
elif sysname == "Windows":
80-
res = subprocess.run(
81-
["C:\\Windows\\System32\\wbem\\wmic.exe", "csproduct", "get", "UUID"],
82-
capture_output=True,
83-
text=True,
84-
timeout=2,
85-
)
86-
lines = [line.strip() for line in res.stdout.splitlines() if line.strip()]
87-
if len(lines) >= 2:
88-
parts.append(lines[1])
89-
except Exception:
90-
logger.exception("Error getting machine ID")
88+
try:
89+
res = subprocess.run(
90+
[
91+
"C:\\Windows\\System32\\wbem\\wmic.exe",
92+
"csproduct",
93+
"get",
94+
"UUID",
95+
],
96+
capture_output=True,
97+
text=True,
98+
timeout=2,
99+
)
100+
lines = [
101+
line.strip() for line in res.stdout.splitlines() if line.strip()
102+
]
103+
if len(lines) >= 2:
104+
parts.append(lines[1])
105+
except Exception: # noqa: S110
106+
pass
107+
else:
108+
generic_id = _get_generic_system_id()
109+
if generic_id:
110+
parts.append(generic_id)
111+
112+
except Exception: # noqa: S110
113+
pass
114+
115+
if len(parts) <= 1:
116+
try:
117+
import socket
118+
119+
parts.append(socket.gethostname())
120+
except Exception: # noqa: S110
121+
pass
122+
123+
try:
124+
parts.append(getpass.getuser())
125+
except Exception: # noqa: S110
126+
pass
127+
128+
try:
129+
parts.append(platform.machine())
130+
parts.append(platform.processor())
131+
except Exception: # noqa: S110
132+
pass
133+
134+
if not parts:
135+
parts.append("unknown-system")
136+
parts.append(str(uuid.uuid4()))
91137

92138
return hashlib.sha256("".join(parts).encode()).hexdigest()
93139

94140

141+
def _get_linux_machine_id() -> str | None:
142+
linux_id_sources = [
143+
"/etc/machine-id",
144+
"/sys/class/dmi/id/product_uuid",
145+
"/proc/sys/kernel/random/boot_id",
146+
"/sys/class/dmi/id/board_serial",
147+
"/sys/class/dmi/id/chassis_serial",
148+
]
149+
150+
for source in linux_id_sources:
151+
try:
152+
path = Path(source)
153+
if path.exists() and path.is_file():
154+
content = path.read_text().strip()
155+
if content and content.lower() not in [
156+
"unknown",
157+
"to be filled by o.e.m.",
158+
"",
159+
]:
160+
return content
161+
except Exception: # noqa: S112, PERF203
162+
continue
163+
164+
try:
165+
import socket
166+
167+
hostname = socket.gethostname()
168+
arch = platform.machine()
169+
if hostname and arch:
170+
return f"{hostname}-{arch}"
171+
except Exception: # noqa: S110
172+
pass
173+
174+
return None
175+
176+
177+
def _get_generic_system_id() -> str | None:
178+
try:
179+
parts = []
180+
181+
try:
182+
import socket
183+
184+
hostname = socket.gethostname()
185+
if hostname:
186+
parts.append(hostname)
187+
except Exception: # noqa: S110
188+
pass
189+
190+
try:
191+
parts.append(platform.machine())
192+
parts.append(platform.processor())
193+
parts.append(platform.architecture()[0])
194+
except Exception: # noqa: S110
195+
pass
196+
197+
try:
198+
container_id = os.environ.get(
199+
"HOSTNAME", os.environ.get("CONTAINER_ID", "")
200+
)
201+
if container_id:
202+
parts.append(container_id)
203+
except Exception: # noqa: S110
204+
pass
205+
206+
if parts:
207+
return "-".join(filter(None, parts))
208+
209+
except Exception: # noqa: S110
210+
pass
211+
212+
return None
213+
214+
95215
def _user_data_file() -> Path:
96216
base = Path(db_storage_path())
97217
base.mkdir(parents=True, exist_ok=True)
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
"""Tests for the machine ID generation functionality in tracing utils."""
2+
3+
from pathlib import Path
4+
from unittest.mock import patch
5+
6+
from crewai.events.listeners.tracing.utils import (
7+
_get_generic_system_id,
8+
_get_linux_machine_id,
9+
_get_machine_id,
10+
)
11+
12+
13+
def test_get_machine_id_basic():
14+
"""Test that _get_machine_id always returns a valid SHA256 hash."""
15+
machine_id = _get_machine_id()
16+
17+
# Should return a 64-character hex string (SHA256)
18+
assert isinstance(machine_id, str)
19+
assert len(machine_id) == 64
20+
assert all(c in "0123456789abcdef" for c in machine_id)
21+
22+
23+
def test_get_machine_id_handles_missing_files():
24+
"""Test that _get_machine_id handles FileNotFoundError gracefully."""
25+
with patch.object(Path, "read_text", side_effect=FileNotFoundError):
26+
machine_id = _get_machine_id()
27+
28+
# Should still return a valid hash even when files are missing
29+
assert isinstance(machine_id, str)
30+
assert len(machine_id) == 64
31+
assert all(c in "0123456789abcdef" for c in machine_id)
32+
33+
34+
def test_get_machine_id_handles_permission_errors():
35+
"""Test that _get_machine_id handles PermissionError gracefully."""
36+
with patch.object(Path, "read_text", side_effect=PermissionError):
37+
machine_id = _get_machine_id()
38+
39+
# Should still return a valid hash even with permission errors
40+
assert isinstance(machine_id, str)
41+
assert len(machine_id) == 64
42+
assert all(c in "0123456789abcdef" for c in machine_id)
43+
44+
45+
def test_get_machine_id_handles_mac_address_failure():
46+
"""Test that _get_machine_id works even if MAC address retrieval fails."""
47+
with patch("uuid.getnode", side_effect=Exception("MAC address error")):
48+
machine_id = _get_machine_id()
49+
50+
# Should still return a valid hash even without MAC address
51+
assert isinstance(machine_id, str)
52+
assert len(machine_id) == 64
53+
assert all(c in "0123456789abcdef" for c in machine_id)
54+
55+
56+
def test_get_linux_machine_id_handles_missing_files():
57+
"""Test that _get_linux_machine_id handles missing files gracefully."""
58+
with patch.object(Path, "exists", return_value=False):
59+
result = _get_linux_machine_id()
60+
61+
# Should return something (hostname-arch fallback) or None
62+
assert result is None or isinstance(result, str)
63+
64+
65+
def test_get_linux_machine_id_handles_file_read_errors():
66+
"""Test that _get_linux_machine_id handles file read errors."""
67+
with (
68+
patch.object(Path, "exists", return_value=True),
69+
patch.object(Path, "is_file", return_value=True),
70+
patch.object(Path, "read_text", side_effect=FileNotFoundError),
71+
):
72+
result = _get_linux_machine_id()
73+
74+
# Should fallback to hostname-based ID or None
75+
assert result is None or isinstance(result, str)
76+
77+
78+
def test_get_generic_system_id_basic():
79+
"""Test that _get_generic_system_id returns reasonable values."""
80+
result = _get_generic_system_id()
81+
82+
# Should return a string or None
83+
assert result is None or isinstance(result, str)
84+
85+
# If it returns a string, it should be non-empty
86+
if result:
87+
assert len(result) > 0
88+
89+
90+
def test_get_generic_system_id_handles_socket_errors():
91+
"""Test that _get_generic_system_id handles socket errors gracefully."""
92+
with patch("socket.gethostname", side_effect=Exception("Socket error")):
93+
result = _get_generic_system_id()
94+
95+
# Should still work or return None
96+
assert result is None or isinstance(result, str)
97+
98+
99+
def test_machine_id_consistency():
100+
"""Test that machine ID is consistent across multiple calls."""
101+
machine_id1 = _get_machine_id()
102+
machine_id2 = _get_machine_id()
103+
104+
# Should be the same across calls (stable fingerprint)
105+
assert machine_id1 == machine_id2
106+
107+
108+
def test_machine_id_always_has_fallback():
109+
"""Test that machine ID always generates something even in worst case."""
110+
with (
111+
patch("uuid.getnode", side_effect=Exception),
112+
patch("platform.system", side_effect=Exception),
113+
patch("socket.gethostname", side_effect=Exception),
114+
patch("getpass.getuser", side_effect=Exception),
115+
patch("platform.machine", side_effect=Exception),
116+
patch("platform.processor", side_effect=Exception),
117+
patch.object(Path, "read_text", side_effect=FileNotFoundError),
118+
):
119+
machine_id = _get_machine_id()
120+
121+
# Even in worst case, should return a valid hash
122+
assert isinstance(machine_id, str)
123+
assert len(machine_id) == 64
124+
assert all(c in "0123456789abcdef" for c in machine_id)

0 commit comments

Comments
 (0)