fractal-server 2.2.0a1__py3-none-any.whl → 2.3.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/models/v1/state.py +1 -2
- fractal_server/app/routes/admin/v1.py +2 -2
- fractal_server/app/routes/admin/v2.py +2 -2
- fractal_server/app/routes/api/v1/job.py +2 -2
- fractal_server/app/routes/api/v1/task_collection.py +4 -4
- fractal_server/app/routes/api/v2/__init__.py +23 -3
- fractal_server/app/routes/api/v2/job.py +2 -2
- fractal_server/app/routes/api/v2/submit.py +6 -0
- fractal_server/app/routes/api/v2/task_collection.py +74 -34
- fractal_server/app/routes/api/v2/task_collection_custom.py +170 -0
- fractal_server/app/routes/api/v2/task_collection_ssh.py +125 -0
- fractal_server/app/routes/aux/_runner.py +10 -2
- fractal_server/app/runner/compress_folder.py +120 -0
- fractal_server/app/runner/executors/slurm/__init__.py +0 -3
- fractal_server/app/runner/executors/slurm/_batching.py +0 -1
- fractal_server/app/runner/executors/slurm/_slurm_config.py +9 -9
- fractal_server/app/runner/executors/slurm/ssh/__init__.py +3 -0
- fractal_server/app/runner/executors/slurm/ssh/_executor_wait_thread.py +112 -0
- fractal_server/app/runner/executors/slurm/ssh/_slurm_job.py +120 -0
- fractal_server/app/runner/executors/slurm/ssh/executor.py +1488 -0
- fractal_server/app/runner/executors/slurm/sudo/__init__.py +3 -0
- fractal_server/app/runner/executors/slurm/{_check_jobs_status.py → sudo/_check_jobs_status.py} +1 -1
- fractal_server/app/runner/executors/slurm/{_executor_wait_thread.py → sudo/_executor_wait_thread.py} +1 -1
- fractal_server/app/runner/executors/slurm/{_subprocess_run_as_user.py → sudo/_subprocess_run_as_user.py} +1 -1
- fractal_server/app/runner/executors/slurm/{executor.py → sudo/executor.py} +12 -12
- fractal_server/app/runner/extract_archive.py +38 -0
- fractal_server/app/runner/v1/__init__.py +78 -40
- fractal_server/app/runner/v1/_slurm/__init__.py +1 -1
- fractal_server/app/runner/v2/__init__.py +147 -62
- fractal_server/app/runner/v2/_local_experimental/__init__.py +22 -12
- fractal_server/app/runner/v2/_local_experimental/executor.py +12 -8
- fractal_server/app/runner/v2/_slurm/__init__.py +1 -6
- fractal_server/app/runner/v2/_slurm_ssh/__init__.py +125 -0
- fractal_server/app/runner/v2/_slurm_ssh/_submit_setup.py +83 -0
- fractal_server/app/runner/v2/_slurm_ssh/get_slurm_config.py +182 -0
- fractal_server/app/runner/v2/runner_functions_low_level.py +9 -11
- fractal_server/app/runner/versions.py +30 -0
- fractal_server/app/schemas/v1/__init__.py +1 -0
- fractal_server/app/schemas/{state.py → v1/state.py} +4 -21
- fractal_server/app/schemas/v2/__init__.py +4 -1
- fractal_server/app/schemas/v2/task_collection.py +101 -30
- fractal_server/config.py +184 -3
- fractal_server/main.py +27 -1
- fractal_server/ssh/__init__.py +4 -0
- fractal_server/ssh/_fabric.py +245 -0
- fractal_server/tasks/utils.py +12 -64
- fractal_server/tasks/v1/background_operations.py +2 -2
- fractal_server/tasks/{endpoint_operations.py → v1/endpoint_operations.py} +7 -12
- fractal_server/tasks/v1/utils.py +67 -0
- fractal_server/tasks/v2/_TaskCollectPip.py +61 -32
- fractal_server/tasks/v2/_venv_pip.py +195 -0
- fractal_server/tasks/v2/background_operations.py +257 -295
- fractal_server/tasks/v2/background_operations_ssh.py +317 -0
- fractal_server/tasks/v2/endpoint_operations.py +136 -0
- fractal_server/tasks/v2/templates/_1_create_venv.sh +46 -0
- fractal_server/tasks/v2/templates/_2_upgrade_pip.sh +30 -0
- fractal_server/tasks/v2/templates/_3_pip_install.sh +32 -0
- fractal_server/tasks/v2/templates/_4_pip_freeze.sh +21 -0
- fractal_server/tasks/v2/templates/_5_pip_show.sh +59 -0
- fractal_server/tasks/v2/utils.py +54 -0
- {fractal_server-2.2.0a1.dist-info → fractal_server-2.3.0.dist-info}/METADATA +4 -2
- {fractal_server-2.2.0a1.dist-info → fractal_server-2.3.0.dist-info}/RECORD +66 -42
- fractal_server/tasks/v2/get_collection_data.py +0 -14
- {fractal_server-2.2.0a1.dist-info → fractal_server-2.3.0.dist-info}/LICENSE +0 -0
- {fractal_server-2.2.0a1.dist-info → fractal_server-2.3.0.dist-info}/WHEEL +0 -0
- {fractal_server-2.2.0a1.dist-info → fractal_server-2.3.0.dist-info}/entry_points.txt +0 -0
@@ -1,14 +1,26 @@
|
|
1
|
+
from datetime import datetime
|
2
|
+
from enum import Enum
|
1
3
|
from pathlib import Path
|
4
|
+
from typing import Any
|
2
5
|
from typing import Literal
|
3
6
|
from typing import Optional
|
4
7
|
|
5
8
|
from pydantic import BaseModel
|
6
|
-
from pydantic import
|
9
|
+
from pydantic import root_validator
|
7
10
|
from pydantic import validator
|
8
11
|
|
9
12
|
from .._validators import valdictkeys
|
10
13
|
from .._validators import valstr
|
11
|
-
from .
|
14
|
+
from fractal_server.app.schemas._validators import valutc
|
15
|
+
from fractal_server.app.schemas.v2 import ManifestV2
|
16
|
+
|
17
|
+
|
18
|
+
class CollectionStatusV2(str, Enum):
|
19
|
+
PENDING = "pending"
|
20
|
+
INSTALLING = "installing"
|
21
|
+
COLLECTING = "collecting"
|
22
|
+
FAIL = "fail"
|
23
|
+
OK = "OK"
|
12
24
|
|
13
25
|
|
14
26
|
class TaskCollectPipV2(BaseModel):
|
@@ -41,18 +53,19 @@ class TaskCollectPipV2(BaseModel):
|
|
41
53
|
package: str
|
42
54
|
package_version: Optional[str] = None
|
43
55
|
package_extras: Optional[str] = None
|
44
|
-
python_version: Optional[
|
56
|
+
python_version: Optional[Literal["3.9", "3.10", "3.11", "3.12"]] = None
|
45
57
|
pinned_package_versions: Optional[dict[str, str]] = None
|
46
58
|
|
59
|
+
_package = validator("package", allow_reuse=True)(valstr("package"))
|
60
|
+
_package_version = validator("package_version", allow_reuse=True)(
|
61
|
+
valstr("package_version")
|
62
|
+
)
|
47
63
|
_pinned_package_versions = validator(
|
48
64
|
"pinned_package_versions", allow_reuse=True
|
49
65
|
)(valdictkeys("pinned_package_versions"))
|
50
66
|
_package_extras = validator("package_extras", allow_reuse=True)(
|
51
67
|
valstr("package_extras")
|
52
68
|
)
|
53
|
-
_python_version = validator("python_version", allow_reuse=True)(
|
54
|
-
valstr("python_version")
|
55
|
-
)
|
56
69
|
|
57
70
|
@validator("package")
|
58
71
|
def package_validator(cls, value):
|
@@ -70,40 +83,98 @@ class TaskCollectPipV2(BaseModel):
|
|
70
83
|
|
71
84
|
@validator("package_version")
|
72
85
|
def package_version_validator(cls, v, values):
|
73
|
-
|
74
|
-
valstr("package_version")(v)
|
75
|
-
|
86
|
+
v = valstr("package_version")(v)
|
76
87
|
if values["package"].endswith(".whl"):
|
77
88
|
raise ValueError(
|
78
|
-
"Cannot provide version when package is a
|
89
|
+
"Cannot provide package version when package is a wheel file."
|
79
90
|
)
|
80
91
|
return v
|
81
92
|
|
82
93
|
|
83
|
-
class
|
94
|
+
class TaskCollectCustomV2(BaseModel):
|
84
95
|
"""
|
85
|
-
TaskCollectStatus class
|
86
|
-
|
87
96
|
Attributes:
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
97
|
+
manifest: Manifest of a Fractal task package (this is typically the
|
98
|
+
content of `__FRACTAL_MANIFEST__.json`).
|
99
|
+
python_interpreter: Absolute path to the Python interpreter to be used
|
100
|
+
for running tasks.
|
101
|
+
source: A common label identifying this package.
|
102
|
+
package_root: The folder where the package is installed.
|
103
|
+
If not provided, it will be extracted via `pip show`
|
104
|
+
(requires `package_name` to be set).
|
105
|
+
package_name: Name of the package, as used for `import <package_name>`;
|
106
|
+
this is then used to extract the package directory (`package_root`)
|
107
|
+
via `pip show <package_name>`.
|
108
|
+
version: Optional version of tasks to be collected.
|
94
109
|
"""
|
95
110
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
111
|
+
manifest: ManifestV2
|
112
|
+
python_interpreter: str
|
113
|
+
source: str
|
114
|
+
package_root: Optional[str]
|
115
|
+
package_name: Optional[str]
|
116
|
+
version: Optional[str]
|
117
|
+
|
118
|
+
# Valstr
|
119
|
+
_python_interpreter = validator("python_interpreter", allow_reuse=True)(
|
120
|
+
valstr("python_interpreter")
|
121
|
+
)
|
122
|
+
_source = validator("source", allow_reuse=True)(valstr("source"))
|
123
|
+
_package_root = validator("package_root", allow_reuse=True)(
|
124
|
+
valstr("package_root", accept_none=True)
|
125
|
+
)
|
126
|
+
_package_name = validator("package_name", allow_reuse=True)(
|
127
|
+
valstr("package_name", accept_none=True)
|
128
|
+
)
|
129
|
+
_version = validator("version", allow_reuse=True)(
|
130
|
+
valstr("version", accept_none=True)
|
131
|
+
)
|
132
|
+
|
133
|
+
@root_validator(pre=True)
|
134
|
+
def one_of_package_root_or_name(cls, values):
|
135
|
+
package_root = values.get("package_root")
|
136
|
+
package_name = values.get("package_name")
|
137
|
+
if (package_root is None and package_name is None) or (
|
138
|
+
package_root is not None and package_name is not None
|
139
|
+
):
|
140
|
+
raise ValueError(
|
141
|
+
"One and only one must be set between "
|
142
|
+
"'package_root' and 'package_name'"
|
143
|
+
)
|
144
|
+
return values
|
102
145
|
|
103
|
-
|
146
|
+
@validator("package_name")
|
147
|
+
def package_name_prevent_injection(cls, value: str):
|
104
148
|
"""
|
105
|
-
|
149
|
+
Remove all whitespace characters, and reject values containing `;`.
|
106
150
|
"""
|
107
|
-
|
108
|
-
|
109
|
-
|
151
|
+
if value is not None:
|
152
|
+
if ";" in value:
|
153
|
+
raise ValueError(f"Invalid package_name: {value}")
|
154
|
+
value = value.replace(" ", "")
|
155
|
+
return value
|
156
|
+
|
157
|
+
@validator("package_root")
|
158
|
+
def package_root_validator(cls, value):
|
159
|
+
if (value is not None) and (not Path(value).is_absolute()):
|
160
|
+
raise ValueError(
|
161
|
+
f"'package_root' must be an absolute path: (given {value})."
|
162
|
+
)
|
163
|
+
return value
|
164
|
+
|
165
|
+
@validator("python_interpreter")
|
166
|
+
def python_interpreter_validator(cls, value):
|
167
|
+
if not Path(value).is_absolute():
|
168
|
+
raise ValueError(
|
169
|
+
f"Python interpreter path must be absolute: (given {value})."
|
170
|
+
)
|
171
|
+
return value
|
172
|
+
|
173
|
+
|
174
|
+
class CollectionStateReadV2(BaseModel):
|
175
|
+
|
176
|
+
id: Optional[int]
|
177
|
+
data: dict[str, Any]
|
178
|
+
timestamp: datetime
|
179
|
+
|
180
|
+
_timestamp = validator("timestamp", allow_reuse=True)(valutc("timestamp"))
|
fractal_server/config.py
CHANGED
@@ -13,6 +13,7 @@
|
|
13
13
|
# Zurich.
|
14
14
|
import logging
|
15
15
|
import shutil
|
16
|
+
import sys
|
16
17
|
from os import environ
|
17
18
|
from os import getenv
|
18
19
|
from os.path import abspath
|
@@ -323,7 +324,10 @@ class Settings(BaseSettings):
|
|
323
324
|
return FRACTAL_RUNNER_WORKING_BASE_DIR_path
|
324
325
|
|
325
326
|
FRACTAL_RUNNER_BACKEND: Literal[
|
326
|
-
"local",
|
327
|
+
"local",
|
328
|
+
"local_experimental",
|
329
|
+
"slurm",
|
330
|
+
"slurm_ssh",
|
327
331
|
] = "local"
|
328
332
|
"""
|
329
333
|
Select which runner backend to use.
|
@@ -366,10 +370,126 @@ class Settings(BaseSettings):
|
|
366
370
|
|
367
371
|
FRACTAL_SLURM_WORKER_PYTHON: Optional[str] = None
|
368
372
|
"""
|
369
|
-
|
370
|
-
not specified, the same interpreter that runs the server is used.
|
373
|
+
Absolute path to Python interpreter that will run the jobs on the SLURM
|
374
|
+
nodes. If not specified, the same interpreter that runs the server is used.
|
371
375
|
"""
|
372
376
|
|
377
|
+
@validator("FRACTAL_SLURM_WORKER_PYTHON", always=True)
|
378
|
+
def absolute_FRACTAL_SLURM_WORKER_PYTHON(cls, v):
|
379
|
+
"""
|
380
|
+
If `FRACTAL_SLURM_WORKER_PYTHON` is a relative path, fail.
|
381
|
+
"""
|
382
|
+
if v is None:
|
383
|
+
return None
|
384
|
+
elif not Path(v).is_absolute():
|
385
|
+
raise FractalConfigurationError(
|
386
|
+
f"Non-absolute value for FRACTAL_SLURM_WORKER_PYTHON={v}"
|
387
|
+
)
|
388
|
+
else:
|
389
|
+
return v
|
390
|
+
|
391
|
+
FRACTAL_TASKS_PYTHON_DEFAULT_VERSION: Optional[
|
392
|
+
Literal["3.9", "3.10", "3.11", "3.12"]
|
393
|
+
] = None
|
394
|
+
"""
|
395
|
+
Default Python version to be used for task collection. Defaults to the
|
396
|
+
current version. Requires the corresponding variable (e.g
|
397
|
+
`FRACTAL_TASKS_PYTHON_3_10`) to be set.
|
398
|
+
"""
|
399
|
+
|
400
|
+
FRACTAL_TASKS_PYTHON_3_9: Optional[str] = None
|
401
|
+
"""
|
402
|
+
Absolute path to the Python 3.9 interpreter that serves as base for virtual
|
403
|
+
environments tasks. Note that this interpreter must have the `venv` module
|
404
|
+
installed. If set, this must be an absolute path. If the version specified
|
405
|
+
in `FRACTAL_TASKS_PYTHON_DEFAULT_VERSION` is `"3.9"` and this attribute is
|
406
|
+
unset, `sys.executable` is used as a default.
|
407
|
+
"""
|
408
|
+
|
409
|
+
FRACTAL_TASKS_PYTHON_3_10: Optional[str] = None
|
410
|
+
"""
|
411
|
+
Same as `FRACTAL_TASKS_PYTHON_3_9`, for Python 3.10.
|
412
|
+
"""
|
413
|
+
|
414
|
+
FRACTAL_TASKS_PYTHON_3_11: Optional[str] = None
|
415
|
+
"""
|
416
|
+
Same as `FRACTAL_TASKS_PYTHON_3_9`, for Python 3.11.
|
417
|
+
"""
|
418
|
+
|
419
|
+
FRACTAL_TASKS_PYTHON_3_12: Optional[str] = None
|
420
|
+
"""
|
421
|
+
Same as `FRACTAL_TASKS_PYTHON_3_9`, for Python 3.12.
|
422
|
+
"""
|
423
|
+
|
424
|
+
@root_validator(pre=True)
|
425
|
+
def check_tasks_python(cls, values) -> None:
|
426
|
+
"""
|
427
|
+
Perform multiple checks of the Python-intepreter variables.
|
428
|
+
|
429
|
+
1. Each `FRACTAL_TASKS_PYTHON_X_Y` variable must be an absolute path,
|
430
|
+
if set.
|
431
|
+
2. If `FRACTAL_TASKS_PYTHON_DEFAULT_VERSION` is unset, use
|
432
|
+
`sys.executable` and set the corresponding
|
433
|
+
`FRACTAL_TASKS_PYTHON_X_Y` (and unset all others).
|
434
|
+
"""
|
435
|
+
|
436
|
+
# `FRACTAL_TASKS_PYTHON_X_Y` variables can only be absolute paths
|
437
|
+
for version in ["3_9", "3_10", "3_11", "3_12"]:
|
438
|
+
key = f"FRACTAL_TASKS_PYTHON_{version}"
|
439
|
+
value = values.get(key)
|
440
|
+
if value is not None and not Path(value).is_absolute():
|
441
|
+
raise FractalConfigurationError(
|
442
|
+
f"Non-absolute value {key}={value}"
|
443
|
+
)
|
444
|
+
|
445
|
+
default_version = values.get("FRACTAL_TASKS_PYTHON_DEFAULT_VERSION")
|
446
|
+
|
447
|
+
if default_version is not None:
|
448
|
+
# "production/slurm" branch
|
449
|
+
# If a default version is set, then the corresponding interpreter
|
450
|
+
# must also be set
|
451
|
+
default_version_undescore = default_version.replace(".", "_")
|
452
|
+
key = f"FRACTAL_TASKS_PYTHON_{default_version_undescore}"
|
453
|
+
value = values.get(key)
|
454
|
+
if value is None:
|
455
|
+
msg = (
|
456
|
+
f"FRACTAL_TASKS_PYTHON_DEFAULT_VERSION={default_version} "
|
457
|
+
f"but {key}={value}."
|
458
|
+
)
|
459
|
+
logging.error(msg)
|
460
|
+
raise FractalConfigurationError(msg)
|
461
|
+
|
462
|
+
else:
|
463
|
+
# If no default version is set, then only `sys.executable` is made
|
464
|
+
# available
|
465
|
+
_info = sys.version_info
|
466
|
+
current_version = f"{_info.major}_{_info.minor}"
|
467
|
+
current_version_dot = f"{_info.major}.{_info.minor}"
|
468
|
+
values[
|
469
|
+
"FRACTAL_TASKS_PYTHON_DEFAULT_VERSION"
|
470
|
+
] = current_version_dot
|
471
|
+
logging.info(
|
472
|
+
"Setting FRACTAL_TASKS_PYTHON_DEFAULT_VERSION to "
|
473
|
+
f"{current_version_dot}"
|
474
|
+
)
|
475
|
+
|
476
|
+
# Unset all existing intepreters variable
|
477
|
+
for _version in ["3_9", "3_10", "3_11", "3_12"]:
|
478
|
+
key = f"FRACTAL_TASKS_PYTHON_{_version}"
|
479
|
+
if _version == current_version:
|
480
|
+
values[key] = sys.executable
|
481
|
+
logging.info(f"Setting {key} to {sys.executable}.")
|
482
|
+
else:
|
483
|
+
value = values.get(key)
|
484
|
+
if value is not None:
|
485
|
+
logging.info(
|
486
|
+
f"Setting {key} to None (given: {value}), "
|
487
|
+
"because FRACTAL_TASKS_PYTHON_DEFAULT_VERSION was "
|
488
|
+
"not set."
|
489
|
+
)
|
490
|
+
values[key] = None
|
491
|
+
return values
|
492
|
+
|
373
493
|
FRACTAL_SLURM_POLL_INTERVAL: int = 5
|
374
494
|
"""
|
375
495
|
Interval to wait (in seconds) before checking whether unfinished job are
|
@@ -392,6 +512,25 @@ class Settings(BaseSettings):
|
|
392
512
|
`JobExecutionError`.
|
393
513
|
"""
|
394
514
|
|
515
|
+
FRACTAL_SLURM_SSH_HOST: Optional[str] = None
|
516
|
+
"""
|
517
|
+
SSH-reachable host where a SLURM client is available.
|
518
|
+
"""
|
519
|
+
FRACTAL_SLURM_SSH_USER: Optional[str] = None
|
520
|
+
"""
|
521
|
+
User on `FRACTAL_SLURM_SSH_HOST`.
|
522
|
+
"""
|
523
|
+
FRACTAL_SLURM_SSH_PRIVATE_KEY_PATH: Optional[str] = None
|
524
|
+
"""
|
525
|
+
Private key for connecting to `FRACTAL_SLURM_SSH_HOST` as
|
526
|
+
`FRACTAL_SLURM_SSH_USER`.
|
527
|
+
"""
|
528
|
+
# FIXME SSH: Split this into two folders (for tasks and for jobs)
|
529
|
+
FRACTAL_SLURM_SSH_WORKING_BASE_DIR: Optional[str] = None
|
530
|
+
"""
|
531
|
+
Remote folder on `FRACTAL_SLURM_SSH_HOST`.
|
532
|
+
"""
|
533
|
+
|
395
534
|
FRACTAL_API_SUBMIT_RATE_LIMIT: int = 2
|
396
535
|
"""
|
397
536
|
Interval to wait (in seconds) to be allowed to call again
|
@@ -480,6 +619,48 @@ class Settings(BaseSettings):
|
|
480
619
|
raise FractalConfigurationError(
|
481
620
|
f"{info} but `squeue` command not found."
|
482
621
|
)
|
622
|
+
elif self.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
|
623
|
+
if self.FRACTAL_SLURM_WORKER_PYTHON is None:
|
624
|
+
raise FractalConfigurationError(
|
625
|
+
f"Must set FRACTAL_SLURM_WORKER_PYTHON when {info}"
|
626
|
+
)
|
627
|
+
if self.FRACTAL_SLURM_SSH_USER is None:
|
628
|
+
raise FractalConfigurationError(
|
629
|
+
f"Must set FRACTAL_SLURM_SSH_USER when {info}"
|
630
|
+
)
|
631
|
+
if self.FRACTAL_SLURM_SSH_HOST is None:
|
632
|
+
raise FractalConfigurationError(
|
633
|
+
f"Must set FRACTAL_SLURM_SSH_HOST when {info}"
|
634
|
+
)
|
635
|
+
if self.FRACTAL_SLURM_SSH_PRIVATE_KEY_PATH is None:
|
636
|
+
raise FractalConfigurationError(
|
637
|
+
f"Must set FRACTAL_SLURM_SSH_PRIVATE_KEY_PATH when {info}"
|
638
|
+
)
|
639
|
+
if self.FRACTAL_SLURM_SSH_WORKING_BASE_DIR is None:
|
640
|
+
raise FractalConfigurationError(
|
641
|
+
f"Must set FRACTAL_SLURM_SSH_WORKING_BASE_DIR when {info}"
|
642
|
+
)
|
643
|
+
|
644
|
+
from fractal_server.app.runner.executors.slurm._slurm_config import ( # noqa: E501
|
645
|
+
load_slurm_config_file,
|
646
|
+
)
|
647
|
+
|
648
|
+
if not self.FRACTAL_SLURM_CONFIG_FILE:
|
649
|
+
raise FractalConfigurationError(
|
650
|
+
f"Must set FRACTAL_SLURM_CONFIG_FILE when {info}"
|
651
|
+
)
|
652
|
+
else:
|
653
|
+
if not self.FRACTAL_SLURM_CONFIG_FILE.exists():
|
654
|
+
raise FractalConfigurationError(
|
655
|
+
f"{info} but FRACTAL_SLURM_CONFIG_FILE="
|
656
|
+
f"{self.FRACTAL_SLURM_CONFIG_FILE} not found."
|
657
|
+
)
|
658
|
+
|
659
|
+
load_slurm_config_file(self.FRACTAL_SLURM_CONFIG_FILE)
|
660
|
+
if not shutil.which("ssh"):
|
661
|
+
raise FractalConfigurationError(
|
662
|
+
f"{info} but `ssh` command not found."
|
663
|
+
)
|
483
664
|
else: # i.e. self.FRACTAL_RUNNER_BACKEND == "local"
|
484
665
|
if self.FRACTAL_LOCAL_CONFIG_FILE:
|
485
666
|
if not self.FRACTAL_LOCAL_CONFIG_FILE.exists():
|
fractal_server/main.py
CHANGED
@@ -20,6 +20,7 @@ from contextlib import asynccontextmanager
|
|
20
20
|
|
21
21
|
from fastapi import FastAPI
|
22
22
|
|
23
|
+
from .app.routes.aux._runner import _backend_supports_shutdown # FIXME: change
|
23
24
|
from .app.runner.shutdown import cleanup_after_shutdown
|
24
25
|
from .app.security import _create_first_user
|
25
26
|
from .config import get_settings
|
@@ -97,17 +98,40 @@ async def lifespan(app: FastAPI):
|
|
97
98
|
is_superuser=True,
|
98
99
|
is_verified=True,
|
99
100
|
)
|
101
|
+
|
102
|
+
if settings.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
|
103
|
+
from fractal_server.ssh._fabric import get_ssh_connection
|
104
|
+
from fractal_server.ssh._fabric import FractalSSH
|
105
|
+
|
106
|
+
connection = get_ssh_connection()
|
107
|
+
app.state.fractal_ssh = FractalSSH(connection=connection)
|
108
|
+
logger.info(
|
109
|
+
f"Created SSH connection "
|
110
|
+
f"({app.state.fractal_ssh.is_connected=})."
|
111
|
+
)
|
112
|
+
else:
|
113
|
+
app.state.fractal_ssh = None
|
114
|
+
|
100
115
|
config_uvicorn_loggers()
|
101
116
|
logger.info("End application startup")
|
102
117
|
reset_logger_handlers(logger)
|
103
118
|
yield
|
104
119
|
logger = get_logger("fractal_server.lifespan")
|
105
120
|
logger.info("Start application shutdown")
|
121
|
+
|
122
|
+
if settings.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
|
123
|
+
logger.info(
|
124
|
+
f"Closing SSH connection "
|
125
|
+
f"(current: {app.state.fractal_ssh.is_connected=})."
|
126
|
+
)
|
127
|
+
|
128
|
+
app.state.fractal_ssh.close()
|
129
|
+
|
106
130
|
logger.info(
|
107
131
|
f"Current worker with pid {os.getpid()} is shutting down. "
|
108
132
|
f"Current jobs: {app.state.jobsV1=}, {app.state.jobsV2=}"
|
109
133
|
)
|
110
|
-
if settings.FRACTAL_RUNNER_BACKEND
|
134
|
+
if _backend_supports_shutdown(settings.FRACTAL_RUNNER_BACKEND):
|
111
135
|
try:
|
112
136
|
await cleanup_after_shutdown(
|
113
137
|
jobsV1=app.state.jobsV1,
|
@@ -120,6 +144,8 @@ async def lifespan(app: FastAPI):
|
|
120
144
|
"some of running jobs are not shutdown properly. "
|
121
145
|
f"Original error: {e}"
|
122
146
|
)
|
147
|
+
else:
|
148
|
+
logger.info("Shutdown not available for this backend runner.")
|
123
149
|
|
124
150
|
logger.info("End application shutdown")
|
125
151
|
reset_logger_handlers(logger)
|