ttnn-visualizer 0.40.3__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/app.py +21 -6
- ttnn_visualizer/csv_queries.py +135 -32
- 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-DSFl5HNA.js → allPaths-wwXsGKJ2.js} +1 -1
- ttnn_visualizer/static/assets/{allPathsLoader-BwkPDbOI.js → allPathsLoader-BK9jqlVe.js} +2 -2
- ttnn_visualizer/static/assets/{index-CWerbNbe.js → index-Ybr1HJxx.js} +202 -202
- ttnn_visualizer/static/assets/{splitPathsBySizeLoader-DoKJAUb8.js → splitPathsBySizeLoader-CauQGZHk.js} +1 -1
- ttnn_visualizer/static/index.html +5 -5
- ttnn_visualizer/tests/test_queries.py +0 -68
- ttnn_visualizer/views.py +115 -9
- {ttnn_visualizer-0.40.3.dist-info → ttnn_visualizer-0.42.0.dist-info}/LICENSE +0 -1
- {ttnn_visualizer-0.40.3.dist-info → ttnn_visualizer-0.42.0.dist-info}/METADATA +2 -3
- {ttnn_visualizer-0.40.3.dist-info → ttnn_visualizer-0.42.0.dist-info}/RECORD +22 -23
- ttnn_visualizer/ssh_client.py +0 -85
- {ttnn_visualizer-0.40.3.dist-info → ttnn_visualizer-0.42.0.dist-info}/LICENSE_understanding.txt +0 -0
- {ttnn_visualizer-0.40.3.dist-info → ttnn_visualizer-0.42.0.dist-info}/WHEEL +0 -0
- {ttnn_visualizer-0.40.3.dist-info → ttnn_visualizer-0.42.0.dist-info}/entry_points.txt +0 -0
- {ttnn_visualizer-0.40.3.dist-info → ttnn_visualizer-0.42.0.dist-info}/top_level.txt +0 -0
ttnn_visualizer/app.py
CHANGED
@@ -16,7 +16,7 @@ from typing import cast
|
|
16
16
|
|
17
17
|
import flask
|
18
18
|
from dotenv import load_dotenv
|
19
|
-
from flask import Flask, jsonify
|
19
|
+
from flask import Flask, abort, jsonify
|
20
20
|
from flask_cors import CORS
|
21
21
|
from werkzeug.debug import DebuggedApplication
|
22
22
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
@@ -46,7 +46,11 @@ def create_app(settings_override=None):
|
|
46
46
|
|
47
47
|
config = cast(DefaultConfig, Config())
|
48
48
|
|
49
|
-
app = Flask(
|
49
|
+
app = Flask(
|
50
|
+
__name__,
|
51
|
+
static_folder=config.STATIC_ASSETS_DIR,
|
52
|
+
static_url_path=f"{config.BASE_PATH}static",
|
53
|
+
)
|
50
54
|
logging.basicConfig(level=app.config.get("LOG_LEVEL", "INFO"))
|
51
55
|
|
52
56
|
app.config.from_object(config)
|
@@ -56,14 +60,17 @@ def create_app(settings_override=None):
|
|
56
60
|
|
57
61
|
middleware(app)
|
58
62
|
|
59
|
-
app.register_blueprint(api, url_prefix=f"{app.config['BASE_PATH']}
|
63
|
+
app.register_blueprint(api, url_prefix=f"{app.config['BASE_PATH']}api")
|
60
64
|
|
61
65
|
extensions(app)
|
62
66
|
|
63
67
|
if flask_env == "production":
|
64
|
-
@app.route(f"{app.config['BASE_PATH']}
|
65
|
-
@app.route("
|
68
|
+
@app.route(f"{app.config['BASE_PATH']}", defaults={"path": ""})
|
69
|
+
@app.route(f"{app.config['BASE_PATH']}<path:path>")
|
66
70
|
def catch_all(path):
|
71
|
+
if path.startswith("static/"):
|
72
|
+
abort(404) # Pass control to Flask's static view
|
73
|
+
|
67
74
|
js_config = {
|
68
75
|
"SERVER_MODE": app.config["SERVER_MODE"],
|
69
76
|
"BASE_PATH": app.config["BASE_PATH"],
|
@@ -78,7 +85,15 @@ def create_app(settings_override=None):
|
|
78
85
|
js,
|
79
86
|
)
|
80
87
|
|
81
|
-
return flask.Response(
|
88
|
+
return flask.Response(
|
89
|
+
html_with_config,
|
90
|
+
mimetype="text/html",
|
91
|
+
headers={
|
92
|
+
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
|
93
|
+
"Pragma": "no-cache",
|
94
|
+
"Expires": "0",
|
95
|
+
},
|
96
|
+
)
|
82
97
|
|
83
98
|
return app
|
84
99
|
|
ttnn_visualizer/csv_queries.py
CHANGED
@@ -2,7 +2,9 @@
|
|
2
2
|
#
|
3
3
|
# SPDX-FileCopyrightText: © 2025 Tenstorrent Inc.
|
4
4
|
import csv
|
5
|
+
import json
|
5
6
|
import os
|
7
|
+
import subprocess
|
6
8
|
import tempfile
|
7
9
|
from io import StringIO
|
8
10
|
from pathlib import Path
|
@@ -12,9 +14,49 @@ import pandas as pd
|
|
12
14
|
from tt_perf_report import perf_report
|
13
15
|
|
14
16
|
from ttnn_visualizer.exceptions import DataFormatError
|
17
|
+
from ttnn_visualizer.models import Instance, RemoteConnection
|
18
|
+
from ttnn_visualizer.exceptions import SSHException, AuthenticationException, NoValidConnectionsError
|
15
19
|
from ttnn_visualizer.models import Instance
|
16
|
-
from ttnn_visualizer.
|
17
|
-
|
20
|
+
from ttnn_visualizer.sftp_operations import read_remote_file
|
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}")
|
18
60
|
|
19
61
|
class LocalCSVQueryRunner:
|
20
62
|
def __init__(self, file_path: str, offset: int = 0):
|
@@ -109,7 +151,38 @@ class RemoteCSVQueryRunner:
|
|
109
151
|
self.remote_connection = remote_connection
|
110
152
|
self.sep = sep
|
111
153
|
self.offset = offset
|
112
|
-
|
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}")
|
113
186
|
|
114
187
|
def execute_query(
|
115
188
|
self,
|
@@ -127,12 +200,7 @@ class RemoteCSVQueryRunner:
|
|
127
200
|
"""
|
128
201
|
# Fetch header row, accounting for the offset
|
129
202
|
header_cmd = f"head -n {self.offset + 1} {self.file_path} | tail -n 1"
|
130
|
-
|
131
|
-
raw_header = stdout.read().decode("utf-8").strip()
|
132
|
-
error = stderr.read().decode("utf-8").strip()
|
133
|
-
|
134
|
-
if error:
|
135
|
-
raise RuntimeError(f"Error fetching header row: {error}")
|
203
|
+
raw_header = self._execute_ssh_command(header_cmd).strip()
|
136
204
|
|
137
205
|
# Sanitize headers
|
138
206
|
headers = [
|
@@ -159,12 +227,7 @@ class RemoteCSVQueryRunner:
|
|
159
227
|
limit_clause = f"| head -n {limit}" if limit else ""
|
160
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}"
|
161
229
|
|
162
|
-
|
163
|
-
output = stdout.read().decode("utf-8").strip()
|
164
|
-
error = stderr.read().decode("utf-8").strip()
|
165
|
-
|
166
|
-
if error:
|
167
|
-
raise RuntimeError(f"Error executing AWK command: {error}")
|
230
|
+
output = self._execute_ssh_command(awk_cmd).strip()
|
168
231
|
|
169
232
|
# Split rows into lists of strings
|
170
233
|
rows = [
|
@@ -204,14 +267,9 @@ class RemoteCSVQueryRunner:
|
|
204
267
|
if total_lines
|
205
268
|
else f"cat {self.file_path}"
|
206
269
|
)
|
207
|
-
|
208
|
-
output = stdout.read().decode("utf-8").strip()
|
209
|
-
error = stderr.read().decode("utf-8").strip()
|
270
|
+
output = self._execute_ssh_command(cmd).strip()
|
210
271
|
|
211
|
-
|
212
|
-
raise RuntimeError(f"Error fetching raw rows: {error}")
|
213
|
-
|
214
|
-
return output.splitlines()[self.offset :]
|
272
|
+
return output.splitlines()[self.offset:]
|
215
273
|
|
216
274
|
def get_csv_header(self) -> Dict[str, int]:
|
217
275
|
"""
|
@@ -219,12 +277,7 @@ class RemoteCSVQueryRunner:
|
|
219
277
|
:return: Dictionary of headers.
|
220
278
|
"""
|
221
279
|
header_cmd = f"head -n {self.offset + 1} {self.file_path} | tail -n 1"
|
222
|
-
|
223
|
-
header = stdout.read().decode("utf-8").strip()
|
224
|
-
error = stderr.read().decode("utf-8").strip()
|
225
|
-
|
226
|
-
if error:
|
227
|
-
raise RuntimeError(f"Error reading CSV header: {error}")
|
280
|
+
header = self._execute_ssh_command(header_cmd).strip()
|
228
281
|
|
229
282
|
# Trim spaces in header names
|
230
283
|
column_names = [name.strip() for name in header.split(self.sep)]
|
@@ -253,10 +306,59 @@ class RemoteCSVQueryRunner:
|
|
253
306
|
|
254
307
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
255
308
|
"""
|
256
|
-
Clean up
|
309
|
+
Clean up resources when exiting context.
|
257
310
|
"""
|
258
|
-
|
259
|
-
|
311
|
+
pass
|
312
|
+
|
313
|
+
|
314
|
+
class NPEQueries:
|
315
|
+
NPE_FOLDER = "npe_viz"
|
316
|
+
MANIFEST_FILE = "manifest.json"
|
317
|
+
|
318
|
+
@staticmethod
|
319
|
+
def get_npe_manifest(instance: Instance):
|
320
|
+
|
321
|
+
if (
|
322
|
+
not instance.remote_connection
|
323
|
+
or instance.remote_connection
|
324
|
+
and not instance.remote_connection.useRemoteQuerying
|
325
|
+
):
|
326
|
+
file_path = Path(
|
327
|
+
instance.performance_path, NPEQueries.NPE_FOLDER, NPEQueries.MANIFEST_FILE
|
328
|
+
)
|
329
|
+
with open(file_path, "r") as f:
|
330
|
+
return json.load(f)
|
331
|
+
else:
|
332
|
+
profiler_folder = instance.remote_profile_folder
|
333
|
+
return read_remote_file(
|
334
|
+
instance.remote_connection,
|
335
|
+
f"{profiler_folder.remotePath}/{NPEQueries.NPE_FOLDER}/{NPEQueries.MANIFEST_FILE}",
|
336
|
+
)
|
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
|
+
|
260
362
|
|
261
363
|
|
262
364
|
class DeviceLogProfilerQueries:
|
@@ -584,6 +686,7 @@ class OpsPerformanceReportQueries:
|
|
584
686
|
"inner_dim_block_size",
|
585
687
|
"output_subblock_h",
|
586
688
|
"output_subblock_w",
|
689
|
+
"global_call_count",
|
587
690
|
"advice",
|
588
691
|
"raw_op_code"
|
589
692
|
]
|
@@ -640,7 +743,7 @@ class OpsPerformanceReportQueries:
|
|
640
743
|
|
641
744
|
for key, value in cls.PASSTHROUGH_COLUMNS.items():
|
642
745
|
op_id = int(row[0])
|
643
|
-
idx = op_id - 2
|
746
|
+
idx = op_id - 2 # IDs in result column one correspond to row numbers in ops perf results csv
|
644
747
|
processed_row[key] = ops_perf_results[idx][value]
|
645
748
|
|
646
749
|
report.append(processed_row)
|
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
|