fractal-server 2.8.1__py3-none-any.whl → 2.9.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.
- fractal_server/__init__.py +1 -1
- fractal_server/app/db/__init__.py +2 -35
- fractal_server/app/models/v2/__init__.py +3 -3
- fractal_server/app/models/v2/task.py +0 -72
- fractal_server/app/models/v2/task_group.py +113 -0
- fractal_server/app/routes/admin/v1.py +13 -30
- fractal_server/app/routes/admin/v2/__init__.py +4 -0
- fractal_server/app/routes/admin/v2/job.py +13 -24
- fractal_server/app/routes/admin/v2/task.py +13 -0
- fractal_server/app/routes/admin/v2/task_group.py +75 -14
- fractal_server/app/routes/admin/v2/task_group_lifecycle.py +267 -0
- fractal_server/app/routes/api/v1/project.py +7 -19
- fractal_server/app/routes/api/v2/__init__.py +11 -2
- fractal_server/app/routes/api/v2/{_aux_functions_task_collection.py → _aux_functions_task_lifecycle.py} +83 -0
- fractal_server/app/routes/api/v2/_aux_functions_tasks.py +27 -17
- fractal_server/app/routes/api/v2/submit.py +19 -24
- fractal_server/app/routes/api/v2/task_collection.py +33 -65
- fractal_server/app/routes/api/v2/task_collection_custom.py +3 -3
- fractal_server/app/routes/api/v2/task_group.py +86 -14
- fractal_server/app/routes/api/v2/task_group_lifecycle.py +272 -0
- fractal_server/app/routes/api/v2/workflow.py +1 -1
- fractal_server/app/routes/api/v2/workflow_import.py +2 -2
- fractal_server/app/routes/auth/current_user.py +60 -17
- fractal_server/app/routes/auth/group.py +67 -39
- fractal_server/app/routes/auth/users.py +97 -99
- fractal_server/app/routes/aux/__init__.py +20 -0
- fractal_server/app/runner/executors/slurm/_slurm_config.py +0 -17
- fractal_server/app/runner/executors/slurm/ssh/executor.py +49 -204
- fractal_server/app/runner/executors/slurm/sudo/executor.py +26 -109
- fractal_server/app/runner/executors/slurm/utils_executors.py +58 -0
- fractal_server/app/runner/v2/_local_experimental/executor.py +2 -1
- fractal_server/app/schemas/_validators.py +0 -15
- fractal_server/app/schemas/user.py +16 -10
- fractal_server/app/schemas/user_group.py +0 -11
- fractal_server/app/schemas/v1/applyworkflow.py +0 -8
- fractal_server/app/schemas/v1/dataset.py +0 -5
- fractal_server/app/schemas/v1/project.py +0 -5
- fractal_server/app/schemas/v1/state.py +0 -5
- fractal_server/app/schemas/v1/workflow.py +0 -5
- fractal_server/app/schemas/v2/__init__.py +4 -2
- fractal_server/app/schemas/v2/dataset.py +0 -6
- fractal_server/app/schemas/v2/job.py +0 -8
- fractal_server/app/schemas/v2/project.py +0 -5
- fractal_server/app/schemas/v2/task_collection.py +0 -21
- fractal_server/app/schemas/v2/task_group.py +59 -8
- fractal_server/app/schemas/v2/workflow.py +0 -5
- fractal_server/app/security/__init__.py +17 -0
- fractal_server/config.py +61 -59
- fractal_server/migrations/versions/d256a7379ab8_taskgroup_activity_and_venv_info_to_.py +117 -0
- fractal_server/ssh/_fabric.py +156 -83
- fractal_server/tasks/utils.py +2 -12
- fractal_server/tasks/v2/local/__init__.py +3 -0
- fractal_server/tasks/v2/local/_utils.py +70 -0
- fractal_server/tasks/v2/local/collect.py +291 -0
- fractal_server/tasks/v2/local/deactivate.py +218 -0
- fractal_server/tasks/v2/local/reactivate.py +159 -0
- fractal_server/tasks/v2/ssh/__init__.py +3 -0
- fractal_server/tasks/v2/ssh/_utils.py +87 -0
- fractal_server/tasks/v2/ssh/collect.py +311 -0
- fractal_server/tasks/v2/ssh/deactivate.py +253 -0
- fractal_server/tasks/v2/ssh/reactivate.py +202 -0
- fractal_server/tasks/v2/templates/{_2_preliminary_pip_operations.sh → 1_create_venv.sh} +6 -7
- fractal_server/tasks/v2/templates/{_3_pip_install.sh → 2_pip_install.sh} +8 -1
- fractal_server/tasks/v2/templates/{_4_pip_freeze.sh → 3_pip_freeze.sh} +0 -7
- fractal_server/tasks/v2/templates/{_5_pip_show.sh → 4_pip_show.sh} +5 -6
- fractal_server/tasks/v2/templates/5_get_venv_size_and_file_number.sh +10 -0
- fractal_server/tasks/v2/templates/6_pip_install_from_freeze.sh +35 -0
- fractal_server/tasks/v2/utils_background.py +42 -127
- fractal_server/tasks/v2/utils_templates.py +32 -2
- fractal_server/utils.py +4 -2
- fractal_server/zip_tools.py +21 -4
- {fractal_server-2.8.1.dist-info → fractal_server-2.9.0.dist-info}/METADATA +3 -5
- {fractal_server-2.8.1.dist-info → fractal_server-2.9.0.dist-info}/RECORD +77 -64
- fractal_server/app/models/v2/collection_state.py +0 -22
- fractal_server/tasks/v2/collection_local.py +0 -357
- fractal_server/tasks/v2/collection_ssh.py +0 -352
- fractal_server/tasks/v2/templates/_1_create_venv.sh +0 -42
- /fractal_server/tasks/v2/{database_operations.py → utils_database.py} +0 -0
- {fractal_server-2.8.1.dist-info → fractal_server-2.9.0.dist-info}/LICENSE +0 -0
- {fractal_server-2.8.1.dist-info → fractal_server-2.9.0.dist-info}/WHEEL +0 -0
- {fractal_server-2.8.1.dist-info → fractal_server-2.9.0.dist-info}/entry_points.txt +0 -0
fractal_server/ssh/_fabric.py
CHANGED
@@ -11,7 +11,6 @@ from typing import Optional
|
|
11
11
|
|
12
12
|
import paramiko.sftp_client
|
13
13
|
from fabric import Connection
|
14
|
-
from fabric import Result
|
15
14
|
from invoke import UnexpectedExit
|
16
15
|
from paramiko.ssh_exception import NoValidConnectionsError
|
17
16
|
|
@@ -116,50 +115,34 @@ class FractalSSH(object):
|
|
116
115
|
def logger(self) -> logging.Logger:
|
117
116
|
return get_logger(self.logger_name)
|
118
117
|
|
119
|
-
def
|
120
|
-
self,
|
121
|
-
*,
|
122
|
-
local: str,
|
123
|
-
remote: str,
|
124
|
-
label: str,
|
125
|
-
lock_timeout: Optional[float] = None,
|
126
|
-
) -> Result:
|
127
|
-
"""
|
128
|
-
Transfer a local file to a remote path, via SFTP.
|
118
|
+
def log_and_raise(self, *, e: Exception, message: str) -> None:
|
129
119
|
"""
|
130
|
-
|
131
|
-
if lock_timeout is not None:
|
132
|
-
actual_lock_timeout = lock_timeout
|
133
|
-
with _acquire_lock_with_timeout(
|
134
|
-
lock=self._lock,
|
135
|
-
label=label,
|
136
|
-
timeout=actual_lock_timeout,
|
137
|
-
):
|
138
|
-
return self._sftp_unsafe().put(local, remote)
|
120
|
+
Log and re-raise an exception from a FractalSSH method.
|
139
121
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
max_concurrent_prefetch_requests=self.sftp_get_max_requests,
|
122
|
+
Arguments:
|
123
|
+
message: Additional message to be logged.
|
124
|
+
e: Original exception
|
125
|
+
"""
|
126
|
+
try:
|
127
|
+
self.logger.error(message)
|
128
|
+
self.logger.error(f"Original Error {type(e)} : \n{str(e)}")
|
129
|
+
# Handle the specific case of `NoValidConnectionsError`s from
|
130
|
+
# paramiko, which store relevant information in the `errors`
|
131
|
+
# attribute
|
132
|
+
if hasattr(e, "errors"):
|
133
|
+
self.logger.error(f"{type(e)=}")
|
134
|
+
for err in e.errors:
|
135
|
+
self.logger.error(f"{err}")
|
136
|
+
except Exception as exception:
|
137
|
+
# Handle unexpected cases, e.g. (1) `e` has no `type`, or
|
138
|
+
# (2) `errors` is not iterable.
|
139
|
+
self.logger.error(
|
140
|
+
"Unexpected Error while handling exception above: "
|
141
|
+
f"{str(exception)}"
|
161
142
|
)
|
162
143
|
|
144
|
+
raise e
|
145
|
+
|
163
146
|
def _run(
|
164
147
|
self, *args, label: str, lock_timeout: Optional[float] = None, **kwargs
|
165
148
|
) -> Any:
|
@@ -187,8 +170,17 @@ class FractalSSH(object):
|
|
187
170
|
label="read_remote_json_file",
|
188
171
|
timeout=self.default_lock_timeout,
|
189
172
|
):
|
190
|
-
|
191
|
-
|
173
|
+
|
174
|
+
try:
|
175
|
+
with self._sftp_unsafe().open(filepath, "r") as f:
|
176
|
+
data = json.load(f)
|
177
|
+
except Exception as e:
|
178
|
+
self.log_and_raise(
|
179
|
+
e=e,
|
180
|
+
message=(
|
181
|
+
f"Error in `read_remote_json_file`, for {filepath=}."
|
182
|
+
),
|
183
|
+
)
|
192
184
|
self.logger.info(f"END reading remote JSON file {filepath}.")
|
193
185
|
return data
|
194
186
|
|
@@ -196,22 +188,56 @@ class FractalSSH(object):
|
|
196
188
|
"""
|
197
189
|
Open the SSH connection and handle exceptions.
|
198
190
|
|
199
|
-
This
|
200
|
-
|
201
|
-
|
191
|
+
This method should always be called at the beginning of background
|
192
|
+
operations that use FractalSSH, so that:
|
193
|
+
|
194
|
+
1. We try to restore unusable connections (e.g. due to closed socket).
|
195
|
+
2. We provide an informative error if connection cannot be established.
|
202
196
|
"""
|
203
|
-
|
197
|
+
self.logger.debug(
|
198
|
+
f"[check_connection] {self._connection.is_connected=}"
|
199
|
+
)
|
200
|
+
if self._connection.is_connected:
|
201
|
+
# Even if the connection appears open, it could be broken for
|
202
|
+
# external reasons (e.g. the socket is closed because the SSH
|
203
|
+
# server was restarted). In these cases, we catch the error and
|
204
|
+
# try to re-open the connection.
|
204
205
|
try:
|
205
|
-
|
206
|
-
|
207
|
-
label="_connection.open",
|
208
|
-
timeout=self.default_lock_timeout,
|
209
|
-
):
|
210
|
-
self._connection.open()
|
211
|
-
except Exception as e:
|
212
|
-
raise RuntimeError(
|
213
|
-
f"Cannot open SSH connection. Original error:\n{str(e)}"
|
206
|
+
self.logger.info(
|
207
|
+
"[check_connection] Run dummy command to check connection."
|
214
208
|
)
|
209
|
+
# Run both an SFTP and an SSH command, as they correspond to
|
210
|
+
# different sockets
|
211
|
+
self.remote_exists("/dummy/path/")
|
212
|
+
self.run_command(cmd="whoami")
|
213
|
+
self.logger.info(
|
214
|
+
"[check_connection] SSH connection is already OK, exit."
|
215
|
+
)
|
216
|
+
return
|
217
|
+
except (OSError, EOFError) as e:
|
218
|
+
self.logger.warning(
|
219
|
+
f"[check_connection] Detected error {str(e)}, re-open."
|
220
|
+
)
|
221
|
+
# Try opening the connection (if it was closed) or to re-open it (if
|
222
|
+
# an error happened).
|
223
|
+
try:
|
224
|
+
self.close()
|
225
|
+
with _acquire_lock_with_timeout(
|
226
|
+
lock=self._lock,
|
227
|
+
label="_connection.open",
|
228
|
+
timeout=self.default_lock_timeout,
|
229
|
+
logger_name=self.logger_name,
|
230
|
+
):
|
231
|
+
self._connection.open()
|
232
|
+
self._connection.client.open_sftp()
|
233
|
+
self.logger.info(
|
234
|
+
"[check_connection] SSH connection opened, exit."
|
235
|
+
)
|
236
|
+
|
237
|
+
except Exception as e:
|
238
|
+
raise RuntimeError(
|
239
|
+
f"Cannot open SSH connection. Original error:\n{str(e)}"
|
240
|
+
)
|
215
241
|
|
216
242
|
def close(self) -> None:
|
217
243
|
"""
|
@@ -228,9 +254,8 @@ class FractalSSH(object):
|
|
228
254
|
timeout=self.default_lock_timeout,
|
229
255
|
):
|
230
256
|
self._connection.close()
|
231
|
-
|
232
|
-
|
233
|
-
self._connection.client.close()
|
257
|
+
if self._connection.client is not None:
|
258
|
+
self._connection.client.close()
|
234
259
|
|
235
260
|
def run_command(
|
236
261
|
self,
|
@@ -288,8 +313,10 @@ class FractalSSH(object):
|
|
288
313
|
f"{prefix} END running '{cmd}' over SSH, "
|
289
314
|
f"elapsed {t_1-t_0:.3f}"
|
290
315
|
)
|
291
|
-
self.logger.debug(
|
292
|
-
self.logger.debug(
|
316
|
+
self.logger.debug("STDOUT:")
|
317
|
+
self.logger.debug(res.stdout)
|
318
|
+
self.logger.debug("STDERR:")
|
319
|
+
self.logger.debug(res.stderr)
|
293
320
|
return res.stdout
|
294
321
|
except NoValidConnectionsError as e:
|
295
322
|
# Case 2: Command fails with a connection error
|
@@ -345,21 +372,29 @@ class FractalSSH(object):
|
|
345
372
|
logger_name: Name of the logger
|
346
373
|
"""
|
347
374
|
try:
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
375
|
+
self.logger.info(
|
376
|
+
f"[send_file] START transfer of '{local}' over SSH."
|
377
|
+
)
|
378
|
+
actual_lock_timeout = self.default_lock_timeout
|
379
|
+
if lock_timeout is not None:
|
380
|
+
actual_lock_timeout = lock_timeout
|
381
|
+
with _acquire_lock_with_timeout(
|
382
|
+
lock=self._lock,
|
354
383
|
label=f"send_file {local=} {remote=}",
|
384
|
+
timeout=actual_lock_timeout,
|
385
|
+
):
|
386
|
+
self._sftp_unsafe().put(local, remote)
|
387
|
+
self.logger.info(
|
388
|
+
f"[send_file] END transfer of '{local}' over SSH."
|
355
389
|
)
|
356
|
-
self.logger.info(f"{prefix} END transfer of '{local}' over SSH.")
|
357
390
|
except Exception as e:
|
358
|
-
self.
|
359
|
-
|
360
|
-
|
391
|
+
self.log_and_raise(
|
392
|
+
e=e,
|
393
|
+
message=(
|
394
|
+
"Error in `send_file`, while "
|
395
|
+
f"transferring {local=} to {remote=}."
|
396
|
+
),
|
361
397
|
)
|
362
|
-
raise e
|
363
398
|
|
364
399
|
def fetch_file(
|
365
400
|
self,
|
@@ -380,19 +415,29 @@ class FractalSSH(object):
|
|
380
415
|
try:
|
381
416
|
prefix = "[fetch_file] "
|
382
417
|
self.logger.info(f"{prefix} START fetching '{remote}' over SSH.")
|
383
|
-
self.
|
384
|
-
|
385
|
-
|
386
|
-
|
418
|
+
actual_lock_timeout = self.default_lock_timeout
|
419
|
+
if lock_timeout is not None:
|
420
|
+
actual_lock_timeout = lock_timeout
|
421
|
+
with _acquire_lock_with_timeout(
|
422
|
+
lock=self._lock,
|
387
423
|
label=f"fetch_file {local=} {remote=}",
|
388
|
-
|
424
|
+
timeout=actual_lock_timeout,
|
425
|
+
):
|
426
|
+
self._sftp_unsafe().get(
|
427
|
+
remote,
|
428
|
+
local,
|
429
|
+
prefetch=self.sftp_get_prefetch,
|
430
|
+
max_concurrent_prefetch_requests=self.sftp_get_max_requests, # noqa E501
|
431
|
+
)
|
389
432
|
self.logger.info(f"{prefix} END fetching '{remote}' over SSH.")
|
390
433
|
except Exception as e:
|
391
|
-
self.
|
392
|
-
|
393
|
-
|
434
|
+
self.log_and_raise(
|
435
|
+
e=e,
|
436
|
+
message=(
|
437
|
+
"Error in `fetch_file`, while "
|
438
|
+
f"Transferring {remote=} to {local=}."
|
439
|
+
),
|
394
440
|
)
|
395
|
-
raise e
|
396
441
|
|
397
442
|
def mkdir(self, *, folder: str, parents: bool = True) -> None:
|
398
443
|
"""
|
@@ -467,10 +512,38 @@ class FractalSSH(object):
|
|
467
512
|
label=f"write_remote_file {path=}",
|
468
513
|
timeout=actual_lock_timeout,
|
469
514
|
):
|
470
|
-
|
471
|
-
|
515
|
+
try:
|
516
|
+
with self._sftp_unsafe().open(filename=path, mode="w") as f:
|
517
|
+
f.write(content)
|
518
|
+
except Exception as e:
|
519
|
+
self.log_and_raise(
|
520
|
+
e=e, message=f"Error in `write_remote_file`, for {path=}."
|
521
|
+
)
|
522
|
+
|
472
523
|
self.logger.info(f"END writing to remote file {path}.")
|
473
524
|
|
525
|
+
def remote_exists(self, path: str) -> bool:
|
526
|
+
"""
|
527
|
+
Return whether a remote file/folder exists
|
528
|
+
"""
|
529
|
+
self.logger.info(f"START remote_file_exists {path}")
|
530
|
+
with _acquire_lock_with_timeout(
|
531
|
+
lock=self._lock,
|
532
|
+
label=f"remote_file_exists {path=}",
|
533
|
+
timeout=self.default_lock_timeout,
|
534
|
+
):
|
535
|
+
try:
|
536
|
+
self._sftp_unsafe().stat(path)
|
537
|
+
self.logger.info(f"END remote_file_exists {path} / True")
|
538
|
+
return True
|
539
|
+
except FileNotFoundError:
|
540
|
+
self.logger.info(f"END remote_file_exists {path} / False")
|
541
|
+
return False
|
542
|
+
except Exception as e:
|
543
|
+
self.log_and_raise(
|
544
|
+
e=e, message=f"Error in `remote_exists`, for {path=}."
|
545
|
+
)
|
546
|
+
|
474
547
|
|
475
548
|
class FractalSSHList(object):
|
476
549
|
"""
|
fractal_server/tasks/utils.py
CHANGED
@@ -30,19 +30,9 @@ def get_log_path(base: Path) -> Path:
|
|
30
30
|
return base / COLLECTION_LOG_FILENAME
|
31
31
|
|
32
32
|
|
33
|
-
def get_freeze_path(base: Path) -> Path:
|
34
|
-
return base / COLLECTION_FREEZE_FILENAME
|
35
|
-
|
36
|
-
|
37
33
|
def get_collection_log_v1(path: Path) -> str:
|
38
34
|
package_path = get_absolute_venv_path_v1(path)
|
39
35
|
log_path = get_log_path(package_path)
|
40
|
-
|
36
|
+
with log_path.open("r") as f:
|
37
|
+
log = f.read()
|
41
38
|
return log
|
42
|
-
|
43
|
-
|
44
|
-
def get_collection_freeze_v1(venv_path: Path) -> str:
|
45
|
-
package_path = get_absolute_venv_path_v1(venv_path)
|
46
|
-
freeze_path = get_freeze_path(package_path)
|
47
|
-
freeze = freeze_path.open().read()
|
48
|
-
return freeze
|
@@ -0,0 +1,70 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
|
3
|
+
from fractal_server.app.schemas.v2 import TaskCreateV2
|
4
|
+
from fractal_server.logger import get_logger
|
5
|
+
from fractal_server.tasks.v2.utils_templates import customize_template
|
6
|
+
from fractal_server.utils import execute_command_sync
|
7
|
+
|
8
|
+
|
9
|
+
def _customize_and_run_template(
|
10
|
+
template_filename: str,
|
11
|
+
replacements: list[tuple[str, str]],
|
12
|
+
script_dir: str,
|
13
|
+
logger_name: str,
|
14
|
+
prefix: int,
|
15
|
+
) -> str:
|
16
|
+
"""
|
17
|
+
Customize one of the template bash scripts.
|
18
|
+
|
19
|
+
Args:
|
20
|
+
template_filename: Filename of the template file (ends with ".sh").
|
21
|
+
replacements: Dictionary of replacements.
|
22
|
+
script_dir: Local folder where the script will be placed.
|
23
|
+
prefix: Prefix for the script filename.
|
24
|
+
"""
|
25
|
+
logger = get_logger(logger_name=logger_name)
|
26
|
+
logger.debug(f"_customize_and_run_template {template_filename} - START")
|
27
|
+
|
28
|
+
# Prepare name and path of script
|
29
|
+
if not template_filename.endswith(".sh"):
|
30
|
+
raise ValueError(
|
31
|
+
f"Invalid {template_filename=} (it must end with '.sh')."
|
32
|
+
)
|
33
|
+
|
34
|
+
script_filename = f"{prefix}{template_filename}"
|
35
|
+
script_path_local = Path(script_dir) / script_filename
|
36
|
+
# Read template
|
37
|
+
customize_template(
|
38
|
+
template_name=template_filename,
|
39
|
+
replacements=replacements,
|
40
|
+
script_path=script_path_local,
|
41
|
+
)
|
42
|
+
cmd = f"bash {script_path_local}"
|
43
|
+
logger.debug(f"Now run '{cmd}' ")
|
44
|
+
stdout = execute_command_sync(command=cmd, logger_name=logger_name)
|
45
|
+
logger.debug(f"_customize_and_run_template {template_filename} - END")
|
46
|
+
return stdout
|
47
|
+
|
48
|
+
|
49
|
+
def check_task_files_exist(task_list: list[TaskCreateV2]) -> None:
|
50
|
+
"""
|
51
|
+
Check that the modules listed in task commands point to existing files.
|
52
|
+
|
53
|
+
Args:
|
54
|
+
task_list:
|
55
|
+
"""
|
56
|
+
for _task in task_list:
|
57
|
+
if _task.command_non_parallel is not None:
|
58
|
+
_task_path = _task.command_non_parallel.split()[1]
|
59
|
+
if not Path(_task_path).exists():
|
60
|
+
raise FileNotFoundError(
|
61
|
+
f"Task `{_task.name}` has `command_non_parallel` "
|
62
|
+
f"pointing to missing file `{_task_path}`."
|
63
|
+
)
|
64
|
+
if _task.command_parallel is not None:
|
65
|
+
_task_path = _task.command_parallel.split()[1]
|
66
|
+
if not Path(_task_path).exists():
|
67
|
+
raise FileNotFoundError(
|
68
|
+
f"Task `{_task.name}` has `command_parallel` "
|
69
|
+
f"pointing to missing file `{_task_path}`."
|
70
|
+
)
|