fractal-server 2.5.1__py3-none-any.whl → 2.6.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/__main__.py +24 -9
- fractal_server/app/models/__init__.py +1 -0
- fractal_server/app/models/security.py +8 -0
- fractal_server/app/models/user_settings.py +38 -0
- fractal_server/app/routes/api/v1/_aux_functions.py +6 -1
- fractal_server/app/routes/api/v1/project.py +11 -24
- fractal_server/app/routes/api/v1/task.py +12 -9
- fractal_server/app/routes/api/v2/_aux_functions.py +6 -1
- fractal_server/app/routes/api/v2/submit.py +29 -21
- fractal_server/app/routes/api/v2/task.py +12 -9
- fractal_server/app/routes/api/v2/task_collection.py +17 -2
- fractal_server/app/routes/api/v2/task_collection_custom.py +6 -1
- fractal_server/app/routes/auth/_aux_auth.py +5 -5
- fractal_server/app/routes/auth/current_user.py +41 -0
- fractal_server/app/routes/auth/users.py +42 -0
- fractal_server/app/routes/aux/validate_user_settings.py +74 -0
- fractal_server/app/runner/executors/slurm/ssh/executor.py +24 -4
- fractal_server/app/runner/executors/slurm/sudo/executor.py +6 -2
- fractal_server/app/runner/v2/__init__.py +5 -7
- fractal_server/app/schemas/__init__.py +2 -0
- fractal_server/app/schemas/user.py +1 -62
- fractal_server/app/schemas/user_settings.py +94 -0
- fractal_server/app/schemas/v2/task_collection.py +5 -4
- fractal_server/app/security/__init__.py +22 -9
- fractal_server/app/user_settings.py +42 -0
- fractal_server/config.py +0 -16
- fractal_server/data_migrations/2_6_0.py +49 -0
- fractal_server/data_migrations/tools.py +17 -0
- fractal_server/main.py +12 -10
- fractal_server/migrations/versions/9c5ae74c9b98_add_user_settings_table.py +74 -0
- fractal_server/ssh/_fabric.py +193 -48
- fractal_server/string_tools.py +2 -0
- fractal_server/tasks/v2/background_operations_ssh.py +14 -5
- {fractal_server-2.5.1.dist-info → fractal_server-2.6.0.dist-info}/METADATA +1 -1
- {fractal_server-2.5.1.dist-info → fractal_server-2.6.0.dist-info}/RECORD +39 -33
- fractal_server/data_migrations/2_4_0.py +0 -61
- {fractal_server-2.5.1.dist-info → fractal_server-2.6.0.dist-info}/LICENSE +0 -0
- {fractal_server-2.5.1.dist-info → fractal_server-2.6.0.dist-info}/WHEEL +0 -0
- {fractal_server-2.5.1.dist-info → fractal_server-2.6.0.dist-info}/entry_points.txt +0 -0
fractal_server/main.py
CHANGED
@@ -92,32 +92,34 @@ async def lifespan(app: FastAPI):
|
|
92
92
|
settings = Inject(get_settings)
|
93
93
|
|
94
94
|
if settings.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
|
95
|
-
from fractal_server.ssh._fabric import get_ssh_connection
|
96
|
-
from fractal_server.ssh._fabric import FractalSSH
|
97
95
|
|
98
|
-
|
99
|
-
|
96
|
+
from fractal_server.ssh._fabric import FractalSSHList
|
97
|
+
|
98
|
+
app.state.fractal_ssh_list = FractalSSHList()
|
99
|
+
|
100
100
|
logger.info(
|
101
|
-
|
102
|
-
f"({app.state.
|
101
|
+
"Added empty FractalSSHList to app.state "
|
102
|
+
f"(id={id(app.state.fractal_ssh_list)})."
|
103
103
|
)
|
104
104
|
else:
|
105
|
-
app.state.
|
105
|
+
app.state.fractal_ssh_list = None
|
106
106
|
|
107
107
|
config_uvicorn_loggers()
|
108
108
|
logger.info("End application startup")
|
109
109
|
reset_logger_handlers(logger)
|
110
|
+
|
110
111
|
yield
|
112
|
+
|
111
113
|
logger = get_logger("fractal_server.lifespan")
|
112
114
|
logger.info("Start application shutdown")
|
113
115
|
|
114
116
|
if settings.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
|
115
117
|
logger.info(
|
116
|
-
|
117
|
-
f"(current: {app.state.
|
118
|
+
"Close FractalSSH connections "
|
119
|
+
f"(current size: {app.state.fractal_ssh_list.size})."
|
118
120
|
)
|
119
121
|
|
120
|
-
app.state.
|
122
|
+
app.state.fractal_ssh_list.close_all()
|
121
123
|
|
122
124
|
logger.info(
|
123
125
|
f"Current worker with pid {os.getpid()} is shutting down. "
|
@@ -0,0 +1,74 @@
|
|
1
|
+
"""Add user_settings table
|
2
|
+
|
3
|
+
Revision ID: 9c5ae74c9b98
|
4
|
+
Revises: d9a140db5d42
|
5
|
+
Create Date: 2024-09-24 12:01:13.393326
|
6
|
+
|
7
|
+
"""
|
8
|
+
import sqlalchemy as sa
|
9
|
+
import sqlmodel
|
10
|
+
from alembic import op
|
11
|
+
|
12
|
+
|
13
|
+
# revision identifiers, used by Alembic.
|
14
|
+
revision = "9c5ae74c9b98"
|
15
|
+
down_revision = "d9a140db5d42"
|
16
|
+
branch_labels = None
|
17
|
+
depends_on = None
|
18
|
+
|
19
|
+
# Manually define constraint name, see issue #1777
|
20
|
+
CONSTRAINT_NAME = "fk_user_oauth_user_settings_id_user_settings"
|
21
|
+
|
22
|
+
|
23
|
+
def upgrade() -> None:
|
24
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
25
|
+
op.create_table(
|
26
|
+
"user_settings",
|
27
|
+
sa.Column("id", sa.Integer(), nullable=False),
|
28
|
+
sa.Column(
|
29
|
+
"slurm_accounts", sa.JSON(), server_default="[]", nullable=False
|
30
|
+
),
|
31
|
+
sa.Column(
|
32
|
+
"ssh_host", sqlmodel.sql.sqltypes.AutoString(), nullable=True
|
33
|
+
),
|
34
|
+
sa.Column(
|
35
|
+
"ssh_username", sqlmodel.sql.sqltypes.AutoString(), nullable=True
|
36
|
+
),
|
37
|
+
sa.Column(
|
38
|
+
"ssh_private_key_path",
|
39
|
+
sqlmodel.sql.sqltypes.AutoString(),
|
40
|
+
nullable=True,
|
41
|
+
),
|
42
|
+
sa.Column(
|
43
|
+
"ssh_tasks_dir", sqlmodel.sql.sqltypes.AutoString(), nullable=True
|
44
|
+
),
|
45
|
+
sa.Column(
|
46
|
+
"ssh_jobs_dir", sqlmodel.sql.sqltypes.AutoString(), nullable=True
|
47
|
+
),
|
48
|
+
sa.Column(
|
49
|
+
"slurm_user", sqlmodel.sql.sqltypes.AutoString(), nullable=True
|
50
|
+
),
|
51
|
+
sa.Column(
|
52
|
+
"cache_dir", sqlmodel.sql.sqltypes.AutoString(), nullable=True
|
53
|
+
),
|
54
|
+
sa.PrimaryKeyConstraint("id"),
|
55
|
+
)
|
56
|
+
with op.batch_alter_table("user_oauth", schema=None) as batch_op:
|
57
|
+
batch_op.add_column(
|
58
|
+
sa.Column("user_settings_id", sa.Integer(), nullable=True)
|
59
|
+
)
|
60
|
+
batch_op.create_foreign_key(
|
61
|
+
CONSTRAINT_NAME, "user_settings", ["user_settings_id"], ["id"]
|
62
|
+
)
|
63
|
+
|
64
|
+
# ### end Alembic commands ###
|
65
|
+
|
66
|
+
|
67
|
+
def downgrade() -> None:
|
68
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
69
|
+
with op.batch_alter_table("user_oauth", schema=None) as batch_op:
|
70
|
+
batch_op.drop_constraint(CONSTRAINT_NAME, type_="foreignkey")
|
71
|
+
batch_op.drop_column("user_settings_id")
|
72
|
+
|
73
|
+
op.drop_table("user_settings")
|
74
|
+
# ### end Alembic commands ###
|
fractal_server/ssh/_fabric.py
CHANGED
@@ -16,20 +16,21 @@ from paramiko.ssh_exception import NoValidConnectionsError
|
|
16
16
|
|
17
17
|
from ..logger import get_logger
|
18
18
|
from ..logger import set_logger
|
19
|
-
from fractal_server.config import get_settings
|
20
19
|
from fractal_server.string_tools import validate_cmd
|
21
|
-
from fractal_server.syringe import Inject
|
22
20
|
|
23
21
|
|
24
22
|
class FractalSSHTimeoutError(RuntimeError):
|
25
23
|
pass
|
26
24
|
|
27
25
|
|
26
|
+
class FractalSSHListTimeoutError(RuntimeError):
|
27
|
+
pass
|
28
|
+
|
29
|
+
|
28
30
|
logger = set_logger(__name__)
|
29
31
|
|
30
32
|
|
31
33
|
class FractalSSH(object):
|
32
|
-
|
33
34
|
"""
|
34
35
|
FIXME SSH: Fix docstring
|
35
36
|
|
@@ -111,7 +112,6 @@ class FractalSSH(object):
|
|
111
112
|
def run(
|
112
113
|
self, *args, lock_timeout: Optional[float] = None, **kwargs
|
113
114
|
) -> Any:
|
114
|
-
|
115
115
|
actual_lock_timeout = self.default_lock_timeout
|
116
116
|
if lock_timeout is not None:
|
117
117
|
actual_lock_timeout = lock_timeout
|
@@ -138,7 +138,19 @@ class FractalSSH(object):
|
|
138
138
|
)
|
139
139
|
|
140
140
|
def close(self) -> None:
|
141
|
-
|
141
|
+
"""
|
142
|
+
Aggressively close `self._connection`.
|
143
|
+
|
144
|
+
When `Connection.is_connected` is `False`, `Connection.close()` does
|
145
|
+
not call `Connection.client.close()`. Thus we do this explicitly here,
|
146
|
+
because we observed cases where `is_connected=False` but the underlying
|
147
|
+
`Transport` object was not closed.
|
148
|
+
"""
|
149
|
+
|
150
|
+
self._connection.close()
|
151
|
+
|
152
|
+
if self._connection.client is not None:
|
153
|
+
self._connection.client.close()
|
142
154
|
|
143
155
|
def run_command(
|
144
156
|
self,
|
@@ -293,21 +305,21 @@ class FractalSSH(object):
|
|
293
305
|
safe_root: If `folder` is not a subfolder of the absolute
|
294
306
|
`safe_root` path, raise an error.
|
295
307
|
"""
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
308
|
+
validate_cmd(folder)
|
309
|
+
validate_cmd(safe_root)
|
310
|
+
|
311
|
+
if " " in folder:
|
312
|
+
raise ValueError(f"folder='{folder}' includes whitespace.")
|
313
|
+
elif " " in safe_root:
|
314
|
+
raise ValueError(f"safe_root='{safe_root}' includes whitespace.")
|
315
|
+
elif not Path(folder).is_absolute():
|
316
|
+
raise ValueError(f"{folder=} is not an absolute path.")
|
317
|
+
elif not Path(safe_root).is_absolute():
|
318
|
+
raise ValueError(f"{safe_root=} is not an absolute path.")
|
319
|
+
elif not (
|
320
|
+
Path(folder).resolve().is_relative_to(Path(safe_root).resolve())
|
306
321
|
):
|
307
|
-
raise ValueError(
|
308
|
-
f"{folder=} argument is invalid or it is not "
|
309
|
-
f"relative to {safe_root=}."
|
310
|
-
)
|
322
|
+
raise ValueError(f"{folder=} is not a subfolder of {safe_root=}.")
|
311
323
|
else:
|
312
324
|
cmd = f"rm -r {folder}"
|
313
325
|
self.run_command(cmd=cmd)
|
@@ -335,36 +347,169 @@ class FractalSSH(object):
|
|
335
347
|
f.write(content)
|
336
348
|
|
337
349
|
|
338
|
-
|
339
|
-
*,
|
340
|
-
host: Optional[str] = None,
|
341
|
-
user: Optional[str] = None,
|
342
|
-
key_filename: Optional[str] = None,
|
343
|
-
) -> Connection:
|
350
|
+
class FractalSSHList(object):
|
344
351
|
"""
|
345
|
-
|
346
|
-
or explicit arguments.
|
352
|
+
Collection of `FractalSSH` objects
|
347
353
|
|
348
|
-
|
349
|
-
|
350
|
-
user:
|
351
|
-
key_filename:
|
354
|
+
Attributes are all private, and access to this collection must be
|
355
|
+
through methods (mostly the `get` one).
|
352
356
|
|
353
|
-
|
354
|
-
|
357
|
+
Attributes:
|
358
|
+
_data:
|
359
|
+
Mapping of unique keys (the SSH-credentials tuples) to
|
360
|
+
`FractalSSH` objects.
|
361
|
+
_lock:
|
362
|
+
A `threading.Lock object`, to be acquired when changing `_data`.
|
363
|
+
_timeout: Timeout for `_lock` acquisition.
|
364
|
+
_logger_name: Logger name.
|
355
365
|
"""
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
366
|
+
|
367
|
+
_data: dict[tuple[str, str, str], FractalSSH]
|
368
|
+
_lock: Lock
|
369
|
+
_timeout: float
|
370
|
+
_logger_name: str
|
371
|
+
|
372
|
+
def __init__(
|
373
|
+
self,
|
374
|
+
*,
|
375
|
+
timeout: float = 5.0,
|
376
|
+
logger_name: str = "fractal_server.FractalSSHList",
|
377
|
+
):
|
378
|
+
self._lock = Lock()
|
379
|
+
self._data = {}
|
380
|
+
self._timeout = timeout
|
381
|
+
self._logger_name = logger_name
|
382
|
+
set_logger(self._logger_name)
|
383
|
+
|
384
|
+
@property
|
385
|
+
def logger(self) -> logging.Logger:
|
386
|
+
"""
|
387
|
+
This property exists so that we never have to propagate the
|
388
|
+
`Logger` object.
|
389
|
+
"""
|
390
|
+
return get_logger(self._logger_name)
|
391
|
+
|
392
|
+
@property
|
393
|
+
def size(self) -> int:
|
394
|
+
"""
|
395
|
+
Number of current key-value pairs in `self._data`.
|
396
|
+
"""
|
397
|
+
return len(self._data.values())
|
398
|
+
|
399
|
+
def get(self, *, host: str, user: str, key_path: str) -> FractalSSH:
|
400
|
+
"""
|
401
|
+
Get the `FractalSSH` for the current credentials, or create one.
|
402
|
+
|
403
|
+
Note: Changing `_data` requires acquiring `_lock`.
|
404
|
+
|
405
|
+
Arguments:
|
406
|
+
host:
|
407
|
+
user:
|
408
|
+
key_path:
|
409
|
+
"""
|
410
|
+
key = (host, user, key_path)
|
411
|
+
fractal_ssh = self._data.get(key, None)
|
412
|
+
if fractal_ssh is not None:
|
413
|
+
self.logger.info(
|
414
|
+
f"Return existing FractalSSH object for {user}@{host}"
|
415
|
+
)
|
416
|
+
return fractal_ssh
|
417
|
+
else:
|
418
|
+
self.logger.info(f"Add new FractalSSH object for {user}@{host}")
|
419
|
+
connection = Connection(
|
420
|
+
host=host,
|
421
|
+
user=user,
|
422
|
+
forward_agent=False,
|
423
|
+
connect_kwargs={
|
424
|
+
"key_filename": key_path,
|
425
|
+
"look_for_keys": False,
|
426
|
+
},
|
427
|
+
)
|
428
|
+
with self.acquire_lock_with_timeout():
|
429
|
+
self._data[key] = FractalSSH(connection=connection)
|
430
|
+
return self._data[key]
|
431
|
+
|
432
|
+
def contains(
|
433
|
+
self,
|
434
|
+
*,
|
435
|
+
host: str,
|
436
|
+
user: str,
|
437
|
+
key_path: str,
|
438
|
+
) -> bool:
|
439
|
+
"""
|
440
|
+
Return whether a given key is present in the collection.
|
441
|
+
|
442
|
+
Arguments:
|
443
|
+
host:
|
444
|
+
user:
|
445
|
+
key_path:
|
446
|
+
"""
|
447
|
+
key = (host, user, key_path)
|
448
|
+
return key in self._data.keys()
|
449
|
+
|
450
|
+
def remove(
|
451
|
+
self,
|
452
|
+
*,
|
453
|
+
host: str,
|
454
|
+
user: str,
|
455
|
+
key_path: str,
|
456
|
+
) -> None:
|
457
|
+
"""
|
458
|
+
Remove a key from `_data` and close the corresponding connection.
|
459
|
+
|
460
|
+
Note: Changing `_data` requires acquiring `_lock`.
|
461
|
+
|
462
|
+
Arguments:
|
463
|
+
host:
|
464
|
+
user:
|
465
|
+
key_path:
|
466
|
+
"""
|
467
|
+
key = (host, user, key_path)
|
468
|
+
with self.acquire_lock_with_timeout():
|
469
|
+
self.logger.info(
|
470
|
+
f"Removing FractalSSH object for {user}@{host} "
|
471
|
+
"from collection."
|
472
|
+
)
|
473
|
+
fractal_ssh_obj = self._data.pop(key)
|
474
|
+
self.logger.info(
|
475
|
+
f"Closing FractalSSH object for {user}@{host} "
|
476
|
+
f"({fractal_ssh_obj.is_connected=})."
|
477
|
+
)
|
478
|
+
fractal_ssh_obj.close()
|
479
|
+
|
480
|
+
def close_all(self, *, timeout: float = 5.0):
|
481
|
+
"""
|
482
|
+
Close all `FractalSSH` objects in the collection.
|
483
|
+
|
484
|
+
Arguments:
|
485
|
+
timeout:
|
486
|
+
Timeout for `FractalSSH._lock` acquisition, to be obtained
|
487
|
+
before closing.
|
488
|
+
"""
|
489
|
+
for key, fractal_ssh_obj in self._data.items():
|
490
|
+
host, user, _ = key[:]
|
491
|
+
self.logger.info(
|
492
|
+
f"Closing FractalSSH object for {user}@{host} "
|
493
|
+
f"({fractal_ssh_obj.is_connected=})."
|
494
|
+
)
|
495
|
+
with fractal_ssh_obj.acquire_timeout(timeout=timeout):
|
496
|
+
fractal_ssh_obj.close()
|
497
|
+
|
498
|
+
@contextmanager
|
499
|
+
def acquire_lock_with_timeout(self) -> Generator[Literal[True], Any, None]:
|
500
|
+
self.logger.debug(
|
501
|
+
f"Trying to acquire lock, with timeout {self._timeout} s"
|
502
|
+
)
|
503
|
+
result = self._lock.acquire(timeout=self._timeout)
|
504
|
+
try:
|
505
|
+
if not result:
|
506
|
+
self.logger.error("Lock was *NOT* acquired.")
|
507
|
+
raise FractalSSHListTimeoutError(
|
508
|
+
f"Failed to acquire lock within {self._timeout} ss"
|
509
|
+
)
|
510
|
+
self.logger.debug("Lock was acquired.")
|
511
|
+
yield result
|
512
|
+
finally:
|
513
|
+
if result:
|
514
|
+
self._lock.release()
|
515
|
+
self.logger.debug("Lock was released")
|
fractal_server/string_tools.py
CHANGED
@@ -60,6 +60,8 @@ def validate_cmd(command: str, allow_char: Optional[str] = None):
|
|
60
60
|
command: command to validate.
|
61
61
|
allow_char: chars to accept among the forbidden ones
|
62
62
|
"""
|
63
|
+
if not isinstance(command, str):
|
64
|
+
raise ValueError(f"{command=} is not a string.")
|
63
65
|
forbidden = set(__NOT_ALLOWED_FOR_COMMANDS__)
|
64
66
|
if allow_char is not None:
|
65
67
|
forbidden = forbidden - set(allow_char)
|
@@ -57,6 +57,7 @@ def _customize_and_run_template(
|
|
57
57
|
tmpdir: str,
|
58
58
|
logger_name: str,
|
59
59
|
fractal_ssh: FractalSSH,
|
60
|
+
tasks_base_dir: str,
|
60
61
|
) -> str:
|
61
62
|
"""
|
62
63
|
Customize one of the template bash scripts, transfer it to the remote host
|
@@ -72,7 +73,6 @@ def _customize_and_run_template(
|
|
72
73
|
"""
|
73
74
|
logger = get_logger(logger_name)
|
74
75
|
logger.debug(f"_customize_and_run_template {script_filename} - START")
|
75
|
-
settings = Inject(get_settings)
|
76
76
|
|
77
77
|
# Read template
|
78
78
|
template_path = templates_folder / script_filename
|
@@ -88,7 +88,7 @@ def _customize_and_run_template(
|
|
88
88
|
|
89
89
|
# Transfer script to remote host
|
90
90
|
script_path_remote = os.path.join(
|
91
|
-
|
91
|
+
tasks_base_dir,
|
92
92
|
f"script_{abs(hash(tmpdir))}{script_filename}",
|
93
93
|
)
|
94
94
|
logger.debug(f"Now transfer {script_path_local=} over SSH.")
|
@@ -111,6 +111,7 @@ def background_collect_pip_ssh(
|
|
111
111
|
state_id: int,
|
112
112
|
task_pkg: _TaskCollectPip,
|
113
113
|
fractal_ssh: FractalSSH,
|
114
|
+
tasks_base_dir: str,
|
114
115
|
) -> None:
|
115
116
|
"""
|
116
117
|
Collect a task package over SSH
|
@@ -121,6 +122,13 @@ def background_collect_pip_ssh(
|
|
121
122
|
NOTE: by making this function sync, it will run within a thread - due to
|
122
123
|
starlette/fastapi handling of background tasks (see
|
123
124
|
https://github.com/encode/starlette/blob/master/starlette/background.py).
|
125
|
+
|
126
|
+
|
127
|
+
Arguments:
|
128
|
+
state_id:
|
129
|
+
task_pkg:
|
130
|
+
fractal_ssh:
|
131
|
+
tasks_base_dir:
|
124
132
|
"""
|
125
133
|
|
126
134
|
# Work within a temporary folder, where also logs will be placed
|
@@ -140,7 +148,6 @@ def background_collect_pip_ssh(
|
|
140
148
|
with next(get_sync_db()) as db:
|
141
149
|
try:
|
142
150
|
# Prepare replacements for task-collection scripts
|
143
|
-
settings = Inject(get_settings)
|
144
151
|
python_bin = get_python_interpreter_v2(
|
145
152
|
python_version=task_pkg.python_version
|
146
153
|
)
|
@@ -163,11 +170,12 @@ def background_collect_pip_ssh(
|
|
163
170
|
f"{install_string}=={task_pkg.package_version}"
|
164
171
|
)
|
165
172
|
package_env_dir = (
|
166
|
-
Path(
|
173
|
+
Path(tasks_base_dir)
|
167
174
|
/ ".fractal"
|
168
175
|
/ f"{task_pkg.package_name}{package_version}"
|
169
176
|
).as_posix()
|
170
177
|
logger.debug(f"{package_env_dir=}")
|
178
|
+
settings = Inject(get_settings)
|
171
179
|
replacements = [
|
172
180
|
("__PACKAGE_NAME__", task_pkg.package_name),
|
173
181
|
("__PACKAGE_ENV_DIR__", package_env_dir),
|
@@ -186,6 +194,7 @@ def background_collect_pip_ssh(
|
|
186
194
|
tmpdir=tmpdir,
|
187
195
|
logger_name=LOGGER_NAME,
|
188
196
|
fractal_ssh=fractal_ssh,
|
197
|
+
tasks_base_dir=tasks_base_dir,
|
189
198
|
)
|
190
199
|
|
191
200
|
fractal_ssh.check_connection()
|
@@ -332,7 +341,7 @@ def background_collect_pip_ssh(
|
|
332
341
|
)
|
333
342
|
fractal_ssh.remove_folder(
|
334
343
|
folder=package_env_dir,
|
335
|
-
safe_root=
|
344
|
+
safe_root=tasks_base_dir,
|
336
345
|
)
|
337
346
|
logger.info(
|
338
347
|
f"Deleted remoted folder {package_env_dir}"
|