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,87 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ from fractal_server.app.models.v2 import TaskGroupV2
5
+ from fractal_server.logger import get_logger
6
+ from fractal_server.ssh._fabric import FractalSSH
7
+ from fractal_server.tasks.v2.utils_templates import customize_template
8
+
9
+
10
+ def _customize_and_run_template(
11
+ *,
12
+ template_filename: str,
13
+ replacements: list[tuple[str, str]],
14
+ script_dir_local: str,
15
+ prefix: str,
16
+ fractal_ssh: FractalSSH,
17
+ script_dir_remote: str,
18
+ logger_name: str,
19
+ ) -> str:
20
+ """
21
+ Customize one of the template bash scripts, transfer it to the remote host
22
+ via SFTP and then run it via SSH.
23
+
24
+ Args:
25
+ template_filename: Filename of the template file (ends with ".sh").
26
+ replacements: Dictionary of replacements.
27
+ script_dir: Local folder where the script will be placed.
28
+ prefix: Prefix for the script filename.
29
+ fractal_ssh: FractalSSH object
30
+ script_dir_remote: Remote scripts directory
31
+ """
32
+ logger = get_logger(logger_name=logger_name)
33
+ logger.debug(f"_customize_and_run_template {template_filename} - START")
34
+ # Prepare name and path of script
35
+ if not template_filename.endswith(".sh"):
36
+ raise ValueError(
37
+ f"Invalid {template_filename=} (it must end with '.sh')."
38
+ )
39
+ script_filename = f"{prefix}_{template_filename}"
40
+ script_path_local = (Path(script_dir_local) / script_filename).as_posix()
41
+
42
+ customize_template(
43
+ template_name=template_filename,
44
+ replacements=replacements,
45
+ script_path=script_path_local,
46
+ )
47
+
48
+ # Transfer script to remote host
49
+ script_path_remote = os.path.join(
50
+ script_dir_remote,
51
+ script_filename,
52
+ )
53
+ logger.debug(f"Now transfer {script_path_local=} over SSH.")
54
+ fractal_ssh.send_file(
55
+ local=script_path_local,
56
+ remote=script_path_remote,
57
+ )
58
+
59
+ # Execute script remotely
60
+ cmd = f"bash {script_path_remote}"
61
+ logger.debug(f"Now run '{cmd}' over SSH.")
62
+ stdout = fractal_ssh.run_command(cmd=cmd)
63
+
64
+ logger.debug(f"_customize_and_run_template {template_filename} - END")
65
+ return stdout
66
+
67
+
68
+ def _copy_wheel_file_ssh(
69
+ *, task_group: TaskGroupV2, fractal_ssh: FractalSSH, logger_name: str
70
+ ) -> str:
71
+ """
72
+ Handle the situation where `task_group.wheel_path` is not part of
73
+ `task_group.path`, by copying `wheel_path` into `path`.
74
+
75
+ Returns:
76
+ The new `wheel_path`.
77
+ """
78
+ logger = get_logger(logger_name=logger_name)
79
+ source = task_group.wheel_path
80
+ dest = (
81
+ Path(task_group.path) / Path(task_group.wheel_path).name
82
+ ).as_posix()
83
+ cmd = f"cp {source} {dest}"
84
+ logger.debug(f"[_copy_wheel_file] START {source=} {dest=}")
85
+ fractal_ssh.run_command(cmd=cmd)
86
+ logger.debug(f"[_copy_wheel_file] END {source=} {dest=}")
87
+ return dest
@@ -0,0 +1,311 @@
1
+ import logging
2
+ import time
3
+ from pathlib import Path
4
+ from tempfile import TemporaryDirectory
5
+
6
+ from ..utils_background import _prepare_tasks_metadata
7
+ from ..utils_background import fail_and_cleanup
8
+ from ..utils_database import create_db_tasks_and_update_task_group
9
+ from fractal_server.app.db import get_sync_db
10
+ from fractal_server.app.models.v2 import TaskGroupActivityV2
11
+ from fractal_server.app.models.v2 import TaskGroupV2
12
+ from fractal_server.app.schemas.v2 import TaskGroupActivityActionV2
13
+ from fractal_server.app.schemas.v2 import TaskGroupActivityStatusV2
14
+ from fractal_server.app.schemas.v2.manifest import ManifestV2
15
+ from fractal_server.logger import set_logger
16
+ from fractal_server.ssh._fabric import FractalSSH
17
+ from fractal_server.tasks.v2.ssh._utils import _copy_wheel_file_ssh
18
+ from fractal_server.tasks.v2.ssh._utils import _customize_and_run_template
19
+ from fractal_server.tasks.v2.utils_background import add_commit_refresh
20
+ from fractal_server.tasks.v2.utils_background import get_current_log
21
+ from fractal_server.tasks.v2.utils_package_names import compare_package_names
22
+ from fractal_server.tasks.v2.utils_python_interpreter import (
23
+ get_python_interpreter_v2,
24
+ )
25
+ from fractal_server.tasks.v2.utils_templates import get_collection_replacements
26
+ from fractal_server.tasks.v2.utils_templates import (
27
+ parse_script_pip_show_stdout,
28
+ )
29
+ from fractal_server.tasks.v2.utils_templates import SCRIPTS_SUBFOLDER
30
+ from fractal_server.utils import get_timestamp
31
+
32
+ LOGGER_NAME = __name__
33
+
34
+
35
+ def collect_ssh(
36
+ *,
37
+ task_group_id: int,
38
+ task_group_activity_id: int,
39
+ fractal_ssh: FractalSSH,
40
+ tasks_base_dir: str,
41
+ ) -> None:
42
+ """
43
+ Collect a task package over SSH
44
+
45
+ This function is run as a background task, therefore exceptions must be
46
+ handled.
47
+
48
+ NOTE: by making this function sync, it runs within a thread - due to
49
+ starlette/fastapi handling of background tasks (see
50
+ https://github.com/encode/starlette/blob/master/starlette/background.py).
51
+
52
+
53
+ Arguments:
54
+ task_group_id:
55
+ task_group_activity_id:
56
+ fractal_ssh:
57
+ tasks_base_dir:
58
+ Only used as a `safe_root` in `remove_dir`, and typically set to
59
+ `user_settings.ssh_tasks_dir`.
60
+ """
61
+
62
+ # Work within a temporary folder, where also logs will be placed
63
+ with TemporaryDirectory() as tmpdir:
64
+ LOGGER_NAME = "task_collection_ssh"
65
+ log_file_path = Path(tmpdir) / "log"
66
+ logger = set_logger(
67
+ logger_name=LOGGER_NAME,
68
+ log_file_path=log_file_path,
69
+ )
70
+
71
+ with next(get_sync_db()) as db:
72
+
73
+ # Get main objects from db
74
+ activity = db.get(TaskGroupActivityV2, task_group_activity_id)
75
+ task_group = db.get(TaskGroupV2, task_group_id)
76
+ if activity is None or task_group is None:
77
+ # Use `logging` directly
78
+ logging.error(
79
+ "Cannot find database rows with "
80
+ f"{task_group_id=} and {task_group_activity_id=}:\n"
81
+ f"{task_group=}\n{activity=}. Exit."
82
+ )
83
+ return
84
+
85
+ # Log some info
86
+ logger.debug("START")
87
+ for key, value in task_group.model_dump().items():
88
+ logger.debug(f"task_group.{key}: {value}")
89
+
90
+ # Check that SSH connection works
91
+ try:
92
+ fractal_ssh.check_connection()
93
+ except Exception as e:
94
+ logger.error("Cannot establish SSH connection.")
95
+ fail_and_cleanup(
96
+ task_group=task_group,
97
+ task_group_activity=activity,
98
+ logger_name=LOGGER_NAME,
99
+ log_file_path=log_file_path,
100
+ exception=e,
101
+ db=db,
102
+ )
103
+ return
104
+
105
+ # Check that the (remote) task_group path does not exist
106
+ if fractal_ssh.remote_exists(task_group.path):
107
+ error_msg = f"{task_group.path} already exists."
108
+ logger.error(error_msg)
109
+ fail_and_cleanup(
110
+ task_group=task_group,
111
+ task_group_activity=activity,
112
+ logger_name=LOGGER_NAME,
113
+ log_file_path=log_file_path,
114
+ exception=FileExistsError(error_msg),
115
+ db=db,
116
+ )
117
+ return
118
+
119
+ try:
120
+
121
+ # Prepare replacements for templates
122
+ replacements = get_collection_replacements(
123
+ task_group=task_group,
124
+ python_bin=get_python_interpreter_v2(
125
+ python_version=task_group.python_version
126
+ ),
127
+ )
128
+
129
+ # Prepare common arguments for `_customize_and_run_template``
130
+ script_dir_remote = (
131
+ Path(task_group.path) / SCRIPTS_SUBFOLDER
132
+ ).as_posix()
133
+ common_args = dict(
134
+ replacements=replacements,
135
+ script_dir_local=(
136
+ Path(tmpdir) / SCRIPTS_SUBFOLDER
137
+ ).as_posix(),
138
+ script_dir_remote=script_dir_remote,
139
+ prefix=(
140
+ f"{int(time.time())}_"
141
+ f"{TaskGroupActivityActionV2.COLLECT}"
142
+ ),
143
+ fractal_ssh=fractal_ssh,
144
+ logger_name=LOGGER_NAME,
145
+ )
146
+
147
+ # Create remote `task_group.path` and `script_dir_remote`
148
+ # folders (note that because of `parents=True` we are in
149
+ # the `no error if existing, make parent directories as
150
+ # needed` scenario for `mkdir`)
151
+ fractal_ssh.mkdir(folder=task_group.path, parents=True)
152
+ fractal_ssh.mkdir(folder=script_dir_remote, parents=True)
153
+
154
+ # Copy wheel file into task group path
155
+ if task_group.wheel_path:
156
+ new_wheel_path = _copy_wheel_file_ssh(
157
+ task_group=task_group,
158
+ fractal_ssh=fractal_ssh,
159
+ logger_name=LOGGER_NAME,
160
+ )
161
+ task_group.wheel_path = new_wheel_path
162
+ task_group = add_commit_refresh(obj=task_group, db=db)
163
+
164
+ logger.debug("installing - START")
165
+
166
+ # Set status to ONGOING and refresh logs
167
+ activity.status = TaskGroupActivityStatusV2.ONGOING
168
+ activity.log = get_current_log(log_file_path)
169
+ activity = add_commit_refresh(obj=activity, db=db)
170
+
171
+ # Run script 1
172
+ stdout = _customize_and_run_template(
173
+ template_filename="1_create_venv.sh",
174
+ **common_args,
175
+ )
176
+ activity.log = get_current_log(log_file_path)
177
+ activity = add_commit_refresh(obj=activity, db=db)
178
+
179
+ # Run script 2
180
+ stdout = _customize_and_run_template(
181
+ template_filename="2_pip_install.sh",
182
+ **common_args,
183
+ )
184
+ activity.log = get_current_log(log_file_path)
185
+ activity = add_commit_refresh(obj=activity, db=db)
186
+
187
+ # Run script 3
188
+ pip_freeze_stdout = _customize_and_run_template(
189
+ template_filename="3_pip_freeze.sh",
190
+ **common_args,
191
+ )
192
+ activity.log = get_current_log(log_file_path)
193
+ activity = add_commit_refresh(obj=activity, db=db)
194
+
195
+ # Run script 4
196
+ stdout = _customize_and_run_template(
197
+ template_filename="4_pip_show.sh",
198
+ **common_args,
199
+ )
200
+ activity.log = get_current_log(log_file_path)
201
+ activity = add_commit_refresh(obj=activity, db=db)
202
+
203
+ # Run script 5
204
+ venv_info = _customize_and_run_template(
205
+ template_filename="5_get_venv_size_and_file_number.sh",
206
+ **common_args,
207
+ )
208
+ venv_size, venv_file_number = venv_info.split()
209
+ activity.log = get_current_log(log_file_path)
210
+ activity = add_commit_refresh(obj=activity, db=db)
211
+
212
+ pkg_attrs = parse_script_pip_show_stdout(stdout)
213
+
214
+ for key, value in pkg_attrs.items():
215
+ logger.debug(f"parsed from pip-show: {key}={value}")
216
+ # Check package_name match between pip show and task-group
217
+ package_name_pip_show = pkg_attrs.get("package_name")
218
+ package_name_task_group = task_group.pkg_name
219
+ compare_package_names(
220
+ pkg_name_pip_show=package_name_pip_show,
221
+ pkg_name_task_group=package_name_task_group,
222
+ logger_name=LOGGER_NAME,
223
+ )
224
+ # Extract/drop parsed attributes
225
+ package_name = package_name_task_group
226
+ python_bin = pkg_attrs.pop("python_bin")
227
+ package_root_parent_remote = pkg_attrs.pop(
228
+ "package_root_parent"
229
+ )
230
+ manifest_path_remote = pkg_attrs.pop("manifest_path")
231
+
232
+ # TODO SSH: Use more robust logic to determine `package_root`.
233
+ # Examples: use `importlib.util.find_spec`, or parse the output
234
+ # of `pip show --files {package_name}`.
235
+ package_name_underscore = package_name.replace("-", "_")
236
+ package_root_remote = (
237
+ Path(package_root_parent_remote) / package_name_underscore
238
+ ).as_posix()
239
+
240
+ # Read and validate remote manifest file
241
+ pkg_manifest_dict = fractal_ssh.read_remote_json_file(
242
+ manifest_path_remote
243
+ )
244
+ logger.info(f"Loaded {manifest_path_remote=}")
245
+ pkg_manifest = ManifestV2(**pkg_manifest_dict)
246
+ logger.info("Manifest is a valid ManifestV2")
247
+
248
+ logger.info("_prepare_tasks_metadata - start")
249
+ task_list = _prepare_tasks_metadata(
250
+ package_manifest=pkg_manifest,
251
+ package_version=task_group.version,
252
+ package_root=Path(package_root_remote),
253
+ python_bin=Path(python_bin),
254
+ )
255
+ logger.info("_prepare_tasks_metadata - end")
256
+
257
+ logger.info("create_db_tasks_and_update_task_group - " "start")
258
+ create_db_tasks_and_update_task_group(
259
+ task_list=task_list,
260
+ task_group_id=task_group.id,
261
+ db=db,
262
+ )
263
+ logger.info("create_db_tasks_and_update_task_group - end")
264
+
265
+ # Update task_group data
266
+ logger.info(
267
+ "Add pip_freeze, venv_size and venv_file_number "
268
+ "to TaskGroupV2 - start"
269
+ )
270
+ task_group.pip_freeze = pip_freeze_stdout
271
+ task_group.venv_size_in_kB = int(venv_size)
272
+ task_group.venv_file_number = int(venv_file_number)
273
+ task_group = add_commit_refresh(obj=task_group, db=db)
274
+ logger.info(
275
+ "Add pip_freeze, venv_size and venv_file_number "
276
+ "to TaskGroupV2 - end"
277
+ )
278
+
279
+ # Finalize (write metadata to DB)
280
+ logger.debug("finalising - START")
281
+ activity.status = TaskGroupActivityStatusV2.OK
282
+ activity.timestamp_ended = get_timestamp()
283
+ activity = add_commit_refresh(obj=activity, db=db)
284
+ logger.debug("finalising - END")
285
+ logger.debug("END")
286
+
287
+ logger.debug("END")
288
+
289
+ except Exception as collection_e:
290
+ # Delete corrupted package dir
291
+ try:
292
+ logger.info(f"Now delete remote folder {task_group.path}")
293
+ fractal_ssh.remove_folder(
294
+ folder=task_group.path,
295
+ safe_root=tasks_base_dir,
296
+ )
297
+ logger.info(f"Deleted remoted folder {task_group.path}")
298
+ except Exception as e_rm:
299
+ logger.error(
300
+ "Removing folder failed. "
301
+ f"Original error:\n{str(e_rm)}"
302
+ )
303
+ fail_and_cleanup(
304
+ task_group=task_group,
305
+ task_group_activity=activity,
306
+ log_file_path=log_file_path,
307
+ logger_name=LOGGER_NAME,
308
+ exception=collection_e,
309
+ db=db,
310
+ )
311
+ return
@@ -0,0 +1,253 @@
1
+ import logging
2
+ import time
3
+ from pathlib import Path
4
+ from tempfile import TemporaryDirectory
5
+
6
+ from ..utils_background import add_commit_refresh
7
+ from ..utils_background import fail_and_cleanup
8
+ from ..utils_templates import get_collection_replacements
9
+ from ._utils import _copy_wheel_file_ssh
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.ssh._fabric import FractalSSH
19
+ from fractal_server.tasks.utils import get_log_path
20
+ from fractal_server.tasks.v2.utils_background import get_current_log
21
+ from fractal_server.tasks.v2.utils_templates import SCRIPTS_SUBFOLDER
22
+ from fractal_server.utils import get_timestamp
23
+
24
+ LOGGER_NAME = __name__
25
+
26
+
27
+ def deactivate_ssh(
28
+ *,
29
+ task_group_activity_id: int,
30
+ task_group_id: int,
31
+ fractal_ssh: FractalSSH,
32
+ tasks_base_dir: str,
33
+ ) -> None:
34
+ """
35
+ Deactivate 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
+ fractal_ssh:
44
+ tasks_base_dir:
45
+ Only used as a `safe_root` in `remove_dir`, and typically set to
46
+ `user_settings.ssh_tasks_dir`.
47
+ """
48
+
49
+ with TemporaryDirectory() as tmpdir:
50
+ log_file_path = get_log_path(Path(tmpdir))
51
+ logger = set_logger(
52
+ logger_name=LOGGER_NAME,
53
+ log_file_path=log_file_path,
54
+ )
55
+
56
+ with next(get_sync_db()) as db:
57
+
58
+ # Get main objects from db
59
+ activity = db.get(TaskGroupActivityV2, task_group_activity_id)
60
+ task_group = db.get(TaskGroupV2, task_group_id)
61
+ if activity is None or task_group is None:
62
+ # Use `logging` directly
63
+ logging.error(
64
+ "Cannot find database rows with "
65
+ f"{task_group_id=} and {task_group_activity_id=}:\n"
66
+ f"{task_group=}\n{activity=}. Exit."
67
+ )
68
+ return
69
+
70
+ # Log some info
71
+ logger.debug("START")
72
+ for key, value in task_group.model_dump().items():
73
+ logger.debug(f"task_group.{key}: {value}")
74
+
75
+ # Check that SSH connection works
76
+ try:
77
+ fractal_ssh.check_connection()
78
+ except Exception as e:
79
+ logger.error("Cannot establish SSH connection.")
80
+ fail_and_cleanup(
81
+ task_group=task_group,
82
+ task_group_activity=activity,
83
+ logger_name=LOGGER_NAME,
84
+ log_file_path=log_file_path,
85
+ exception=e,
86
+ db=db,
87
+ )
88
+ return
89
+
90
+ # Check that the (local) task_group venv_path does exist
91
+ if not fractal_ssh.remote_exists(task_group.venv_path):
92
+ error_msg = f"{task_group.venv_path} does not exist."
93
+ logger.error(error_msg)
94
+ fail_and_cleanup(
95
+ task_group=task_group,
96
+ task_group_activity=activity,
97
+ logger_name=LOGGER_NAME,
98
+ log_file_path=log_file_path,
99
+ exception=FileNotFoundError(error_msg),
100
+ db=db,
101
+ )
102
+ return
103
+
104
+ try:
105
+
106
+ activity.status = TaskGroupActivityStatusV2.ONGOING
107
+ activity = add_commit_refresh(obj=activity, db=db)
108
+
109
+ if task_group.pip_freeze is None:
110
+ logger.warning(
111
+ "Recreate pip-freeze information, since "
112
+ f"{task_group.pip_freeze=}. NOTE: this should only "
113
+ "happen for task groups created before 2.9.0."
114
+ )
115
+
116
+ # Prepare replacements for templates
117
+ replacements = get_collection_replacements(
118
+ task_group=task_group,
119
+ python_bin="/not/applicable",
120
+ )
121
+
122
+ # Define script_dir_remote and create it if missing
123
+ script_dir_remote = (
124
+ Path(task_group.path) / SCRIPTS_SUBFOLDER
125
+ ).as_posix()
126
+ fractal_ssh.mkdir(folder=script_dir_remote, parents=True)
127
+
128
+ # Prepare arguments for `_customize_and_run_template`
129
+ common_args = dict(
130
+ replacements=replacements,
131
+ script_dir_local=(
132
+ Path(tmpdir) / SCRIPTS_SUBFOLDER
133
+ ).as_posix(),
134
+ script_dir_remote=script_dir_remote,
135
+ prefix=(
136
+ f"{int(time.time())}_"
137
+ f"{TaskGroupActivityActionV2.DEACTIVATE}"
138
+ ),
139
+ fractal_ssh=fractal_ssh,
140
+ logger_name=LOGGER_NAME,
141
+ )
142
+
143
+ # Run `pip freeze`
144
+ pip_freeze_stdout = _customize_and_run_template(
145
+ template_filename="3_pip_freeze.sh",
146
+ **common_args,
147
+ )
148
+
149
+ # Update pip-freeze data
150
+ logger.info("Add pip freeze stdout to TaskGroupV2 - start")
151
+ activity.log = get_current_log(log_file_path)
152
+ activity = add_commit_refresh(obj=activity, db=db)
153
+ task_group.pip_freeze = pip_freeze_stdout
154
+ task_group = add_commit_refresh(obj=task_group, db=db)
155
+ logger.info("Add pip freeze stdout to TaskGroupV2 - end")
156
+
157
+ # Handle some specific cases for wheel-file case
158
+ if task_group.origin == TaskGroupV2OriginEnum.WHEELFILE:
159
+
160
+ logger.info(
161
+ f"Handle specific cases for {task_group.origin=}."
162
+ )
163
+
164
+ # Blocking situation: `wheel_path` is not set or points
165
+ # to a missing path
166
+ if (
167
+ task_group.wheel_path is None
168
+ or not fractal_ssh.remote_exists(task_group.wheel_path)
169
+ ):
170
+ error_msg = (
171
+ "Invalid wheel path for task group with "
172
+ f"{task_group_id=}. {task_group.wheel_path=} is "
173
+ "unset or does not exist."
174
+ )
175
+ logger.error(error_msg)
176
+ fail_and_cleanup(
177
+ task_group=task_group,
178
+ task_group_activity=activity,
179
+ logger_name=LOGGER_NAME,
180
+ log_file_path=log_file_path,
181
+ exception=FileNotFoundError(error_msg),
182
+ db=db,
183
+ )
184
+ return
185
+
186
+ # Recoverable situation: `wheel_path` was not yet copied
187
+ # over to the correct server-side folder
188
+ wheel_path_parent_dir = Path(task_group.wheel_path).parent
189
+ if wheel_path_parent_dir != Path(task_group.path):
190
+ logger.warning(
191
+ f"{wheel_path_parent_dir.as_posix()} differs from "
192
+ f"{task_group.path}. NOTE: this should only "
193
+ "happen for task groups created before 2.9.0."
194
+ )
195
+
196
+ if task_group.wheel_path not in task_group.pip_freeze:
197
+ raise ValueError(
198
+ f"Cannot find {task_group.wheel_path=} in "
199
+ "pip-freeze data. Exit."
200
+ )
201
+
202
+ logger.info(
203
+ f"Now copy wheel file into {task_group.path}."
204
+ )
205
+ new_wheel_path = _copy_wheel_file_ssh(
206
+ task_group=task_group,
207
+ fractal_ssh=fractal_ssh,
208
+ logger_name=LOGGER_NAME,
209
+ )
210
+ logger.info(f"Copied wheel file to {new_wheel_path}.")
211
+
212
+ task_group.wheel_path = new_wheel_path
213
+ new_pip_freeze = task_group.pip_freeze.replace(
214
+ task_group.wheel_path,
215
+ new_wheel_path,
216
+ )
217
+ task_group.pip_freeze = new_pip_freeze
218
+ task_group = add_commit_refresh(obj=task_group, db=db)
219
+ logger.info(
220
+ "Updated `wheel_path` and `pip_freeze` "
221
+ "task-group attributes."
222
+ )
223
+
224
+ # We now have all required information for reactivating the
225
+ # virtual environment at a later point
226
+
227
+ # Actually mark the task group as non-active
228
+ logger.info("Now setting `active=False`.")
229
+ task_group.active = False
230
+ task_group = add_commit_refresh(obj=task_group, db=db)
231
+
232
+ # Proceed with deactivation
233
+ logger.info(f"Now removing {task_group.venv_path}.")
234
+ fractal_ssh.remove_folder(
235
+ folder=task_group.venv_path,
236
+ safe_root=tasks_base_dir,
237
+ )
238
+ logger.info(f"All good, {task_group.venv_path} removed.")
239
+ activity.status = TaskGroupActivityStatusV2.OK
240
+ activity.log = get_current_log(log_file_path)
241
+ activity.timestamp_ended = get_timestamp()
242
+ activity = add_commit_refresh(obj=activity, db=db)
243
+
244
+ except Exception as e:
245
+ fail_and_cleanup(
246
+ task_group=task_group,
247
+ task_group_activity=activity,
248
+ logger_name=LOGGER_NAME,
249
+ log_file_path=log_file_path,
250
+ exception=e,
251
+ db=db,
252
+ )
253
+ return