fractal-server 2.5.1__py3-none-any.whl → 2.6.0a0__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 +93 -0
- 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 +179 -34
- fractal_server/tasks/v2/background_operations_ssh.py +14 -5
- {fractal_server-2.5.1.dist-info → fractal_server-2.6.0a0.dist-info}/METADATA +1 -1
- {fractal_server-2.5.1.dist-info → fractal_server-2.6.0a0.dist-info}/RECORD +37 -31
- fractal_server/data_migrations/2_4_0.py +0 -61
- {fractal_server-2.5.1.dist-info → fractal_server-2.6.0a0.dist-info}/LICENSE +0 -0
- {fractal_server-2.5.1.dist-info → fractal_server-2.6.0a0.dist-info}/WHEEL +0 -0
- {fractal_server-2.5.1.dist-info → fractal_server-2.6.0a0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,74 @@
|
|
1
|
+
from fastapi import HTTPException
|
2
|
+
from fastapi import status
|
3
|
+
from pydantic import BaseModel
|
4
|
+
from pydantic import ValidationError
|
5
|
+
|
6
|
+
from fractal_server.app.db import AsyncSession
|
7
|
+
from fractal_server.app.models import UserOAuth
|
8
|
+
from fractal_server.app.models import UserSettings
|
9
|
+
from fractal_server.app.user_settings import SlurmSshUserSettings
|
10
|
+
from fractal_server.app.user_settings import SlurmSudoUserSettings
|
11
|
+
from fractal_server.logger import set_logger
|
12
|
+
|
13
|
+
logger = set_logger(__name__)
|
14
|
+
|
15
|
+
|
16
|
+
def verify_user_has_settings(user: UserOAuth) -> None:
|
17
|
+
"""
|
18
|
+
Check that the `user.user_settings_id` foreign-key is set.
|
19
|
+
|
20
|
+
NOTE: This check will become useless when we make the foreign-key column
|
21
|
+
required, but for the moment (as of v2.6.0) we have to keep it in place.
|
22
|
+
|
23
|
+
Arguments:
|
24
|
+
user: The user to be checked.
|
25
|
+
"""
|
26
|
+
if user.user_settings_id is None:
|
27
|
+
raise HTTPException(
|
28
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
29
|
+
detail=f"Error: user '{user.email}' has no settings.",
|
30
|
+
)
|
31
|
+
|
32
|
+
|
33
|
+
async def validate_user_settings(
|
34
|
+
*, user: UserOAuth, backend: str, db: AsyncSession
|
35
|
+
) -> UserSettings:
|
36
|
+
"""
|
37
|
+
Get a UserSettings object and validate it based on a given Fractal backend.
|
38
|
+
|
39
|
+
Arguments:
|
40
|
+
user: The user whose settings we should validate.
|
41
|
+
backend: The value of `FRACTAL_RUNNER_BACKEND`
|
42
|
+
db: An async DB session
|
43
|
+
|
44
|
+
Returns:
|
45
|
+
`UserSetting` object associated to `user`, if valid.
|
46
|
+
"""
|
47
|
+
|
48
|
+
verify_user_has_settings(user)
|
49
|
+
|
50
|
+
user_settings = await db.get(UserSettings, user.user_settings_id)
|
51
|
+
|
52
|
+
if backend == "slurm_ssh":
|
53
|
+
UserSettingsValidationModel = SlurmSshUserSettings
|
54
|
+
elif backend == "slurm":
|
55
|
+
UserSettingsValidationModel = SlurmSudoUserSettings
|
56
|
+
else:
|
57
|
+
# For other backends, we don't validate anything
|
58
|
+
UserSettingsValidationModel = BaseModel
|
59
|
+
|
60
|
+
try:
|
61
|
+
UserSettingsValidationModel(**user_settings.model_dump())
|
62
|
+
except ValidationError as e:
|
63
|
+
error_msg = (
|
64
|
+
"User settings are not valid for "
|
65
|
+
f"FRACTAL_RUNNER_BACKEND='{backend}'. "
|
66
|
+
f"Original error: {str(e)}"
|
67
|
+
)
|
68
|
+
logger.warning(error_msg)
|
69
|
+
raise HTTPException(
|
70
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
71
|
+
detail=error_msg,
|
72
|
+
)
|
73
|
+
|
74
|
+
return user_settings
|
@@ -163,17 +163,34 @@ class FractalSlurmSSHExecutor(SlurmExecutor):
|
|
163
163
|
settings = Inject(get_settings)
|
164
164
|
self.python_remote = settings.FRACTAL_SLURM_WORKER_PYTHON
|
165
165
|
if self.python_remote is None:
|
166
|
+
self._stop_and_join_wait_thread()
|
166
167
|
raise ValueError("FRACTAL_SLURM_WORKER_PYTHON is not set. Exit.")
|
167
168
|
|
168
169
|
# Initialize connection and perform handshake
|
169
170
|
self.fractal_ssh = fractal_ssh
|
170
171
|
logger.warning(self.fractal_ssh)
|
171
|
-
|
172
|
+
try:
|
173
|
+
self.handshake()
|
174
|
+
except Exception as e:
|
175
|
+
logger.warning(
|
176
|
+
"Stop/join waiting thread and then "
|
177
|
+
f"re-raise original error {str(e)}"
|
178
|
+
)
|
179
|
+
self._stop_and_join_wait_thread()
|
180
|
+
raise e
|
172
181
|
|
173
182
|
# Set/validate parameters for SLURM submission scripts
|
174
183
|
self.slurm_account = slurm_account
|
175
184
|
self.common_script_lines = common_script_lines or []
|
176
|
-
|
185
|
+
try:
|
186
|
+
self._validate_common_script_lines()
|
187
|
+
except Exception as e:
|
188
|
+
logger.warning(
|
189
|
+
"Stop/join waiting thread and then "
|
190
|
+
f"re-raise original error {str(e)}"
|
191
|
+
)
|
192
|
+
self._stop_and_join_wait_thread()
|
193
|
+
raise e
|
177
194
|
|
178
195
|
# Set/initialize some more options
|
179
196
|
self.keep_pickle_files = keep_pickle_files
|
@@ -1385,6 +1402,10 @@ class FractalSlurmSSHExecutor(SlurmExecutor):
|
|
1385
1402
|
self.fractal_ssh.run_command(cmd=scancel_command)
|
1386
1403
|
logger.debug("Executor shutdown: end")
|
1387
1404
|
|
1405
|
+
def _stop_and_join_wait_thread(self):
|
1406
|
+
self.wait_thread.stop()
|
1407
|
+
self.wait_thread.join()
|
1408
|
+
|
1388
1409
|
def __exit__(self, *args, **kwargs):
|
1389
1410
|
"""
|
1390
1411
|
See
|
@@ -1393,8 +1414,7 @@ class FractalSlurmSSHExecutor(SlurmExecutor):
|
|
1393
1414
|
logger.debug(
|
1394
1415
|
"[FractalSlurmSSHExecutor.__exit__] Stop and join `wait_thread`"
|
1395
1416
|
)
|
1396
|
-
self.
|
1397
|
-
self.wait_thread.join()
|
1417
|
+
self._stop_and_join_wait_thread()
|
1398
1418
|
logger.debug("[FractalSlurmSSHExecutor.__exit__] End")
|
1399
1419
|
|
1400
1420
|
def run_squeue(self, job_ids):
|
@@ -259,6 +259,7 @@ class FractalSlurmExecutor(SlurmExecutor):
|
|
259
259
|
for line in self.common_script_lines
|
260
260
|
if line.startswith("#SBATCH --account=")
|
261
261
|
)
|
262
|
+
self._stop_and_join_wait_thread()
|
262
263
|
raise RuntimeError(
|
263
264
|
"Invalid line in `FractalSlurmExecutor.common_script_lines`: "
|
264
265
|
f"'{invalid_line}'.\n"
|
@@ -1287,6 +1288,10 @@ class FractalSlurmExecutor(SlurmExecutor):
|
|
1287
1288
|
|
1288
1289
|
logger.debug("Executor shutdown: end")
|
1289
1290
|
|
1291
|
+
def _stop_and_join_wait_thread(self):
|
1292
|
+
self.wait_thread.stop()
|
1293
|
+
self.wait_thread.join()
|
1294
|
+
|
1290
1295
|
def __exit__(self, *args, **kwargs):
|
1291
1296
|
"""
|
1292
1297
|
See
|
@@ -1295,6 +1300,5 @@ class FractalSlurmExecutor(SlurmExecutor):
|
|
1295
1300
|
logger.debug(
|
1296
1301
|
"[FractalSlurmExecutor.__exit__] Stop and join `wait_thread`"
|
1297
1302
|
)
|
1298
|
-
self.
|
1299
|
-
self.wait_thread.join()
|
1303
|
+
self._stop_and_join_wait_thread()
|
1300
1304
|
logger.debug("[FractalSlurmExecutor.__exit__] End")
|
@@ -42,6 +42,7 @@ from .handle_failed_job import assemble_filters_failed_job
|
|
42
42
|
from .handle_failed_job import assemble_history_failed_job
|
43
43
|
from .handle_failed_job import assemble_images_failed_job
|
44
44
|
from fractal_server import __VERSION__
|
45
|
+
from fractal_server.app.models import UserSettings
|
45
46
|
|
46
47
|
_backends = {}
|
47
48
|
_backends["local"] = local_process_workflow
|
@@ -76,6 +77,7 @@ async def submit_workflow(
|
|
76
77
|
workflow_id: int,
|
77
78
|
dataset_id: int,
|
78
79
|
job_id: int,
|
80
|
+
user_settings: UserSettings,
|
79
81
|
worker_init: Optional[str] = None,
|
80
82
|
slurm_user: Optional[str] = None,
|
81
83
|
user_cache_dir: Optional[str] = None,
|
@@ -196,8 +198,7 @@ async def submit_workflow(
|
|
196
198
|
elif FRACTAL_RUNNER_BACKEND == "slurm_ssh":
|
197
199
|
# Folder creation is deferred to _process_workflow
|
198
200
|
WORKFLOW_DIR_REMOTE = (
|
199
|
-
Path(
|
200
|
-
/ WORKFLOW_DIR_LOCAL.name
|
201
|
+
Path(user_settings.ssh_jobs_dir) / WORKFLOW_DIR_LOCAL.name
|
201
202
|
)
|
202
203
|
else:
|
203
204
|
logger.error(
|
@@ -270,11 +271,8 @@ async def submit_workflow(
|
|
270
271
|
logger.debug(f"slurm_account: {job.slurm_account}")
|
271
272
|
logger.debug(f"worker_init: {worker_init}")
|
272
273
|
elif FRACTAL_RUNNER_BACKEND == "slurm_ssh":
|
273
|
-
logger.debug(f"
|
274
|
-
logger.debug(f"
|
275
|
-
logger.debug(
|
276
|
-
f"base dir: {settings.FRACTAL_SLURM_SSH_WORKING_BASE_DIR}"
|
277
|
-
)
|
274
|
+
logger.debug(f"ssh_user: {user_settings.ssh_username}")
|
275
|
+
logger.debug(f"base dir: {user_settings.ssh_tasks_dir}")
|
278
276
|
logger.debug(f"worker_init: {worker_init}")
|
279
277
|
logger.debug(f"job.id: {job.id}")
|
280
278
|
logger.debug(f"job.working_dir: {job.working_dir}")
|
@@ -3,14 +3,10 @@ from typing import Optional
|
|
3
3
|
from fastapi_users import schemas
|
4
4
|
from pydantic import BaseModel
|
5
5
|
from pydantic import Extra
|
6
|
-
from pydantic import Field
|
7
6
|
from pydantic import validator
|
8
|
-
from pydantic.types import StrictStr
|
9
7
|
|
10
|
-
from ._validators import val_absolute_path
|
11
8
|
from ._validators import val_unique_list
|
12
9
|
from ._validators import valstr
|
13
|
-
from fractal_server.string_tools import validate_cmd
|
14
10
|
|
15
11
|
__all__ = (
|
16
12
|
"UserRead",
|
@@ -41,16 +37,10 @@ class UserRead(schemas.BaseUser[int]):
|
|
41
37
|
Schema for `User` read from database.
|
42
38
|
|
43
39
|
Attributes:
|
44
|
-
slurm_user:
|
45
|
-
cache_dir:
|
46
40
|
username:
|
47
|
-
slurm_accounts:
|
48
41
|
"""
|
49
42
|
|
50
|
-
slurm_user: Optional[str]
|
51
|
-
cache_dir: Optional[str]
|
52
43
|
username: Optional[str]
|
53
|
-
slurm_accounts: list[str]
|
54
44
|
group_names: Optional[list[str]] = None
|
55
45
|
group_ids: Optional[list[int]] = None
|
56
46
|
oauth_accounts: list[OAuthAccountRead]
|
@@ -61,32 +51,14 @@ class UserUpdate(schemas.BaseUserUpdate):
|
|
61
51
|
Schema for `User` update.
|
62
52
|
|
63
53
|
Attributes:
|
64
|
-
slurm_user:
|
65
|
-
cache_dir:
|
66
54
|
username:
|
67
|
-
slurm_accounts:
|
68
55
|
"""
|
69
56
|
|
70
|
-
slurm_user: Optional[str]
|
71
|
-
cache_dir: Optional[str]
|
72
57
|
username: Optional[str]
|
73
|
-
slurm_accounts: Optional[list[StrictStr]]
|
74
58
|
|
75
59
|
# Validators
|
76
|
-
_slurm_user = validator("slurm_user", allow_reuse=True)(
|
77
|
-
valstr("slurm_user")
|
78
|
-
)
|
79
60
|
_username = validator("username", allow_reuse=True)(valstr("username"))
|
80
61
|
|
81
|
-
_slurm_accounts = validator("slurm_accounts", allow_reuse=True)(
|
82
|
-
val_unique_list("slurm_accounts")
|
83
|
-
)
|
84
|
-
|
85
|
-
@validator("cache_dir")
|
86
|
-
def cache_dir_validator(cls, value):
|
87
|
-
validate_cmd(value)
|
88
|
-
return val_absolute_path("cache_dir")(value)
|
89
|
-
|
90
62
|
@validator(
|
91
63
|
"is_active",
|
92
64
|
"is_verified",
|
@@ -106,21 +78,9 @@ class UserUpdateStrict(BaseModel, extra=Extra.forbid):
|
|
106
78
|
Schema for `User` self-editing.
|
107
79
|
|
108
80
|
Attributes:
|
109
|
-
cache_dir:
|
110
|
-
slurm_accounts:
|
111
81
|
"""
|
112
82
|
|
113
|
-
|
114
|
-
slurm_accounts: Optional[list[StrictStr]]
|
115
|
-
|
116
|
-
_slurm_accounts = validator("slurm_accounts", allow_reuse=True)(
|
117
|
-
val_unique_list("slurm_accounts")
|
118
|
-
)
|
119
|
-
|
120
|
-
@validator("cache_dir")
|
121
|
-
def cache_dir_validator(cls, value):
|
122
|
-
validate_cmd(value)
|
123
|
-
return val_absolute_path("cache_dir")(value)
|
83
|
+
pass
|
124
84
|
|
125
85
|
|
126
86
|
class UserUpdateWithNewGroupIds(UserUpdate):
|
@@ -136,32 +96,11 @@ class UserCreate(schemas.BaseUserCreate):
|
|
136
96
|
Schema for `User` creation.
|
137
97
|
|
138
98
|
Attributes:
|
139
|
-
slurm_user:
|
140
|
-
cache_dir:
|
141
99
|
username:
|
142
|
-
slurm_accounts:
|
143
100
|
"""
|
144
101
|
|
145
|
-
slurm_user: Optional[str]
|
146
|
-
cache_dir: Optional[str]
|
147
102
|
username: Optional[str]
|
148
|
-
slurm_accounts: list[StrictStr] = Field(default_factory=list)
|
149
103
|
|
150
104
|
# Validators
|
151
105
|
|
152
|
-
@validator("slurm_accounts")
|
153
|
-
def slurm_accounts_validator(cls, value):
|
154
|
-
for i, element in enumerate(value):
|
155
|
-
value[i] = valstr(attribute=f"slurm_accounts[{i}]")(element)
|
156
|
-
val_unique_list("slurm_accounts")(value)
|
157
|
-
return value
|
158
|
-
|
159
|
-
_slurm_user = validator("slurm_user", allow_reuse=True)(
|
160
|
-
valstr("slurm_user")
|
161
|
-
)
|
162
106
|
_username = validator("username", allow_reuse=True)(valstr("username"))
|
163
|
-
|
164
|
-
@validator("cache_dir")
|
165
|
-
def cache_dir_validator(cls, value):
|
166
|
-
validate_cmd(value)
|
167
|
-
return val_absolute_path("cache_dir")(value)
|
@@ -0,0 +1,93 @@
|
|
1
|
+
from typing import Optional
|
2
|
+
|
3
|
+
from pydantic import BaseModel
|
4
|
+
from pydantic import Extra
|
5
|
+
from pydantic import validator
|
6
|
+
from pydantic.types import StrictStr
|
7
|
+
|
8
|
+
from ._validators import val_absolute_path
|
9
|
+
from ._validators import val_unique_list
|
10
|
+
from ._validators import valstr
|
11
|
+
from fractal_server.string_tools import validate_cmd
|
12
|
+
|
13
|
+
__all__ = (
|
14
|
+
"UserSettingsRead",
|
15
|
+
"UserSettingsReadStrict",
|
16
|
+
"UserSettingsUpdate",
|
17
|
+
"UserSettingsUpdateStrict",
|
18
|
+
)
|
19
|
+
|
20
|
+
|
21
|
+
class UserSettingsRead(BaseModel):
|
22
|
+
id: int
|
23
|
+
ssh_host: Optional[str] = None
|
24
|
+
ssh_username: Optional[str] = None
|
25
|
+
ssh_private_key_path: Optional[str] = None
|
26
|
+
ssh_tasks_dir: Optional[str] = None
|
27
|
+
ssh_jobs_dir: Optional[str] = None
|
28
|
+
slurm_user: Optional[str] = None
|
29
|
+
slurm_accounts: list[str]
|
30
|
+
cache_dir: Optional[str] = None
|
31
|
+
|
32
|
+
|
33
|
+
class UserSettingsReadStrict(BaseModel):
|
34
|
+
slurm_user: Optional[str] = None
|
35
|
+
slurm_accounts: list[str]
|
36
|
+
cache_dir: Optional[str] = None
|
37
|
+
|
38
|
+
|
39
|
+
class UserSettingsUpdate(BaseModel, extra=Extra.forbid):
|
40
|
+
ssh_host: Optional[str] = None
|
41
|
+
ssh_username: Optional[str] = None
|
42
|
+
ssh_private_key_path: Optional[str] = None
|
43
|
+
ssh_tasks_dir: Optional[str] = None
|
44
|
+
ssh_jobs_dir: Optional[str] = None
|
45
|
+
slurm_user: Optional[str] = None
|
46
|
+
slurm_accounts: Optional[list[StrictStr]] = None
|
47
|
+
cache_dir: Optional[str] = None
|
48
|
+
|
49
|
+
_ssh_host = validator("ssh_host", allow_reuse=True)(valstr("ssh_host"))
|
50
|
+
_ssh_username = validator("ssh_username", allow_reuse=True)(
|
51
|
+
valstr("ssh_username")
|
52
|
+
)
|
53
|
+
_ssh_private_key_path = validator(
|
54
|
+
"ssh_private_key_path", allow_reuse=True
|
55
|
+
)(val_absolute_path("ssh_private_key_path"))
|
56
|
+
|
57
|
+
_ssh_tasks_dir = validator("ssh_tasks_dir", allow_reuse=True)(
|
58
|
+
val_absolute_path("ssh_tasks_dir")
|
59
|
+
)
|
60
|
+
_ssh_jobs_dir = validator("ssh_jobs_dir", allow_reuse=True)(
|
61
|
+
val_absolute_path("ssh_jobs_dir")
|
62
|
+
)
|
63
|
+
|
64
|
+
_slurm_user = validator("slurm_user", allow_reuse=True)(
|
65
|
+
valstr("slurm_user")
|
66
|
+
)
|
67
|
+
|
68
|
+
@validator("slurm_accounts")
|
69
|
+
def slurm_accounts_validator(cls, value):
|
70
|
+
if value is None:
|
71
|
+
return value
|
72
|
+
for i, item in enumerate(value):
|
73
|
+
value[i] = valstr(f"slurm_accounts[{i}]")(item)
|
74
|
+
return val_unique_list("slurm_accounts")(value)
|
75
|
+
|
76
|
+
@validator("cache_dir")
|
77
|
+
def cache_dir_validator(cls, value):
|
78
|
+
validate_cmd(value)
|
79
|
+
return val_absolute_path("cache_dir")(value)
|
80
|
+
|
81
|
+
|
82
|
+
class UserSettingsUpdateStrict(BaseModel, extra=Extra.forbid):
|
83
|
+
slurm_accounts: Optional[list[StrictStr]] = None
|
84
|
+
cache_dir: Optional[str] = None
|
85
|
+
|
86
|
+
_slurm_accounts = validator("slurm_accounts", allow_reuse=True)(
|
87
|
+
val_unique_list("slurm_accounts")
|
88
|
+
)
|
89
|
+
|
90
|
+
@validator("cache_dir")
|
91
|
+
def cache_dir_validator(cls, value):
|
92
|
+
validate_cmd(value)
|
93
|
+
return val_absolute_path("cache_dir")(value)
|
@@ -54,6 +54,7 @@ from fractal_server.app.models import LinkUserGroup
|
|
54
54
|
from fractal_server.app.models import OAuthAccount
|
55
55
|
from fractal_server.app.models import UserGroup
|
56
56
|
from fractal_server.app.models import UserOAuth
|
57
|
+
from fractal_server.app.models import UserSettings
|
57
58
|
from fractal_server.app.schemas.user import UserCreate
|
58
59
|
from fractal_server.logger import set_logger
|
59
60
|
|
@@ -193,6 +194,8 @@ class UserManager(IntegerIDMixin, BaseUserManager[UserOAuth, int]):
|
|
193
194
|
async def on_after_register(
|
194
195
|
self, user: UserOAuth, request: Optional[Request] = None
|
195
196
|
):
|
197
|
+
logger = set_logger("fractal_server.on_after_register")
|
198
|
+
|
196
199
|
logger.info(
|
197
200
|
f"New-user registration completed ({user.id=}, {user.email=})."
|
198
201
|
)
|
@@ -204,24 +207,30 @@ class UserManager(IntegerIDMixin, BaseUserManager[UserOAuth, int]):
|
|
204
207
|
res = await db.execute(stm)
|
205
208
|
default_group = res.scalar_one_or_none()
|
206
209
|
if default_group is None:
|
207
|
-
logger.
|
210
|
+
logger.warning(
|
208
211
|
f"No group found with name {FRACTAL_DEFAULT_GROUP_NAME}"
|
209
212
|
)
|
210
213
|
else:
|
211
|
-
logger.warning(
|
212
|
-
f"START adding {user.email} user to group "
|
213
|
-
f"{default_group.id=}."
|
214
|
-
)
|
215
214
|
link = LinkUserGroup(
|
216
215
|
user_id=user.id, group_id=default_group.id
|
217
216
|
)
|
218
217
|
db.add(link)
|
219
218
|
await db.commit()
|
220
|
-
logger.
|
221
|
-
f"
|
222
|
-
f"{default_group.id=}."
|
219
|
+
logger.info(
|
220
|
+
f"Added {user.email} user to group {default_group.id=}."
|
223
221
|
)
|
224
222
|
|
223
|
+
this_user = await db.get(UserOAuth, user.id)
|
224
|
+
|
225
|
+
this_user.settings = UserSettings()
|
226
|
+
await db.merge(this_user)
|
227
|
+
await db.commit()
|
228
|
+
await db.refresh(this_user)
|
229
|
+
logger.info(
|
230
|
+
f"Associated empty settings (id={this_user.user_settings_id}) "
|
231
|
+
f"to '{this_user.email}'."
|
232
|
+
)
|
233
|
+
|
225
234
|
|
226
235
|
async def get_user_manager(
|
227
236
|
user_db: SQLModelUserDatabaseAsync = Depends(get_user_db),
|
@@ -294,9 +303,13 @@ async def _create_first_user(
|
|
294
303
|
kwargs["username"] = username
|
295
304
|
user = await user_manager.create(UserCreate(**kwargs))
|
296
305
|
function_logger.info(f"User '{user.email}' created")
|
297
|
-
|
298
306
|
except UserAlreadyExists:
|
299
307
|
function_logger.warning(f"User '{email}' already exists")
|
308
|
+
except Exception as e:
|
309
|
+
function_logger.error(
|
310
|
+
f"ERROR in _create_first_user, original error {str(e)}"
|
311
|
+
)
|
312
|
+
raise e
|
300
313
|
finally:
|
301
314
|
function_logger.info(f"END _create_first_user, with email '{email}'")
|
302
315
|
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# TODO: move this file to the appropriate path
|
2
|
+
from pydantic import BaseModel
|
3
|
+
|
4
|
+
|
5
|
+
class SlurmSshUserSettings(BaseModel):
|
6
|
+
"""
|
7
|
+
Subset of user settings which must be present for task collection and job
|
8
|
+
execution when using the Slurm-SSH runner.
|
9
|
+
|
10
|
+
Attributes:
|
11
|
+
ssh_host: SSH-reachable host where a SLURM client is available.
|
12
|
+
ssh_username: User on `ssh_host`.
|
13
|
+
ssh_private_key_path: Path of private SSH key for `ssh_username`.
|
14
|
+
ssh_tasks_dir: Task-venvs base folder on `ssh_host`.
|
15
|
+
ssh_jobs_dir: Jobs base folder on `ssh_host`.
|
16
|
+
slurm_accounts:
|
17
|
+
List of SLURM accounts, to be used upon Fractal job submission.
|
18
|
+
"""
|
19
|
+
|
20
|
+
ssh_host: str
|
21
|
+
ssh_username: str
|
22
|
+
ssh_private_key_path: str
|
23
|
+
ssh_tasks_dir: str
|
24
|
+
ssh_jobs_dir: str
|
25
|
+
slurm_accounts: list[str]
|
26
|
+
|
27
|
+
|
28
|
+
class SlurmSudoUserSettings(BaseModel):
|
29
|
+
"""
|
30
|
+
Subset of user settings which must be present for task collection and job
|
31
|
+
execution when using the Slurm-sudo runner.
|
32
|
+
|
33
|
+
Attributes:
|
34
|
+
slurm_user: User to be impersonated via `sudo -u`.
|
35
|
+
cache_dir: Folder where `slurm_user` can write.
|
36
|
+
slurm_accounts:
|
37
|
+
List of SLURM accounts, to be used upon Fractal job submission.
|
38
|
+
"""
|
39
|
+
|
40
|
+
slurm_user: str
|
41
|
+
cache_dir: str
|
42
|
+
slurm_accounts: list[str]
|
fractal_server/config.py
CHANGED
@@ -631,22 +631,6 @@ class Settings(BaseSettings):
|
|
631
631
|
raise FractalConfigurationError(
|
632
632
|
f"Must set FRACTAL_SLURM_WORKER_PYTHON when {info}"
|
633
633
|
)
|
634
|
-
if self.FRACTAL_SLURM_SSH_USER is None:
|
635
|
-
raise FractalConfigurationError(
|
636
|
-
f"Must set FRACTAL_SLURM_SSH_USER when {info}"
|
637
|
-
)
|
638
|
-
if self.FRACTAL_SLURM_SSH_HOST is None:
|
639
|
-
raise FractalConfigurationError(
|
640
|
-
f"Must set FRACTAL_SLURM_SSH_HOST when {info}"
|
641
|
-
)
|
642
|
-
if self.FRACTAL_SLURM_SSH_PRIVATE_KEY_PATH is None:
|
643
|
-
raise FractalConfigurationError(
|
644
|
-
f"Must set FRACTAL_SLURM_SSH_PRIVATE_KEY_PATH when {info}"
|
645
|
-
)
|
646
|
-
if self.FRACTAL_SLURM_SSH_WORKING_BASE_DIR is None:
|
647
|
-
raise FractalConfigurationError(
|
648
|
-
f"Must set FRACTAL_SLURM_SSH_WORKING_BASE_DIR when {info}"
|
649
|
-
)
|
650
634
|
|
651
635
|
from fractal_server.app.runner.executors.slurm._slurm_config import ( # noqa: E501
|
652
636
|
load_slurm_config_file,
|
@@ -0,0 +1,49 @@
|
|
1
|
+
import logging
|
2
|
+
|
3
|
+
from sqlalchemy import select
|
4
|
+
|
5
|
+
from fractal_server.app.db import get_sync_db
|
6
|
+
from fractal_server.app.models import UserOAuth
|
7
|
+
from fractal_server.app.models import UserSettings
|
8
|
+
from fractal_server.config import get_settings
|
9
|
+
from fractal_server.data_migrations.tools import _check_current_version
|
10
|
+
from fractal_server.syringe import Inject
|
11
|
+
|
12
|
+
|
13
|
+
def fix_db():
|
14
|
+
logger = logging.getLogger("fix_db")
|
15
|
+
logger.warning("START execution of fix_db function")
|
16
|
+
_check_current_version("2.6.0")
|
17
|
+
|
18
|
+
global_settings = Inject(get_settings)
|
19
|
+
|
20
|
+
with next(get_sync_db()) as db:
|
21
|
+
users = db.execute(select(UserOAuth)).scalars().unique().all()
|
22
|
+
for user in sorted(users, key=lambda x: x.id):
|
23
|
+
logger.warning(f"START handling user {user.id}: '{user.email}'")
|
24
|
+
user_settings = UserSettings(
|
25
|
+
# SSH
|
26
|
+
ssh_host=global_settings.FRACTAL_SLURM_SSH_HOST,
|
27
|
+
ssh_username=global_settings.FRACTAL_SLURM_SSH_USER,
|
28
|
+
ssh_private_key_path=(
|
29
|
+
global_settings.FRACTAL_SLURM_SSH_PRIVATE_KEY_PATH
|
30
|
+
),
|
31
|
+
ssh_tasks_dir=(
|
32
|
+
global_settings.FRACTAL_SLURM_SSH_WORKING_BASE_DIR
|
33
|
+
),
|
34
|
+
ssh_jobs_dir=(
|
35
|
+
global_settings.FRACTAL_SLURM_SSH_WORKING_BASE_DIR
|
36
|
+
),
|
37
|
+
# SUDO
|
38
|
+
slurm_user=user.slurm_user,
|
39
|
+
slurm_accounts=user.slurm_accounts,
|
40
|
+
cache_dir=user.cache_dir,
|
41
|
+
)
|
42
|
+
user.settings = user_settings
|
43
|
+
db.add(user)
|
44
|
+
db.commit()
|
45
|
+
db.refresh(user)
|
46
|
+
logger.warning(f"New user {user.id} settings:\n{user.settings}")
|
47
|
+
logger.warning(f"END handling user {user.id}: '{user.email}'")
|
48
|
+
|
49
|
+
logger.warning("END of execution of fix_db function")
|
@@ -0,0 +1,17 @@
|
|
1
|
+
from packaging.version import parse
|
2
|
+
|
3
|
+
import fractal_server
|
4
|
+
|
5
|
+
|
6
|
+
def _check_current_version(expected_version: str):
|
7
|
+
# Check that this module matches with the current version
|
8
|
+
module_version = parse(expected_version)
|
9
|
+
current_version = parse(fractal_server.__VERSION__)
|
10
|
+
if (
|
11
|
+
current_version.major != module_version.major
|
12
|
+
or current_version.minor != module_version.minor
|
13
|
+
or current_version.micro != module_version.micro
|
14
|
+
):
|
15
|
+
raise RuntimeError(
|
16
|
+
f"{fractal_server.__VERSION__=} not matching with {__file__=}"
|
17
|
+
)
|
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. "
|