ttnn-visualizer 0.41.0__py3-none-any.whl → 0.42.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ttnn_visualizer/csv_queries.py +106 -30
- ttnn_visualizer/decorators.py +7 -4
- ttnn_visualizer/exceptions.py +16 -0
- ttnn_visualizer/queries.py +3 -109
- ttnn_visualizer/remote_sqlite_setup.py +97 -19
- ttnn_visualizer/requirements.txt +1 -3
- ttnn_visualizer/sftp_operations.py +637 -219
- ttnn_visualizer/static/assets/{allPaths-4_pFqSAW.js → allPaths-wwXsGKJ2.js} +1 -1
- ttnn_visualizer/static/assets/{allPathsLoader-CpLPTLlt.js → allPathsLoader-BK9jqlVe.js} +2 -2
- ttnn_visualizer/static/assets/{index-DFVwehlj.js → index-Ybr1HJxx.js} +206 -206
- ttnn_visualizer/static/assets/{splitPathsBySizeLoader-D-RvsTqO.js → splitPathsBySizeLoader-CauQGZHk.js} +1 -1
- ttnn_visualizer/static/index.html +1 -1
- ttnn_visualizer/tests/test_queries.py +0 -68
- ttnn_visualizer/views.py +97 -2
- {ttnn_visualizer-0.41.0.dist-info → ttnn_visualizer-0.42.0.dist-info}/LICENSE +0 -1
- {ttnn_visualizer-0.41.0.dist-info → ttnn_visualizer-0.42.0.dist-info}/METADATA +2 -3
- {ttnn_visualizer-0.41.0.dist-info → ttnn_visualizer-0.42.0.dist-info}/RECORD +21 -22
- ttnn_visualizer/ssh_client.py +0 -85
- {ttnn_visualizer-0.41.0.dist-info → ttnn_visualizer-0.42.0.dist-info}/LICENSE_understanding.txt +0 -0
- {ttnn_visualizer-0.41.0.dist-info → ttnn_visualizer-0.42.0.dist-info}/WHEEL +0 -0
- {ttnn_visualizer-0.41.0.dist-info → ttnn_visualizer-0.42.0.dist-info}/entry_points.txt +0 -0
- {ttnn_visualizer-0.41.0.dist-info → ttnn_visualizer-0.42.0.dist-info}/top_level.txt +0 -0
ttnn_visualizer/csv_queries.py
CHANGED
@@ -4,6 +4,7 @@
|
|
4
4
|
import csv
|
5
5
|
import json
|
6
6
|
import os
|
7
|
+
import subprocess
|
7
8
|
import tempfile
|
8
9
|
from io import StringIO
|
9
10
|
from pathlib import Path
|
@@ -13,10 +14,50 @@ import pandas as pd
|
|
13
14
|
from tt_perf_report import perf_report
|
14
15
|
|
15
16
|
from ttnn_visualizer.exceptions import DataFormatError
|
17
|
+
from ttnn_visualizer.models import Instance, RemoteConnection
|
18
|
+
from ttnn_visualizer.exceptions import SSHException, AuthenticationException, NoValidConnectionsError
|
16
19
|
from ttnn_visualizer.models import Instance
|
17
|
-
from ttnn_visualizer.ssh_client import get_client
|
18
20
|
from ttnn_visualizer.sftp_operations import read_remote_file
|
19
21
|
|
22
|
+
|
23
|
+
def handle_ssh_subprocess_error(e: subprocess.CalledProcessError, remote_connection: RemoteConnection):
|
24
|
+
"""
|
25
|
+
Convert subprocess SSH errors to appropriate SSH exceptions.
|
26
|
+
|
27
|
+
:param e: The subprocess.CalledProcessError
|
28
|
+
:param remote_connection: The RemoteConnection object for context
|
29
|
+
:raises: SSHException, AuthenticationException, or NoValidConnectionsError
|
30
|
+
"""
|
31
|
+
stderr = e.stderr.lower() if e.stderr else ""
|
32
|
+
|
33
|
+
# Check for authentication failures
|
34
|
+
if any(auth_err in stderr for auth_err in [
|
35
|
+
"permission denied",
|
36
|
+
"authentication failed",
|
37
|
+
"publickey",
|
38
|
+
"password",
|
39
|
+
"host key verification failed"
|
40
|
+
]):
|
41
|
+
raise AuthenticationException(f"SSH authentication failed: {e.stderr}")
|
42
|
+
|
43
|
+
# Check for connection failures
|
44
|
+
elif any(conn_err in stderr for conn_err in [
|
45
|
+
"connection refused",
|
46
|
+
"network is unreachable",
|
47
|
+
"no route to host",
|
48
|
+
"name or service not known",
|
49
|
+
"connection timed out"
|
50
|
+
]):
|
51
|
+
raise NoValidConnectionsError(f"SSH connection failed: {e.stderr}")
|
52
|
+
|
53
|
+
# Check for general SSH protocol errors
|
54
|
+
elif "ssh:" in stderr or "protocol" in stderr:
|
55
|
+
raise SSHException(f"SSH protocol error: {e.stderr}")
|
56
|
+
|
57
|
+
# Default to generic SSH exception
|
58
|
+
else:
|
59
|
+
raise SSHException(f"SSH command failed: {e.stderr}")
|
60
|
+
|
20
61
|
class LocalCSVQueryRunner:
|
21
62
|
def __init__(self, file_path: str, offset: int = 0):
|
22
63
|
self.file_path = file_path
|
@@ -110,7 +151,38 @@ class RemoteCSVQueryRunner:
|
|
110
151
|
self.remote_connection = remote_connection
|
111
152
|
self.sep = sep
|
112
153
|
self.offset = offset
|
113
|
-
|
154
|
+
|
155
|
+
def _execute_ssh_command(self, command: str) -> str:
|
156
|
+
"""Execute an SSH command and return the output."""
|
157
|
+
ssh_cmd = ["ssh"]
|
158
|
+
|
159
|
+
# Handle non-standard SSH port
|
160
|
+
if self.remote_connection.port != 22:
|
161
|
+
ssh_cmd.extend(["-p", str(self.remote_connection.port)])
|
162
|
+
|
163
|
+
ssh_cmd.extend([
|
164
|
+
f"{self.remote_connection.username}@{self.remote_connection.host}",
|
165
|
+
command
|
166
|
+
])
|
167
|
+
|
168
|
+
try:
|
169
|
+
result = subprocess.run(
|
170
|
+
ssh_cmd,
|
171
|
+
capture_output=True,
|
172
|
+
text=True,
|
173
|
+
check=True,
|
174
|
+
timeout=30
|
175
|
+
)
|
176
|
+
return result.stdout
|
177
|
+
except subprocess.CalledProcessError as e:
|
178
|
+
if e.returncode == 255: # SSH protocol errors
|
179
|
+
handle_ssh_subprocess_error(e, self.remote_connection)
|
180
|
+
# This line should never be reached as handle_ssh_subprocess_error raises an exception
|
181
|
+
raise RuntimeError(f"SSH command failed: {e.stderr}")
|
182
|
+
else:
|
183
|
+
raise RuntimeError(f"SSH command failed: {e.stderr}")
|
184
|
+
except subprocess.TimeoutExpired:
|
185
|
+
raise RuntimeError(f"SSH command timed out: {command}")
|
114
186
|
|
115
187
|
def execute_query(
|
116
188
|
self,
|
@@ -128,12 +200,7 @@ class RemoteCSVQueryRunner:
|
|
128
200
|
"""
|
129
201
|
# Fetch header row, accounting for the offset
|
130
202
|
header_cmd = f"head -n {self.offset + 1} {self.file_path} | tail -n 1"
|
131
|
-
|
132
|
-
raw_header = stdout.read().decode("utf-8").strip()
|
133
|
-
error = stderr.read().decode("utf-8").strip()
|
134
|
-
|
135
|
-
if error:
|
136
|
-
raise RuntimeError(f"Error fetching header row: {error}")
|
203
|
+
raw_header = self._execute_ssh_command(header_cmd).strip()
|
137
204
|
|
138
205
|
# Sanitize headers
|
139
206
|
headers = [
|
@@ -160,12 +227,7 @@ class RemoteCSVQueryRunner:
|
|
160
227
|
limit_clause = f"| head -n {limit}" if limit else ""
|
161
228
|
awk_cmd = f"awk -F'{self.sep}' 'NR > {self.offset + 1} {f'&& {awk_filter}' if awk_filter else ''} {{print}}' {self.file_path} {limit_clause}"
|
162
229
|
|
163
|
-
|
164
|
-
output = stdout.read().decode("utf-8").strip()
|
165
|
-
error = stderr.read().decode("utf-8").strip()
|
166
|
-
|
167
|
-
if error:
|
168
|
-
raise RuntimeError(f"Error executing AWK command: {error}")
|
230
|
+
output = self._execute_ssh_command(awk_cmd).strip()
|
169
231
|
|
170
232
|
# Split rows into lists of strings
|
171
233
|
rows = [
|
@@ -205,12 +267,7 @@ class RemoteCSVQueryRunner:
|
|
205
267
|
if total_lines
|
206
268
|
else f"cat {self.file_path}"
|
207
269
|
)
|
208
|
-
|
209
|
-
output = stdout.read().decode("utf-8").strip()
|
210
|
-
error = stderr.read().decode("utf-8").strip()
|
211
|
-
|
212
|
-
if error:
|
213
|
-
raise RuntimeError(f"Error fetching raw rows: {error}")
|
270
|
+
output = self._execute_ssh_command(cmd).strip()
|
214
271
|
|
215
272
|
return output.splitlines()[self.offset:]
|
216
273
|
|
@@ -220,12 +277,7 @@ class RemoteCSVQueryRunner:
|
|
220
277
|
:return: Dictionary of headers.
|
221
278
|
"""
|
222
279
|
header_cmd = f"head -n {self.offset + 1} {self.file_path} | tail -n 1"
|
223
|
-
|
224
|
-
header = stdout.read().decode("utf-8").strip()
|
225
|
-
error = stderr.read().decode("utf-8").strip()
|
226
|
-
|
227
|
-
if error:
|
228
|
-
raise RuntimeError(f"Error reading CSV header: {error}")
|
280
|
+
header = self._execute_ssh_command(header_cmd).strip()
|
229
281
|
|
230
282
|
# Trim spaces in header names
|
231
283
|
column_names = [name.strip() for name in header.split(self.sep)]
|
@@ -254,10 +306,9 @@ class RemoteCSVQueryRunner:
|
|
254
306
|
|
255
307
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
256
308
|
"""
|
257
|
-
Clean up
|
309
|
+
Clean up resources when exiting context.
|
258
310
|
"""
|
259
|
-
|
260
|
-
self.ssh_client.close()
|
311
|
+
pass
|
261
312
|
|
262
313
|
|
263
314
|
class NPEQueries:
|
@@ -267,7 +318,6 @@ class NPEQueries:
|
|
267
318
|
@staticmethod
|
268
319
|
def get_npe_manifest(instance: Instance):
|
269
320
|
|
270
|
-
|
271
321
|
if (
|
272
322
|
not instance.remote_connection
|
273
323
|
or instance.remote_connection
|
@@ -285,6 +335,31 @@ class NPEQueries:
|
|
285
335
|
f"{profiler_folder.remotePath}/{NPEQueries.NPE_FOLDER}/{NPEQueries.MANIFEST_FILE}",
|
286
336
|
)
|
287
337
|
|
338
|
+
@staticmethod
|
339
|
+
def get_npe_timeline(instance: Instance, filename: str):
|
340
|
+
if not filename:
|
341
|
+
raise ValueError("filename parameter is required and cannot be None or empty")
|
342
|
+
|
343
|
+
if (
|
344
|
+
not instance.remote_connection
|
345
|
+
or not instance.remote_connection.useRemoteQuerying
|
346
|
+
):
|
347
|
+
if not instance.performance_path:
|
348
|
+
raise ValueError("instance.performance_path is None")
|
349
|
+
|
350
|
+
file_path = Path(
|
351
|
+
instance.performance_path, NPEQueries.NPE_FOLDER, filename
|
352
|
+
)
|
353
|
+
with open(file_path, "r") as f:
|
354
|
+
return json.load(f)
|
355
|
+
else:
|
356
|
+
profiler_folder = instance.remote_profile_folder
|
357
|
+
return read_remote_file(
|
358
|
+
instance.remote_connection,
|
359
|
+
f"{profiler_folder.remotePath}/{NPEQueries.NPE_FOLDER}/{filename}",
|
360
|
+
)
|
361
|
+
|
362
|
+
|
288
363
|
|
289
364
|
class DeviceLogProfilerQueries:
|
290
365
|
DEVICE_LOG_FILE = "profile_log_device.csv"
|
@@ -611,6 +686,7 @@ class OpsPerformanceReportQueries:
|
|
611
686
|
"inner_dim_block_size",
|
612
687
|
"output_subblock_h",
|
613
688
|
"output_subblock_w",
|
689
|
+
"global_call_count",
|
614
690
|
"advice",
|
615
691
|
"raw_op_code"
|
616
692
|
]
|
ttnn_visualizer/decorators.py
CHANGED
@@ -9,13 +9,10 @@ from ttnn_visualizer.enums import ConnectionTestStates
|
|
9
9
|
|
10
10
|
from functools import wraps
|
11
11
|
from flask import abort, request, session
|
12
|
-
from
|
12
|
+
from ttnn_visualizer.exceptions import (
|
13
13
|
AuthenticationException,
|
14
14
|
NoValidConnectionsError,
|
15
15
|
SSHException,
|
16
|
-
)
|
17
|
-
|
18
|
-
from ttnn_visualizer.exceptions import (
|
19
16
|
RemoteConnectionException,
|
20
17
|
NoProjectsException,
|
21
18
|
RemoteSqliteException,
|
@@ -37,6 +34,12 @@ def with_instance(func):
|
|
37
34
|
abort(404)
|
38
35
|
|
39
36
|
instance_query_data = get_or_create_instance(instance_id=instance_id)
|
37
|
+
|
38
|
+
# Handle case where get_or_create_instance returns None due to database error
|
39
|
+
if instance_query_data is None:
|
40
|
+
current_app.logger.error(f"Failed to get or create instance with ID: {instance_id}")
|
41
|
+
abort(500)
|
42
|
+
|
40
43
|
instance = instance_query_data.to_pydantic()
|
41
44
|
|
42
45
|
kwargs["instance"] = instance
|
ttnn_visualizer/exceptions.py
CHANGED
@@ -43,5 +43,21 @@ class DataFormatError(Exception):
|
|
43
43
|
class InvalidReportPath(Exception):
|
44
44
|
pass
|
45
45
|
|
46
|
+
|
46
47
|
class InvalidProfilerPath(Exception):
|
47
48
|
pass
|
49
|
+
|
50
|
+
|
51
|
+
class SSHException(Exception):
|
52
|
+
"""Base SSH exception for subprocess SSH operations"""
|
53
|
+
pass
|
54
|
+
|
55
|
+
|
56
|
+
class AuthenticationException(SSHException):
|
57
|
+
"""Raised when SSH authentication fails"""
|
58
|
+
pass
|
59
|
+
|
60
|
+
|
61
|
+
class NoValidConnectionsError(SSHException):
|
62
|
+
"""Raised when SSH connection cannot be established"""
|
63
|
+
pass
|
ttnn_visualizer/queries.py
CHANGED
@@ -2,7 +2,6 @@
|
|
2
2
|
#
|
3
3
|
# SPDX-FileCopyrightText: © 2025 Tenstorrent AI ULC
|
4
4
|
|
5
|
-
import json
|
6
5
|
from typing import Generator, Dict, Any, Union
|
7
6
|
|
8
7
|
from ttnn_visualizer.exceptions import (
|
@@ -23,11 +22,9 @@ from ttnn_visualizer.models import (
|
|
23
22
|
ProducersConsumers,
|
24
23
|
TensorComparisonRecord,
|
25
24
|
)
|
26
|
-
from ttnn_visualizer.ssh_client import get_client
|
27
25
|
import sqlite3
|
28
26
|
from typing import List, Optional
|
29
27
|
from pathlib import Path
|
30
|
-
import paramiko
|
31
28
|
|
32
29
|
|
33
30
|
class LocalQueryRunner:
|
@@ -63,111 +60,10 @@ class LocalQueryRunner:
|
|
63
60
|
self.connection.close()
|
64
61
|
|
65
62
|
|
66
|
-
class RemoteQueryRunner:
|
67
|
-
column_delimiter = "|||"
|
68
|
-
|
69
|
-
def __init__(self, instance: Instance):
|
70
|
-
self.instance = instance
|
71
|
-
self._validate_instance()
|
72
|
-
self.ssh_client = self._get_ssh_client(self.instance.remote_connection)
|
73
|
-
self.sqlite_binary = self.instance.remote_connection.sqliteBinaryPath
|
74
|
-
self.remote_db_path = str(
|
75
|
-
Path(self.instance.remote_profiler_folder.remotePath, "db.sqlite")
|
76
|
-
)
|
77
|
-
|
78
|
-
def _validate_instance(self):
|
79
|
-
"""
|
80
|
-
Validate that the instance has all required remote connection attributes.
|
81
|
-
"""
|
82
|
-
if (
|
83
|
-
not self.instance.remote_connection
|
84
|
-
or not self.instance.remote_connection.sqliteBinaryPath
|
85
|
-
or not self.instance.remote_profiler_folder
|
86
|
-
or not self.instance.remote_profiler_folder.remotePath
|
87
|
-
):
|
88
|
-
raise ValueError(
|
89
|
-
"Remote connections require remote path and sqliteBinaryPath"
|
90
|
-
)
|
91
|
-
|
92
|
-
def _get_ssh_client(self, remote_connection) -> paramiko.SSHClient:
|
93
|
-
"""
|
94
|
-
Retrieve the SSH client for the given remote connection.
|
95
|
-
"""
|
96
|
-
return get_client(remote_connection=remote_connection)
|
97
|
-
|
98
|
-
def _format_query(self, query: str, params: Optional[List] = None) -> str:
|
99
|
-
"""
|
100
|
-
Format the query by replacing placeholders with properly quoted parameters.
|
101
|
-
"""
|
102
|
-
if not params:
|
103
|
-
return query
|
104
|
-
|
105
|
-
formatted_params = [
|
106
|
-
f"'{param}'" if isinstance(param, str) else str(param) for param in params
|
107
|
-
]
|
108
|
-
return query.replace("?", "{}").format(*formatted_params)
|
109
|
-
|
110
|
-
def _build_command(self, formatted_query: str) -> str:
|
111
|
-
"""
|
112
|
-
Build the remote SQLite command.
|
113
|
-
"""
|
114
|
-
return f'{self.sqlite_binary} {self.remote_db_path} "{formatted_query}" -json'
|
115
|
-
|
116
|
-
def _execute_ssh_command(self, command: str) -> tuple:
|
117
|
-
"""
|
118
|
-
Execute the SSH command and return the standard output and error.
|
119
|
-
"""
|
120
|
-
stdin, stdout, stderr = self.ssh_client.exec_command(command)
|
121
|
-
output = stdout.read().decode("utf-8").strip()
|
122
|
-
error_output = stderr.read().decode("utf-8").strip()
|
123
|
-
return output, error_output
|
124
|
-
|
125
|
-
def _parse_output(self, output: str, command: str) -> List:
|
126
|
-
"""
|
127
|
-
Parse the output from the SQLite command. Attempt JSON parsing first,
|
128
|
-
then fall back to line-based parsing.
|
129
|
-
"""
|
130
|
-
if not output.strip():
|
131
|
-
return []
|
132
|
-
|
133
|
-
try:
|
134
|
-
rows = json.loads(output)
|
135
|
-
return [tuple(row.values()) for row in rows]
|
136
|
-
except json.JSONDecodeError:
|
137
|
-
print(
|
138
|
-
f"Output is not valid JSON, attempting manual parsing.\nCommand: {command}"
|
139
|
-
)
|
140
|
-
return [tuple(line.split("|")) for line in output.splitlines()]
|
141
|
-
|
142
|
-
def execute_query(self, query: str, params: Optional[List] = None) -> List:
|
143
|
-
"""
|
144
|
-
Execute a remote SQLite query using the instance's SSH client.
|
145
|
-
"""
|
146
|
-
self._validate_instance()
|
147
|
-
formatted_query = self._format_query(query, params)
|
148
|
-
command = self._build_command(formatted_query)
|
149
|
-
output, error_output = self._execute_ssh_command(command)
|
150
|
-
|
151
|
-
if error_output:
|
152
|
-
raise RuntimeError(
|
153
|
-
f"Error executing query remotely: {error_output}\nCommand: {command}"
|
154
|
-
)
|
155
|
-
|
156
|
-
return self._parse_output(output, command)
|
157
|
-
|
158
|
-
def close(self):
|
159
|
-
"""
|
160
|
-
Close the SSH connection.
|
161
|
-
"""
|
162
|
-
if self.ssh_client:
|
163
|
-
self.ssh_client.close()
|
164
|
-
|
165
|
-
|
166
63
|
class DatabaseQueries:
|
167
64
|
|
168
65
|
instance: Optional[Instance] = None
|
169
|
-
|
170
|
-
query_runner: LocalQueryRunner | RemoteQueryRunner
|
66
|
+
query_runner: LocalQueryRunner
|
171
67
|
|
172
68
|
def __init__(self, instance: Optional[Instance] = None, connection=None):
|
173
69
|
self.instance = instance
|
@@ -181,7 +77,7 @@ class DatabaseQueries:
|
|
181
77
|
)
|
182
78
|
remote_connection = instance.remote_connection if instance else None
|
183
79
|
if remote_connection and remote_connection.useRemoteQuerying:
|
184
|
-
|
80
|
+
raise NotImplementedError("Remote querying is not implemented yet")
|
185
81
|
else:
|
186
82
|
self.query_runner = LocalQueryRunner(instance=instance)
|
187
83
|
|
@@ -382,7 +278,5 @@ class DatabaseQueries:
|
|
382
278
|
return self
|
383
279
|
|
384
280
|
def __exit__(self, exc_type, exc_value, traceback):
|
385
|
-
if isinstance(self.query_runner,
|
386
|
-
self.query_runner.close()
|
387
|
-
elif isinstance(self.query_runner, LocalQueryRunner):
|
281
|
+
if isinstance(self.query_runner, LocalQueryRunner):
|
388
282
|
self.query_runner.close()
|
@@ -3,29 +3,109 @@
|
|
3
3
|
# SPDX-FileCopyrightText: © 2025 Tenstorrent AI ULC
|
4
4
|
|
5
5
|
import re
|
6
|
+
import subprocess
|
6
7
|
|
7
8
|
from ttnn_visualizer.decorators import remote_exception_handler
|
8
9
|
from ttnn_visualizer.enums import ConnectionTestStates
|
9
|
-
from ttnn_visualizer.exceptions import RemoteSqliteException
|
10
|
+
from ttnn_visualizer.exceptions import RemoteSqliteException, SSHException, AuthenticationException, NoValidConnectionsError
|
10
11
|
from ttnn_visualizer.models import RemoteConnection
|
11
|
-
|
12
|
+
|
13
|
+
|
14
|
+
def handle_ssh_subprocess_error(e: subprocess.CalledProcessError, remote_connection: RemoteConnection):
|
15
|
+
"""
|
16
|
+
Convert subprocess SSH errors to appropriate SSH exceptions.
|
17
|
+
|
18
|
+
:param e: The subprocess.CalledProcessError
|
19
|
+
:param remote_connection: The RemoteConnection object for context
|
20
|
+
:raises: SSHException, AuthenticationException, or NoValidConnectionsError
|
21
|
+
"""
|
22
|
+
stderr = e.stderr.lower() if e.stderr else ""
|
23
|
+
|
24
|
+
# Check for authentication failures
|
25
|
+
if any(auth_err in stderr for auth_err in [
|
26
|
+
"permission denied",
|
27
|
+
"authentication failed",
|
28
|
+
"publickey",
|
29
|
+
"password",
|
30
|
+
"host key verification failed"
|
31
|
+
]):
|
32
|
+
raise AuthenticationException(f"SSH authentication failed: {e.stderr}")
|
33
|
+
|
34
|
+
# Check for connection failures
|
35
|
+
elif any(conn_err in stderr for conn_err in [
|
36
|
+
"connection refused",
|
37
|
+
"network is unreachable",
|
38
|
+
"no route to host",
|
39
|
+
"name or service not known",
|
40
|
+
"connection timed out"
|
41
|
+
]):
|
42
|
+
raise NoValidConnectionsError(f"SSH connection failed: {e.stderr}")
|
43
|
+
|
44
|
+
# Check for general SSH protocol errors
|
45
|
+
elif "ssh:" in stderr or "protocol" in stderr:
|
46
|
+
raise SSHException(f"SSH protocol error: {e.stderr}")
|
47
|
+
|
48
|
+
# Default to generic SSH exception
|
49
|
+
else:
|
50
|
+
raise SSHException(f"SSH command failed: {e.stderr}")
|
12
51
|
|
13
52
|
MINIMUM_SQLITE_VERSION = "3.38.0"
|
14
53
|
|
15
54
|
|
55
|
+
def _execute_ssh_command(remote_connection: RemoteConnection, command: str) -> str:
|
56
|
+
"""Execute an SSH command and return the output."""
|
57
|
+
ssh_cmd = ["ssh"]
|
58
|
+
|
59
|
+
# Handle non-standard SSH port
|
60
|
+
if remote_connection.port != 22:
|
61
|
+
ssh_cmd.extend(["-p", str(remote_connection.port)])
|
62
|
+
|
63
|
+
ssh_cmd.extend([
|
64
|
+
f"{remote_connection.username}@{remote_connection.host}",
|
65
|
+
command
|
66
|
+
])
|
67
|
+
|
68
|
+
try:
|
69
|
+
result = subprocess.run(
|
70
|
+
ssh_cmd,
|
71
|
+
capture_output=True,
|
72
|
+
text=True,
|
73
|
+
check=True,
|
74
|
+
timeout=30
|
75
|
+
)
|
76
|
+
return result.stdout
|
77
|
+
except subprocess.CalledProcessError as e:
|
78
|
+
if e.returncode == 255: # SSH protocol errors
|
79
|
+
handle_ssh_subprocess_error(e, remote_connection)
|
80
|
+
# This line should never be reached as handle_ssh_subprocess_error raises an exception
|
81
|
+
raise RemoteSqliteException(
|
82
|
+
message=f"SSH command failed: {e.stderr}",
|
83
|
+
status=ConnectionTestStates.FAILED,
|
84
|
+
)
|
85
|
+
else:
|
86
|
+
raise RemoteSqliteException(
|
87
|
+
message=f"SSH command failed: {e.stderr}",
|
88
|
+
status=ConnectionTestStates.FAILED,
|
89
|
+
)
|
90
|
+
except subprocess.TimeoutExpired:
|
91
|
+
raise RemoteSqliteException(
|
92
|
+
message=f"SSH command timed out: {command}",
|
93
|
+
status=ConnectionTestStates.FAILED,
|
94
|
+
)
|
95
|
+
|
96
|
+
|
16
97
|
def find_sqlite_binary(connection):
|
17
98
|
"""Check if SQLite is installed on the remote machine and return its path."""
|
18
|
-
ssh_client = get_client(connection)
|
19
99
|
try:
|
20
|
-
|
21
|
-
binary_path =
|
22
|
-
error = stderr.read().decode().strip()
|
100
|
+
output = _execute_ssh_command(connection, "which sqlite3")
|
101
|
+
binary_path = output.strip()
|
23
102
|
if binary_path:
|
24
103
|
print(f"SQLite binary found at: {binary_path}")
|
25
104
|
return binary_path
|
26
|
-
elif error:
|
27
|
-
print(f"Error checking SQLite binary: {error}")
|
28
105
|
return None
|
106
|
+
except RemoteSqliteException:
|
107
|
+
# Re-raise RemoteSqliteException as-is
|
108
|
+
raise
|
29
109
|
except Exception as e:
|
30
110
|
raise RemoteSqliteException(
|
31
111
|
message=f"Error finding SQLite binary: {str(e)}",
|
@@ -33,17 +113,13 @@ def find_sqlite_binary(connection):
|
|
33
113
|
)
|
34
114
|
|
35
115
|
|
36
|
-
def is_sqlite_executable(
|
116
|
+
def is_sqlite_executable(remote_connection: RemoteConnection, binary_path):
|
37
117
|
"""Check if the SQLite binary is executable by trying to run it."""
|
38
118
|
try:
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
if error:
|
44
|
-
raise Exception(f"Error while trying to run SQLite binary: {error}")
|
45
|
-
|
46
|
-
version = get_sqlite_version(output)
|
119
|
+
output = _execute_ssh_command(remote_connection, f"{binary_path} --version")
|
120
|
+
version_output = output.strip()
|
121
|
+
|
122
|
+
version = get_sqlite_version(version_output)
|
47
123
|
if not is_version_at_least(version, MINIMUM_SQLITE_VERSION):
|
48
124
|
raise Exception(
|
49
125
|
f"SQLite version {version} is below the required minimum of {MINIMUM_SQLITE_VERSION}."
|
@@ -52,6 +128,9 @@ def is_sqlite_executable(ssh_client, binary_path):
|
|
52
128
|
print(f"SQLite binary at {binary_path} is executable. Version: {version}")
|
53
129
|
return True
|
54
130
|
|
131
|
+
except RemoteSqliteException:
|
132
|
+
# Re-raise RemoteSqliteException as-is
|
133
|
+
raise
|
55
134
|
except Exception as e:
|
56
135
|
raise Exception(f"Error checking SQLite executability: {str(e)}")
|
57
136
|
|
@@ -76,8 +155,7 @@ def is_version_at_least(version, minimum_version):
|
|
76
155
|
@remote_exception_handler
|
77
156
|
def check_sqlite_path(remote_connection: RemoteConnection):
|
78
157
|
try:
|
79
|
-
|
80
|
-
is_sqlite_executable(client, remote_connection.sqliteBinaryPath)
|
158
|
+
is_sqlite_executable(remote_connection, remote_connection.sqliteBinaryPath)
|
81
159
|
except Exception as e:
|
82
160
|
raise RemoteSqliteException(message=str(e), status=ConnectionTestStates.FAILED)
|
83
161
|
|
ttnn_visualizer/requirements.txt
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
Flask==3.1.1
|
2
2
|
gunicorn~=22.0.0
|
3
3
|
uvicorn==0.30.1
|
4
|
-
paramiko~=3.4.0
|
5
4
|
flask_cors==4.0.1
|
6
5
|
pydantic==2.7.3
|
7
6
|
pydantic_core==2.18.4
|
@@ -16,9 +15,8 @@ wheel
|
|
16
15
|
build
|
17
16
|
PyYAML==6.0.2
|
18
17
|
python-dotenv==1.0.1
|
19
|
-
tt-perf-report==1.0.
|
18
|
+
tt-perf-report==1.0.7
|
20
19
|
zstd==1.5.7.0
|
21
20
|
|
22
21
|
# Dev dependencies
|
23
22
|
mypy
|
24
|
-
types-paramiko
|