ttnn-visualizer 0.41.0__py3-none-any.whl → 0.43.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/__init__.py +0 -1
- ttnn_visualizer/app.py +15 -4
- ttnn_visualizer/csv_queries.py +150 -40
- ttnn_visualizer/decorators.py +42 -16
- ttnn_visualizer/exceptions.py +45 -1
- ttnn_visualizer/file_uploads.py +1 -0
- ttnn_visualizer/instances.py +42 -15
- ttnn_visualizer/models.py +12 -7
- ttnn_visualizer/queries.py +3 -109
- ttnn_visualizer/remote_sqlite_setup.py +104 -19
- ttnn_visualizer/requirements.txt +2 -3
- ttnn_visualizer/serializers.py +1 -0
- ttnn_visualizer/settings.py +9 -5
- ttnn_visualizer/sftp_operations.py +657 -220
- ttnn_visualizer/sockets.py +9 -3
- ttnn_visualizer/static/assets/{allPaths-4_pFqSAW.js → allPaths-BQN_j7ek.js} +1 -1
- ttnn_visualizer/static/assets/{allPathsLoader-CpLPTLlt.js → allPathsLoader-BvkkQ77q.js} +2 -2
- ttnn_visualizer/static/assets/index-B-fsa5Ru.js +1 -0
- ttnn_visualizer/static/assets/{index-DFVwehlj.js → index-Bng0kcmi.js} +214 -214
- ttnn_visualizer/static/assets/{index-C1rJBrMl.css → index-C-t6jBt9.css} +1 -1
- ttnn_visualizer/static/assets/index-DLOviMB1.js +1 -0
- ttnn_visualizer/static/assets/{splitPathsBySizeLoader-D-RvsTqO.js → splitPathsBySizeLoader-Cl0NRdfL.js} +1 -1
- ttnn_visualizer/static/index.html +2 -2
- ttnn_visualizer/tests/__init__.py +0 -1
- ttnn_visualizer/tests/test_queries.py +0 -69
- ttnn_visualizer/tests/test_serializers.py +2 -2
- ttnn_visualizer/utils.py +7 -3
- ttnn_visualizer/views.py +315 -52
- {ttnn_visualizer-0.41.0.dist-info → ttnn_visualizer-0.43.0.dist-info}/LICENSE +0 -1
- {ttnn_visualizer-0.41.0.dist-info → ttnn_visualizer-0.43.0.dist-info}/METADATA +6 -3
- ttnn_visualizer-0.43.0.dist-info/RECORD +45 -0
- ttnn_visualizer/ssh_client.py +0 -85
- ttnn_visualizer/static/assets/index-BKzgFDAn.js +0 -1
- ttnn_visualizer/static/assets/index-BvSuWPlB.js +0 -1
- ttnn_visualizer-0.41.0.dist-info/RECORD +0 -46
- {ttnn_visualizer-0.41.0.dist-info → ttnn_visualizer-0.43.0.dist-info}/LICENSE_understanding.txt +0 -0
- {ttnn_visualizer-0.41.0.dist-info → ttnn_visualizer-0.43.0.dist-info}/WHEEL +0 -0
- {ttnn_visualizer-0.41.0.dist-info → ttnn_visualizer-0.43.0.dist-info}/entry_points.txt +0 -0
- {ttnn_visualizer-0.41.0.dist-info → ttnn_visualizer-0.43.0.dist-info}/top_level.txt +0 -0
@@ -6,25 +6,29 @@ import json
|
|
6
6
|
import logging
|
7
7
|
import re
|
8
8
|
import time
|
9
|
+
import subprocess
|
9
10
|
from pathlib import Path
|
10
11
|
from stat import S_ISDIR
|
11
12
|
from threading import Thread
|
12
13
|
from typing import List, Optional
|
13
14
|
|
14
15
|
from flask import current_app
|
15
|
-
from paramiko.client import SSHClient
|
16
|
-
from paramiko.sftp_client import SFTPClient
|
17
16
|
|
18
17
|
from ttnn_visualizer.decorators import remote_exception_handler
|
19
18
|
from ttnn_visualizer.enums import ConnectionTestStates
|
20
|
-
from ttnn_visualizer.exceptions import
|
19
|
+
from ttnn_visualizer.exceptions import (
|
20
|
+
NoProjectsException,
|
21
|
+
RemoteConnectionException,
|
22
|
+
SSHException,
|
23
|
+
AuthenticationException,
|
24
|
+
NoValidConnectionsError,
|
25
|
+
)
|
21
26
|
from ttnn_visualizer.models import RemoteConnection, RemoteReportFolder
|
22
27
|
from ttnn_visualizer.sockets import (
|
23
28
|
FileProgress,
|
24
29
|
FileStatus,
|
25
30
|
emit_file_status,
|
26
31
|
)
|
27
|
-
from ttnn_visualizer.ssh_client import get_client
|
28
32
|
from ttnn_visualizer.utils import update_last_synced
|
29
33
|
|
30
34
|
logger = logging.getLogger(__name__)
|
@@ -34,6 +38,53 @@ TEST_PROFILER_FILE = "profile_log_device.csv"
|
|
34
38
|
REPORT_DATA_DIRECTORY = Path(__file__).parent.absolute().joinpath("data")
|
35
39
|
|
36
40
|
|
41
|
+
def handle_ssh_subprocess_error(
|
42
|
+
e: subprocess.CalledProcessError, remote_connection: RemoteConnection
|
43
|
+
):
|
44
|
+
"""
|
45
|
+
Convert subprocess SSH errors to appropriate SSH exceptions.
|
46
|
+
|
47
|
+
:param e: The subprocess.CalledProcessError
|
48
|
+
:param remote_connection: The RemoteConnection object for context
|
49
|
+
:raises: SSHException, AuthenticationException, or NoValidConnectionsError
|
50
|
+
"""
|
51
|
+
stderr = e.stderr.lower() if e.stderr else ""
|
52
|
+
|
53
|
+
# Check for authentication failures
|
54
|
+
if any(
|
55
|
+
auth_err in stderr
|
56
|
+
for auth_err in [
|
57
|
+
"permission denied",
|
58
|
+
"authentication failed",
|
59
|
+
"publickey",
|
60
|
+
"password",
|
61
|
+
"host key verification failed",
|
62
|
+
]
|
63
|
+
):
|
64
|
+
raise AuthenticationException(f"SSH authentication failed: {e.stderr}")
|
65
|
+
|
66
|
+
# Check for connection failures
|
67
|
+
elif any(
|
68
|
+
conn_err in stderr
|
69
|
+
for conn_err in [
|
70
|
+
"connection refused",
|
71
|
+
"network is unreachable",
|
72
|
+
"no route to host",
|
73
|
+
"name or service not known",
|
74
|
+
"connection timed out",
|
75
|
+
]
|
76
|
+
):
|
77
|
+
raise NoValidConnectionsError(f"SSH connection failed: {e.stderr}")
|
78
|
+
|
79
|
+
# Check for general SSH protocol errors
|
80
|
+
elif "ssh:" in stderr or "protocol" in stderr:
|
81
|
+
raise SSHException(f"SSH protocol error: {e.stderr}")
|
82
|
+
|
83
|
+
# Default to generic SSH exception
|
84
|
+
else:
|
85
|
+
raise SSHException(f"SSH command failed: {e.stderr}")
|
86
|
+
|
87
|
+
|
37
88
|
def start_background_task(task, *args):
|
38
89
|
with current_app.app_context():
|
39
90
|
if current_app.config["USE_WEBSOCKETS"]:
|
@@ -57,31 +108,49 @@ def resolve_file_path(remote_connection, file_path: str) -> str:
|
|
57
108
|
:return: The resolved file path.
|
58
109
|
:raises FileNotFoundError: If no files match the pattern.
|
59
110
|
"""
|
60
|
-
ssh_client = get_client(remote_connection)
|
61
|
-
|
62
111
|
if "*" in file_path:
|
63
|
-
command
|
64
|
-
|
65
|
-
|
66
|
-
|
112
|
+
# Build SSH command to list files matching the pattern
|
113
|
+
ssh_cmd = [
|
114
|
+
"ssh",
|
115
|
+
f"{remote_connection.username}@{remote_connection.host}",
|
116
|
+
]
|
67
117
|
|
68
|
-
|
69
|
-
|
118
|
+
# Handle non-standard SSH port
|
119
|
+
if remote_connection.port != 22:
|
120
|
+
ssh_cmd.extend(["-p", str(remote_connection.port)])
|
70
121
|
|
71
|
-
#
|
72
|
-
|
122
|
+
# Add the ls command
|
123
|
+
ssh_cmd.append(f"ls -1 {file_path}")
|
73
124
|
|
74
|
-
|
125
|
+
try:
|
126
|
+
result = subprocess.run(ssh_cmd, capture_output=True, text=True, check=True)
|
127
|
+
|
128
|
+
files = result.stdout.strip().splitlines()
|
129
|
+
|
130
|
+
if not files or (len(files) == 1 and files[0] == ""):
|
131
|
+
raise FileNotFoundError(f"No files found matching pattern: {file_path}")
|
132
|
+
|
133
|
+
# Return the first file found
|
134
|
+
return files[0]
|
135
|
+
|
136
|
+
except subprocess.CalledProcessError as e:
|
137
|
+
logger.error(f"SSH command failed: {e}")
|
138
|
+
logger.error(f"stderr: {e.stderr}")
|
75
139
|
|
140
|
+
# Check if it's an SSH-specific error (authentication, connection, etc.)
|
141
|
+
if e.returncode == 255: # SSH returns 255 for SSH protocol errors
|
142
|
+
handle_ssh_subprocess_error(e, remote_connection)
|
143
|
+
else:
|
144
|
+
# File not found or other command error
|
145
|
+
raise FileNotFoundError(f"No files found matching pattern: {file_path}")
|
146
|
+
except Exception as e:
|
147
|
+
logger.error(f"Error resolving file path: {e}")
|
148
|
+
raise FileNotFoundError(f"Error resolving file path: {file_path}")
|
76
149
|
|
77
|
-
|
78
|
-
"""Calculate the total size of the folder before compression."""
|
79
|
-
stdin, stdout, stderr = client.exec_command(f"du -sb {folder_path}")
|
80
|
-
size_info = stdout.read().decode().strip().split("\t")[0]
|
81
|
-
return int(size_info)
|
150
|
+
return file_path
|
82
151
|
|
83
152
|
|
84
|
-
def get_cluster_desc_path(
|
153
|
+
def get_cluster_desc_path(remote_connection: RemoteConnection) -> Optional[str]:
|
85
154
|
"""
|
86
155
|
List all folders matching '/tmp/umd_*' on the remote machine, filter for those containing
|
87
156
|
'cluster_descriptor.yaml', and return the full path to the most recently modified YAML file.
|
@@ -94,37 +163,78 @@ def get_cluster_desc_path(ssh_client) -> Optional[str]:
|
|
94
163
|
cluster_desc_file = "cluster_descriptor.yaml"
|
95
164
|
|
96
165
|
try:
|
97
|
-
#
|
98
|
-
|
99
|
-
|
166
|
+
# Build SSH command to list folders matching '/tmp/umd_*'
|
167
|
+
ssh_cmd = [
|
168
|
+
"ssh",
|
169
|
+
f"{remote_connection.username}@{remote_connection.host}",
|
170
|
+
]
|
171
|
+
|
172
|
+
# Handle non-standard SSH port
|
173
|
+
if remote_connection.port != 22:
|
174
|
+
ssh_cmd.extend(["-p", str(remote_connection.port)])
|
175
|
+
|
176
|
+
# Add the ls command
|
177
|
+
ssh_cmd.append("ls -1d /tmp/umd_* 2>/dev/null")
|
178
|
+
|
179
|
+
# Execute SSH command to list folders
|
180
|
+
result = subprocess.run(
|
181
|
+
ssh_cmd,
|
182
|
+
capture_output=True,
|
183
|
+
text=True,
|
184
|
+
check=False, # Don't raise exception on non-zero exit (in case no folders found)
|
185
|
+
)
|
100
186
|
|
101
187
|
# Get the list of folders
|
102
|
-
folder_paths =
|
188
|
+
folder_paths = (
|
189
|
+
result.stdout.strip().splitlines() if result.stdout.strip() else []
|
190
|
+
)
|
103
191
|
|
104
192
|
if not folder_paths:
|
105
193
|
logger.info("No folders found matching the pattern '/tmp/umd_*'")
|
106
194
|
return None
|
107
195
|
|
108
196
|
# Check each folder for 'cluster_descriptor.yaml' and track the most recent one
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
197
|
+
for folder in folder_paths:
|
198
|
+
yaml_file_path = f"{folder}/{cluster_desc_file}"
|
199
|
+
|
200
|
+
# Build SSH command to check if file exists and get its modification time
|
201
|
+
stat_cmd = [
|
202
|
+
"ssh",
|
203
|
+
"-o",
|
204
|
+
"PasswordAuthentication=no",
|
205
|
+
f"{remote_connection.username}@{remote_connection.host}",
|
206
|
+
]
|
207
|
+
|
208
|
+
if remote_connection.port != 22:
|
209
|
+
stat_cmd.extend(["-p", str(remote_connection.port)])
|
210
|
+
|
211
|
+
# Use stat to get modification time (seconds since epoch)
|
212
|
+
stat_cmd.append(f"stat -c %Y '{yaml_file_path}' 2>/dev/null")
|
213
|
+
|
214
|
+
try:
|
215
|
+
stat_result = subprocess.run(
|
216
|
+
stat_cmd, capture_output=True, text=True, check=True
|
217
|
+
)
|
218
|
+
|
219
|
+
mod_time = float(stat_result.stdout.strip())
|
220
|
+
|
221
|
+
# Update the latest file if this one is newer
|
222
|
+
if mod_time > latest_mod_time:
|
223
|
+
latest_mod_time = mod_time
|
224
|
+
latest_yaml_path = yaml_file_path
|
225
|
+
logger.info(f"Found newer {cluster_desc_file}: {yaml_file_path}")
|
226
|
+
|
227
|
+
except subprocess.CalledProcessError as e:
|
228
|
+
# Check if it's an SSH-specific error
|
229
|
+
if e.returncode == 255: # SSH returns 255 for SSH protocol errors
|
230
|
+
handle_ssh_subprocess_error(e, remote_connection)
|
231
|
+
else:
|
232
|
+
# File not found or other command error
|
126
233
|
logger.debug(f"'{cluster_desc_file}' not found in: {folder}")
|
127
234
|
continue
|
235
|
+
except ValueError:
|
236
|
+
logger.debug(f"'{cluster_desc_file}' not found in: {folder}")
|
237
|
+
continue
|
128
238
|
|
129
239
|
if latest_yaml_path:
|
130
240
|
logger.info(
|
@@ -142,173 +252,404 @@ def get_cluster_desc_path(ssh_client) -> Optional[str]:
|
|
142
252
|
message=f"Failed to get '{cluster_desc_file}' path",
|
143
253
|
status=ConnectionTestStates.FAILED,
|
144
254
|
)
|
145
|
-
finally:
|
146
|
-
ssh_client.close()
|
147
255
|
|
148
256
|
|
149
257
|
@remote_exception_handler
|
150
258
|
def get_cluster_desc(remote_connection: RemoteConnection):
|
151
|
-
|
152
|
-
cluster_path = get_cluster_desc_path(client)
|
259
|
+
cluster_path = get_cluster_desc_path(remote_connection)
|
153
260
|
if cluster_path:
|
154
261
|
return read_remote_file(remote_connection, cluster_path)
|
155
262
|
else:
|
156
263
|
return None
|
157
264
|
|
158
265
|
|
159
|
-
def walk_sftp_directory(sftp: SFTPClient, remote_path: str):
|
160
|
-
"""SFTP implementation of os.walk."""
|
161
|
-
files, folders = [], []
|
162
|
-
for f in sftp.listdir_attr(remote_path):
|
163
|
-
if S_ISDIR(f.st_mode if f.st_mode else 0):
|
164
|
-
folders.append(f.filename)
|
165
|
-
else:
|
166
|
-
files.append(f.filename)
|
167
|
-
return files, folders
|
168
|
-
|
169
|
-
|
170
266
|
def is_excluded(file_path, exclude_patterns):
|
171
|
-
"""Check if
|
172
|
-
|
267
|
+
"""Check if a file path should be excluded based on patterns."""
|
268
|
+
for pattern in exclude_patterns:
|
269
|
+
if pattern in file_path:
|
270
|
+
return True
|
271
|
+
return False
|
173
272
|
|
174
273
|
|
175
274
|
@remote_exception_handler
|
176
275
|
def sync_files_and_directories(
|
177
|
-
|
276
|
+
remote_connection: RemoteConnection,
|
277
|
+
remote_profiler_folder: str,
|
278
|
+
destination_dir: Path,
|
279
|
+
exclude_patterns=None,
|
280
|
+
sid=None,
|
178
281
|
):
|
179
|
-
"""Download files and directories
|
180
|
-
exclude_patterns =
|
181
|
-
exclude_patterns or []
|
182
|
-
) # Default to an empty list if not provided
|
183
|
-
|
184
|
-
with client.open_sftp() as sftp:
|
185
|
-
# Ensure the destination directory exists
|
186
|
-
destination_dir.mkdir(parents=True, exist_ok=True)
|
187
|
-
finished_files = 0 # Initialize finished files counter
|
188
|
-
|
189
|
-
# Recursively handle files and folders in the current directory
|
190
|
-
def download_directory_contents(remote_dir, local_dir):
|
191
|
-
# Ensure the local directory exists
|
192
|
-
local_dir.mkdir(parents=True, exist_ok=True)
|
193
|
-
|
194
|
-
# Get files and folders in the remote directory
|
195
|
-
files, folders = walk_sftp_directory(sftp, remote_dir)
|
196
|
-
total_files = len(files)
|
197
|
-
|
198
|
-
# Function to download a file with progress reporting
|
199
|
-
def download_file(remote_file_path, local_file_path, index):
|
200
|
-
nonlocal finished_files
|
201
|
-
# Download file with progress callback
|
202
|
-
logger.info(f"Downloading {remote_file_path}")
|
203
|
-
download_file_with_progress(
|
204
|
-
sftp,
|
205
|
-
remote_file_path,
|
206
|
-
local_file_path,
|
207
|
-
sid,
|
208
|
-
total_files,
|
209
|
-
finished_files,
|
210
|
-
)
|
211
|
-
logger.info(f"Finished downloading {remote_file_path}")
|
212
|
-
finished_files += 1
|
282
|
+
"""Download files and directories using SFTP with progress reporting."""
|
283
|
+
exclude_patterns = exclude_patterns or []
|
213
284
|
|
214
|
-
|
215
|
-
|
216
|
-
remote_file_path = f"{remote_dir}/{file}"
|
217
|
-
local_file_path = Path(local_dir, file)
|
285
|
+
# Ensure the destination directory exists
|
286
|
+
destination_dir.mkdir(parents=True, exist_ok=True)
|
218
287
|
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
continue
|
288
|
+
logger.info(
|
289
|
+
f"Starting SFTP sync from {remote_profiler_folder} to {destination_dir}"
|
290
|
+
)
|
223
291
|
|
224
|
-
|
292
|
+
# First, get list of all files and directories
|
293
|
+
logger.info("Getting remote file and directory lists...")
|
294
|
+
all_files = get_remote_file_list(
|
295
|
+
remote_connection, remote_profiler_folder, exclude_patterns
|
296
|
+
)
|
297
|
+
all_dirs = get_remote_directory_list(
|
298
|
+
remote_connection, remote_profiler_folder, exclude_patterns
|
299
|
+
)
|
225
300
|
|
226
|
-
|
227
|
-
for folder in folders:
|
228
|
-
remote_subdir = f"{remote_dir}/{folder}"
|
229
|
-
local_subdir = local_dir / folder
|
230
|
-
if is_excluded(remote_subdir, exclude_patterns):
|
231
|
-
logger.info(
|
232
|
-
f"Skipping directory {remote_subdir} (excluded by pattern)"
|
233
|
-
)
|
234
|
-
continue
|
235
|
-
download_directory_contents(remote_subdir, local_subdir)
|
301
|
+
logger.info(f"Found {len(all_files)} files and {len(all_dirs)} directories to sync")
|
236
302
|
|
237
|
-
|
238
|
-
|
303
|
+
# Create local directory structure
|
304
|
+
logger.info("Creating local directory structure...")
|
305
|
+
for remote_dir in all_dirs:
|
306
|
+
try:
|
307
|
+
# Calculate relative path from the base remote folder
|
308
|
+
relative_path = Path(remote_dir).relative_to(remote_profiler_folder)
|
309
|
+
local_dir = destination_dir / relative_path
|
310
|
+
local_dir.mkdir(parents=True, exist_ok=True)
|
311
|
+
except ValueError:
|
312
|
+
# Skip if remote_dir is not relative to remote_profiler_folder
|
313
|
+
continue
|
239
314
|
|
240
|
-
|
241
|
-
|
315
|
+
# Download files with progress reporting
|
316
|
+
total_files = len(all_files)
|
317
|
+
finished_files = 0
|
242
318
|
|
243
|
-
|
244
|
-
final_progress = FileProgress(
|
245
|
-
current_file_name="", # No specific file for the final status
|
246
|
-
number_of_files=0,
|
247
|
-
percent_of_current=100,
|
248
|
-
finished_files=finished_files,
|
249
|
-
status=FileStatus.FINISHED,
|
250
|
-
)
|
319
|
+
logger.info(f"Starting download of {total_files} files...")
|
251
320
|
|
252
|
-
|
253
|
-
|
254
|
-
|
321
|
+
for remote_file in all_files:
|
322
|
+
try:
|
323
|
+
# Calculate relative path from the base remote folder
|
324
|
+
relative_path = Path(remote_file).relative_to(remote_profiler_folder)
|
325
|
+
local_file = destination_dir / relative_path
|
255
326
|
|
327
|
+
# Download the file using SFTP
|
328
|
+
download_single_file_sftp(remote_connection, remote_file, local_file)
|
256
329
|
|
257
|
-
|
258
|
-
sftp, remote_path, local_path, sid, total_files, finished_files
|
259
|
-
):
|
260
|
-
"""Download a file and emit progress using FileProgress."""
|
261
|
-
try:
|
330
|
+
finished_files += 1
|
262
331
|
|
263
|
-
|
264
|
-
percent_of_current = (transferred / total) * 100
|
332
|
+
# Emit progress
|
265
333
|
progress = FileProgress(
|
266
|
-
current_file_name=
|
334
|
+
current_file_name=str(relative_path),
|
267
335
|
number_of_files=total_files,
|
268
|
-
percent_of_current=
|
336
|
+
percent_of_current=100, # We don't get per-file progress with SFTP
|
269
337
|
finished_files=finished_files,
|
270
338
|
status=FileStatus.DOWNLOADING,
|
271
339
|
)
|
272
|
-
emit_file_status(progress, sid)
|
273
340
|
|
274
|
-
|
275
|
-
|
341
|
+
if current_app.config["USE_WEBSOCKETS"]:
|
342
|
+
emit_file_status(progress, sid)
|
343
|
+
|
344
|
+
if finished_files % 10 == 0: # Log every 10 files
|
345
|
+
logger.info(f"Downloaded {finished_files}/{total_files} files")
|
346
|
+
|
347
|
+
except ValueError:
|
348
|
+
# Skip if remote_file is not relative to remote_profiler_folder
|
349
|
+
logger.warning(f"Skipping file outside base folder: {remote_file}")
|
350
|
+
continue
|
351
|
+
except Exception as e:
|
352
|
+
logger.error(f"Failed to download {remote_file}: {e}")
|
353
|
+
# Continue with other files rather than failing completely
|
354
|
+
continue
|
355
|
+
|
356
|
+
# Create a .last-synced file in directory
|
357
|
+
update_last_synced(destination_dir)
|
358
|
+
|
359
|
+
# Emit final status
|
360
|
+
final_progress = FileProgress(
|
361
|
+
current_file_name="",
|
362
|
+
number_of_files=total_files,
|
363
|
+
percent_of_current=100,
|
364
|
+
finished_files=finished_files,
|
365
|
+
status=FileStatus.FINISHED,
|
366
|
+
)
|
367
|
+
|
368
|
+
if current_app.config["USE_WEBSOCKETS"]:
|
369
|
+
emit_file_status(final_progress, sid)
|
370
|
+
|
371
|
+
logger.info(
|
372
|
+
f"SFTP sync completed. Downloaded {finished_files}/{total_files} files."
|
373
|
+
)
|
374
|
+
|
375
|
+
|
376
|
+
def get_remote_file_list(
|
377
|
+
remote_connection: RemoteConnection, remote_folder: str, exclude_patterns=None
|
378
|
+
) -> List[str]:
|
379
|
+
"""Get a list of all files in the remote directory recursively, applying exclusion patterns."""
|
380
|
+
exclude_patterns = exclude_patterns or []
|
381
|
+
|
382
|
+
# Build SSH command to find all files recursively
|
383
|
+
ssh_cmd = ["ssh", "-o", "PasswordAuthentication=no"]
|
384
|
+
|
385
|
+
# Handle non-standard SSH port
|
386
|
+
if remote_connection.port != 22:
|
387
|
+
ssh_cmd.extend(["-p", str(remote_connection.port)])
|
388
|
+
|
389
|
+
ssh_cmd.extend(
|
390
|
+
[
|
391
|
+
f"{remote_connection.username}@{remote_connection.host}",
|
392
|
+
f"find '{remote_folder}' -type f",
|
393
|
+
]
|
394
|
+
)
|
395
|
+
|
396
|
+
try:
|
397
|
+
result = subprocess.run(
|
398
|
+
ssh_cmd, capture_output=True, text=True, check=True, timeout=60
|
399
|
+
)
|
400
|
+
|
401
|
+
all_files = result.stdout.strip().splitlines()
|
402
|
+
|
403
|
+
# Filter out excluded files
|
404
|
+
filtered_files = []
|
405
|
+
for file_path in all_files:
|
406
|
+
if not is_excluded(file_path, exclude_patterns):
|
407
|
+
filtered_files.append(file_path.strip())
|
408
|
+
|
409
|
+
return filtered_files
|
410
|
+
|
411
|
+
except subprocess.CalledProcessError as e:
|
412
|
+
if e.returncode == 255: # SSH protocol errors
|
413
|
+
handle_ssh_subprocess_error(e, remote_connection)
|
414
|
+
return []
|
415
|
+
else:
|
416
|
+
logger.error(f"Error getting file list: {e.stderr}")
|
417
|
+
return []
|
418
|
+
except subprocess.TimeoutExpired:
|
419
|
+
logger.error(f"Timeout getting file list from: {remote_folder}")
|
420
|
+
return []
|
421
|
+
except Exception as e:
|
422
|
+
logger.error(f"Error getting file list: {e}")
|
423
|
+
return []
|
424
|
+
|
425
|
+
|
426
|
+
def get_remote_directory_list(
|
427
|
+
remote_connection: RemoteConnection, remote_folder: str, exclude_patterns=None
|
428
|
+
) -> List[str]:
|
429
|
+
"""Get a list of all directories in the remote directory recursively, applying exclusion patterns."""
|
430
|
+
exclude_patterns = exclude_patterns or []
|
431
|
+
|
432
|
+
# Build SSH command to find all directories recursively
|
433
|
+
ssh_cmd = ["ssh", "-o", "PasswordAuthentication=no"]
|
434
|
+
|
435
|
+
# Handle non-standard SSH port
|
436
|
+
if remote_connection.port != 22:
|
437
|
+
ssh_cmd.extend(["-p", str(remote_connection.port)])
|
438
|
+
|
439
|
+
ssh_cmd.extend(
|
440
|
+
[
|
441
|
+
f"{remote_connection.username}@{remote_connection.host}",
|
442
|
+
f"find '{remote_folder}' -type d",
|
443
|
+
]
|
444
|
+
)
|
445
|
+
|
446
|
+
try:
|
447
|
+
result = subprocess.run(
|
448
|
+
ssh_cmd, capture_output=True, text=True, check=True, timeout=60
|
449
|
+
)
|
450
|
+
|
451
|
+
all_dirs = result.stdout.strip().splitlines()
|
452
|
+
|
453
|
+
# Filter out excluded directories
|
454
|
+
filtered_dirs = []
|
455
|
+
for dir_path in all_dirs:
|
456
|
+
if not is_excluded(dir_path, exclude_patterns):
|
457
|
+
filtered_dirs.append(dir_path.strip())
|
458
|
+
|
459
|
+
return filtered_dirs
|
460
|
+
|
461
|
+
except subprocess.CalledProcessError as e:
|
462
|
+
if e.returncode == 255: # SSH protocol errors
|
463
|
+
handle_ssh_subprocess_error(e, remote_connection)
|
464
|
+
return []
|
465
|
+
else:
|
466
|
+
logger.error(f"Error getting directory list: {e.stderr}")
|
467
|
+
return []
|
468
|
+
except subprocess.TimeoutExpired:
|
469
|
+
logger.error(f"Timeout getting directory list from: {remote_folder}")
|
470
|
+
return []
|
471
|
+
except Exception as e:
|
472
|
+
logger.error(f"Error getting directory list: {e}")
|
473
|
+
return []
|
474
|
+
|
475
|
+
|
476
|
+
def download_single_file_sftp(
|
477
|
+
remote_connection: RemoteConnection, remote_file: str, local_file: Path
|
478
|
+
):
|
479
|
+
"""Download a single file using SFTP."""
|
480
|
+
# Ensure local directory exists
|
481
|
+
local_file.parent.mkdir(parents=True, exist_ok=True)
|
482
|
+
|
483
|
+
# Build SFTP command
|
484
|
+
sftp_cmd = ["sftp", "-o", "PasswordAuthentication=no"]
|
485
|
+
|
486
|
+
# Handle non-standard SSH port
|
487
|
+
if remote_connection.port != 22:
|
488
|
+
sftp_cmd.extend(["-P", str(remote_connection.port)])
|
489
|
+
|
490
|
+
# Add batch mode and other options
|
491
|
+
sftp_cmd.extend(
|
492
|
+
[
|
493
|
+
"-b",
|
494
|
+
"-", # Read commands from stdin
|
495
|
+
f"{remote_connection.username}@{remote_connection.host}",
|
496
|
+
]
|
497
|
+
)
|
498
|
+
|
499
|
+
# SFTP commands to execute
|
500
|
+
sftp_commands = f"get '{remote_file}' '{local_file}'\nquit\n"
|
501
|
+
|
502
|
+
try:
|
503
|
+
result = subprocess.run(
|
504
|
+
sftp_cmd,
|
505
|
+
input=sftp_commands,
|
506
|
+
capture_output=True,
|
507
|
+
text=True,
|
508
|
+
check=True,
|
509
|
+
timeout=300, # 5 minute timeout per file
|
510
|
+
)
|
511
|
+
|
512
|
+
logger.debug(f"Downloaded: {remote_file} -> {local_file}")
|
276
513
|
|
277
|
-
except
|
278
|
-
|
279
|
-
|
514
|
+
except subprocess.CalledProcessError as e:
|
515
|
+
if e.returncode == 255: # SSH protocol errors
|
516
|
+
handle_ssh_subprocess_error(e, remote_connection)
|
517
|
+
else:
|
518
|
+
logger.error(f"Error downloading file {remote_file}: {e.stderr}")
|
519
|
+
raise RuntimeError(f"Failed to download {remote_file}")
|
520
|
+
except subprocess.TimeoutExpired:
|
521
|
+
logger.error(f"Timeout downloading file: {remote_file}")
|
522
|
+
raise RuntimeError(f"Timeout downloading {remote_file}")
|
523
|
+
except Exception as e:
|
524
|
+
logger.error(f"Error downloading file {remote_file}: {e}")
|
525
|
+
raise RuntimeError(f"Failed to download {remote_file}")
|
280
526
|
|
281
527
|
|
282
528
|
def get_remote_profiler_folder_from_config_path(
|
283
|
-
|
529
|
+
remote_connection: RemoteConnection, config_path: str
|
284
530
|
) -> RemoteReportFolder:
|
285
531
|
"""Read a remote config file and return RemoteFolder object."""
|
286
|
-
|
287
|
-
|
288
|
-
|
532
|
+
try:
|
533
|
+
# Build SSH command to get file modification time
|
534
|
+
stat_cmd = [
|
535
|
+
"ssh",
|
536
|
+
"-o",
|
537
|
+
"PasswordAuthentication=no",
|
538
|
+
f"{remote_connection.username}@{remote_connection.host}",
|
539
|
+
]
|
540
|
+
|
541
|
+
# Handle non-standard SSH port
|
542
|
+
if remote_connection.port != 22:
|
543
|
+
stat_cmd.extend(["-p", str(remote_connection.port)])
|
544
|
+
|
545
|
+
# Get modification time using stat command
|
546
|
+
stat_cmd.append(f"stat -c %Y '{config_path}' 2>/dev/null")
|
547
|
+
|
548
|
+
stat_result = subprocess.run(
|
549
|
+
stat_cmd, capture_output=True, text=True, check=True
|
550
|
+
)
|
551
|
+
|
552
|
+
last_modified = int(float(stat_result.stdout.strip()))
|
289
553
|
|
554
|
+
# Build SSH command to read file content
|
555
|
+
cat_cmd = [
|
556
|
+
"ssh",
|
557
|
+
"-o",
|
558
|
+
"PasswordAuthentication=no",
|
559
|
+
f"{remote_connection.username}@{remote_connection.host}",
|
560
|
+
]
|
561
|
+
|
562
|
+
if remote_connection.port != 22:
|
563
|
+
cat_cmd.extend(["-p", str(remote_connection.port)])
|
564
|
+
|
565
|
+
# Read file content using cat command
|
566
|
+
cat_cmd.append(f"cat '{config_path}'")
|
567
|
+
|
568
|
+
cat_result = subprocess.run(cat_cmd, capture_output=True, text=True, check=True)
|
569
|
+
|
570
|
+
# Parse JSON data
|
571
|
+
data = json.loads(cat_result.stdout)
|
290
572
|
report_name = data.get("report_name")
|
291
573
|
logger.info(f"********* report_name: {report_name}")
|
292
574
|
|
293
575
|
return RemoteReportFolder(
|
294
576
|
remotePath=str(Path(config_path).parent),
|
295
577
|
reportName=report_name,
|
296
|
-
lastModified=
|
297
|
-
|
298
|
-
|
578
|
+
lastModified=last_modified,
|
579
|
+
)
|
580
|
+
|
581
|
+
except subprocess.CalledProcessError as e:
|
582
|
+
logger.error(f"SSH command failed while reading config: {e}")
|
583
|
+
logger.error(f"stderr: {e.stderr}")
|
584
|
+
|
585
|
+
# Check if it's an SSH-specific error (authentication, connection, etc.)
|
586
|
+
if e.returncode == 255: # SSH returns 255 for SSH protocol errors
|
587
|
+
handle_ssh_subprocess_error(e, remote_connection)
|
588
|
+
# This line never executes as handle_ssh_subprocess_error raises an exception
|
589
|
+
return RemoteReportFolder(
|
590
|
+
remotePath=str(Path(config_path).parent),
|
591
|
+
reportName="",
|
592
|
+
lastModified=int(time.time()),
|
593
|
+
)
|
594
|
+
else:
|
595
|
+
# Fall back to current time if we can't get modification time
|
596
|
+
return RemoteReportFolder(
|
597
|
+
remotePath=str(Path(config_path).parent),
|
598
|
+
reportName="",
|
599
|
+
lastModified=int(time.time()),
|
600
|
+
)
|
601
|
+
except (json.JSONDecodeError, ValueError) as e:
|
602
|
+
logger.error(f"Error parsing config file {config_path}: {e}")
|
603
|
+
# Fall back to current time and no report name
|
604
|
+
return RemoteReportFolder(
|
605
|
+
remotePath=str(Path(config_path).parent),
|
606
|
+
reportName="",
|
607
|
+
lastModified=int(time.time()),
|
299
608
|
)
|
300
609
|
|
301
610
|
|
302
611
|
def get_remote_performance_folder(
|
303
|
-
|
612
|
+
remote_connection: RemoteConnection, profile_folder: str
|
304
613
|
) -> RemoteReportFolder:
|
305
|
-
"""
|
306
|
-
attributes = sftp.stat(str(profile_folder))
|
614
|
+
"""Get remote performance folder info and return RemoteFolder object."""
|
307
615
|
performance_name = profile_folder.split("/")[-1]
|
308
616
|
remote_path = profile_folder
|
309
|
-
|
310
|
-
|
311
|
-
|
617
|
+
|
618
|
+
# Get modification time using subprocess SSH command
|
619
|
+
try:
|
620
|
+
ssh_command = ["ssh", "-o", "PasswordAuthentication=no"]
|
621
|
+
if remote_connection.port != 22:
|
622
|
+
ssh_command.extend(["-p", str(remote_connection.port)])
|
623
|
+
ssh_command.extend(
|
624
|
+
[
|
625
|
+
f"{remote_connection.username}@{remote_connection.host}",
|
626
|
+
f"stat -c %Y '{profile_folder}'",
|
627
|
+
]
|
628
|
+
)
|
629
|
+
|
630
|
+
result = subprocess.run(ssh_command, capture_output=True, text=True, timeout=30)
|
631
|
+
|
632
|
+
if result.returncode == 0:
|
633
|
+
last_modified = int(result.stdout.strip())
|
634
|
+
else:
|
635
|
+
# If stat fails, handle SSH errors
|
636
|
+
if result.returncode == 255:
|
637
|
+
handle_ssh_subprocess_error(
|
638
|
+
subprocess.CalledProcessError(
|
639
|
+
result.returncode, ssh_command, result.stdout, result.stderr
|
640
|
+
),
|
641
|
+
remote_connection,
|
642
|
+
)
|
643
|
+
logger.warning(
|
644
|
+
f"Could not get modification time for {profile_folder}, using current time"
|
645
|
+
)
|
646
|
+
last_modified = int(time.time())
|
647
|
+
except (subprocess.TimeoutExpired, subprocess.CalledProcessError, ValueError) as e:
|
648
|
+
logger.warning(
|
649
|
+
f"Error getting modification time for {profile_folder}: {e}, using current time"
|
650
|
+
)
|
651
|
+
last_modified = int(time.time())
|
652
|
+
|
312
653
|
return RemoteReportFolder(
|
313
654
|
remotePath=str(remote_path),
|
314
655
|
reportName=str(performance_name),
|
@@ -321,37 +662,49 @@ def read_remote_file(
|
|
321
662
|
remote_connection,
|
322
663
|
remote_path=None,
|
323
664
|
):
|
324
|
-
"""Read a remote file."""
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
else:
|
330
|
-
path = Path(remote_connection.profilerPath)
|
665
|
+
"""Read a remote file using SSH cat command."""
|
666
|
+
if remote_path:
|
667
|
+
path = Path(remote_path)
|
668
|
+
else:
|
669
|
+
path = Path(remote_connection.profilerPath)
|
331
670
|
|
332
|
-
|
333
|
-
directory_path = str(path.parent)
|
334
|
-
file_name = str(path.name)
|
671
|
+
logger.info(f"Reading remote file {path}")
|
335
672
|
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
673
|
+
# Build SSH command to read the file
|
674
|
+
ssh_cmd = ["ssh", "-o", "PasswordAuthentication=no"]
|
675
|
+
|
676
|
+
# Handle non-standard SSH port
|
677
|
+
if remote_connection.port != 22:
|
678
|
+
ssh_cmd.extend(["-p", str(remote_connection.port)])
|
679
|
+
|
680
|
+
ssh_cmd.extend(
|
681
|
+
[f"{remote_connection.username}@{remote_connection.host}", f"cat '{path}'"]
|
682
|
+
)
|
683
|
+
|
684
|
+
try:
|
685
|
+
result = subprocess.run(ssh_cmd, capture_output=True, check=True, timeout=30)
|
686
|
+
return result.stdout
|
687
|
+
except subprocess.CalledProcessError as e:
|
688
|
+
if e.returncode == 255: # SSH protocol errors
|
689
|
+
handle_ssh_subprocess_error(e, remote_connection)
|
343
690
|
return None
|
344
|
-
|
345
|
-
|
691
|
+
else:
|
692
|
+
# File not found or other command error
|
693
|
+
logger.error(f"File not found or cannot be read: {path}")
|
346
694
|
return None
|
695
|
+
except subprocess.TimeoutExpired:
|
696
|
+
logger.error(f"Timeout reading remote file: {path}")
|
697
|
+
return None
|
698
|
+
except Exception as e:
|
699
|
+
logger.error(f"Error reading remote file {path}: {e}")
|
700
|
+
return None
|
347
701
|
|
348
702
|
|
349
703
|
@remote_exception_handler
|
350
704
|
def check_remote_path_for_reports(remote_connection):
|
351
705
|
"""Check the remote path for config files."""
|
352
|
-
ssh_client = get_client(remote_connection)
|
353
706
|
remote_config_paths = find_folders_by_files(
|
354
|
-
|
707
|
+
remote_connection, remote_connection.profilerPath, [TEST_CONFIG_FILE]
|
355
708
|
)
|
356
709
|
if not remote_config_paths:
|
357
710
|
raise NoProjectsException(
|
@@ -362,42 +715,122 @@ def check_remote_path_for_reports(remote_connection):
|
|
362
715
|
|
363
716
|
@remote_exception_handler
|
364
717
|
def check_remote_path_exists(remote_connection: RemoteConnection, path_key: str):
|
365
|
-
|
366
|
-
|
367
|
-
|
718
|
+
"""Check if a remote path exists using SSH test command."""
|
719
|
+
path = getattr(remote_connection, path_key)
|
720
|
+
|
721
|
+
# Build SSH command to test if path exists
|
722
|
+
ssh_cmd = ["ssh", "-o", "PasswordAuthentication=no"]
|
723
|
+
|
724
|
+
# Handle non-standard SSH port
|
725
|
+
if remote_connection.port != 22:
|
726
|
+
ssh_cmd.extend(["-p", str(remote_connection.port)])
|
727
|
+
|
728
|
+
ssh_cmd.extend(
|
729
|
+
[f"{remote_connection.username}@{remote_connection.host}", f"test -d '{path}'"]
|
730
|
+
)
|
731
|
+
|
368
732
|
try:
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
733
|
+
result = subprocess.run(ssh_cmd, capture_output=True, check=True, timeout=10)
|
734
|
+
# If command succeeds, directory exists
|
735
|
+
return True
|
736
|
+
except subprocess.CalledProcessError as e:
|
737
|
+
if e.returncode == 255: # SSH protocol errors
|
738
|
+
handle_ssh_subprocess_error(e, remote_connection)
|
374
739
|
else:
|
375
|
-
|
376
|
-
|
377
|
-
|
740
|
+
# Directory does not exist or is inaccessible
|
741
|
+
if path_key == "performancePath":
|
742
|
+
message = "Performance directory does not exist or cannot be accessed"
|
743
|
+
else:
|
744
|
+
message = "Profiler directory does not exist or cannot be accessed"
|
745
|
+
|
746
|
+
logger.error(message)
|
747
|
+
raise RemoteConnectionException(
|
748
|
+
message=message, status=ConnectionTestStates.FAILED
|
749
|
+
)
|
750
|
+
except subprocess.TimeoutExpired:
|
751
|
+
logger.error(f"Timeout checking remote path: {path}")
|
378
752
|
raise RemoteConnectionException(
|
379
|
-
message=
|
753
|
+
message=f"Timeout checking remote path: {path}",
|
754
|
+
status=ConnectionTestStates.FAILED,
|
380
755
|
)
|
381
756
|
|
382
757
|
|
383
758
|
def find_folders_by_files(
|
384
|
-
|
759
|
+
remote_connection: RemoteConnection, root_folder: str, file_names: List[str]
|
385
760
|
) -> List[str]:
|
386
761
|
"""Given a remote path, return a list of top-level folders that contain any of the specified files."""
|
387
762
|
matched_folders: List[str] = []
|
388
|
-
with ssh_client.open_sftp() as sftp:
|
389
|
-
all_files = sftp.listdir_attr(root_folder)
|
390
|
-
top_level_directories = filter(lambda e: S_ISDIR(e.st_mode), all_files)
|
391
763
|
|
392
|
-
|
393
|
-
|
394
|
-
directory_files = sftp.listdir(str(dirname))
|
764
|
+
# Build SSH command to find directories in root_folder
|
765
|
+
ssh_cmd = ["ssh", "-o", "PasswordAuthentication=no"]
|
395
766
|
|
396
|
-
|
397
|
-
|
398
|
-
|
767
|
+
# Handle non-standard SSH port
|
768
|
+
if remote_connection.port != 22:
|
769
|
+
ssh_cmd.extend(["-p", str(remote_connection.port)])
|
399
770
|
|
400
|
-
|
771
|
+
ssh_cmd.extend(
|
772
|
+
[
|
773
|
+
f"{remote_connection.username}@{remote_connection.host}",
|
774
|
+
f"find '{root_folder}' -maxdepth 1 -type d -not -path '{root_folder}'",
|
775
|
+
]
|
776
|
+
)
|
777
|
+
|
778
|
+
try:
|
779
|
+
result = subprocess.run(
|
780
|
+
ssh_cmd, capture_output=True, text=True, check=True, timeout=30
|
781
|
+
)
|
782
|
+
|
783
|
+
directories = result.stdout.strip().splitlines()
|
784
|
+
|
785
|
+
# For each directory, check if it contains any of the specified files
|
786
|
+
for directory in directories:
|
787
|
+
directory = directory.strip()
|
788
|
+
if not directory:
|
789
|
+
continue
|
790
|
+
|
791
|
+
# Build SSH command to check for files in this directory
|
792
|
+
file_checks = []
|
793
|
+
for file_name in file_names:
|
794
|
+
file_checks.append(f"test -f '{directory}/{file_name}'")
|
795
|
+
|
796
|
+
# Use OR logic to check if any of the files exist
|
797
|
+
check_cmd = ["ssh", "-o", "PasswordAuthentication=no"]
|
798
|
+
if remote_connection.port != 22:
|
799
|
+
check_cmd.extend(["-p", str(remote_connection.port)])
|
800
|
+
|
801
|
+
check_cmd.extend(
|
802
|
+
[
|
803
|
+
f"{remote_connection.username}@{remote_connection.host}",
|
804
|
+
f"({' || '.join(file_checks)})",
|
805
|
+
]
|
806
|
+
)
|
807
|
+
|
808
|
+
try:
|
809
|
+
check_result = subprocess.run(
|
810
|
+
check_cmd, capture_output=True, check=True, timeout=10
|
811
|
+
)
|
812
|
+
# If command succeeds, at least one file exists
|
813
|
+
matched_folders.append(directory)
|
814
|
+
except subprocess.CalledProcessError:
|
815
|
+
# None of the files exist in this directory, skip it
|
816
|
+
continue
|
817
|
+
|
818
|
+
return matched_folders
|
819
|
+
|
820
|
+
except subprocess.CalledProcessError as e:
|
821
|
+
if e.returncode == 255: # SSH protocol errors
|
822
|
+
handle_ssh_subprocess_error(e, remote_connection)
|
823
|
+
# This line should never be reached as handle_ssh_subprocess_error raises an exception
|
824
|
+
return []
|
825
|
+
else:
|
826
|
+
logger.error(f"Error finding folders: {e.stderr}")
|
827
|
+
return []
|
828
|
+
except subprocess.TimeoutExpired:
|
829
|
+
logger.error(f"Timeout finding folders in: {root_folder}")
|
830
|
+
return []
|
831
|
+
except Exception as e:
|
832
|
+
logger.error(f"Error finding folders: {e}")
|
833
|
+
return []
|
401
834
|
|
402
835
|
|
403
836
|
@remote_exception_handler
|
@@ -405,19 +838,24 @@ def get_remote_performance_folders(
|
|
405
838
|
remote_connection: RemoteConnection,
|
406
839
|
) -> List[RemoteReportFolder]:
|
407
840
|
"""Return a list of remote folders containing a profile_log_device file."""
|
408
|
-
|
841
|
+
if remote_connection.performancePath is None:
|
842
|
+
error = "Performance path is not configured for this connection"
|
843
|
+
logger.error(error)
|
844
|
+
raise NoProjectsException(status=ConnectionTestStates.FAILED, message=error)
|
845
|
+
|
409
846
|
performance_paths = find_folders_by_files(
|
410
|
-
|
847
|
+
remote_connection, remote_connection.performancePath, [TEST_PROFILER_FILE]
|
411
848
|
)
|
412
849
|
if not performance_paths:
|
413
850
|
error = f"No profiler paths found at {remote_connection.performancePath}"
|
414
851
|
logger.info(error)
|
415
852
|
raise NoProjectsException(status=ConnectionTestStates.FAILED, message=error)
|
416
853
|
remote_folder_data = []
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
854
|
+
for path in performance_paths:
|
855
|
+
remote_folder_data.append(
|
856
|
+
get_remote_performance_folder(remote_connection, path)
|
857
|
+
)
|
858
|
+
return remote_folder_data
|
421
859
|
|
422
860
|
|
423
861
|
@remote_exception_handler
|
@@ -425,21 +863,19 @@ def get_remote_profiler_folders(
|
|
425
863
|
remote_connection: RemoteConnection,
|
426
864
|
) -> List[RemoteReportFolder]:
|
427
865
|
"""Return a list of remote folders containing a config.json file."""
|
428
|
-
client = get_client(remote_connection)
|
429
866
|
remote_config_paths = find_folders_by_files(
|
430
|
-
|
867
|
+
remote_connection, remote_connection.profilerPath, [TEST_CONFIG_FILE]
|
431
868
|
)
|
432
869
|
if not remote_config_paths:
|
433
870
|
error = f"No projects found at {remote_connection.profilerPath}"
|
434
871
|
logger.info(error)
|
435
872
|
raise NoProjectsException(status=ConnectionTestStates.FAILED, message=error)
|
436
873
|
remote_folder_data = []
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
remote_folder_data.append(remote_folder)
|
874
|
+
for config_path in remote_config_paths:
|
875
|
+
remote_folder = get_remote_profiler_folder_from_config_path(
|
876
|
+
remote_connection, str(Path(config_path).joinpath(TEST_CONFIG_FILE))
|
877
|
+
)
|
878
|
+
remote_folder_data.append(remote_folder)
|
443
879
|
return remote_folder_data
|
444
880
|
|
445
881
|
|
@@ -452,15 +888,18 @@ def sync_remote_profiler_folders(
|
|
452
888
|
sid=None,
|
453
889
|
):
|
454
890
|
"""Main function to sync test folders, handles both compressed and individual syncs."""
|
455
|
-
client = get_client(remote_connection)
|
456
891
|
profiler_folder = Path(remote_folder_path).name
|
457
892
|
destination_dir = Path(
|
458
|
-
REPORT_DATA_DIRECTORY,
|
893
|
+
REPORT_DATA_DIRECTORY,
|
894
|
+
path_prefix,
|
895
|
+
remote_connection.host,
|
896
|
+
current_app.config["PROFILER_DIRECTORY_NAME"],
|
897
|
+
profiler_folder,
|
459
898
|
)
|
460
899
|
destination_dir.mkdir(parents=True, exist_ok=True)
|
461
900
|
|
462
901
|
sync_files_and_directories(
|
463
|
-
|
902
|
+
remote_connection, remote_folder_path, destination_dir, exclude_patterns, sid
|
464
903
|
)
|
465
904
|
|
466
905
|
|
@@ -472,7 +911,6 @@ def sync_remote_performance_folders(
|
|
472
911
|
exclude_patterns: Optional[List[str]] = None,
|
473
912
|
sid=None,
|
474
913
|
):
|
475
|
-
client = get_client(remote_connection)
|
476
914
|
remote_folder_path = profile.remotePath
|
477
915
|
profile_folder = Path(remote_folder_path).name
|
478
916
|
destination_dir = Path(
|
@@ -483,7 +921,6 @@ def sync_remote_performance_folders(
|
|
483
921
|
profile_folder,
|
484
922
|
)
|
485
923
|
destination_dir.mkdir(parents=True, exist_ok=True)
|
486
|
-
|
487
924
|
sync_files_and_directories(
|
488
|
-
|
925
|
+
remote_connection, remote_folder_path, destination_dir, exclude_patterns, sid
|
489
926
|
)
|