ttnn-visualizer 0.44.0__py3-none-any.whl → 0.45.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.
@@ -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 []
@@ -1 +1 @@
1
- import{I as n}from"./index-DLOviMB1.js";import{I as e}from"./index-B-fsa5Ru.js";import{p as r,I as s}from"./index-CemTxGZ4.js";function I(o,t){var a=r(o);return t===s.STANDARD?n[a]:e[a]}function p(o){return r(o)}export{n as IconSvgPaths16,e as IconSvgPaths20,I as getIconPaths,p as iconNameToPathsRecordKey};
1
+ import{I as n}from"./index-DLOviMB1.js";import{I as e}from"./index-B-fsa5Ru.js";import{p as r,I as s}from"./index-BgTcwiDZ.js";function I(o,t){var a=r(o);return t===s.STANDARD?n[a]:e[a]}function p(o){return r(o)}export{n as IconSvgPaths16,e as IconSvgPaths20,I as getIconPaths,p as iconNameToPathsRecordKey};
@@ -1,2 +1,2 @@
1
- const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/allPaths-dhyS1SXV.js","assets/index-DLOviMB1.js","assets/index-B-fsa5Ru.js","assets/index-CemTxGZ4.js","assets/index-DdkKoj59.css"])))=>i.map(i=>d[i]);
2
- import{_ as o,a as n,b as i}from"./index-CemTxGZ4.js";var _=function(e,a){return o(void 0,void 0,void 0,function(){var t;return n(this,function(r){switch(r.label){case 0:return[4,i(()=>import("./allPaths-dhyS1SXV.js"),__vite__mapDeps([0,1,2,3,4]))];case 1:return t=r.sent().getIconPaths,[2,t(e,a)]}})})};export{_ as allPathsLoader};
1
+ const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/allPaths-CWDYwlGf.js","assets/index-DLOviMB1.js","assets/index-B-fsa5Ru.js","assets/index-BgTcwiDZ.js","assets/index-cjyfcubn.css"])))=>i.map(i=>d[i]);
2
+ import{_ as o,a as n,b as i}from"./index-BgTcwiDZ.js";var _=function(e,a){return o(void 0,void 0,void 0,function(){var t;return n(this,function(r){switch(r.label){case 0:return[4,i(()=>import("./allPaths-CWDYwlGf.js"),__vite__mapDeps([0,1,2,3,4]))];case 1:return t=r.sent().getIconPaths,[2,t(e,a)]}})})};export{_ as allPathsLoader};