ttnn-visualizer 0.44.1__py3-none-any.whl → 0.46.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 +1 -54
- ttnn_visualizer/decorators.py +3 -3
- ttnn_visualizer/exceptions.py +7 -1
- ttnn_visualizer/models.py +1 -0
- ttnn_visualizer/remote_sqlite_setup.py +6 -82
- ttnn_visualizer/sftp_operations.py +34 -106
- ttnn_visualizer/ssh_client.py +352 -0
- ttnn_visualizer/static/assets/allPaths-esBqnTg5.js +1 -0
- ttnn_visualizer/static/assets/allPathsLoader-KPOKJ-lr.js +2 -0
- ttnn_visualizer/static/assets/index-03c8d4Gh.js +1 -0
- ttnn_visualizer/static/assets/{index-B2fHW2_O.js → index-BANm1CMY.js} +383 -383
- ttnn_visualizer/static/assets/index-BuHal8Ii.css +7 -0
- ttnn_visualizer/static/assets/index-PKNBViIU.js +1 -0
- ttnn_visualizer/static/assets/splitPathsBySizeLoader-DYuDhweD.js +1 -0
- ttnn_visualizer/static/index.html +2 -2
- ttnn_visualizer/views.py +80 -181
- {ttnn_visualizer-0.44.1.dist-info → ttnn_visualizer-0.46.0.dist-info}/METADATA +7 -3
- ttnn_visualizer-0.46.0.dist-info/RECORD +43 -0
- {ttnn_visualizer-0.44.1.dist-info → ttnn_visualizer-0.46.0.dist-info}/licenses/LICENSE +6 -0
- ttnn_visualizer/static/assets/allPaths-CFKU23gh.js +0 -1
- ttnn_visualizer/static/assets/allPathsLoader-CpaihUCo.js +0 -2
- ttnn_visualizer/static/assets/index-B-fsa5Ru.js +0 -1
- ttnn_visualizer/static/assets/index-BueCaPcI.css +0 -7
- ttnn_visualizer/static/assets/index-DLOviMB1.js +0 -1
- ttnn_visualizer/static/assets/splitPathsBySizeLoader-BEb-7YZm.js +0 -1
- ttnn_visualizer-0.44.1.dist-info/RECORD +0 -42
- {ttnn_visualizer-0.44.1.dist-info → ttnn_visualizer-0.46.0.dist-info}/WHEEL +0 -0
- {ttnn_visualizer-0.44.1.dist-info → ttnn_visualizer-0.46.0.dist-info}/entry_points.txt +0 -0
- {ttnn_visualizer-0.44.1.dist-info → ttnn_visualizer-0.46.0.dist-info}/licenses/LICENSE_understanding.txt +0 -0
- {ttnn_visualizer-0.44.1.dist-info → ttnn_visualizer-0.46.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,352 @@
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
2
|
+
#
|
3
|
+
# SPDX-FileCopyrightText: © 2025 Tenstorrent AI ULC
|
4
|
+
|
5
|
+
import logging
|
6
|
+
import subprocess
|
7
|
+
from pathlib import Path
|
8
|
+
from typing import List, Optional, Union
|
9
|
+
|
10
|
+
from ttnn_visualizer.enums import ConnectionTestStates
|
11
|
+
from ttnn_visualizer.exceptions import (
|
12
|
+
AuthenticationException,
|
13
|
+
AuthenticationFailedException,
|
14
|
+
NoValidConnectionsError,
|
15
|
+
RemoteConnectionException,
|
16
|
+
SSHException,
|
17
|
+
)
|
18
|
+
from ttnn_visualizer.models import RemoteConnection
|
19
|
+
|
20
|
+
logger = logging.getLogger(__name__)
|
21
|
+
|
22
|
+
|
23
|
+
class SSHClient:
|
24
|
+
"""
|
25
|
+
Centralized SSH client that handles all SSH/SFTP operations with consistent
|
26
|
+
error handling and logging.
|
27
|
+
"""
|
28
|
+
|
29
|
+
def __init__(self, connection: RemoteConnection):
|
30
|
+
self.connection = connection
|
31
|
+
self._base_ssh_cmd = self._build_base_ssh_cmd()
|
32
|
+
self._base_sftp_cmd = self._build_base_sftp_cmd()
|
33
|
+
|
34
|
+
def _build_base_ssh_cmd(self) -> List[str]:
|
35
|
+
"""Build the base SSH command with common options."""
|
36
|
+
cmd = ["ssh", "-o", "PasswordAuthentication=no"]
|
37
|
+
|
38
|
+
if self.connection.port != 22:
|
39
|
+
cmd.extend(["-p", str(self.connection.port)])
|
40
|
+
|
41
|
+
cmd.append(f"{self.connection.username}@{self.connection.host}")
|
42
|
+
return cmd
|
43
|
+
|
44
|
+
def _build_base_sftp_cmd(self) -> List[str]:
|
45
|
+
"""Build the base SFTP command with common options."""
|
46
|
+
cmd = ["sftp", "-o", "PasswordAuthentication=no"]
|
47
|
+
|
48
|
+
if self.connection.port != 22:
|
49
|
+
cmd.extend(["-P", str(self.connection.port)])
|
50
|
+
|
51
|
+
cmd.extend(["-b", "-"]) # Read commands from stdin
|
52
|
+
cmd.append(f"{self.connection.username}@{self.connection.host}")
|
53
|
+
return cmd
|
54
|
+
|
55
|
+
def _handle_subprocess_error(self, e: subprocess.CalledProcessError):
|
56
|
+
"""
|
57
|
+
Convert subprocess SSH errors to appropriate SSH exceptions.
|
58
|
+
|
59
|
+
:param e: The subprocess.CalledProcessError
|
60
|
+
:raises: SSHException, AuthenticationException, or NoValidConnectionsError
|
61
|
+
"""
|
62
|
+
stderr = e.stderr.lower() if e.stderr else ""
|
63
|
+
raw_error = e.stderr.strip() if e.stderr else "No stderr output"
|
64
|
+
|
65
|
+
# Log the raw SSH error for debugging
|
66
|
+
logger.warning(
|
67
|
+
f"SSH error for {self.connection.username}@{self.connection.host}: {raw_error}"
|
68
|
+
)
|
69
|
+
|
70
|
+
# Store raw error for exceptions that need it
|
71
|
+
self._last_raw_error = raw_error
|
72
|
+
|
73
|
+
# Check for authentication failures
|
74
|
+
if any(
|
75
|
+
auth_err in stderr
|
76
|
+
for auth_err in [
|
77
|
+
"permission denied",
|
78
|
+
"authentication failed",
|
79
|
+
"publickey",
|
80
|
+
"password",
|
81
|
+
"host key verification failed",
|
82
|
+
]
|
83
|
+
):
|
84
|
+
raise AuthenticationException(
|
85
|
+
f"SSH authentication failed: {self.connection.username}@{self.connection.host}: Permission denied (publickey,password)"
|
86
|
+
)
|
87
|
+
|
88
|
+
# Check for connection failures (including DNS resolution failures)
|
89
|
+
elif any(
|
90
|
+
conn_err in stderr
|
91
|
+
for conn_err in [
|
92
|
+
"connection refused",
|
93
|
+
"network is unreachable",
|
94
|
+
"no route to host",
|
95
|
+
"name or service not known",
|
96
|
+
"could not resolve hostname",
|
97
|
+
"connection timed out",
|
98
|
+
"nodename nor servname provided",
|
99
|
+
]
|
100
|
+
):
|
101
|
+
raise NoValidConnectionsError(f"SSH connection failed: {e.stderr}")
|
102
|
+
|
103
|
+
# Check for general SSH protocol errors
|
104
|
+
elif "ssh:" in stderr or "protocol" in stderr:
|
105
|
+
raise SSHException(f"SSH protocol error: {e.stderr}")
|
106
|
+
|
107
|
+
# Default to generic SSH exception
|
108
|
+
else:
|
109
|
+
raise SSHException(f"SSH command failed: {e.stderr}")
|
110
|
+
|
111
|
+
def execute_command(self, command: str, timeout: int = 30) -> str:
|
112
|
+
"""
|
113
|
+
Execute a command on the remote server via SSH.
|
114
|
+
|
115
|
+
:param command: The command to execute
|
116
|
+
:param timeout: Timeout in seconds
|
117
|
+
:return: Command output (stdout)
|
118
|
+
:raises: AuthenticationException, NoValidConnectionsError, SSHException
|
119
|
+
"""
|
120
|
+
ssh_cmd = self._base_ssh_cmd + [command]
|
121
|
+
|
122
|
+
logger.debug(f"Executing SSH command on {self.connection.host}: {command}")
|
123
|
+
|
124
|
+
try:
|
125
|
+
result = subprocess.run(
|
126
|
+
ssh_cmd, capture_output=True, text=True, check=True, timeout=timeout
|
127
|
+
)
|
128
|
+
return result.stdout
|
129
|
+
except subprocess.CalledProcessError as e:
|
130
|
+
if e.returncode == 255: # SSH protocol errors
|
131
|
+
self._handle_subprocess_error(e)
|
132
|
+
else:
|
133
|
+
raise SSHException(f"Command failed: {e.stderr}")
|
134
|
+
except subprocess.TimeoutExpired:
|
135
|
+
logger.warning(
|
136
|
+
f"SSH command timed out for {self.connection.username}@{self.connection.host}: {command}"
|
137
|
+
)
|
138
|
+
raise SSHException(f"SSH command timed out: {command}")
|
139
|
+
|
140
|
+
def test_connection(self) -> bool:
|
141
|
+
"""
|
142
|
+
Test SSH connection by running a simple command.
|
143
|
+
|
144
|
+
:return: True if connection successful
|
145
|
+
:raises: AuthenticationFailedException, RemoteConnectionException
|
146
|
+
"""
|
147
|
+
try:
|
148
|
+
log_message = f"Testing SSH connection to {self.connection.username}@{self.connection.host}"
|
149
|
+
if self.connection.port != 22:
|
150
|
+
log_message += f" on port {self.connection.port}"
|
151
|
+
logger.info(log_message)
|
152
|
+
|
153
|
+
self.execute_command("echo 'SSH connection test'", timeout=10)
|
154
|
+
return True
|
155
|
+
except AuthenticationException as e:
|
156
|
+
# Convert to AuthenticationFailedException for proper HTTP 422 response
|
157
|
+
user_message = (
|
158
|
+
"SSH authentication failed. This application requires SSH key-based authentication. "
|
159
|
+
"Please ensure your SSH public key is added to the authorized_keys file on the remote server. "
|
160
|
+
"Password authentication is not supported."
|
161
|
+
)
|
162
|
+
logger.info(
|
163
|
+
f"SSH authentication failed for {self.connection.username}@{self.connection.host}"
|
164
|
+
)
|
165
|
+
# Get the raw error details from the last SSH operation
|
166
|
+
raw_error = getattr(self, "_last_raw_error", None)
|
167
|
+
raise AuthenticationFailedException(message=user_message, detail=raw_error)
|
168
|
+
except NoValidConnectionsError as ssh_err:
|
169
|
+
user_message = (
|
170
|
+
f"Unable to establish SSH connection to {self.connection.host}. "
|
171
|
+
"Please check the hostname, port, and network connectivity. "
|
172
|
+
"Ensure SSH key-based authentication is properly configured."
|
173
|
+
)
|
174
|
+
# Get the raw error details from the last SSH operation
|
175
|
+
raw_error = getattr(self, "_last_raw_error", None)
|
176
|
+
raise RemoteConnectionException(
|
177
|
+
message=user_message,
|
178
|
+
status=ConnectionTestStates.FAILED,
|
179
|
+
detail=raw_error,
|
180
|
+
)
|
181
|
+
except SSHException as ssh_err:
|
182
|
+
# Add debug logging to understand what we're getting
|
183
|
+
logger.debug(f"SSHException caught in test_connection: '{str(ssh_err)}'")
|
184
|
+
|
185
|
+
# Check if this is a timeout - should match original "SSH connection test timed out" message
|
186
|
+
ssh_err_str = str(ssh_err).lower()
|
187
|
+
if "timed out" in ssh_err_str or "timeout" in ssh_err_str:
|
188
|
+
timeout_message = "SSH connection test timed out"
|
189
|
+
logger.warning(
|
190
|
+
f"SSH timeout for {self.connection.username}@{self.connection.host}: {timeout_message}"
|
191
|
+
)
|
192
|
+
# Store timeout as raw error
|
193
|
+
raw_error = str(ssh_err)
|
194
|
+
raise RemoteConnectionException(
|
195
|
+
message=timeout_message,
|
196
|
+
status=ConnectionTestStates.FAILED,
|
197
|
+
detail=raw_error,
|
198
|
+
)
|
199
|
+
else:
|
200
|
+
# This should match the original SSHException handling:
|
201
|
+
# "SSH connection error to {host}: {str(ssh_err)}. Ensure SSH key-based authentication is properly configured."
|
202
|
+
user_message = f"SSH connection error to {self.connection.host}: {str(ssh_err)}. Ensure SSH key-based authentication is properly configured."
|
203
|
+
logger.warning(
|
204
|
+
f"SSH error for {self.connection.username}@{self.connection.host}: {user_message}"
|
205
|
+
)
|
206
|
+
raw_error = getattr(self, "_last_raw_error", str(ssh_err))
|
207
|
+
raise RemoteConnectionException(
|
208
|
+
message=user_message,
|
209
|
+
status=ConnectionTestStates.FAILED,
|
210
|
+
detail=raw_error,
|
211
|
+
)
|
212
|
+
|
213
|
+
def read_file(
|
214
|
+
self, remote_path: Union[str, Path], timeout: int = 30
|
215
|
+
) -> Optional[bytes]:
|
216
|
+
"""
|
217
|
+
Read a remote file using SSH cat command.
|
218
|
+
|
219
|
+
:param remote_path: Path to the remote file
|
220
|
+
:param timeout: Timeout in seconds
|
221
|
+
:return: File contents as bytes, or None if file not found
|
222
|
+
:raises: AuthenticationException, NoValidConnectionsError, SSHException
|
223
|
+
"""
|
224
|
+
path = Path(remote_path)
|
225
|
+
logger.info(f"Reading remote file {path}")
|
226
|
+
|
227
|
+
try:
|
228
|
+
result = self.execute_command(f"cat '{path}'", timeout=timeout)
|
229
|
+
return result.encode("utf-8")
|
230
|
+
except SSHException as e:
|
231
|
+
if "No such file" in str(e) or "cannot open" in str(e):
|
232
|
+
logger.error(f"File not found or cannot be read: {path}")
|
233
|
+
return None
|
234
|
+
raise
|
235
|
+
|
236
|
+
def check_path_exists(
|
237
|
+
self, remote_path: Union[str, Path], timeout: int = 10
|
238
|
+
) -> bool:
|
239
|
+
"""
|
240
|
+
Check if a remote path exists.
|
241
|
+
|
242
|
+
:param remote_path: Path to check
|
243
|
+
:param timeout: Timeout in seconds
|
244
|
+
:return: True if path exists
|
245
|
+
"""
|
246
|
+
path = Path(remote_path)
|
247
|
+
logger.debug(f"Checking if remote path exists: {path}")
|
248
|
+
|
249
|
+
try:
|
250
|
+
self.execute_command(f"test -e '{path}'", timeout=timeout)
|
251
|
+
return True
|
252
|
+
except SSHException:
|
253
|
+
return False
|
254
|
+
|
255
|
+
def download_file(
|
256
|
+
self,
|
257
|
+
remote_path: Union[str, Path],
|
258
|
+
local_path: Union[str, Path],
|
259
|
+
timeout: int = 300,
|
260
|
+
):
|
261
|
+
"""
|
262
|
+
Download a file using SFTP.
|
263
|
+
|
264
|
+
:param remote_path: Remote file path
|
265
|
+
:param local_path: Local destination path
|
266
|
+
:param timeout: Timeout in seconds
|
267
|
+
:raises: AuthenticationException, NoValidConnectionsError, SSHException
|
268
|
+
"""
|
269
|
+
remote_path = Path(remote_path)
|
270
|
+
local_path = Path(local_path)
|
271
|
+
|
272
|
+
# Ensure local directory exists
|
273
|
+
local_path.parent.mkdir(parents=True, exist_ok=True)
|
274
|
+
|
275
|
+
# SFTP commands to execute
|
276
|
+
sftp_commands = f"get '{remote_path}' '{local_path}'\nquit\n"
|
277
|
+
|
278
|
+
logger.debug(f"Downloading: {remote_path} -> {local_path}")
|
279
|
+
|
280
|
+
try:
|
281
|
+
result = subprocess.run(
|
282
|
+
self._base_sftp_cmd,
|
283
|
+
input=sftp_commands,
|
284
|
+
capture_output=True,
|
285
|
+
text=True,
|
286
|
+
check=True,
|
287
|
+
timeout=timeout,
|
288
|
+
)
|
289
|
+
logger.debug(f"Downloaded successfully: {remote_path} -> {local_path}")
|
290
|
+
except subprocess.CalledProcessError as e:
|
291
|
+
if e.returncode == 255: # SSH protocol errors
|
292
|
+
self._handle_subprocess_error(e)
|
293
|
+
else:
|
294
|
+
logger.error(
|
295
|
+
f"SFTP error for {self.connection.username}@{self.connection.host} downloading {remote_path}: {e.stderr}"
|
296
|
+
)
|
297
|
+
raise SSHException(f"Failed to download {remote_path}")
|
298
|
+
except subprocess.TimeoutExpired:
|
299
|
+
logger.error(
|
300
|
+
f"SFTP timeout for {self.connection.username}@{self.connection.host} downloading file: {remote_path}"
|
301
|
+
)
|
302
|
+
raise SSHException(f"Timeout downloading {remote_path}")
|
303
|
+
|
304
|
+
def get_file_stat(
|
305
|
+
self, remote_path: Union[str, Path], timeout: int = 10
|
306
|
+
) -> Optional[dict]:
|
307
|
+
"""
|
308
|
+
Get file statistics for a remote path.
|
309
|
+
|
310
|
+
:param remote_path: Remote file path
|
311
|
+
:param timeout: Timeout in seconds
|
312
|
+
:return: Dict with file stats or None if file doesn't exist
|
313
|
+
"""
|
314
|
+
path = Path(remote_path)
|
315
|
+
|
316
|
+
try:
|
317
|
+
# Use stat command to get file information
|
318
|
+
result = self.execute_command(
|
319
|
+
f"stat -c '%s %Y %F' '{path}' 2>/dev/null || echo 'NOT_FOUND'",
|
320
|
+
timeout=timeout,
|
321
|
+
)
|
322
|
+
|
323
|
+
if result.strip() == "NOT_FOUND":
|
324
|
+
return None
|
325
|
+
|
326
|
+
parts = result.strip().split(" ", 2)
|
327
|
+
if len(parts) >= 3:
|
328
|
+
return {"size": int(parts[0]), "mtime": int(parts[1]), "type": parts[2]}
|
329
|
+
except (SSHException, ValueError):
|
330
|
+
return None
|
331
|
+
|
332
|
+
return None
|
333
|
+
|
334
|
+
def list_directory(
|
335
|
+
self, remote_path: Union[str, Path], timeout: int = 30
|
336
|
+
) -> List[str]:
|
337
|
+
"""
|
338
|
+
List contents of a remote directory.
|
339
|
+
|
340
|
+
:param remote_path: Remote directory path
|
341
|
+
:param timeout: Timeout in seconds
|
342
|
+
:return: List of file/directory names
|
343
|
+
"""
|
344
|
+
path = Path(remote_path)
|
345
|
+
|
346
|
+
try:
|
347
|
+
result = self.execute_command(
|
348
|
+
f"ls -1 '{path}' 2>/dev/null", timeout=timeout
|
349
|
+
)
|
350
|
+
return [line.strip() for line in result.split("\n") if line.strip()]
|
351
|
+
except SSHException:
|
352
|
+
return []
|
@@ -0,0 +1 @@
|
|
1
|
+
import{I as s}from"./index-03c8d4Gh.js";import{I as r}from"./index-PKNBViIU.js";import{p as n,I as c}from"./index-BANm1CMY.js";function p(t,a){const o=n(t);return a===c.STANDARD?s[o]:r[o]}export{s as IconSvgPaths16,r as IconSvgPaths20,p as getIconPaths};
|
@@ -0,0 +1,2 @@
|
|
1
|
+
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/allPaths-esBqnTg5.js","assets/index-03c8d4Gh.js","assets/index-PKNBViIU.js","assets/index-BANm1CMY.js","assets/index-BuHal8Ii.css"])))=>i.map(i=>d[i]);
|
2
|
+
import{_ as e}from"./index-BANm1CMY.js";const s=async(t,a)=>{const{getIconPaths:o}=await e(async()=>{const{getIconPaths:r}=await import("./allPaths-esBqnTg5.js");return{getIconPaths:r}},__vite__mapDeps([0,1,2,3,4]));return o(t,a)};export{s as allPathsLoader};
|