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/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__VERSION__ = "2.
|
1
|
+
__VERSION__ = "2.6.0"
|
fractal_server/__main__.py
CHANGED
@@ -43,9 +43,17 @@ openapi_parser.add_argument(
|
|
43
43
|
)
|
44
44
|
|
45
45
|
# fractalctl set-db
|
46
|
-
subparsers.add_parser(
|
46
|
+
set_db_parser = subparsers.add_parser(
|
47
47
|
"set-db",
|
48
|
-
description=
|
48
|
+
description=(
|
49
|
+
"Initialise/upgrade database schemas and create first group&user."
|
50
|
+
),
|
51
|
+
)
|
52
|
+
set_db_parser.add_argument(
|
53
|
+
"--skip-init-data",
|
54
|
+
action="store_true",
|
55
|
+
help="If set, do not try creating first group and user.",
|
56
|
+
default=False,
|
49
57
|
)
|
50
58
|
|
51
59
|
# fractalctl update-db-data
|
@@ -66,27 +74,34 @@ def save_openapi(dest="openapi.json"):
|
|
66
74
|
json.dump(openapi_schema, f)
|
67
75
|
|
68
76
|
|
69
|
-
def set_db():
|
77
|
+
def set_db(skip_init_data: bool = False):
|
70
78
|
"""
|
71
|
-
|
79
|
+
Upgrade database schema *and* create first group/user
|
72
80
|
|
73
81
|
Call alembic to upgrade to the latest migration.
|
74
|
-
|
75
82
|
Ref: https://stackoverflow.com/a/56683030/283972
|
83
|
+
|
84
|
+
Arguments:
|
85
|
+
skip_init_data: If `True`, skip creation of first group and user.
|
76
86
|
"""
|
77
|
-
import alembic.config
|
78
|
-
from pathlib import Path
|
79
|
-
import fractal_server
|
80
87
|
from fractal_server.app.security import _create_first_user
|
81
88
|
from fractal_server.app.security import _create_first_group
|
82
89
|
from fractal_server.syringe import Inject
|
83
90
|
from fractal_server.config import get_settings
|
84
91
|
|
92
|
+
import alembic.config
|
93
|
+
from pathlib import Path
|
94
|
+
import fractal_server
|
95
|
+
|
85
96
|
alembic_ini = Path(fractal_server.__file__).parent / "alembic.ini"
|
86
97
|
alembic_args = ["-c", alembic_ini.as_posix(), "upgrade", "head"]
|
87
98
|
print(f"START: Run alembic.config, with argv={alembic_args}")
|
88
99
|
alembic.config.main(argv=alembic_args)
|
89
100
|
print("END: alembic.config")
|
101
|
+
|
102
|
+
if skip_init_data:
|
103
|
+
return
|
104
|
+
|
90
105
|
# Insert default group
|
91
106
|
print()
|
92
107
|
_create_first_group()
|
@@ -179,7 +194,7 @@ def run():
|
|
179
194
|
if args.cmd == "openapi":
|
180
195
|
save_openapi(dest=args.openapi_file)
|
181
196
|
elif args.cmd == "set-db":
|
182
|
-
set_db()
|
197
|
+
set_db(skip_init_data=args.skip_init_data)
|
183
198
|
elif args.cmd == "update-db-data":
|
184
199
|
update_db_data()
|
185
200
|
elif args.cmd == "start":
|
@@ -7,5 +7,6 @@ from .linkusergroup import LinkUserGroup # noqa: F401
|
|
7
7
|
from .linkuserproject import LinkUserProject # noqa: F401
|
8
8
|
from .linkuserproject import LinkUserProjectV2 # noqa: F401
|
9
9
|
from .security import * # noqa
|
10
|
+
from .user_settings import UserSettings # noqa
|
10
11
|
from .v1 import * # noqa
|
11
12
|
from .v2 import * # noqa
|
@@ -20,6 +20,7 @@ from sqlmodel import Field
|
|
20
20
|
from sqlmodel import Relationship
|
21
21
|
from sqlmodel import SQLModel
|
22
22
|
|
23
|
+
from .user_settings import UserSettings
|
23
24
|
from fractal_server.utils import get_timestamp
|
24
25
|
|
25
26
|
|
@@ -104,6 +105,13 @@ class UserOAuth(SQLModel, table=True):
|
|
104
105
|
sa_relationship_kwargs={"lazy": "joined", "cascade": "all, delete"},
|
105
106
|
)
|
106
107
|
|
108
|
+
user_settings_id: Optional[int] = Field(
|
109
|
+
foreign_key="user_settings.id", default=None
|
110
|
+
)
|
111
|
+
settings: Optional[UserSettings] = Relationship(
|
112
|
+
sa_relationship_kwargs=dict(lazy="selectin", cascade="all, delete")
|
113
|
+
)
|
114
|
+
|
107
115
|
class Config:
|
108
116
|
orm_mode = True
|
109
117
|
|
@@ -0,0 +1,38 @@
|
|
1
|
+
from typing import Optional
|
2
|
+
|
3
|
+
from sqlalchemy import Column
|
4
|
+
from sqlalchemy.types import JSON
|
5
|
+
from sqlmodel import Field
|
6
|
+
from sqlmodel import SQLModel
|
7
|
+
|
8
|
+
|
9
|
+
class UserSettings(SQLModel, table=True):
|
10
|
+
"""
|
11
|
+
Comprehensive list of user settings.
|
12
|
+
|
13
|
+
Attributes:
|
14
|
+
id: ID of database object
|
15
|
+
slurm_accounts:
|
16
|
+
List of SLURM accounts, to be used upon Fractal job submission.
|
17
|
+
ssh_host: SSH-reachable host where a SLURM client is available.
|
18
|
+
ssh_username: User on `ssh_host`.
|
19
|
+
ssh_private_key_path: Path of private SSH key for `ssh_username`.
|
20
|
+
ssh_tasks_dir: Task-venvs base folder on `ssh_host`.
|
21
|
+
ssh_jobs_dir: Jobs base folder on `ssh_host`.
|
22
|
+
slurm_user: Local user, to be impersonated via `sudo -u`
|
23
|
+
cache_dir: Folder where `slurm_user` can write.
|
24
|
+
"""
|
25
|
+
|
26
|
+
__tablename__ = "user_settings"
|
27
|
+
|
28
|
+
id: Optional[int] = Field(default=None, primary_key=True)
|
29
|
+
slurm_accounts: list[str] = Field(
|
30
|
+
sa_column=Column(JSON, server_default="[]", nullable=False)
|
31
|
+
)
|
32
|
+
ssh_host: Optional[str] = None
|
33
|
+
ssh_username: Optional[str] = None
|
34
|
+
ssh_private_key_path: Optional[str] = None
|
35
|
+
ssh_tasks_dir: Optional[str] = None
|
36
|
+
ssh_jobs_dir: Optional[str] = None
|
37
|
+
slurm_user: Optional[str] = None
|
38
|
+
cache_dir: Optional[str] = None
|
@@ -22,6 +22,7 @@ from ....models.v1 import Task
|
|
22
22
|
from ....models.v1 import Workflow
|
23
23
|
from ....models.v1 import WorkflowTask
|
24
24
|
from ....schemas.v1 import JobStatusTypeV1
|
25
|
+
from ...aux.validate_user_settings import verify_user_has_settings
|
25
26
|
from fractal_server.app.models import UserOAuth
|
26
27
|
|
27
28
|
|
@@ -367,7 +368,11 @@ async def _get_task_check_owner(
|
|
367
368
|
),
|
368
369
|
)
|
369
370
|
else:
|
370
|
-
|
371
|
+
if user.username:
|
372
|
+
owner = user.username
|
373
|
+
else:
|
374
|
+
verify_user_has_settings(user)
|
375
|
+
owner = user.settings.slurm_user
|
371
376
|
if owner != task.owner:
|
372
377
|
raise HTTPException(
|
373
378
|
status_code=status.HTTP_403_FORBIDDEN,
|
@@ -34,6 +34,7 @@ from ....schemas.v1 import JobStatusTypeV1
|
|
34
34
|
from ....schemas.v1 import ProjectCreateV1
|
35
35
|
from ....schemas.v1 import ProjectReadV1
|
36
36
|
from ....schemas.v1 import ProjectUpdateV1
|
37
|
+
from ...aux.validate_user_settings import validate_user_settings
|
37
38
|
from ._aux_functions import _check_project_exists
|
38
39
|
from ._aux_functions import _get_dataset_check_owner
|
39
40
|
from ._aux_functions import _get_project_check_owner
|
@@ -321,25 +322,11 @@ async def apply_workflow(
|
|
321
322
|
),
|
322
323
|
)
|
323
324
|
|
324
|
-
#
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
330
|
-
detail=(
|
331
|
-
f"FRACTAL_RUNNER_BACKEND={backend}, "
|
332
|
-
f"but {user.slurm_user=}."
|
333
|
-
),
|
334
|
-
)
|
335
|
-
if not user.cache_dir:
|
336
|
-
raise HTTPException(
|
337
|
-
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
338
|
-
detail=(
|
339
|
-
f"FRACTAL_RUNNER_BACKEND={backend}, "
|
340
|
-
f"but {user.cache_dir=}."
|
341
|
-
),
|
342
|
-
)
|
325
|
+
# Validate user settings
|
326
|
+
FRACTAL_RUNNER_BACKEND = settings.FRACTAL_RUNNER_BACKEND
|
327
|
+
user_settings = await validate_user_settings(
|
328
|
+
user=user, backend=FRACTAL_RUNNER_BACKEND, db=db
|
329
|
+
)
|
343
330
|
|
344
331
|
# Check that datasets have the right number of resources
|
345
332
|
if not input_dataset.resource_list:
|
@@ -386,7 +373,7 @@ async def apply_workflow(
|
|
386
373
|
)
|
387
374
|
|
388
375
|
if apply_workflow.slurm_account is not None:
|
389
|
-
if apply_workflow.slurm_account not in
|
376
|
+
if apply_workflow.slurm_account not in user_settings.slurm_accounts:
|
390
377
|
raise HTTPException(
|
391
378
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
392
379
|
detail=(
|
@@ -395,8 +382,8 @@ async def apply_workflow(
|
|
395
382
|
),
|
396
383
|
)
|
397
384
|
else:
|
398
|
-
if len(
|
399
|
-
apply_workflow.slurm_account =
|
385
|
+
if len(user_settings.slurm_accounts) > 0:
|
386
|
+
apply_workflow.slurm_account = user_settings.slurm_accounts[0]
|
400
387
|
|
401
388
|
# Add new ApplyWorkflow object to DB
|
402
389
|
job = ApplyWorkflow(
|
@@ -480,8 +467,8 @@ async def apply_workflow(
|
|
480
467
|
output_dataset_id=output_dataset.id,
|
481
468
|
job_id=job.id,
|
482
469
|
worker_init=apply_workflow.worker_init,
|
483
|
-
slurm_user=
|
484
|
-
user_cache_dir=
|
470
|
+
slurm_user=user_settings.slurm_user,
|
471
|
+
user_cache_dir=user_settings.cache_dir,
|
485
472
|
)
|
486
473
|
request.app.state.jobsV1.append(job.id)
|
487
474
|
logger.info(
|
@@ -17,6 +17,7 @@ from ....models.v2 import TaskV2
|
|
17
17
|
from ....schemas.v1 import TaskCreateV1
|
18
18
|
from ....schemas.v1 import TaskReadV1
|
19
19
|
from ....schemas.v1 import TaskUpdateV1
|
20
|
+
from ...aux.validate_user_settings import verify_user_has_settings
|
20
21
|
from ._aux_functions import _get_task_check_owner
|
21
22
|
from ._aux_functions import _raise_if_v1_is_read_only
|
22
23
|
from fractal_server.app.models import UserOAuth
|
@@ -126,16 +127,18 @@ async def create_task(
|
|
126
127
|
# Set task.owner attribute
|
127
128
|
if user.username:
|
128
129
|
owner = user.username
|
129
|
-
elif user.slurm_user:
|
130
|
-
owner = user.slurm_user
|
131
130
|
else:
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
131
|
+
verify_user_has_settings(user)
|
132
|
+
if user.settings.slurm_user:
|
133
|
+
owner = user.settings.slurm_user
|
134
|
+
else:
|
135
|
+
raise HTTPException(
|
136
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
137
|
+
detail=(
|
138
|
+
"Cannot add a new task because current user does not "
|
139
|
+
"have `username` or `slurm_user` attributes."
|
140
|
+
),
|
141
|
+
)
|
139
142
|
|
140
143
|
# Prepend owner to task.source
|
141
144
|
task.source = f"{owner}:{task.source}"
|
@@ -21,6 +21,7 @@ from ....models.v2 import TaskV2
|
|
21
21
|
from ....models.v2 import WorkflowTaskV2
|
22
22
|
from ....models.v2 import WorkflowV2
|
23
23
|
from ....schemas.v2 import JobStatusTypeV2
|
24
|
+
from ...aux.validate_user_settings import verify_user_has_settings
|
24
25
|
from fractal_server.app.models import UserOAuth
|
25
26
|
from fractal_server.images import Filters
|
26
27
|
|
@@ -362,7 +363,11 @@ async def _get_task_check_owner(
|
|
362
363
|
),
|
363
364
|
)
|
364
365
|
else:
|
365
|
-
|
366
|
+
if user.username:
|
367
|
+
owner = user.username
|
368
|
+
else:
|
369
|
+
verify_user_has_settings(user)
|
370
|
+
owner = user.settings.slurm_user
|
366
371
|
if owner != task.owner:
|
367
372
|
raise HTTPException(
|
368
373
|
status_code=status.HTTP_403_FORBIDDEN,
|
@@ -27,6 +27,7 @@ from ....runner.v2 import submit_workflow
|
|
27
27
|
from ....schemas.v2 import JobCreateV2
|
28
28
|
from ....schemas.v2 import JobReadV2
|
29
29
|
from ....schemas.v2 import JobStatusTypeV2
|
30
|
+
from ...aux.validate_user_settings import validate_user_settings
|
30
31
|
from ._aux_functions import _get_dataset_check_owner
|
31
32
|
from ._aux_functions import _get_workflow_check_owner
|
32
33
|
from ._aux_functions import clean_app_job_list_v2
|
@@ -109,19 +110,11 @@ async def apply_workflow(
|
|
109
110
|
),
|
110
111
|
)
|
111
112
|
|
112
|
-
#
|
113
|
+
# Validate user settings
|
113
114
|
FRACTAL_RUNNER_BACKEND = settings.FRACTAL_RUNNER_BACKEND
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
118
|
-
detail=f"{FRACTAL_RUNNER_BACKEND=}, but {user.slurm_user=}.",
|
119
|
-
)
|
120
|
-
if not user.cache_dir:
|
121
|
-
raise HTTPException(
|
122
|
-
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
123
|
-
detail=f"{FRACTAL_RUNNER_BACKEND=}, but {user.cache_dir=}.",
|
124
|
-
)
|
115
|
+
user_settings = await validate_user_settings(
|
116
|
+
user=user, backend=FRACTAL_RUNNER_BACKEND, db=db
|
117
|
+
)
|
125
118
|
|
126
119
|
# Check that no other job with the same dataset_id is SUBMITTED
|
127
120
|
stm = (
|
@@ -140,7 +133,7 @@ async def apply_workflow(
|
|
140
133
|
)
|
141
134
|
|
142
135
|
if job_create.slurm_account is not None:
|
143
|
-
if job_create.slurm_account not in
|
136
|
+
if job_create.slurm_account not in user_settings.slurm_accounts:
|
144
137
|
raise HTTPException(
|
145
138
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
146
139
|
detail=(
|
@@ -149,8 +142,8 @@ async def apply_workflow(
|
|
149
142
|
),
|
150
143
|
)
|
151
144
|
else:
|
152
|
-
if len(
|
153
|
-
job_create.slurm_account =
|
145
|
+
if len(user_settings.slurm_accounts) > 0:
|
146
|
+
job_create.slurm_account = user_settings.slurm_accounts[0]
|
154
147
|
|
155
148
|
# Add new Job object to DB
|
156
149
|
job = JobV2(
|
@@ -224,12 +217,11 @@ async def apply_workflow(
|
|
224
217
|
WORKFLOW_DIR_REMOTE = WORKFLOW_DIR_LOCAL
|
225
218
|
elif FRACTAL_RUNNER_BACKEND == "slurm":
|
226
219
|
WORKFLOW_DIR_REMOTE = (
|
227
|
-
Path(
|
220
|
+
Path(user_settings.cache_dir) / f"{WORKFLOW_DIR_LOCAL.name}"
|
228
221
|
)
|
229
222
|
elif FRACTAL_RUNNER_BACKEND == "slurm_ssh":
|
230
223
|
WORKFLOW_DIR_REMOTE = (
|
231
|
-
Path(
|
232
|
-
/ f"{WORKFLOW_DIR_LOCAL.name}"
|
224
|
+
Path(user_settings.ssh_jobs_dir) / f"{WORKFLOW_DIR_LOCAL.name}"
|
233
225
|
)
|
234
226
|
|
235
227
|
# Update job folders in the db
|
@@ -238,15 +230,31 @@ async def apply_workflow(
|
|
238
230
|
await db.merge(job)
|
239
231
|
await db.commit()
|
240
232
|
|
233
|
+
# User appropriate FractalSSH object
|
234
|
+
if settings.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
|
235
|
+
ssh_credentials = dict(
|
236
|
+
user=user_settings.ssh_username,
|
237
|
+
host=user_settings.ssh_host,
|
238
|
+
key_path=user_settings.ssh_private_key_path,
|
239
|
+
)
|
240
|
+
fractal_ssh_list = request.app.state.fractal_ssh_list
|
241
|
+
fractal_ssh = fractal_ssh_list.get(**ssh_credentials)
|
242
|
+
else:
|
243
|
+
fractal_ssh = None
|
244
|
+
|
245
|
+
# Expunge user settings from db, to use in background task
|
246
|
+
db.expunge(user_settings)
|
247
|
+
|
241
248
|
background_tasks.add_task(
|
242
249
|
submit_workflow,
|
243
250
|
workflow_id=workflow.id,
|
244
251
|
dataset_id=dataset.id,
|
245
252
|
job_id=job.id,
|
253
|
+
user_settings=user_settings,
|
246
254
|
worker_init=job.worker_init,
|
247
|
-
slurm_user=
|
248
|
-
user_cache_dir=
|
249
|
-
fractal_ssh=
|
255
|
+
slurm_user=user_settings.slurm_user,
|
256
|
+
user_cache_dir=user_settings.cache_dir,
|
257
|
+
fractal_ssh=fractal_ssh,
|
250
258
|
)
|
251
259
|
request.app.state.jobsV2.append(job.id)
|
252
260
|
logger.info(
|
@@ -18,6 +18,7 @@ from ....models.v2 import WorkflowV2
|
|
18
18
|
from ....schemas.v2 import TaskCreateV2
|
19
19
|
from ....schemas.v2 import TaskReadV2
|
20
20
|
from ....schemas.v2 import TaskUpdateV2
|
21
|
+
from ...aux.validate_user_settings import verify_user_has_settings
|
21
22
|
from ._aux_functions import _get_task_check_owner
|
22
23
|
from fractal_server.app.models import UserOAuth
|
23
24
|
from fractal_server.app.routes.auth import current_active_user
|
@@ -150,16 +151,18 @@ async def create_task(
|
|
150
151
|
# Set task.owner attribute
|
151
152
|
if user.username:
|
152
153
|
owner = user.username
|
153
|
-
elif user.slurm_user:
|
154
|
-
owner = user.slurm_user
|
155
154
|
else:
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
155
|
+
verify_user_has_settings(user)
|
156
|
+
if user.settings.slurm_user:
|
157
|
+
owner = user.settings.slurm_user
|
158
|
+
else:
|
159
|
+
raise HTTPException(
|
160
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
161
|
+
detail=(
|
162
|
+
"Cannot add a new task because current user does not "
|
163
|
+
"have `username` or `slurm_user` attributes."
|
164
|
+
),
|
165
|
+
)
|
163
166
|
|
164
167
|
# Prepend owner to task.source
|
165
168
|
task.source = f"{owner}:{task.source}"
|
@@ -25,6 +25,7 @@ from ....schemas.v2 import CollectionStateReadV2
|
|
25
25
|
from ....schemas.v2 import CollectionStatusV2
|
26
26
|
from ....schemas.v2 import TaskCollectPipV2
|
27
27
|
from ....schemas.v2 import TaskReadV2
|
28
|
+
from ...aux.validate_user_settings import validate_user_settings
|
28
29
|
from fractal_server.app.models import UserOAuth
|
29
30
|
from fractal_server.app.routes.auth import current_active_user
|
30
31
|
from fractal_server.app.routes.auth import current_active_verified_user
|
@@ -41,7 +42,6 @@ from fractal_server.tasks.v2.endpoint_operations import download_package
|
|
41
42
|
from fractal_server.tasks.v2.endpoint_operations import inspect_package
|
42
43
|
from fractal_server.tasks.v2.utils import get_python_interpreter_v2
|
43
44
|
|
44
|
-
|
45
45
|
router = APIRouter()
|
46
46
|
|
47
47
|
logger = set_logger(__name__)
|
@@ -107,6 +107,11 @@ async def collect_tasks_pip(
|
|
107
107
|
detail=f"Invalid task-collection object. Original error: {e}",
|
108
108
|
)
|
109
109
|
|
110
|
+
# Validate user settings (backend-specific)
|
111
|
+
user_settings = await validate_user_settings(
|
112
|
+
user=user, backend=settings.FRACTAL_RUNNER_BACKEND, db=db
|
113
|
+
)
|
114
|
+
|
110
115
|
# END of SSH/non-SSH common part
|
111
116
|
|
112
117
|
if settings.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
|
@@ -124,11 +129,21 @@ async def collect_tasks_pip(
|
|
124
129
|
db.add(state)
|
125
130
|
await db.commit()
|
126
131
|
|
132
|
+
# User appropriate FractalSSH object
|
133
|
+
ssh_credentials = dict(
|
134
|
+
user=user_settings.ssh_username,
|
135
|
+
host=user_settings.ssh_host,
|
136
|
+
key_path=user_settings.ssh_private_key_path,
|
137
|
+
)
|
138
|
+
fractal_ssh_list = request.app.state.fractal_ssh_list
|
139
|
+
fractal_ssh = fractal_ssh_list.get(**ssh_credentials)
|
140
|
+
|
127
141
|
background_tasks.add_task(
|
128
142
|
background_collect_pip_ssh,
|
129
143
|
state.id,
|
130
144
|
task_pkg,
|
131
|
-
|
145
|
+
fractal_ssh,
|
146
|
+
user_settings.ssh_tasks_dir,
|
132
147
|
)
|
133
148
|
|
134
149
|
response.status_code = status.HTTP_201_CREATED
|
@@ -18,6 +18,7 @@ from ....models.v2 import TaskV2
|
|
18
18
|
from ....schemas.v2 import TaskCollectCustomV2
|
19
19
|
from ....schemas.v2 import TaskCreateV2
|
20
20
|
from ....schemas.v2 import TaskReadV2
|
21
|
+
from ...aux.validate_user_settings import verify_user_has_settings
|
21
22
|
from fractal_server.app.models import UserOAuth
|
22
23
|
from fractal_server.app.routes.auth import current_active_verified_user
|
23
24
|
from fractal_server.string_tools import validate_cmd
|
@@ -112,7 +113,11 @@ async def collect_task_custom(
|
|
112
113
|
package_root = Path(task_collect.package_root)
|
113
114
|
|
114
115
|
# Set task.owner attribute
|
115
|
-
|
116
|
+
if user.username:
|
117
|
+
owner = user.username
|
118
|
+
else:
|
119
|
+
verify_user_has_settings(user)
|
120
|
+
owner = user.settings.slurm_user
|
116
121
|
if owner is None:
|
117
122
|
raise HTTPException(
|
118
123
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
@@ -3,11 +3,11 @@ from fastapi import status
|
|
3
3
|
from sqlalchemy.ext.asyncio import AsyncSession
|
4
4
|
from sqlmodel import select
|
5
5
|
|
6
|
-
from
|
7
|
-
from
|
8
|
-
from
|
9
|
-
from
|
10
|
-
from
|
6
|
+
from fractal_server.app.models.linkusergroup import LinkUserGroup
|
7
|
+
from fractal_server.app.models.security import UserGroup
|
8
|
+
from fractal_server.app.models.security import UserOAuth
|
9
|
+
from fractal_server.app.schemas.user import UserRead
|
10
|
+
from fractal_server.app.schemas.user_group import UserGroupRead
|
11
11
|
|
12
12
|
|
13
13
|
async def _get_single_user_with_group_names(
|
@@ -11,8 +11,12 @@ from ...db import get_async_db
|
|
11
11
|
from ...schemas.user import UserRead
|
12
12
|
from ...schemas.user import UserUpdate
|
13
13
|
from ...schemas.user import UserUpdateStrict
|
14
|
+
from ..aux.validate_user_settings import verify_user_has_settings
|
14
15
|
from ._aux_auth import _get_single_user_with_group_names
|
15
16
|
from fractal_server.app.models import UserOAuth
|
17
|
+
from fractal_server.app.models import UserSettings
|
18
|
+
from fractal_server.app.schemas import UserSettingsReadStrict
|
19
|
+
from fractal_server.app.schemas import UserSettingsUpdateStrict
|
16
20
|
from fractal_server.app.security import get_user_manager
|
17
21
|
from fractal_server.app.security import UserManager
|
18
22
|
|
@@ -62,3 +66,40 @@ async def patch_current_user(
|
|
62
66
|
patched_user, db
|
63
67
|
)
|
64
68
|
return patched_user_with_groups
|
69
|
+
|
70
|
+
|
71
|
+
@router_current_user.get(
|
72
|
+
"/current-user/settings/", response_model=UserSettingsReadStrict
|
73
|
+
)
|
74
|
+
async def get_current_user_settings(
|
75
|
+
current_user: UserOAuth = Depends(current_active_user),
|
76
|
+
db: AsyncSession = Depends(get_async_db),
|
77
|
+
) -> UserSettingsReadStrict:
|
78
|
+
|
79
|
+
verify_user_has_settings(current_user)
|
80
|
+
user_settings = await db.get(UserSettings, current_user.user_settings_id)
|
81
|
+
return user_settings
|
82
|
+
|
83
|
+
|
84
|
+
@router_current_user.patch(
|
85
|
+
"/current-user/settings/", response_model=UserSettingsReadStrict
|
86
|
+
)
|
87
|
+
async def patch_current_user_settings(
|
88
|
+
settings_update: UserSettingsUpdateStrict,
|
89
|
+
current_user: UserOAuth = Depends(current_active_user),
|
90
|
+
db: AsyncSession = Depends(get_async_db),
|
91
|
+
) -> UserSettingsReadStrict:
|
92
|
+
|
93
|
+
verify_user_has_settings(current_user)
|
94
|
+
current_user_settings = await db.get(
|
95
|
+
UserSettings, current_user.user_settings_id
|
96
|
+
)
|
97
|
+
|
98
|
+
for k, v in settings_update.dict(exclude_unset=True).items():
|
99
|
+
setattr(current_user_settings, k, v)
|
100
|
+
|
101
|
+
db.add(current_user_settings)
|
102
|
+
await db.commit()
|
103
|
+
await db.refresh(current_user_settings)
|
104
|
+
|
105
|
+
return current_user_settings
|
@@ -19,11 +19,15 @@ from ...db import get_async_db
|
|
19
19
|
from ...schemas.user import UserRead
|
20
20
|
from ...schemas.user import UserUpdate
|
21
21
|
from ...schemas.user import UserUpdateWithNewGroupIds
|
22
|
+
from ..aux.validate_user_settings import verify_user_has_settings
|
22
23
|
from ._aux_auth import _get_single_user_with_group_ids
|
23
24
|
from fractal_server.app.models import LinkUserGroup
|
24
25
|
from fractal_server.app.models import UserGroup
|
25
26
|
from fractal_server.app.models import UserOAuth
|
27
|
+
from fractal_server.app.models import UserSettings
|
26
28
|
from fractal_server.app.routes.auth._aux_auth import _user_or_404
|
29
|
+
from fractal_server.app.schemas import UserSettingsRead
|
30
|
+
from fractal_server.app.schemas import UserSettingsUpdate
|
27
31
|
from fractal_server.app.security import get_user_manager
|
28
32
|
from fractal_server.app.security import UserManager
|
29
33
|
from fractal_server.logger import set_logger
|
@@ -196,3 +200,41 @@ async def list_users(
|
|
196
200
|
)
|
197
201
|
|
198
202
|
return user_list
|
203
|
+
|
204
|
+
|
205
|
+
@router_users.get(
|
206
|
+
"/users/{user_id}/settings/", response_model=UserSettingsRead
|
207
|
+
)
|
208
|
+
async def get_user_settings(
|
209
|
+
user_id: int,
|
210
|
+
superuser: UserOAuth = Depends(current_active_superuser),
|
211
|
+
db: AsyncSession = Depends(get_async_db),
|
212
|
+
) -> UserSettingsRead:
|
213
|
+
|
214
|
+
user = await _user_or_404(user_id=user_id, db=db)
|
215
|
+
verify_user_has_settings(user)
|
216
|
+
user_settings = await db.get(UserSettings, user.user_settings_id)
|
217
|
+
return user_settings
|
218
|
+
|
219
|
+
|
220
|
+
@router_users.patch(
|
221
|
+
"/users/{user_id}/settings/", response_model=UserSettingsRead
|
222
|
+
)
|
223
|
+
async def patch_user_settings(
|
224
|
+
user_id: int,
|
225
|
+
settings_update: UserSettingsUpdate,
|
226
|
+
superuser: UserOAuth = Depends(current_active_superuser),
|
227
|
+
db: AsyncSession = Depends(get_async_db),
|
228
|
+
) -> UserSettingsRead:
|
229
|
+
user = await _user_or_404(user_id=user_id, db=db)
|
230
|
+
verify_user_has_settings(user)
|
231
|
+
user_settings = await db.get(UserSettings, user.user_settings_id)
|
232
|
+
|
233
|
+
for k, v in settings_update.dict(exclude_unset=True).items():
|
234
|
+
setattr(user_settings, k, v)
|
235
|
+
|
236
|
+
db.add(user_settings)
|
237
|
+
await db.commit()
|
238
|
+
await db.refresh(user_settings)
|
239
|
+
|
240
|
+
return user_settings
|