fractal-server 2.8.0__py3-none-any.whl → 2.9.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.
Files changed (82) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/app/db/__init__.py +2 -35
  3. fractal_server/app/models/v2/__init__.py +3 -3
  4. fractal_server/app/models/v2/task.py +0 -72
  5. fractal_server/app/models/v2/task_group.py +113 -0
  6. fractal_server/app/routes/admin/v1.py +13 -30
  7. fractal_server/app/routes/admin/v2/__init__.py +4 -0
  8. fractal_server/app/routes/admin/v2/job.py +13 -24
  9. fractal_server/app/routes/admin/v2/task.py +13 -0
  10. fractal_server/app/routes/admin/v2/task_group.py +75 -14
  11. fractal_server/app/routes/admin/v2/task_group_lifecycle.py +267 -0
  12. fractal_server/app/routes/api/v1/project.py +7 -19
  13. fractal_server/app/routes/api/v2/__init__.py +11 -2
  14. fractal_server/app/routes/api/v2/{_aux_functions_task_collection.py → _aux_functions_task_lifecycle.py} +83 -0
  15. fractal_server/app/routes/api/v2/_aux_functions_tasks.py +27 -17
  16. fractal_server/app/routes/api/v2/submit.py +19 -24
  17. fractal_server/app/routes/api/v2/task_collection.py +33 -65
  18. fractal_server/app/routes/api/v2/task_collection_custom.py +3 -3
  19. fractal_server/app/routes/api/v2/task_group.py +86 -14
  20. fractal_server/app/routes/api/v2/task_group_lifecycle.py +272 -0
  21. fractal_server/app/routes/api/v2/workflow.py +1 -1
  22. fractal_server/app/routes/api/v2/workflow_import.py +2 -2
  23. fractal_server/app/routes/auth/current_user.py +60 -17
  24. fractal_server/app/routes/auth/group.py +67 -39
  25. fractal_server/app/routes/auth/users.py +97 -99
  26. fractal_server/app/routes/aux/__init__.py +20 -0
  27. fractal_server/app/runner/executors/slurm/_slurm_config.py +0 -17
  28. fractal_server/app/runner/executors/slurm/ssh/executor.py +49 -204
  29. fractal_server/app/runner/executors/slurm/sudo/executor.py +26 -109
  30. fractal_server/app/runner/executors/slurm/utils_executors.py +58 -0
  31. fractal_server/app/runner/v2/_local_experimental/executor.py +2 -1
  32. fractal_server/app/schemas/_validators.py +1 -16
  33. fractal_server/app/schemas/user.py +16 -10
  34. fractal_server/app/schemas/user_group.py +0 -11
  35. fractal_server/app/schemas/v1/applyworkflow.py +0 -8
  36. fractal_server/app/schemas/v1/dataset.py +0 -5
  37. fractal_server/app/schemas/v1/project.py +0 -5
  38. fractal_server/app/schemas/v1/state.py +0 -5
  39. fractal_server/app/schemas/v1/workflow.py +0 -5
  40. fractal_server/app/schemas/v2/__init__.py +4 -2
  41. fractal_server/app/schemas/v2/dataset.py +1 -7
  42. fractal_server/app/schemas/v2/job.py +0 -8
  43. fractal_server/app/schemas/v2/project.py +0 -5
  44. fractal_server/app/schemas/v2/task_collection.py +13 -31
  45. fractal_server/app/schemas/v2/task_group.py +59 -8
  46. fractal_server/app/schemas/v2/workflow.py +0 -5
  47. fractal_server/app/security/__init__.py +17 -0
  48. fractal_server/config.py +61 -59
  49. fractal_server/migrations/versions/d256a7379ab8_taskgroup_activity_and_venv_info_to_.py +117 -0
  50. fractal_server/ssh/_fabric.py +156 -83
  51. fractal_server/string_tools.py +10 -3
  52. fractal_server/tasks/utils.py +2 -12
  53. fractal_server/tasks/v2/local/__init__.py +3 -0
  54. fractal_server/tasks/v2/local/_utils.py +70 -0
  55. fractal_server/tasks/v2/local/collect.py +291 -0
  56. fractal_server/tasks/v2/local/deactivate.py +218 -0
  57. fractal_server/tasks/v2/local/reactivate.py +159 -0
  58. fractal_server/tasks/v2/ssh/__init__.py +3 -0
  59. fractal_server/tasks/v2/ssh/_utils.py +87 -0
  60. fractal_server/tasks/v2/ssh/collect.py +311 -0
  61. fractal_server/tasks/v2/ssh/deactivate.py +253 -0
  62. fractal_server/tasks/v2/ssh/reactivate.py +202 -0
  63. fractal_server/tasks/v2/templates/{_2_preliminary_pip_operations.sh → 1_create_venv.sh} +6 -7
  64. fractal_server/tasks/v2/templates/{_3_pip_install.sh → 2_pip_install.sh} +8 -1
  65. fractal_server/tasks/v2/templates/{_4_pip_freeze.sh → 3_pip_freeze.sh} +0 -7
  66. fractal_server/tasks/v2/templates/{_5_pip_show.sh → 4_pip_show.sh} +5 -6
  67. fractal_server/tasks/v2/templates/5_get_venv_size_and_file_number.sh +10 -0
  68. fractal_server/tasks/v2/templates/6_pip_install_from_freeze.sh +35 -0
  69. fractal_server/tasks/v2/utils_background.py +42 -127
  70. fractal_server/tasks/v2/utils_templates.py +32 -2
  71. fractal_server/utils.py +4 -2
  72. fractal_server/zip_tools.py +21 -4
  73. {fractal_server-2.8.0.dist-info → fractal_server-2.9.0.dist-info}/METADATA +3 -5
  74. {fractal_server-2.8.0.dist-info → fractal_server-2.9.0.dist-info}/RECORD +78 -65
  75. fractal_server/app/models/v2/collection_state.py +0 -22
  76. fractal_server/tasks/v2/collection_local.py +0 -357
  77. fractal_server/tasks/v2/collection_ssh.py +0 -352
  78. fractal_server/tasks/v2/templates/_1_create_venv.sh +0 -42
  79. /fractal_server/tasks/v2/{database_operations.py → utils_database.py} +0 -0
  80. {fractal_server-2.8.0.dist-info → fractal_server-2.9.0.dist-info}/LICENSE +0 -0
  81. {fractal_server-2.8.0.dist-info → fractal_server-2.9.0.dist-info}/WHEEL +0 -0
  82. {fractal_server-2.8.0.dist-info → fractal_server-2.9.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,291 @@
1
+ import json
2
+ import logging
3
+ import shutil
4
+ import time
5
+ from pathlib import Path
6
+ from tempfile import TemporaryDirectory
7
+
8
+ from ..utils_database import create_db_tasks_and_update_task_group
9
+ from ._utils import _customize_and_run_template
10
+ from fractal_server.app.db import get_sync_db
11
+ from fractal_server.app.models.v2 import TaskGroupActivityV2
12
+ from fractal_server.app.models.v2 import TaskGroupV2
13
+ from fractal_server.app.schemas.v2 import TaskGroupActivityActionV2
14
+ from fractal_server.app.schemas.v2 import TaskGroupActivityStatusV2
15
+ from fractal_server.app.schemas.v2.manifest import ManifestV2
16
+ from fractal_server.logger import get_logger
17
+ from fractal_server.logger import set_logger
18
+ from fractal_server.tasks.utils import get_log_path
19
+ from fractal_server.tasks.v2.local._utils import check_task_files_exist
20
+ from fractal_server.tasks.v2.utils_background import _prepare_tasks_metadata
21
+ from fractal_server.tasks.v2.utils_background import add_commit_refresh
22
+ from fractal_server.tasks.v2.utils_background import fail_and_cleanup
23
+ from fractal_server.tasks.v2.utils_background import get_current_log
24
+ from fractal_server.tasks.v2.utils_package_names import compare_package_names
25
+ from fractal_server.tasks.v2.utils_python_interpreter import (
26
+ get_python_interpreter_v2,
27
+ )
28
+ from fractal_server.tasks.v2.utils_templates import get_collection_replacements
29
+ from fractal_server.tasks.v2.utils_templates import (
30
+ parse_script_pip_show_stdout,
31
+ )
32
+ from fractal_server.tasks.v2.utils_templates import SCRIPTS_SUBFOLDER
33
+ from fractal_server.utils import get_timestamp
34
+
35
+ LOGGER_NAME = __name__
36
+
37
+
38
+ def _copy_wheel_file_local(task_group: TaskGroupV2) -> str:
39
+ logger = get_logger(LOGGER_NAME)
40
+ source = task_group.wheel_path
41
+ dest = (
42
+ Path(task_group.path) / Path(task_group.wheel_path).name
43
+ ).as_posix()
44
+ logger.debug(f"[_copy_wheel_file] START {source=} {dest=}")
45
+ shutil.copy(task_group.wheel_path, task_group.path)
46
+ logger.debug(f"[_copy_wheel_file] END {source=} {dest=}")
47
+ return dest
48
+
49
+
50
+ def collect_local(
51
+ *,
52
+ task_group_activity_id: int,
53
+ task_group_id: int,
54
+ ) -> None:
55
+ """
56
+ Collect a task package.
57
+
58
+ This function is run as a background task, therefore exceptions must be
59
+ handled.
60
+
61
+ NOTE: by making this function sync, it runs within a thread - due to
62
+ starlette/fastapi handling of background tasks (see
63
+ https://github.com/encode/starlette/blob/master/starlette/background.py).
64
+
65
+
66
+ Arguments:
67
+ task_group_id:
68
+ task_group_activity_id:
69
+ """
70
+
71
+ with TemporaryDirectory() as tmpdir:
72
+ log_file_path = get_log_path(Path(tmpdir))
73
+ logger = set_logger(
74
+ logger_name=LOGGER_NAME,
75
+ log_file_path=log_file_path,
76
+ )
77
+
78
+ with next(get_sync_db()) as db:
79
+
80
+ # Get main objects from db
81
+ activity = db.get(TaskGroupActivityV2, task_group_activity_id)
82
+ task_group = db.get(TaskGroupV2, task_group_id)
83
+ if activity is None or task_group is None:
84
+ # Use `logging` directly
85
+ logging.error(
86
+ "Cannot find database rows with "
87
+ f"{task_group_id=} and {task_group_activity_id=}:\n"
88
+ f"{task_group=}\n{activity=}. Exit."
89
+ )
90
+ return
91
+
92
+ # Log some info
93
+ logger.debug("START")
94
+ for key, value in task_group.model_dump().items():
95
+ logger.debug(f"task_group.{key}: {value}")
96
+
97
+ # Check that the (local) task_group path does exist
98
+ if Path(task_group.path).exists():
99
+ error_msg = f"{task_group.path} already exists."
100
+ logger.error(error_msg)
101
+ fail_and_cleanup(
102
+ task_group=task_group,
103
+ task_group_activity=activity,
104
+ logger_name=LOGGER_NAME,
105
+ log_file_path=log_file_path,
106
+ exception=FileExistsError(error_msg),
107
+ db=db,
108
+ )
109
+ return
110
+
111
+ try:
112
+
113
+ # Create task_group.path folder
114
+ Path(task_group.path).mkdir(parents=True)
115
+ logger.debug(f"Created {task_group.path}")
116
+
117
+ # Copy wheel file into task group path
118
+ if task_group.wheel_path:
119
+ new_wheel_path = _copy_wheel_file_local(
120
+ task_group=task_group
121
+ )
122
+ task_group.wheel_path = new_wheel_path
123
+ task_group = add_commit_refresh(obj=task_group, db=db)
124
+
125
+ # Prepare replacements for templates
126
+ replacements = get_collection_replacements(
127
+ task_group=task_group,
128
+ python_bin=get_python_interpreter_v2(
129
+ python_version=task_group.python_version
130
+ ),
131
+ )
132
+
133
+ # Prepare common arguments for `_customize_and_run_template``
134
+ common_args = dict(
135
+ replacements=replacements,
136
+ script_dir=(
137
+ Path(task_group.path) / SCRIPTS_SUBFOLDER
138
+ ).as_posix(),
139
+ prefix=(
140
+ f"{int(time.time())}_"
141
+ f"{TaskGroupActivityActionV2.COLLECT}_"
142
+ ),
143
+ logger_name=LOGGER_NAME,
144
+ )
145
+
146
+ # Set status to ONGOING and refresh logs
147
+ activity.status = TaskGroupActivityStatusV2.ONGOING
148
+ activity.log = get_current_log(log_file_path)
149
+ activity = add_commit_refresh(obj=activity, db=db)
150
+
151
+ # Run script 1
152
+ stdout = _customize_and_run_template(
153
+ template_filename="1_create_venv.sh",
154
+ **common_args,
155
+ )
156
+ activity.log = get_current_log(log_file_path)
157
+ activity = add_commit_refresh(obj=activity, db=db)
158
+
159
+ # Run script 2
160
+ stdout = _customize_and_run_template(
161
+ template_filename="2_pip_install.sh",
162
+ **common_args,
163
+ )
164
+ activity.log = get_current_log(log_file_path)
165
+ activity = add_commit_refresh(obj=activity, db=db)
166
+
167
+ # Run script 3
168
+ pip_freeze_stdout = _customize_and_run_template(
169
+ template_filename="3_pip_freeze.sh",
170
+ **common_args,
171
+ )
172
+ activity.log = get_current_log(log_file_path)
173
+ activity = add_commit_refresh(obj=activity, db=db)
174
+
175
+ # Run script 4
176
+ stdout = _customize_and_run_template(
177
+ template_filename="4_pip_show.sh",
178
+ **common_args,
179
+ )
180
+ activity.log = get_current_log(log_file_path)
181
+ activity = add_commit_refresh(obj=activity, db=db)
182
+
183
+ # Run script 5
184
+ venv_info = _customize_and_run_template(
185
+ template_filename="5_get_venv_size_and_file_number.sh",
186
+ **common_args,
187
+ )
188
+ venv_size, venv_file_number = venv_info.split()
189
+ activity.log = get_current_log(log_file_path)
190
+ activity = add_commit_refresh(obj=activity, db=db)
191
+
192
+ pkg_attrs = parse_script_pip_show_stdout(stdout)
193
+ for key, value in pkg_attrs.items():
194
+ logger.debug(f"Parsed from pip-show: {key}={value}")
195
+ # Check package_name match between pip show and task-group
196
+ task_group = db.get(TaskGroupV2, task_group_id)
197
+ package_name_pip_show = pkg_attrs.get("package_name")
198
+ package_name_task_group = task_group.pkg_name
199
+ compare_package_names(
200
+ pkg_name_pip_show=package_name_pip_show,
201
+ pkg_name_task_group=package_name_task_group,
202
+ logger_name=LOGGER_NAME,
203
+ )
204
+ # Extract/drop parsed attributes
205
+ package_name = package_name_task_group
206
+ python_bin = pkg_attrs.pop("python_bin")
207
+ package_root_parent = pkg_attrs.pop("package_root_parent")
208
+
209
+ # TODO : Use more robust logic to determine `package_root`.
210
+ # Examples: use `importlib.util.find_spec`, or parse the
211
+ # output of `pip show --files {package_name}`.
212
+ package_name_underscore = package_name.replace("-", "_")
213
+ package_root = (
214
+ Path(package_root_parent) / package_name_underscore
215
+ ).as_posix()
216
+
217
+ # Read and validate manifest file
218
+ manifest_path = pkg_attrs.pop("manifest_path")
219
+ logger.info(f"now loading {manifest_path=}")
220
+ with open(manifest_path) as json_data:
221
+ pkg_manifest_dict = json.load(json_data)
222
+ logger.info(f"loaded {manifest_path=}")
223
+ logger.info("now validating manifest content")
224
+ pkg_manifest = ManifestV2(**pkg_manifest_dict)
225
+ logger.info("validated manifest content")
226
+ activity.log = get_current_log(log_file_path)
227
+ activity = add_commit_refresh(obj=activity, db=db)
228
+
229
+ logger.info("_prepare_tasks_metadata - start")
230
+ task_list = _prepare_tasks_metadata(
231
+ package_manifest=pkg_manifest,
232
+ package_version=task_group.version,
233
+ package_root=Path(package_root),
234
+ python_bin=Path(python_bin),
235
+ )
236
+ check_task_files_exist(task_list=task_list)
237
+ logger.info("_prepare_tasks_metadata - end")
238
+ activity.log = get_current_log(log_file_path)
239
+ activity = add_commit_refresh(obj=activity, db=db)
240
+
241
+ logger.info("create_db_tasks_and_update_task_group - " "start")
242
+ create_db_tasks_and_update_task_group(
243
+ task_list=task_list,
244
+ task_group_id=task_group.id,
245
+ db=db,
246
+ )
247
+ logger.info("create_db_tasks_and_update_task_group - end")
248
+
249
+ # Update task_group data
250
+ logger.info(
251
+ "Add pip_freeze, venv_size and venv_file_number "
252
+ "to TaskGroupV2 - start"
253
+ )
254
+ task_group.pip_freeze = pip_freeze_stdout
255
+ task_group.venv_size_in_kB = int(venv_size)
256
+ task_group.venv_file_number = int(venv_file_number)
257
+ task_group = add_commit_refresh(obj=task_group, db=db)
258
+ logger.info(
259
+ "Add pip_freeze, venv_size and venv_file_number "
260
+ "to TaskGroupV2 - end"
261
+ )
262
+
263
+ # Finalize (write metadata to DB)
264
+ logger.debug("finalising - START")
265
+ activity.status = TaskGroupActivityStatusV2.OK
266
+ activity.timestamp_ended = get_timestamp()
267
+ activity = add_commit_refresh(obj=activity, db=db)
268
+ logger.debug("finalising - END")
269
+ logger.debug("END")
270
+
271
+ except Exception as collection_e:
272
+ # Delete corrupted package dir
273
+ try:
274
+ logger.info(f"Now delete folder {task_group.path}")
275
+ shutil.rmtree(task_group.path)
276
+ logger.info(f"Deleted folder {task_group.path}")
277
+ except Exception as rm_e:
278
+ logger.error(
279
+ "Removing folder failed.\n"
280
+ f"Original error:\n{str(rm_e)}"
281
+ )
282
+
283
+ fail_and_cleanup(
284
+ task_group=task_group,
285
+ task_group_activity=activity,
286
+ logger_name=LOGGER_NAME,
287
+ log_file_path=log_file_path,
288
+ exception=collection_e,
289
+ db=db,
290
+ )
291
+ return
@@ -0,0 +1,218 @@
1
+ import logging
2
+ import shutil
3
+ import time
4
+ from pathlib import Path
5
+ from tempfile import TemporaryDirectory
6
+
7
+ from ..utils_background import add_commit_refresh
8
+ from ..utils_background import fail_and_cleanup
9
+ from ..utils_templates import get_collection_replacements
10
+ from ._utils import _customize_and_run_template
11
+ from fractal_server.app.db import get_sync_db
12
+ from fractal_server.app.models.v2 import TaskGroupActivityV2
13
+ from fractal_server.app.models.v2 import TaskGroupV2
14
+ from fractal_server.app.schemas.v2 import TaskGroupActivityActionV2
15
+ from fractal_server.app.schemas.v2 import TaskGroupV2OriginEnum
16
+ from fractal_server.app.schemas.v2.task_group import TaskGroupActivityStatusV2
17
+ from fractal_server.logger import set_logger
18
+ from fractal_server.tasks.utils import get_log_path
19
+ from fractal_server.tasks.v2.utils_background import get_current_log
20
+ from fractal_server.tasks.v2.utils_templates import SCRIPTS_SUBFOLDER
21
+ from fractal_server.utils import get_timestamp
22
+
23
+ LOGGER_NAME = __name__
24
+
25
+
26
+ def deactivate_local(
27
+ *,
28
+ task_group_activity_id: int,
29
+ task_group_id: int,
30
+ ) -> None:
31
+ """
32
+ Deactivate a task group venv.
33
+
34
+ This function is run as a background task, therefore exceptions must be
35
+ handled.
36
+
37
+ Arguments:
38
+ task_group_id:
39
+ task_group_activity_id:
40
+ """
41
+
42
+ with TemporaryDirectory() as tmpdir:
43
+ log_file_path = get_log_path(Path(tmpdir))
44
+ logger = set_logger(
45
+ logger_name=LOGGER_NAME,
46
+ log_file_path=log_file_path,
47
+ )
48
+
49
+ with next(get_sync_db()) as db:
50
+
51
+ # Get main objects from db
52
+ activity = db.get(TaskGroupActivityV2, task_group_activity_id)
53
+ task_group = db.get(TaskGroupV2, task_group_id)
54
+ if activity is None or task_group is None:
55
+ # Use `logging` directly
56
+ logging.error(
57
+ "Cannot find database rows with "
58
+ f"{task_group_id=} and {task_group_activity_id=}:\n"
59
+ f"{task_group=}\n{activity=}. Exit."
60
+ )
61
+ return
62
+
63
+ # Log some info
64
+ logger.debug("START")
65
+
66
+ for key, value in task_group.model_dump().items():
67
+ logger.debug(f"task_group.{key}: {value}")
68
+
69
+ # Check that the (local) task_group venv_path does exist
70
+ if not Path(task_group.venv_path).exists():
71
+ error_msg = f"{task_group.venv_path} does not exist."
72
+ logger.error(error_msg)
73
+ fail_and_cleanup(
74
+ task_group=task_group,
75
+ task_group_activity=activity,
76
+ logger_name=LOGGER_NAME,
77
+ log_file_path=log_file_path,
78
+ exception=FileNotFoundError(error_msg),
79
+ db=db,
80
+ )
81
+ return
82
+
83
+ try:
84
+
85
+ activity.status = TaskGroupActivityStatusV2.ONGOING
86
+ activity = add_commit_refresh(obj=activity, db=db)
87
+
88
+ if task_group.pip_freeze is None:
89
+ logger.warning(
90
+ "Recreate pip-freeze information, since "
91
+ f"{task_group.pip_freeze=}. NOTE: this should only "
92
+ "happen for task groups created before 2.9.0."
93
+ )
94
+ # Prepare replacements for templates
95
+ replacements = get_collection_replacements(
96
+ task_group=task_group,
97
+ python_bin="/not/applicable",
98
+ )
99
+
100
+ # Prepare common arguments for _customize_and_run_template
101
+ common_args = dict(
102
+ replacements=replacements,
103
+ script_dir=(
104
+ Path(task_group.path) / SCRIPTS_SUBFOLDER
105
+ ).as_posix(),
106
+ prefix=(
107
+ f"{int(time.time())}_"
108
+ f"{TaskGroupActivityActionV2.DEACTIVATE}_"
109
+ ),
110
+ logger_name=LOGGER_NAME,
111
+ )
112
+
113
+ # Update pip-freeze data
114
+ pip_freeze_stdout = _customize_and_run_template(
115
+ template_filename="3_pip_freeze.sh",
116
+ **common_args,
117
+ )
118
+ logger.info("Add pip freeze stdout to TaskGroupV2 - start")
119
+ activity.log = get_current_log(log_file_path)
120
+ activity = add_commit_refresh(obj=activity, db=db)
121
+ task_group.pip_freeze = pip_freeze_stdout
122
+ task_group = add_commit_refresh(obj=task_group, db=db)
123
+ logger.info("Add pip freeze stdout to TaskGroupV2 - end")
124
+
125
+ # Handle some specific cases for wheel-file case
126
+ if task_group.origin == TaskGroupV2OriginEnum.WHEELFILE:
127
+
128
+ logger.info(
129
+ f"Handle specific cases for {task_group.origin=}."
130
+ )
131
+
132
+ # Blocking situation: `wheel_path` is not set or points
133
+ # to a missing path
134
+ if (
135
+ task_group.wheel_path is None
136
+ or not Path(task_group.wheel_path).exists()
137
+ ):
138
+ error_msg = (
139
+ "Invalid wheel path for task group with "
140
+ f"{task_group_id=}. {task_group.wheel_path=} is "
141
+ "unset or does not exist."
142
+ )
143
+ logger.error(error_msg)
144
+ fail_and_cleanup(
145
+ task_group=task_group,
146
+ task_group_activity=activity,
147
+ logger_name=LOGGER_NAME,
148
+ log_file_path=log_file_path,
149
+ exception=FileNotFoundError(error_msg),
150
+ db=db,
151
+ )
152
+ return
153
+
154
+ # Recoverable situation: `wheel_path` was not yet copied
155
+ # over to the correct server-side folder
156
+ wheel_path_parent_dir = Path(task_group.wheel_path).parent
157
+ if wheel_path_parent_dir != Path(task_group.path):
158
+ logger.warning(
159
+ f"{wheel_path_parent_dir.as_posix()} differs from "
160
+ f"{task_group.path}. NOTE: this should only "
161
+ "happen for task groups created before 2.9.0."
162
+ )
163
+
164
+ if task_group.wheel_path not in task_group.pip_freeze:
165
+ raise ValueError(
166
+ f"Cannot find {task_group.wheel_path=} in "
167
+ "pip-freeze data. Exit."
168
+ )
169
+
170
+ logger.info(
171
+ f"Now copy wheel file into {task_group.path}."
172
+ )
173
+ new_wheel_path = (
174
+ Path(task_group.path)
175
+ / Path(task_group.wheel_path).name
176
+ ).as_posix()
177
+ shutil.copy(task_group.wheel_path, new_wheel_path)
178
+ logger.info(f"Copied wheel file to {new_wheel_path}.")
179
+
180
+ task_group.wheel_path = new_wheel_path
181
+ new_pip_freeze = task_group.pip_freeze.replace(
182
+ task_group.wheel_path,
183
+ new_wheel_path,
184
+ )
185
+ task_group.pip_freeze = new_pip_freeze
186
+ task_group = add_commit_refresh(obj=task_group, db=db)
187
+ logger.info(
188
+ "Updated `wheel_path` and `pip_freeze` "
189
+ "task-group attributes."
190
+ )
191
+
192
+ # We now have all required information for reactivating the
193
+ # virtual environment at a later point
194
+
195
+ # Actually mark the task group as non-active
196
+ logger.info("Now setting `active=False`.")
197
+ task_group.active = False
198
+ task_group = add_commit_refresh(obj=task_group, db=db)
199
+
200
+ # Proceed with deactivation
201
+ logger.info(f"Now removing {task_group.venv_path}.")
202
+ shutil.rmtree(task_group.venv_path)
203
+ logger.info(f"All good, {task_group.venv_path} removed.")
204
+ activity.status = TaskGroupActivityStatusV2.OK
205
+ activity.log = get_current_log(log_file_path)
206
+ activity.timestamp_ended = get_timestamp()
207
+ activity = add_commit_refresh(obj=activity, db=db)
208
+
209
+ except Exception as e:
210
+ fail_and_cleanup(
211
+ task_group=task_group,
212
+ task_group_activity=activity,
213
+ logger_name=LOGGER_NAME,
214
+ log_file_path=log_file_path,
215
+ exception=e,
216
+ db=db,
217
+ )
218
+ return
@@ -0,0 +1,159 @@
1
+ import logging
2
+ import shutil
3
+ import time
4
+ from pathlib import Path
5
+ from tempfile import TemporaryDirectory
6
+
7
+ from ..utils_background import add_commit_refresh
8
+ from ..utils_background import fail_and_cleanup
9
+ from ..utils_templates import get_collection_replacements
10
+ from ._utils import _customize_and_run_template
11
+ from fractal_server.app.db import get_sync_db
12
+ from fractal_server.app.models.v2 import TaskGroupActivityV2
13
+ from fractal_server.app.models.v2 import TaskGroupV2
14
+ from fractal_server.app.schemas.v2 import TaskGroupActivityActionV2
15
+ from fractal_server.app.schemas.v2.task_group import TaskGroupActivityStatusV2
16
+ from fractal_server.logger import set_logger
17
+ from fractal_server.tasks.utils import get_log_path
18
+ from fractal_server.tasks.v2.utils_background import get_current_log
19
+ from fractal_server.tasks.v2.utils_python_interpreter import (
20
+ get_python_interpreter_v2,
21
+ )
22
+ from fractal_server.tasks.v2.utils_templates import SCRIPTS_SUBFOLDER
23
+ from fractal_server.utils import get_timestamp
24
+
25
+
26
+ LOGGER_NAME = __name__
27
+
28
+
29
+ def reactivate_local(
30
+ *,
31
+ task_group_activity_id: int,
32
+ task_group_id: int,
33
+ ) -> None:
34
+ """
35
+ Reactivate a task group venv.
36
+
37
+ This function is run as a background task, therefore exceptions must be
38
+ handled.
39
+
40
+ Arguments:
41
+ task_group_id:
42
+ task_group_activity_id:
43
+ """
44
+
45
+ with TemporaryDirectory() as tmpdir:
46
+ log_file_path = get_log_path(Path(tmpdir))
47
+ logger = set_logger(
48
+ logger_name=LOGGER_NAME,
49
+ log_file_path=log_file_path,
50
+ )
51
+
52
+ with next(get_sync_db()) as db:
53
+
54
+ # Get main objects from db
55
+ activity = db.get(TaskGroupActivityV2, task_group_activity_id)
56
+ task_group = db.get(TaskGroupV2, task_group_id)
57
+ if activity is None or task_group is None:
58
+ # Use `logging` directly
59
+ logging.error(
60
+ "Cannot find database rows with "
61
+ f"{task_group_id=} and {task_group_activity_id=}:\n"
62
+ f"{task_group=}\n{activity=}. Exit."
63
+ )
64
+ return
65
+
66
+ # Log some info
67
+ logger.debug("START")
68
+
69
+ for key, value in task_group.model_dump().items():
70
+ logger.debug(f"task_group.{key}: {value}")
71
+
72
+ # Check that the (local) task_group venv_path does not exist
73
+ if Path(task_group.venv_path).exists():
74
+ error_msg = f"{task_group.venv_path} already exists."
75
+ logger.error(error_msg)
76
+ fail_and_cleanup(
77
+ task_group=task_group,
78
+ task_group_activity=activity,
79
+ logger_name=LOGGER_NAME,
80
+ log_file_path=log_file_path,
81
+ exception=FileExistsError(error_msg),
82
+ db=db,
83
+ )
84
+ return
85
+
86
+ try:
87
+ activity.status = TaskGroupActivityStatusV2.ONGOING
88
+ activity = add_commit_refresh(obj=activity, db=db)
89
+
90
+ # Prepare replacements for templates
91
+ replacements = get_collection_replacements(
92
+ task_group=task_group,
93
+ python_bin=get_python_interpreter_v2(
94
+ python_version=task_group.python_version
95
+ ),
96
+ )
97
+ with open(f"{tmpdir}/pip_freeze.txt", "w") as f:
98
+ f.write(task_group.pip_freeze)
99
+ replacements.append(
100
+ ("__PIP_FREEZE_FILE__", f"{tmpdir}/pip_freeze.txt")
101
+ )
102
+ # Prepare common arguments for `_customize_and_run_template``
103
+ common_args = dict(
104
+ replacements=replacements,
105
+ script_dir=(
106
+ Path(task_group.path) / SCRIPTS_SUBFOLDER
107
+ ).as_posix(),
108
+ prefix=(
109
+ f"{int(time.time())}_"
110
+ f"{TaskGroupActivityActionV2.REACTIVATE}_"
111
+ ),
112
+ logger_name=LOGGER_NAME,
113
+ )
114
+
115
+ logger.debug("start - create venv")
116
+ _customize_and_run_template(
117
+ template_filename="1_create_venv.sh",
118
+ **common_args,
119
+ )
120
+ logger.debug("end - create venv")
121
+ activity.log = get_current_log(log_file_path)
122
+ activity.timestamp_ended = get_timestamp()
123
+ activity = add_commit_refresh(obj=activity, db=db)
124
+
125
+ logger.debug("start - install from pip freeze")
126
+ _customize_and_run_template(
127
+ template_filename="6_pip_install_from_freeze.sh",
128
+ **common_args,
129
+ )
130
+ logger.debug("end - install from pip freeze")
131
+ activity.log = get_current_log(log_file_path)
132
+ activity.status = TaskGroupActivityStatusV2.OK
133
+ activity.timestamp_ended = get_timestamp()
134
+ activity = add_commit_refresh(obj=activity, db=db)
135
+ task_group.active = True
136
+ task_group = add_commit_refresh(obj=task_group, db=db)
137
+ logger.debug("END")
138
+
139
+ except Exception as reactivate_e:
140
+ # Delete corrupted venv_path
141
+ try:
142
+ logger.info(f"Now delete folder {task_group.venv_path}")
143
+ shutil.rmtree(task_group.venv_path)
144
+ logger.info(f"Deleted folder {task_group.venv_path}")
145
+ except Exception as rm_e:
146
+ logger.error(
147
+ "Removing folder failed.\n"
148
+ f"Original error:\n{str(rm_e)}"
149
+ )
150
+
151
+ fail_and_cleanup(
152
+ task_group=task_group,
153
+ task_group_activity=activity,
154
+ logger_name=LOGGER_NAME,
155
+ log_file_path=log_file_path,
156
+ exception=reactivate_e,
157
+ db=db,
158
+ )
159
+ return
@@ -0,0 +1,3 @@
1
+ from .collect import collect_ssh # noqa
2
+ from .deactivate import deactivate_ssh # noqa
3
+ from .reactivate import reactivate_ssh # noqa