fractal-server 2.16.5__py3-none-any.whl → 2.17.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 +178 -52
- fractal_server/app/db/__init__.py +9 -11
- fractal_server/app/models/security.py +30 -22
- fractal_server/app/models/user_settings.py +5 -4
- fractal_server/app/models/v2/__init__.py +4 -0
- fractal_server/app/models/v2/job.py +3 -4
- fractal_server/app/models/v2/profile.py +16 -0
- fractal_server/app/models/v2/project.py +5 -0
- fractal_server/app/models/v2/resource.py +130 -0
- fractal_server/app/models/v2/task_group.py +4 -0
- fractal_server/app/routes/admin/v2/__init__.py +4 -0
- fractal_server/app/routes/admin/v2/_aux_functions.py +55 -0
- fractal_server/app/routes/admin/v2/accounting.py +3 -3
- fractal_server/app/routes/admin/v2/impersonate.py +2 -2
- fractal_server/app/routes/admin/v2/job.py +51 -15
- fractal_server/app/routes/admin/v2/profile.py +100 -0
- fractal_server/app/routes/admin/v2/project.py +2 -2
- fractal_server/app/routes/admin/v2/resource.py +222 -0
- fractal_server/app/routes/admin/v2/task.py +59 -32
- fractal_server/app/routes/admin/v2/task_group.py +17 -12
- fractal_server/app/routes/admin/v2/task_group_lifecycle.py +52 -86
- fractal_server/app/routes/api/__init__.py +45 -8
- fractal_server/app/routes/api/v2/_aux_functions.py +17 -1
- fractal_server/app/routes/api/v2/_aux_functions_history.py +2 -2
- fractal_server/app/routes/api/v2/_aux_functions_task_lifecycle.py +3 -3
- fractal_server/app/routes/api/v2/_aux_functions_tasks.py +55 -19
- fractal_server/app/routes/api/v2/_aux_task_group_disambiguation.py +21 -17
- fractal_server/app/routes/api/v2/dataset.py +10 -19
- fractal_server/app/routes/api/v2/history.py +8 -8
- fractal_server/app/routes/api/v2/images.py +5 -5
- fractal_server/app/routes/api/v2/job.py +8 -8
- fractal_server/app/routes/api/v2/pre_submission_checks.py +3 -3
- fractal_server/app/routes/api/v2/project.py +15 -7
- fractal_server/app/routes/api/v2/status_legacy.py +2 -2
- fractal_server/app/routes/api/v2/submit.py +49 -42
- fractal_server/app/routes/api/v2/task.py +26 -8
- fractal_server/app/routes/api/v2/task_collection.py +39 -50
- fractal_server/app/routes/api/v2/task_collection_custom.py +10 -6
- fractal_server/app/routes/api/v2/task_collection_pixi.py +34 -42
- fractal_server/app/routes/api/v2/task_group.py +19 -9
- fractal_server/app/routes/api/v2/task_group_lifecycle.py +43 -86
- fractal_server/app/routes/api/v2/task_version_update.py +3 -3
- fractal_server/app/routes/api/v2/workflow.py +9 -9
- fractal_server/app/routes/api/v2/workflow_import.py +29 -16
- fractal_server/app/routes/api/v2/workflowtask.py +5 -5
- fractal_server/app/routes/auth/__init__.py +34 -5
- fractal_server/app/routes/auth/_aux_auth.py +39 -20
- fractal_server/app/routes/auth/current_user.py +56 -67
- fractal_server/app/routes/auth/group.py +29 -46
- fractal_server/app/routes/auth/oauth.py +55 -38
- fractal_server/app/routes/auth/register.py +2 -2
- fractal_server/app/routes/auth/router.py +4 -2
- fractal_server/app/routes/auth/users.py +29 -53
- fractal_server/app/routes/aux/_runner.py +2 -1
- fractal_server/app/routes/aux/validate_user_profile.py +62 -0
- fractal_server/app/schemas/__init__.py +0 -1
- fractal_server/app/schemas/user.py +43 -13
- fractal_server/app/schemas/user_group.py +2 -1
- fractal_server/app/schemas/v2/__init__.py +12 -0
- fractal_server/app/schemas/v2/profile.py +78 -0
- fractal_server/app/schemas/v2/resource.py +137 -0
- fractal_server/app/schemas/v2/task_collection.py +11 -3
- fractal_server/app/schemas/v2/task_group.py +5 -0
- fractal_server/app/security/__init__.py +174 -75
- fractal_server/app/security/signup_email.py +52 -34
- fractal_server/config/__init__.py +27 -0
- fractal_server/config/_data.py +68 -0
- fractal_server/config/_database.py +59 -0
- fractal_server/config/_email.py +133 -0
- fractal_server/config/_main.py +78 -0
- fractal_server/config/_oauth.py +69 -0
- fractal_server/config/_settings_config.py +7 -0
- fractal_server/data_migrations/2_17_0.py +339 -0
- fractal_server/images/tools.py +3 -3
- fractal_server/logger.py +3 -3
- fractal_server/main.py +17 -23
- fractal_server/migrations/naming_convention.py +1 -1
- fractal_server/migrations/versions/83bc2ad3ffcc_2_17_0.py +195 -0
- fractal_server/runner/config/__init__.py +2 -0
- fractal_server/runner/config/_local.py +21 -0
- fractal_server/runner/config/_slurm.py +129 -0
- fractal_server/runner/config/slurm_mem_to_MB.py +63 -0
- fractal_server/runner/exceptions.py +4 -0
- fractal_server/runner/executors/base_runner.py +17 -7
- fractal_server/runner/executors/local/get_local_config.py +21 -86
- fractal_server/runner/executors/local/runner.py +48 -5
- fractal_server/runner/executors/slurm_common/_batching.py +2 -2
- fractal_server/runner/executors/slurm_common/base_slurm_runner.py +60 -26
- fractal_server/runner/executors/slurm_common/get_slurm_config.py +39 -55
- fractal_server/runner/executors/slurm_common/remote.py +1 -1
- fractal_server/runner/executors/slurm_common/slurm_config.py +214 -0
- fractal_server/runner/executors/slurm_common/slurm_job_task_models.py +1 -1
- fractal_server/runner/executors/slurm_ssh/runner.py +12 -14
- fractal_server/runner/executors/slurm_sudo/_subprocess_run_as_user.py +2 -2
- fractal_server/runner/executors/slurm_sudo/runner.py +12 -12
- fractal_server/runner/v2/_local.py +36 -21
- fractal_server/runner/v2/_slurm_ssh.py +41 -4
- fractal_server/runner/v2/_slurm_sudo.py +42 -12
- fractal_server/runner/v2/db_tools.py +1 -1
- fractal_server/runner/v2/runner.py +3 -11
- fractal_server/runner/v2/runner_functions.py +42 -28
- fractal_server/runner/v2/submit_workflow.py +88 -109
- fractal_server/runner/versions.py +8 -3
- fractal_server/ssh/_fabric.py +6 -6
- fractal_server/tasks/config/__init__.py +3 -0
- fractal_server/tasks/config/_pixi.py +127 -0
- fractal_server/tasks/config/_python.py +51 -0
- fractal_server/tasks/v2/local/_utils.py +7 -7
- fractal_server/tasks/v2/local/collect.py +13 -5
- fractal_server/tasks/v2/local/collect_pixi.py +26 -10
- fractal_server/tasks/v2/local/deactivate.py +7 -1
- fractal_server/tasks/v2/local/deactivate_pixi.py +5 -1
- fractal_server/tasks/v2/local/delete.py +5 -1
- fractal_server/tasks/v2/local/reactivate.py +13 -5
- fractal_server/tasks/v2/local/reactivate_pixi.py +27 -9
- fractal_server/tasks/v2/ssh/_pixi_slurm_ssh.py +11 -10
- fractal_server/tasks/v2/ssh/_utils.py +6 -7
- fractal_server/tasks/v2/ssh/collect.py +19 -12
- fractal_server/tasks/v2/ssh/collect_pixi.py +34 -16
- fractal_server/tasks/v2/ssh/deactivate.py +12 -8
- fractal_server/tasks/v2/ssh/deactivate_pixi.py +14 -10
- fractal_server/tasks/v2/ssh/delete.py +12 -9
- fractal_server/tasks/v2/ssh/reactivate.py +18 -12
- fractal_server/tasks/v2/ssh/reactivate_pixi.py +36 -17
- fractal_server/tasks/v2/templates/4_pip_show.sh +4 -6
- fractal_server/tasks/v2/utils_database.py +2 -2
- fractal_server/tasks/v2/utils_pixi.py +3 -0
- fractal_server/tasks/v2/utils_python_interpreter.py +8 -16
- fractal_server/tasks/v2/utils_templates.py +7 -10
- fractal_server/utils.py +1 -1
- {fractal_server-2.16.5.dist-info → fractal_server-2.17.0.dist-info}/METADATA +8 -10
- {fractal_server-2.16.5.dist-info → fractal_server-2.17.0.dist-info}/RECORD +137 -118
- {fractal_server-2.16.5.dist-info → fractal_server-2.17.0.dist-info}/WHEEL +1 -1
- fractal_server/app/routes/aux/validate_user_settings.py +0 -73
- fractal_server/app/schemas/user_settings.py +0 -67
- fractal_server/app/user_settings.py +0 -42
- fractal_server/config.py +0 -906
- fractal_server/data_migrations/2_14_10.py +0 -48
- fractal_server/runner/executors/slurm_common/_slurm_config.py +0 -471
- /fractal_server/{runner → app}/shutdown.py +0 -0
- {fractal_server-2.16.5.dist-info → fractal_server-2.17.0.dist-info}/entry_points.txt +0 -0
- {fractal_server-2.16.5.dist-info → fractal_server-2.17.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from dotenv.main import DotEnv
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
from sqlalchemy.orm import Session
|
|
10
|
+
from sqlalchemy.sql.operators import is_
|
|
11
|
+
from sqlalchemy.sql.operators import is_not
|
|
12
|
+
from sqlmodel import select
|
|
13
|
+
|
|
14
|
+
from fractal_server.app.db import get_sync_db
|
|
15
|
+
from fractal_server.app.models import Profile
|
|
16
|
+
from fractal_server.app.models import ProjectV2
|
|
17
|
+
from fractal_server.app.models import Resource
|
|
18
|
+
from fractal_server.app.models import TaskGroupV2
|
|
19
|
+
from fractal_server.app.models import UserOAuth
|
|
20
|
+
from fractal_server.app.models import UserSettings
|
|
21
|
+
from fractal_server.app.schemas.v2.profile import cast_serialize_profile
|
|
22
|
+
from fractal_server.app.schemas.v2.resource import cast_serialize_resource
|
|
23
|
+
from fractal_server.config import get_settings
|
|
24
|
+
from fractal_server.runner.config import JobRunnerConfigLocal
|
|
25
|
+
from fractal_server.runner.config import JobRunnerConfigSLURM
|
|
26
|
+
from fractal_server.tasks.config import TasksPixiSettings
|
|
27
|
+
from fractal_server.tasks.config import TasksPythonSettings
|
|
28
|
+
from fractal_server.types import AbsolutePathStr
|
|
29
|
+
from fractal_server.types import ListUniqueNonEmptyString
|
|
30
|
+
from fractal_server.urls import normalize_url
|
|
31
|
+
|
|
32
|
+
logging.basicConfig(level=logging.INFO)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class UserUpdateInfo(BaseModel):
|
|
36
|
+
user_id: int
|
|
37
|
+
project_dir: AbsolutePathStr
|
|
38
|
+
slurm_accounts: ListUniqueNonEmptyString
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ProfileUsersUpdateInfo(BaseModel):
|
|
42
|
+
data: dict[str, Any]
|
|
43
|
+
user_updates: list[UserUpdateInfo]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _get_user_settings(user: UserOAuth, db: Session) -> UserSettings:
|
|
47
|
+
if user.user_settings_id is None:
|
|
48
|
+
sys.exit(f"User {user.email} is active but {user.user_settings_id=}.")
|
|
49
|
+
user_settings = db.get(UserSettings, user.user_settings_id)
|
|
50
|
+
return user_settings
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def assert_user_setting_key(
|
|
54
|
+
user: UserOAuth,
|
|
55
|
+
user_settings: UserSettings,
|
|
56
|
+
keys: list[str],
|
|
57
|
+
) -> None:
|
|
58
|
+
for key in keys:
|
|
59
|
+
if getattr(user_settings, key) is None:
|
|
60
|
+
sys.exit(
|
|
61
|
+
f"User {user.email} is active and verified but their "
|
|
62
|
+
f"user settings have {key}=None."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def prepare_profile_and_user_updates() -> dict[str, ProfileUsersUpdateInfo]:
|
|
67
|
+
settings = get_settings()
|
|
68
|
+
profiles_and_users: dict[str, ProfileUsersUpdateInfo] = {}
|
|
69
|
+
with next(get_sync_db()) as db:
|
|
70
|
+
# Get active&verified users
|
|
71
|
+
res = db.execute(
|
|
72
|
+
select(UserOAuth)
|
|
73
|
+
.where(is_(UserOAuth.is_active, True))
|
|
74
|
+
.where(is_(UserOAuth.is_verified, True))
|
|
75
|
+
.order_by(UserOAuth.id)
|
|
76
|
+
)
|
|
77
|
+
for user in res.unique().scalars().all():
|
|
78
|
+
# Get user settings
|
|
79
|
+
user_settings = _get_user_settings(user=user, db=db)
|
|
80
|
+
assert_user_setting_key(user, user_settings, ["project_dir"])
|
|
81
|
+
|
|
82
|
+
# Prepare profile data and user update
|
|
83
|
+
new_profile_data = dict()
|
|
84
|
+
if settings.FRACTAL_RUNNER_BACKEND == "local":
|
|
85
|
+
username = None
|
|
86
|
+
if settings.FRACTAL_RUNNER_BACKEND == "slurm_sudo":
|
|
87
|
+
assert_user_setting_key(user, user_settings, ["slurm_user"])
|
|
88
|
+
username = user_settings.slurm_user
|
|
89
|
+
elif settings.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
|
|
90
|
+
assert_user_setting_key(
|
|
91
|
+
user,
|
|
92
|
+
user_settings,
|
|
93
|
+
[
|
|
94
|
+
"ssh_username",
|
|
95
|
+
"ssh_private_key_path",
|
|
96
|
+
"ssh_tasks_dir",
|
|
97
|
+
"ssh_jobs_dir",
|
|
98
|
+
],
|
|
99
|
+
)
|
|
100
|
+
username = user_settings.ssh_username
|
|
101
|
+
new_profile_data.update(
|
|
102
|
+
ssh_key_path=user_settings.ssh_private_key_path,
|
|
103
|
+
tasks_remote_dir=normalize_url(
|
|
104
|
+
user_settings.ssh_tasks_dir
|
|
105
|
+
),
|
|
106
|
+
jobs_remote_dir=normalize_url(user_settings.ssh_jobs_dir),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
new_profile_data.update(
|
|
110
|
+
name=f"Profile {username}",
|
|
111
|
+
username=username,
|
|
112
|
+
resource_type=settings.FRACTAL_RUNNER_BACKEND,
|
|
113
|
+
)
|
|
114
|
+
cast_serialize_profile(new_profile_data)
|
|
115
|
+
|
|
116
|
+
user_update_info = UserUpdateInfo(
|
|
117
|
+
user_id=user.id,
|
|
118
|
+
project_dir=normalize_url(user_settings.project_dir),
|
|
119
|
+
slurm_accounts=user_settings.slurm_accounts or [],
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if username in profiles_and_users.keys():
|
|
123
|
+
if profiles_and_users[username].data != new_profile_data:
|
|
124
|
+
error_msg = (
|
|
125
|
+
"Profile data mismatch.\n"
|
|
126
|
+
f"{profiles_and_users[username].data=}\n"
|
|
127
|
+
f"{new_profile_data=}"
|
|
128
|
+
)
|
|
129
|
+
logging.error(error_msg)
|
|
130
|
+
sys.exit(error_msg)
|
|
131
|
+
profiles_and_users[username].user_updates.append(
|
|
132
|
+
user_update_info
|
|
133
|
+
)
|
|
134
|
+
else:
|
|
135
|
+
profiles_and_users[username] = ProfileUsersUpdateInfo(
|
|
136
|
+
data=new_profile_data,
|
|
137
|
+
user_updates=[user_update_info],
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return profiles_and_users
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def get_old_dotenv_variables() -> dict[str, str | None]:
|
|
144
|
+
"""
|
|
145
|
+
See
|
|
146
|
+
https://github.com/fractal-analytics-platform/fractal-server/blob/2.16.x/fractal_server/config.py
|
|
147
|
+
"""
|
|
148
|
+
OLD_DOTENV_FILE = ".fractal_server.env.old"
|
|
149
|
+
return dict(
|
|
150
|
+
**DotEnv(
|
|
151
|
+
dotenv_path=OLD_DOTENV_FILE,
|
|
152
|
+
override=False,
|
|
153
|
+
).dict()
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def get_TasksPythonSettings(
|
|
158
|
+
old_config: dict[str, str | None]
|
|
159
|
+
) -> dict[str, Any]:
|
|
160
|
+
versions = {}
|
|
161
|
+
for version_underscore in ["3_9", "3_10", "3_11", "3_12"]:
|
|
162
|
+
key = f"FRACTAL_TASKS_PYTHON_{version_underscore}"
|
|
163
|
+
version_dot = version_underscore.replace("_", ".")
|
|
164
|
+
value = old_config.get(key, None)
|
|
165
|
+
if value is not None:
|
|
166
|
+
versions[version_dot] = value
|
|
167
|
+
obj = TasksPythonSettings(
|
|
168
|
+
default_version=old_config["FRACTAL_TASKS_PYTHON_DEFAULT_VERSION"],
|
|
169
|
+
versions=versions,
|
|
170
|
+
pip_cache_dir=old_config.get("FRACTAL_PIP_CACHE_DIR", None),
|
|
171
|
+
)
|
|
172
|
+
return obj.model_dump()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def get_TasksPixiSettings(old_config: dict[str, str | None]) -> dict[str, Any]:
|
|
176
|
+
pixi_file = old_config.get("FRACTAL_PIXI_CONFIG_FILE", None)
|
|
177
|
+
if pixi_file is None:
|
|
178
|
+
return {}
|
|
179
|
+
with open(pixi_file) as f:
|
|
180
|
+
old_pixi_config = json.load(f)
|
|
181
|
+
TasksPixiSettings(**old_pixi_config)
|
|
182
|
+
return old_pixi_config
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def get_JobRunnerConfigSLURM(
|
|
186
|
+
old_config: dict[str, str | None]
|
|
187
|
+
) -> dict[str, Any]:
|
|
188
|
+
slurm_file = old_config["FRACTAL_SLURM_CONFIG_FILE"]
|
|
189
|
+
with open(slurm_file) as f:
|
|
190
|
+
old_slurm_config = json.load(f)
|
|
191
|
+
JobRunnerConfigSLURM(**old_slurm_config)
|
|
192
|
+
return old_slurm_config
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def get_JobRunnerConfigLocal(
|
|
196
|
+
old_config: dict[str, str | None]
|
|
197
|
+
) -> dict[str, Any]:
|
|
198
|
+
local_file = old_config.get("FRACTAL_LOCAL_CONFIG_FILE", None)
|
|
199
|
+
if local_file is None or not Path(local_file).exists():
|
|
200
|
+
return JobRunnerConfigLocal().model_dump()
|
|
201
|
+
else:
|
|
202
|
+
with open(local_file) as f:
|
|
203
|
+
old_local_config = json.load(f)
|
|
204
|
+
JobRunnerConfigLocal(**old_local_config)
|
|
205
|
+
return old_local_config
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def get_ssh_host() -> str:
|
|
209
|
+
with next(get_sync_db()) as db:
|
|
210
|
+
res = db.execute(
|
|
211
|
+
select(UserSettings.ssh_host).where(
|
|
212
|
+
is_not(UserSettings.ssh_host, None)
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
hosts = res.scalars().all()
|
|
216
|
+
if len(set(hosts)) > 1:
|
|
217
|
+
host = max(set(hosts), key=hosts.count)
|
|
218
|
+
print(f"MOST FREQUENT HOST: {host}")
|
|
219
|
+
else:
|
|
220
|
+
host = hosts[0]
|
|
221
|
+
return host
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def prepare_resource_data(old_config: dict[str, str | None]) -> dict[str, Any]:
|
|
225
|
+
settings = get_settings()
|
|
226
|
+
|
|
227
|
+
resource_data = dict(
|
|
228
|
+
type=settings.FRACTAL_RUNNER_BACKEND,
|
|
229
|
+
name="Resource Name",
|
|
230
|
+
tasks_python_config=get_TasksPythonSettings(old_config),
|
|
231
|
+
tasks_pixi_config=get_TasksPixiSettings(old_config),
|
|
232
|
+
tasks_local_dir=old_config["FRACTAL_TASKS_DIR"],
|
|
233
|
+
jobs_local_dir=old_config["FRACTAL_RUNNER_WORKING_BASE_DIR"],
|
|
234
|
+
jobs_poll_interval=int(
|
|
235
|
+
old_config.get("FRACTAL_SLURM_POLL_INTERVAL", 15)
|
|
236
|
+
),
|
|
237
|
+
)
|
|
238
|
+
if settings.FRACTAL_RUNNER_BACKEND == "local":
|
|
239
|
+
resource_data["jobs_runner_config"] = get_JobRunnerConfigLocal(
|
|
240
|
+
old_config
|
|
241
|
+
)
|
|
242
|
+
elif settings.FRACTAL_RUNNER_BACKEND == "slurm_sudo":
|
|
243
|
+
resource_data["jobs_slurm_python_worker"] = old_config[
|
|
244
|
+
"FRACTAL_SLURM_WORKER_PYTHON"
|
|
245
|
+
]
|
|
246
|
+
resource_data["jobs_runner_config"] = get_JobRunnerConfigSLURM(
|
|
247
|
+
old_config
|
|
248
|
+
)
|
|
249
|
+
else:
|
|
250
|
+
resource_data["jobs_slurm_python_worker"] = old_config[
|
|
251
|
+
"FRACTAL_SLURM_WORKER_PYTHON"
|
|
252
|
+
]
|
|
253
|
+
resource_data["jobs_runner_config"] = get_JobRunnerConfigSLURM(
|
|
254
|
+
old_config
|
|
255
|
+
)
|
|
256
|
+
resource_data["host"] = get_ssh_host()
|
|
257
|
+
|
|
258
|
+
resource_data = cast_serialize_resource(resource_data)
|
|
259
|
+
|
|
260
|
+
return resource_data
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def fix_db():
|
|
264
|
+
logging.info("START preliminary checks.")
|
|
265
|
+
|
|
266
|
+
# Read old env file
|
|
267
|
+
old_config = get_old_dotenv_variables()
|
|
268
|
+
|
|
269
|
+
# Prepare resource data
|
|
270
|
+
logging.info("START prepare_resource_data")
|
|
271
|
+
resource_data = prepare_resource_data(old_config)
|
|
272
|
+
logging.info("END prepare_resource_data")
|
|
273
|
+
|
|
274
|
+
# Prepare profile/users data
|
|
275
|
+
logging.info("START prepare_profile_and_user_updates")
|
|
276
|
+
profile_and_user_updates = prepare_profile_and_user_updates()
|
|
277
|
+
logging.info("END prepare_profile_and_user_updates")
|
|
278
|
+
|
|
279
|
+
logging.info("END preliminary checks.")
|
|
280
|
+
print()
|
|
281
|
+
|
|
282
|
+
with next(get_sync_db()) as db:
|
|
283
|
+
# Create new resource
|
|
284
|
+
resource = Resource(**resource_data)
|
|
285
|
+
db.add(resource)
|
|
286
|
+
db.commit()
|
|
287
|
+
db.refresh(resource)
|
|
288
|
+
db.expunge(resource)
|
|
289
|
+
resource_id = resource.id
|
|
290
|
+
logging.info(f"Created resource with {resource_id=}.")
|
|
291
|
+
|
|
292
|
+
# Update task groups
|
|
293
|
+
res = db.execute(select(TaskGroupV2).order_by(TaskGroupV2.id))
|
|
294
|
+
for taskgroup in res.scalars().all():
|
|
295
|
+
taskgroup.resource_id = resource_id
|
|
296
|
+
db.add(taskgroup)
|
|
297
|
+
db.commit()
|
|
298
|
+
logging.info(f"Set {resource_id=} foreign key for all task groups.")
|
|
299
|
+
|
|
300
|
+
# Update projects
|
|
301
|
+
res = db.execute(select(ProjectV2).order_by(ProjectV2.id))
|
|
302
|
+
for project in res.scalars().all():
|
|
303
|
+
project.resource_id = resource_id
|
|
304
|
+
db.add(project)
|
|
305
|
+
db.commit()
|
|
306
|
+
logging.info(f"Set {resource_id=} foreign key for all projects.")
|
|
307
|
+
print()
|
|
308
|
+
|
|
309
|
+
db.expunge_all()
|
|
310
|
+
|
|
311
|
+
for _, info in profile_and_user_updates.items():
|
|
312
|
+
# Create profile
|
|
313
|
+
profile_data = info.data
|
|
314
|
+
profile_data["resource_id"] = resource_id
|
|
315
|
+
profile = Profile(**profile_data)
|
|
316
|
+
db.add(profile)
|
|
317
|
+
db.commit()
|
|
318
|
+
db.refresh(profile)
|
|
319
|
+
db.expunge(profile)
|
|
320
|
+
profile_id = profile.id
|
|
321
|
+
logging.info(
|
|
322
|
+
f"Created profile '{profile.name}', with {profile.id=}."
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
# Update users
|
|
326
|
+
for user_update in info.user_updates:
|
|
327
|
+
user = db.get(UserOAuth, user_update.user_id)
|
|
328
|
+
user.profile_id = profile_id
|
|
329
|
+
user.project_dir = user_update.project_dir
|
|
330
|
+
user.slurm_accounts = user_update.slurm_accounts
|
|
331
|
+
db.add(user)
|
|
332
|
+
logging.info(f"Updated {user.email} with {user.project_dir=}.")
|
|
333
|
+
logging.info(
|
|
334
|
+
f"Associated {user.email} to profile {profile.name}."
|
|
335
|
+
)
|
|
336
|
+
print()
|
|
337
|
+
db.commit()
|
|
338
|
+
|
|
339
|
+
logging.info("END - all ok.")
|
fractal_server/images/tools.py
CHANGED
|
@@ -16,7 +16,7 @@ def find_image_by_zarr_url(
|
|
|
16
16
|
"""
|
|
17
17
|
Return a copy of the image with a given zarr_url, and its positional index.
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Args:
|
|
20
20
|
images: List of images.
|
|
21
21
|
zarr_url: Path that the returned image must have.
|
|
22
22
|
|
|
@@ -40,7 +40,7 @@ def match_filter(
|
|
|
40
40
|
"""
|
|
41
41
|
Find whether an image matches a filter set.
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
Args:
|
|
44
44
|
image: A single image.
|
|
45
45
|
type_filters:
|
|
46
46
|
attribute_filters:
|
|
@@ -70,7 +70,7 @@ def filter_image_list(
|
|
|
70
70
|
"""
|
|
71
71
|
Compute a sublist with images that match a filter set.
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
Args:
|
|
74
74
|
images: A list of images.
|
|
75
75
|
type_filters:
|
|
76
76
|
attribute_filters:
|
fractal_server/logger.py
CHANGED
|
@@ -44,7 +44,7 @@ def get_logger(logger_name: str | None = None) -> logging.Logger:
|
|
|
44
44
|
close_logger(logger)
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
Args:
|
|
48
48
|
logger_name: Name of logger
|
|
49
49
|
Returns:
|
|
50
50
|
Logger with name `logger_name`
|
|
@@ -124,7 +124,7 @@ def close_logger(logger: logging.Logger) -> None:
|
|
|
124
124
|
"""
|
|
125
125
|
Close all handlers associated to a `logging.Logger` object
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
Args:
|
|
128
128
|
logger: The actual logger
|
|
129
129
|
"""
|
|
130
130
|
for handle in logger.handlers:
|
|
@@ -135,7 +135,7 @@ def reset_logger_handlers(logger: logging.Logger) -> None:
|
|
|
135
135
|
"""
|
|
136
136
|
Close and remove all handlers associated to a `logging.Logger` object
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
Args:
|
|
139
139
|
logger: The actual logger
|
|
140
140
|
"""
|
|
141
141
|
close_logger(logger)
|
fractal_server/main.py
CHANGED
|
@@ -1,34 +1,22 @@
|
|
|
1
|
-
# Copyright 2022 (C) Friedrich Miescher Institute for Biomedical Research and
|
|
2
|
-
# University of Zurich
|
|
3
|
-
#
|
|
4
|
-
# Original authors:
|
|
5
|
-
# Jacopo Nespolo <jacopo.nespolo@exact-lab.it>
|
|
6
|
-
# Marco Franzon <marco.franzon@exact-lab.it>
|
|
7
|
-
# Tommaso Comaprin <tommaso.comparin@exact-lab.it>
|
|
8
|
-
#
|
|
9
|
-
# This file is part of Fractal and was originally developed by eXact lab S.r.l.
|
|
10
|
-
# <exact-lab.it> under contract with Liberali Lab from the Friedrich Miescher
|
|
11
|
-
# Institute for Biomedical Research and Pelkmans Lab from the University of
|
|
12
|
-
# Zurich.
|
|
13
|
-
"""
|
|
14
|
-
# Application factory
|
|
15
|
-
|
|
16
|
-
This module sets up the FastAPI application that serves the Fractal Server.
|
|
17
|
-
"""
|
|
18
1
|
import os
|
|
19
2
|
from contextlib import asynccontextmanager
|
|
3
|
+
from itertools import chain
|
|
20
4
|
|
|
21
5
|
from fastapi import FastAPI
|
|
22
6
|
|
|
23
7
|
from .app.routes.aux._runner import _backend_supports_shutdown
|
|
8
|
+
from .app.shutdown import cleanup_after_shutdown
|
|
9
|
+
from .config import get_data_settings
|
|
10
|
+
from .config import get_db_settings
|
|
11
|
+
from .config import get_email_settings
|
|
24
12
|
from .config import get_settings
|
|
25
13
|
from .logger import config_uvicorn_loggers
|
|
26
14
|
from .logger import get_logger
|
|
27
15
|
from .logger import reset_logger_handlers
|
|
28
16
|
from .logger import set_logger
|
|
29
|
-
from .runner.shutdown import cleanup_after_shutdown
|
|
30
17
|
from .syringe import Inject
|
|
31
18
|
from fractal_server import __VERSION__
|
|
19
|
+
from fractal_server.app.schemas.v2 import ResourceType
|
|
32
20
|
|
|
33
21
|
|
|
34
22
|
def collect_routers(app: FastAPI) -> None:
|
|
@@ -63,11 +51,17 @@ def check_settings() -> None:
|
|
|
63
51
|
ValidationError: If the configuration is invalid.
|
|
64
52
|
"""
|
|
65
53
|
settings = Inject(get_settings)
|
|
66
|
-
|
|
67
|
-
|
|
54
|
+
db_settings = Inject(get_db_settings)
|
|
55
|
+
email_settings = Inject(get_email_settings)
|
|
56
|
+
data_settings = Inject(get_data_settings)
|
|
68
57
|
logger = set_logger("fractal_server_settings")
|
|
69
58
|
logger.debug("Fractal Settings:")
|
|
70
|
-
for key, value in
|
|
59
|
+
for key, value in chain(
|
|
60
|
+
db_settings.model_dump().items(),
|
|
61
|
+
settings.model_dump().items(),
|
|
62
|
+
email_settings.model_dump().items(),
|
|
63
|
+
data_settings.model_dump().items(),
|
|
64
|
+
):
|
|
71
65
|
if any(s in key.upper() for s in ["PASSWORD", "SECRET", "KEY"]):
|
|
72
66
|
value = "*****"
|
|
73
67
|
logger.debug(f" {key}: {value}")
|
|
@@ -82,7 +76,7 @@ async def lifespan(app: FastAPI):
|
|
|
82
76
|
check_settings()
|
|
83
77
|
settings = Inject(get_settings)
|
|
84
78
|
|
|
85
|
-
if settings.FRACTAL_RUNNER_BACKEND ==
|
|
79
|
+
if settings.FRACTAL_RUNNER_BACKEND == ResourceType.SLURM_SSH:
|
|
86
80
|
from fractal_server.ssh._fabric import FractalSSHList
|
|
87
81
|
|
|
88
82
|
app.state.fractal_ssh_list = FractalSSHList()
|
|
@@ -103,7 +97,7 @@ async def lifespan(app: FastAPI):
|
|
|
103
97
|
logger = get_logger("fractal_server.lifespan")
|
|
104
98
|
logger.info("[teardown] START")
|
|
105
99
|
|
|
106
|
-
if settings.FRACTAL_RUNNER_BACKEND ==
|
|
100
|
+
if settings.FRACTAL_RUNNER_BACKEND == ResourceType.SLURM_SSH:
|
|
107
101
|
logger.info(
|
|
108
102
|
"[teardown] Close FractalSSH connections "
|
|
109
103
|
f"(current size: {app.state.fractal_ssh_list.size})."
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
NAMING_CONVENTION = {
|
|
2
2
|
"ix": "ix_%(column_0_label)s",
|
|
3
3
|
"uq": "uq_%(table_name)s_%(column_0_name)s",
|
|
4
|
-
"ck": "ck_%(table_name)s_
|
|
4
|
+
"ck": "ck_%(table_name)s_%(constraint_name)s",
|
|
5
5
|
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
|
|
6
6
|
"pk": "pk_%(table_name)s",
|
|
7
7
|
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""2.17.0
|
|
2
|
+
|
|
3
|
+
Revision ID: 83bc2ad3ffcc
|
|
4
|
+
Revises: 981d588fe248
|
|
5
|
+
Create Date: 2025-10-30 14:16:53.639006
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
import sqlalchemy as sa
|
|
9
|
+
import sqlmodel
|
|
10
|
+
from alembic import op
|
|
11
|
+
from sqlalchemy.dialects import postgresql
|
|
12
|
+
|
|
13
|
+
# revision identifiers, used by Alembic.
|
|
14
|
+
revision = "83bc2ad3ffcc"
|
|
15
|
+
down_revision = "981d588fe248"
|
|
16
|
+
branch_labels = None
|
|
17
|
+
depends_on = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def upgrade() -> None:
|
|
21
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
22
|
+
op.create_table(
|
|
23
|
+
"resource",
|
|
24
|
+
sa.Column("id", sa.Integer(), nullable=False),
|
|
25
|
+
sa.Column("type", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
|
26
|
+
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
|
27
|
+
sa.Column(
|
|
28
|
+
"timestamp_created", sa.DateTime(timezone=True), nullable=False
|
|
29
|
+
),
|
|
30
|
+
sa.Column("host", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
|
31
|
+
sa.Column(
|
|
32
|
+
"jobs_local_dir",
|
|
33
|
+
sqlmodel.sql.sqltypes.AutoString(),
|
|
34
|
+
nullable=False,
|
|
35
|
+
),
|
|
36
|
+
sa.Column(
|
|
37
|
+
"jobs_runner_config",
|
|
38
|
+
postgresql.JSONB(astext_type=sa.Text()),
|
|
39
|
+
server_default="{}",
|
|
40
|
+
nullable=False,
|
|
41
|
+
),
|
|
42
|
+
sa.Column(
|
|
43
|
+
"jobs_slurm_python_worker",
|
|
44
|
+
sqlmodel.sql.sqltypes.AutoString(),
|
|
45
|
+
nullable=True,
|
|
46
|
+
),
|
|
47
|
+
sa.Column("jobs_poll_interval", sa.Integer(), nullable=False),
|
|
48
|
+
sa.Column(
|
|
49
|
+
"tasks_local_dir",
|
|
50
|
+
sqlmodel.sql.sqltypes.AutoString(),
|
|
51
|
+
nullable=False,
|
|
52
|
+
),
|
|
53
|
+
sa.Column(
|
|
54
|
+
"tasks_python_config",
|
|
55
|
+
postgresql.JSONB(astext_type=sa.Text()),
|
|
56
|
+
server_default="{}",
|
|
57
|
+
nullable=False,
|
|
58
|
+
),
|
|
59
|
+
sa.Column(
|
|
60
|
+
"tasks_pixi_config",
|
|
61
|
+
postgresql.JSONB(astext_type=sa.Text()),
|
|
62
|
+
server_default="{}",
|
|
63
|
+
nullable=False,
|
|
64
|
+
),
|
|
65
|
+
sa.CheckConstraint(
|
|
66
|
+
"(type = 'local') OR (jobs_slurm_python_worker IS NOT NULL)",
|
|
67
|
+
name=op.f("ck_resource_jobs_slurm_python_worker_set"),
|
|
68
|
+
),
|
|
69
|
+
sa.CheckConstraint(
|
|
70
|
+
"type IN ('local', 'slurm_sudo', 'slurm_ssh')",
|
|
71
|
+
name=op.f("ck_resource_correct_type"),
|
|
72
|
+
),
|
|
73
|
+
sa.PrimaryKeyConstraint("id", name=op.f("pk_resource")),
|
|
74
|
+
sa.UniqueConstraint("name", name=op.f("uq_resource_name")),
|
|
75
|
+
)
|
|
76
|
+
op.create_table(
|
|
77
|
+
"profile",
|
|
78
|
+
sa.Column("id", sa.Integer(), nullable=False),
|
|
79
|
+
sa.Column("resource_id", sa.Integer(), nullable=False),
|
|
80
|
+
sa.Column(
|
|
81
|
+
"resource_type", sqlmodel.sql.sqltypes.AutoString(), nullable=False
|
|
82
|
+
),
|
|
83
|
+
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
|
84
|
+
sa.Column(
|
|
85
|
+
"username", sqlmodel.sql.sqltypes.AutoString(), nullable=True
|
|
86
|
+
),
|
|
87
|
+
sa.Column(
|
|
88
|
+
"ssh_key_path", sqlmodel.sql.sqltypes.AutoString(), nullable=True
|
|
89
|
+
),
|
|
90
|
+
sa.Column(
|
|
91
|
+
"jobs_remote_dir",
|
|
92
|
+
sqlmodel.sql.sqltypes.AutoString(),
|
|
93
|
+
nullable=True,
|
|
94
|
+
),
|
|
95
|
+
sa.Column(
|
|
96
|
+
"tasks_remote_dir",
|
|
97
|
+
sqlmodel.sql.sqltypes.AutoString(),
|
|
98
|
+
nullable=True,
|
|
99
|
+
),
|
|
100
|
+
sa.ForeignKeyConstraint(
|
|
101
|
+
["resource_id"],
|
|
102
|
+
["resource.id"],
|
|
103
|
+
name=op.f("fk_profile_resource_id_resource"),
|
|
104
|
+
ondelete="RESTRICT",
|
|
105
|
+
),
|
|
106
|
+
sa.PrimaryKeyConstraint("id", name=op.f("pk_profile")),
|
|
107
|
+
sa.UniqueConstraint("name", name=op.f("uq_profile_name")),
|
|
108
|
+
)
|
|
109
|
+
with op.batch_alter_table("projectv2", schema=None) as batch_op:
|
|
110
|
+
batch_op.add_column(
|
|
111
|
+
sa.Column("resource_id", sa.Integer(), nullable=True)
|
|
112
|
+
)
|
|
113
|
+
batch_op.create_foreign_key(
|
|
114
|
+
batch_op.f("fk_projectv2_resource_id_resource"),
|
|
115
|
+
"resource",
|
|
116
|
+
["resource_id"],
|
|
117
|
+
["id"],
|
|
118
|
+
ondelete="RESTRICT",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
with op.batch_alter_table("taskgroupv2", schema=None) as batch_op:
|
|
122
|
+
batch_op.add_column(
|
|
123
|
+
sa.Column("resource_id", sa.Integer(), nullable=True)
|
|
124
|
+
)
|
|
125
|
+
batch_op.create_foreign_key(
|
|
126
|
+
batch_op.f("fk_taskgroupv2_resource_id_resource"),
|
|
127
|
+
"resource",
|
|
128
|
+
["resource_id"],
|
|
129
|
+
["id"],
|
|
130
|
+
ondelete="RESTRICT",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
with op.batch_alter_table("user_oauth", schema=None) as batch_op:
|
|
134
|
+
batch_op.add_column(
|
|
135
|
+
sa.Column("profile_id", sa.Integer(), nullable=True)
|
|
136
|
+
)
|
|
137
|
+
batch_op.add_column(
|
|
138
|
+
sa.Column(
|
|
139
|
+
"project_dir",
|
|
140
|
+
sa.String(),
|
|
141
|
+
server_default="/PLACEHOLDER",
|
|
142
|
+
nullable=False,
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
batch_op.add_column(
|
|
146
|
+
sa.Column(
|
|
147
|
+
"slurm_accounts",
|
|
148
|
+
postgresql.ARRAY(sa.String()),
|
|
149
|
+
server_default="{}",
|
|
150
|
+
nullable=True,
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
batch_op.create_foreign_key(
|
|
154
|
+
batch_op.f("fk_user_oauth_profile_id_profile"),
|
|
155
|
+
"profile",
|
|
156
|
+
["profile_id"],
|
|
157
|
+
["id"],
|
|
158
|
+
ondelete="RESTRICT",
|
|
159
|
+
)
|
|
160
|
+
batch_op.drop_column("username")
|
|
161
|
+
|
|
162
|
+
# ### end Alembic commands ###
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def downgrade() -> None:
|
|
166
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
167
|
+
with op.batch_alter_table("user_oauth", schema=None) as batch_op:
|
|
168
|
+
batch_op.add_column(
|
|
169
|
+
sa.Column(
|
|
170
|
+
"username", sa.VARCHAR(), autoincrement=False, nullable=True
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
batch_op.drop_constraint(
|
|
174
|
+
batch_op.f("fk_user_oauth_profile_id_profile"), type_="foreignkey"
|
|
175
|
+
)
|
|
176
|
+
batch_op.drop_column("slurm_accounts")
|
|
177
|
+
batch_op.drop_column("project_dir")
|
|
178
|
+
batch_op.drop_column("profile_id")
|
|
179
|
+
|
|
180
|
+
with op.batch_alter_table("taskgroupv2", schema=None) as batch_op:
|
|
181
|
+
batch_op.drop_constraint(
|
|
182
|
+
batch_op.f("fk_taskgroupv2_resource_id_resource"),
|
|
183
|
+
type_="foreignkey",
|
|
184
|
+
)
|
|
185
|
+
batch_op.drop_column("resource_id")
|
|
186
|
+
|
|
187
|
+
with op.batch_alter_table("projectv2", schema=None) as batch_op:
|
|
188
|
+
batch_op.drop_constraint(
|
|
189
|
+
batch_op.f("fk_projectv2_resource_id_resource"), type_="foreignkey"
|
|
190
|
+
)
|
|
191
|
+
batch_op.drop_column("resource_id")
|
|
192
|
+
|
|
193
|
+
op.drop_table("profile")
|
|
194
|
+
op.drop_table("resource")
|
|
195
|
+
# ### end Alembic commands ###
|