fractal-server 2.14.16__py3-none-any.whl → 2.15.0a1__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/app/models/v2/task_group.py +17 -5
- fractal_server/app/routes/admin/v2/task_group_lifecycle.py +2 -2
- fractal_server/app/routes/api/v2/__init__.py +6 -0
- fractal_server/app/routes/api/v2/task_collection.py +3 -3
- fractal_server/app/routes/api/v2/task_collection_custom.py +2 -2
- fractal_server/app/routes/api/v2/task_collection_pixi.py +236 -0
- fractal_server/app/routes/api/v2/task_group_lifecycle.py +26 -7
- fractal_server/app/schemas/v2/__init__.py +2 -1
- fractal_server/app/schemas/v2/dumps.py +1 -1
- fractal_server/app/schemas/v2/task_collection.py +1 -1
- fractal_server/app/schemas/v2/task_group.py +16 -5
- fractal_server/config.py +42 -0
- fractal_server/migrations/versions/b1e7f7a1ff71_task_group_for_pixi.py +53 -0
- fractal_server/ssh/_fabric.py +26 -0
- fractal_server/tasks/v2/local/__init__.py +3 -0
- fractal_server/tasks/v2/local/_utils.py +7 -2
- fractal_server/tasks/v2/local/collect.py +23 -24
- fractal_server/tasks/v2/local/collect_pixi.py +234 -0
- fractal_server/tasks/v2/local/deactivate.py +36 -39
- fractal_server/tasks/v2/local/deactivate_pixi.py +102 -0
- fractal_server/tasks/v2/local/reactivate.py +9 -16
- fractal_server/tasks/v2/local/reactivate_pixi.py +146 -0
- fractal_server/tasks/v2/ssh/__init__.py +3 -0
- fractal_server/tasks/v2/ssh/_utils.py +5 -5
- fractal_server/tasks/v2/ssh/collect.py +23 -28
- fractal_server/tasks/v2/ssh/collect_pixi.py +306 -0
- fractal_server/tasks/v2/ssh/deactivate.py +39 -45
- fractal_server/tasks/v2/ssh/deactivate_pixi.py +128 -0
- fractal_server/tasks/v2/ssh/reactivate.py +8 -15
- fractal_server/tasks/v2/ssh/reactivate_pixi.py +108 -0
- fractal_server/tasks/v2/templates/pixi_1_extract.sh +40 -0
- fractal_server/tasks/v2/templates/pixi_2_install.sh +48 -0
- fractal_server/tasks/v2/templates/pixi_3_post_install.sh +80 -0
- fractal_server/tasks/v2/utils_background.py +43 -8
- fractal_server/tasks/v2/utils_pixi.py +38 -0
- {fractal_server-2.14.16.dist-info → fractal_server-2.15.0a1.dist-info}/METADATA +1 -1
- {fractal_server-2.14.16.dist-info → fractal_server-2.15.0a1.dist-info}/RECORD +41 -29
- {fractal_server-2.14.16.dist-info → fractal_server-2.15.0a1.dist-info}/LICENSE +0 -0
- {fractal_server-2.14.16.dist-info → fractal_server-2.15.0a1.dist-info}/WHEEL +0 -0
- {fractal_server-2.14.16.dist-info → fractal_server-2.15.0a1.dist-info}/entry_points.txt +0 -0
fractal_server/ssh/_fabric.py
CHANGED
@@ -204,6 +204,32 @@ class FractalSSH:
|
|
204
204
|
self.logger.info(f"END reading remote JSON file {filepath}.")
|
205
205
|
return data
|
206
206
|
|
207
|
+
def read_remote_text_file(self, filepath: str) -> dict[str, Any]:
|
208
|
+
"""
|
209
|
+
Read a remote text file into a string.
|
210
|
+
|
211
|
+
Note from paramiko docs:
|
212
|
+
> The Python 'b' flag is ignored, since SSH treats all files as binary.
|
213
|
+
"""
|
214
|
+
self.logger.info(f"START reading remote text file {filepath}.")
|
215
|
+
with _acquire_lock_with_timeout(
|
216
|
+
lock=self._lock,
|
217
|
+
label="read_remote_text_file",
|
218
|
+
timeout=self.default_lock_timeout,
|
219
|
+
):
|
220
|
+
try:
|
221
|
+
with self._sftp_unsafe().open(filepath, "r") as f:
|
222
|
+
data = f.read().decode()
|
223
|
+
except Exception as e:
|
224
|
+
self.log_and_raise(
|
225
|
+
e=e,
|
226
|
+
message=(
|
227
|
+
f"Error in `read_remote_text_file`, for {filepath=}."
|
228
|
+
),
|
229
|
+
)
|
230
|
+
self.logger.info(f"END reading remote text file {filepath}.")
|
231
|
+
return data
|
232
|
+
|
207
233
|
def check_connection(self) -> None:
|
208
234
|
"""
|
209
235
|
Open the SSH connection and handle exceptions.
|
@@ -1,3 +1,6 @@
|
|
1
1
|
from .collect import collect_local # noqa
|
2
|
+
from .collect_pixi import collect_local_pixi # noqa
|
2
3
|
from .deactivate import deactivate_local # noqa
|
4
|
+
from .deactivate_pixi import deactivate_local_pixi # noqa
|
3
5
|
from .reactivate import reactivate_local # noqa
|
6
|
+
from .reactivate_pixi import reactivate_local_pixi # noqa
|
@@ -50,19 +50,24 @@ def check_task_files_exist(task_list: list[TaskCreateV2]) -> None:
|
|
50
50
|
"""
|
51
51
|
Check that the modules listed in task commands point to existing files.
|
52
52
|
|
53
|
+
Note: commands may be like `/one/python /another/task.py` or
|
54
|
+
`/one/pixi [...] /another/task.py`, and in both cases `split()[-1]`
|
55
|
+
returns `/another/task.py`.
|
56
|
+
|
53
57
|
Args:
|
54
58
|
task_list:
|
55
59
|
"""
|
60
|
+
|
56
61
|
for _task in task_list:
|
57
62
|
if _task.command_non_parallel is not None:
|
58
|
-
_task_path = _task.command_non_parallel.split()[1]
|
63
|
+
_task_path = _task.command_non_parallel.split()[-1]
|
59
64
|
if not Path(_task_path).exists():
|
60
65
|
raise FileNotFoundError(
|
61
66
|
f"Task `{_task.name}` has `command_non_parallel` "
|
62
67
|
f"pointing to missing file `{_task_path}`."
|
63
68
|
)
|
64
69
|
if _task.command_parallel is not None:
|
65
|
-
_task_path = _task.command_parallel.split()[1]
|
70
|
+
_task_path = _task.command_parallel.split()[-1]
|
66
71
|
if not Path(_task_path).exists():
|
67
72
|
raise FileNotFoundError(
|
68
73
|
f"Task `{_task.name}` has `command_parallel` "
|
@@ -1,5 +1,4 @@
|
|
1
1
|
import json
|
2
|
-
import logging
|
3
2
|
import shutil
|
4
3
|
import time
|
5
4
|
from pathlib import Path
|
@@ -8,20 +7,22 @@ from tempfile import TemporaryDirectory
|
|
8
7
|
from ..utils_database import create_db_tasks_and_update_task_group_sync
|
9
8
|
from ._utils import _customize_and_run_template
|
10
9
|
from fractal_server.app.db import get_sync_db
|
11
|
-
from fractal_server.app.models.v2 import TaskGroupActivityV2
|
12
10
|
from fractal_server.app.models.v2 import TaskGroupV2
|
11
|
+
from fractal_server.app.schemas.v2 import FractalUploadedFile
|
13
12
|
from fractal_server.app.schemas.v2 import TaskGroupActivityActionV2
|
14
13
|
from fractal_server.app.schemas.v2 import TaskGroupActivityStatusV2
|
15
|
-
from fractal_server.app.schemas.v2 import WheelFile
|
16
14
|
from fractal_server.app.schemas.v2.manifest import ManifestV2
|
17
15
|
from fractal_server.logger import reset_logger_handlers
|
18
16
|
from fractal_server.logger import set_logger
|
19
17
|
from fractal_server.tasks.utils import get_log_path
|
20
18
|
from fractal_server.tasks.v2.local._utils import check_task_files_exist
|
21
|
-
from fractal_server.tasks.v2.utils_background import _prepare_tasks_metadata
|
22
19
|
from fractal_server.tasks.v2.utils_background import add_commit_refresh
|
23
20
|
from fractal_server.tasks.v2.utils_background import fail_and_cleanup
|
21
|
+
from fractal_server.tasks.v2.utils_background import (
|
22
|
+
get_activity_and_task_group,
|
23
|
+
)
|
24
24
|
from fractal_server.tasks.v2.utils_background import get_current_log
|
25
|
+
from fractal_server.tasks.v2.utils_background import prepare_tasks_metadata
|
25
26
|
from fractal_server.tasks.v2.utils_package_names import compare_package_names
|
26
27
|
from fractal_server.tasks.v2.utils_python_interpreter import (
|
27
28
|
get_python_interpreter_v2,
|
@@ -38,7 +39,7 @@ def collect_local(
|
|
38
39
|
*,
|
39
40
|
task_group_activity_id: int,
|
40
41
|
task_group_id: int,
|
41
|
-
wheel_file:
|
42
|
+
wheel_file: FractalUploadedFile | None = None,
|
42
43
|
) -> None:
|
43
44
|
"""
|
44
45
|
Collect a task package.
|
@@ -67,16 +68,12 @@ def collect_local(
|
|
67
68
|
)
|
68
69
|
|
69
70
|
with next(get_sync_db()) as db:
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
"Cannot find database rows with "
|
77
|
-
f"{task_group_id=} and {task_group_activity_id=}:\n"
|
78
|
-
f"{task_group=}\n{activity=}. Exit."
|
79
|
-
)
|
71
|
+
success, task_group, activity = get_activity_and_task_group(
|
72
|
+
task_group_activity_id=task_group_activity_id,
|
73
|
+
task_group_id=task_group_id,
|
74
|
+
db=db,
|
75
|
+
)
|
76
|
+
if not success:
|
80
77
|
return
|
81
78
|
|
82
79
|
# Log some info
|
@@ -103,16 +100,18 @@ def collect_local(
|
|
103
100
|
Path(task_group.path).mkdir(parents=True)
|
104
101
|
logger.info(f"Created {task_group.path}")
|
105
102
|
|
106
|
-
# Write wheel file and set task_group.
|
103
|
+
# Write wheel file and set task_group.archive_path
|
107
104
|
if wheel_file is not None:
|
108
105
|
|
109
|
-
|
106
|
+
archive_path = (
|
110
107
|
Path(task_group.path) / wheel_file.filename
|
111
108
|
).as_posix()
|
112
|
-
logger.info(
|
113
|
-
|
109
|
+
logger.info(
|
110
|
+
f"Write wheel-file contents into {archive_path}"
|
111
|
+
)
|
112
|
+
with open(archive_path, "wb") as f:
|
114
113
|
f.write(wheel_file.contents)
|
115
|
-
task_group.
|
114
|
+
task_group.archive_path = archive_path
|
116
115
|
task_group = add_commit_refresh(obj=task_group, db=db)
|
117
116
|
|
118
117
|
# Prepare replacements for templates
|
@@ -220,7 +219,7 @@ def collect_local(
|
|
220
219
|
activity = add_commit_refresh(obj=activity, db=db)
|
221
220
|
|
222
221
|
logger.info("_prepare_tasks_metadata - start")
|
223
|
-
task_list =
|
222
|
+
task_list = prepare_tasks_metadata(
|
224
223
|
package_manifest=pkg_manifest,
|
225
224
|
package_version=task_group.version,
|
226
225
|
package_root=Path(package_root),
|
@@ -241,15 +240,15 @@ def collect_local(
|
|
241
240
|
|
242
241
|
# Update task_group data
|
243
242
|
logger.info(
|
244
|
-
"Add
|
243
|
+
"Add env_info, venv_size and venv_file_number "
|
245
244
|
"to TaskGroupV2 - start"
|
246
245
|
)
|
247
|
-
task_group.
|
246
|
+
task_group.env_info = pip_freeze_stdout
|
248
247
|
task_group.venv_size_in_kB = int(venv_size)
|
249
248
|
task_group.venv_file_number = int(venv_file_number)
|
250
249
|
task_group = add_commit_refresh(obj=task_group, db=db)
|
251
250
|
logger.info(
|
252
|
-
"Add
|
251
|
+
"Add env_info, venv_size and venv_file_number "
|
253
252
|
"to TaskGroupV2 - end"
|
254
253
|
)
|
255
254
|
|
@@ -0,0 +1,234 @@
|
|
1
|
+
import json
|
2
|
+
import shutil
|
3
|
+
import time
|
4
|
+
from pathlib import Path
|
5
|
+
from tempfile import TemporaryDirectory
|
6
|
+
|
7
|
+
from ..utils_database import create_db_tasks_and_update_task_group_sync
|
8
|
+
from ..utils_pixi import parse_collect_stdout
|
9
|
+
from ..utils_pixi import SOURCE_DIR_NAME
|
10
|
+
from fractal_server.app.db import get_sync_db
|
11
|
+
from fractal_server.app.schemas.v2 import FractalUploadedFile
|
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.config import get_settings
|
16
|
+
from fractal_server.logger import reset_logger_handlers
|
17
|
+
from fractal_server.logger import set_logger
|
18
|
+
from fractal_server.syringe import Inject
|
19
|
+
from fractal_server.tasks.utils import get_log_path
|
20
|
+
from fractal_server.tasks.v2.local._utils import _customize_and_run_template
|
21
|
+
from fractal_server.tasks.v2.local._utils import check_task_files_exist
|
22
|
+
from fractal_server.tasks.v2.utils_background import add_commit_refresh
|
23
|
+
from fractal_server.tasks.v2.utils_background import fail_and_cleanup
|
24
|
+
from fractal_server.tasks.v2.utils_background import (
|
25
|
+
get_activity_and_task_group,
|
26
|
+
)
|
27
|
+
from fractal_server.tasks.v2.utils_background import get_current_log
|
28
|
+
from fractal_server.tasks.v2.utils_background import prepare_tasks_metadata
|
29
|
+
from fractal_server.tasks.v2.utils_templates import SCRIPTS_SUBFOLDER
|
30
|
+
from fractal_server.utils import get_timestamp
|
31
|
+
|
32
|
+
|
33
|
+
def collect_local_pixi(
|
34
|
+
*,
|
35
|
+
task_group_activity_id: int,
|
36
|
+
task_group_id: int,
|
37
|
+
tar_gz_file: FractalUploadedFile,
|
38
|
+
) -> None:
|
39
|
+
settings = Inject(get_settings)
|
40
|
+
|
41
|
+
LOGGER_NAME = f"{__name__}.ID{task_group_activity_id}"
|
42
|
+
|
43
|
+
with TemporaryDirectory() as tmpdir:
|
44
|
+
log_file_path = get_log_path(Path(tmpdir))
|
45
|
+
logger = set_logger(
|
46
|
+
logger_name=LOGGER_NAME,
|
47
|
+
log_file_path=log_file_path,
|
48
|
+
)
|
49
|
+
|
50
|
+
with next(get_sync_db()) as db:
|
51
|
+
success, task_group, activity = get_activity_and_task_group(
|
52
|
+
task_group_activity_id=task_group_activity_id,
|
53
|
+
task_group_id=task_group_id,
|
54
|
+
db=db,
|
55
|
+
)
|
56
|
+
if not success:
|
57
|
+
return
|
58
|
+
|
59
|
+
logger.info("START")
|
60
|
+
for key, value in task_group.model_dump().items():
|
61
|
+
logger.debug(f"task_group.{key}: {value}")
|
62
|
+
|
63
|
+
if Path(task_group.path).exists():
|
64
|
+
error_msg = f"{task_group.path} already exists."
|
65
|
+
logger.error(error_msg)
|
66
|
+
fail_and_cleanup(
|
67
|
+
task_group=task_group,
|
68
|
+
task_group_activity=activity,
|
69
|
+
logger_name=LOGGER_NAME,
|
70
|
+
log_file_path=log_file_path,
|
71
|
+
exception=FileExistsError(error_msg),
|
72
|
+
db=db,
|
73
|
+
)
|
74
|
+
return
|
75
|
+
|
76
|
+
# Set `pixi_home`
|
77
|
+
pixi_home = settings.pixi.versions[task_group.pixi_version]
|
78
|
+
|
79
|
+
try:
|
80
|
+
Path(task_group.path).mkdir(parents=True)
|
81
|
+
logger.info(f"Created {task_group.path}")
|
82
|
+
archive_path = Path(
|
83
|
+
task_group.path, tar_gz_file.filename
|
84
|
+
).as_posix()
|
85
|
+
logger.info(f"Write tar.gz-file contents into {archive_path}.")
|
86
|
+
with open(archive_path, "wb") as f:
|
87
|
+
f.write(tar_gz_file.contents)
|
88
|
+
task_group.archive_path = archive_path
|
89
|
+
task_group = add_commit_refresh(obj=task_group, db=db)
|
90
|
+
|
91
|
+
common_args = dict(
|
92
|
+
replacements={
|
93
|
+
("__PIXI_HOME__", pixi_home),
|
94
|
+
("__PACKAGE_DIR__", task_group.path),
|
95
|
+
("__TAR_GZ_PATH__", archive_path),
|
96
|
+
(
|
97
|
+
"__IMPORT_PACKAGE_NAME__",
|
98
|
+
task_group.pkg_name.replace("-", "_"),
|
99
|
+
),
|
100
|
+
("__SOURCE_DIR_NAME__", SOURCE_DIR_NAME),
|
101
|
+
},
|
102
|
+
script_dir=Path(
|
103
|
+
task_group.path, SCRIPTS_SUBFOLDER
|
104
|
+
).as_posix(),
|
105
|
+
prefix=(
|
106
|
+
f"{int(time.time())}_"
|
107
|
+
f"{TaskGroupActivityActionV2.COLLECT}_"
|
108
|
+
),
|
109
|
+
logger_name=LOGGER_NAME,
|
110
|
+
)
|
111
|
+
|
112
|
+
activity.status = TaskGroupActivityStatusV2.ONGOING
|
113
|
+
activity.log = get_current_log(log_file_path)
|
114
|
+
activity = add_commit_refresh(obj=activity, db=db)
|
115
|
+
|
116
|
+
# Run script 1
|
117
|
+
_customize_and_run_template(
|
118
|
+
template_filename="pixi_1_extract.sh",
|
119
|
+
**common_args,
|
120
|
+
)
|
121
|
+
activity.log = get_current_log(log_file_path)
|
122
|
+
activity = add_commit_refresh(obj=activity, db=db)
|
123
|
+
|
124
|
+
# Run script 2
|
125
|
+
_customize_and_run_template(
|
126
|
+
template_filename="pixi_2_install.sh",
|
127
|
+
**common_args,
|
128
|
+
)
|
129
|
+
activity.log = get_current_log(log_file_path)
|
130
|
+
activity = add_commit_refresh(obj=activity, db=db)
|
131
|
+
|
132
|
+
# Run script 3
|
133
|
+
stdout = _customize_and_run_template(
|
134
|
+
template_filename="pixi_3_post_install.sh",
|
135
|
+
**common_args,
|
136
|
+
)
|
137
|
+
activity.log = get_current_log(log_file_path)
|
138
|
+
activity = add_commit_refresh(obj=activity, db=db)
|
139
|
+
|
140
|
+
# Parse stdout
|
141
|
+
parsed_output = parse_collect_stdout(stdout)
|
142
|
+
package_root = parsed_output["package_root"]
|
143
|
+
venv_size = parsed_output["venv_size"]
|
144
|
+
venv_file_number = parsed_output["venv_file_number"]
|
145
|
+
project_python_wrapper = parsed_output[
|
146
|
+
"project_python_wrapper"
|
147
|
+
]
|
148
|
+
|
149
|
+
# Read and validate manifest
|
150
|
+
# NOTE: we are only supporting the manifest path being relative
|
151
|
+
# to the top-level folder
|
152
|
+
manifest_path = f"{package_root}/__FRACTAL_MANIFEST__.json"
|
153
|
+
with open(manifest_path) as json_data:
|
154
|
+
pkg_manifest_dict = json.load(json_data)
|
155
|
+
logger.info(f"loaded {manifest_path=}")
|
156
|
+
logger.info("now validating manifest content")
|
157
|
+
pkg_manifest = ManifestV2(**pkg_manifest_dict)
|
158
|
+
logger.info("validated manifest content")
|
159
|
+
activity.log = get_current_log(log_file_path)
|
160
|
+
activity = add_commit_refresh(obj=activity, db=db)
|
161
|
+
|
162
|
+
logger.info("_prepare_tasks_metadata - start")
|
163
|
+
task_list = prepare_tasks_metadata(
|
164
|
+
package_manifest=pkg_manifest,
|
165
|
+
package_version=task_group.version,
|
166
|
+
package_root=Path(package_root),
|
167
|
+
project_python_wrapper=Path(project_python_wrapper),
|
168
|
+
)
|
169
|
+
check_task_files_exist(task_list=task_list)
|
170
|
+
logger.info("_prepare_tasks_metadata - end")
|
171
|
+
activity.log = get_current_log(log_file_path)
|
172
|
+
activity = add_commit_refresh(obj=activity, db=db)
|
173
|
+
|
174
|
+
logger.info("create_db_tasks_and_update_task_group - start")
|
175
|
+
create_db_tasks_and_update_task_group_sync(
|
176
|
+
task_list=task_list,
|
177
|
+
task_group_id=task_group.id,
|
178
|
+
db=db,
|
179
|
+
)
|
180
|
+
logger.info("create_db_tasks_and_update_task_group - end")
|
181
|
+
|
182
|
+
# Update task_group data
|
183
|
+
logger.info(
|
184
|
+
"Add env_info, venv_size and venv_file_number "
|
185
|
+
"to TaskGroupV2 - start"
|
186
|
+
)
|
187
|
+
with Path(
|
188
|
+
task_group.path,
|
189
|
+
SOURCE_DIR_NAME,
|
190
|
+
"pixi.lock",
|
191
|
+
).open() as f:
|
192
|
+
pixi_lock_contents = f.read()
|
193
|
+
|
194
|
+
# NOTE: see issue 2626 about whether to keep `pixi.lock` files
|
195
|
+
# in the database
|
196
|
+
task_group.env_info = pixi_lock_contents
|
197
|
+
task_group.venv_size_in_kB = int(venv_size)
|
198
|
+
task_group.venv_file_number = int(venv_file_number)
|
199
|
+
task_group = add_commit_refresh(obj=task_group, db=db)
|
200
|
+
logger.info(
|
201
|
+
"Add env_info, venv_size and venv_file_number "
|
202
|
+
"to TaskGroupV2 - end"
|
203
|
+
)
|
204
|
+
|
205
|
+
# Finalize (write metadata to DB)
|
206
|
+
logger.info("finalising - START")
|
207
|
+
activity.status = TaskGroupActivityStatusV2.OK
|
208
|
+
activity.timestamp_ended = get_timestamp()
|
209
|
+
activity = add_commit_refresh(obj=activity, db=db)
|
210
|
+
logger.info("finalising - END")
|
211
|
+
logger.info("END")
|
212
|
+
|
213
|
+
reset_logger_handlers(logger)
|
214
|
+
|
215
|
+
except Exception as collection_e:
|
216
|
+
# Delete corrupted package dir
|
217
|
+
try:
|
218
|
+
logger.info(f"Now delete folder {task_group.path}")
|
219
|
+
shutil.rmtree(task_group.path)
|
220
|
+
logger.info(f"Deleted folder {task_group.path}")
|
221
|
+
except Exception as rm_e:
|
222
|
+
logger.error(
|
223
|
+
"Removing folder failed.\n"
|
224
|
+
f"Original error:\n{str(rm_e)}"
|
225
|
+
)
|
226
|
+
|
227
|
+
fail_and_cleanup(
|
228
|
+
task_group=task_group,
|
229
|
+
task_group_activity=activity,
|
230
|
+
logger_name=LOGGER_NAME,
|
231
|
+
log_file_path=log_file_path,
|
232
|
+
exception=collection_e,
|
233
|
+
db=db,
|
234
|
+
)
|
@@ -1,4 +1,3 @@
|
|
1
|
-
import logging
|
2
1
|
import shutil
|
3
2
|
import time
|
4
3
|
from pathlib import Path
|
@@ -6,11 +5,10 @@ from tempfile import TemporaryDirectory
|
|
6
5
|
|
7
6
|
from ..utils_background import add_commit_refresh
|
8
7
|
from ..utils_background import fail_and_cleanup
|
8
|
+
from ..utils_background import get_activity_and_task_group
|
9
9
|
from ..utils_templates import get_collection_replacements
|
10
10
|
from ._utils import _customize_and_run_template
|
11
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
12
|
from fractal_server.app.schemas.v2 import TaskGroupActivityActionV2
|
15
13
|
from fractal_server.app.schemas.v2 import TaskGroupV2OriginEnum
|
16
14
|
from fractal_server.app.schemas.v2.task_group import TaskGroupActivityStatusV2
|
@@ -49,17 +47,12 @@ def deactivate_local(
|
|
49
47
|
)
|
50
48
|
|
51
49
|
with next(get_sync_db()) as db:
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
logging.error(
|
59
|
-
"Cannot find database rows with "
|
60
|
-
f"{task_group_id=} and {task_group_activity_id=}:\n"
|
61
|
-
f"{task_group=}\n{activity=}. Exit."
|
62
|
-
)
|
50
|
+
success, task_group, activity = get_activity_and_task_group(
|
51
|
+
task_group_activity_id=task_group_activity_id,
|
52
|
+
task_group_id=task_group_id,
|
53
|
+
db=db,
|
54
|
+
)
|
55
|
+
if not success:
|
63
56
|
return
|
64
57
|
|
65
58
|
# Log some info
|
@@ -87,10 +80,10 @@ def deactivate_local(
|
|
87
80
|
activity.status = TaskGroupActivityStatusV2.ONGOING
|
88
81
|
activity = add_commit_refresh(obj=activity, db=db)
|
89
82
|
|
90
|
-
if task_group.
|
83
|
+
if task_group.env_info is None:
|
91
84
|
logger.warning(
|
92
85
|
"Recreate pip-freeze information, since "
|
93
|
-
f"{task_group.
|
86
|
+
f"{task_group.env_info=}. NOTE: this should only "
|
94
87
|
"happen for task groups created before 2.9.0."
|
95
88
|
)
|
96
89
|
# Prepare replacements for templates
|
@@ -120,7 +113,7 @@ def deactivate_local(
|
|
120
113
|
logger.info("Add pip freeze stdout to TaskGroupV2 - start")
|
121
114
|
activity.log = get_current_log(log_file_path)
|
122
115
|
activity = add_commit_refresh(obj=activity, db=db)
|
123
|
-
task_group.
|
116
|
+
task_group.env_info = pip_freeze_stdout
|
124
117
|
task_group = add_commit_refresh(obj=task_group, db=db)
|
125
118
|
logger.info("Add pip freeze stdout to TaskGroupV2 - end")
|
126
119
|
|
@@ -131,15 +124,15 @@ def deactivate_local(
|
|
131
124
|
f"Handle specific cases for {task_group.origin=}."
|
132
125
|
)
|
133
126
|
|
134
|
-
# Blocking situation: `
|
127
|
+
# Blocking situation: `archive_path` is not set or points
|
135
128
|
# to a missing path
|
136
129
|
if (
|
137
|
-
task_group.
|
138
|
-
or not Path(task_group.
|
130
|
+
task_group.archive_path is None
|
131
|
+
or not Path(task_group.archive_path).exists()
|
139
132
|
):
|
140
133
|
error_msg = (
|
141
134
|
"Invalid wheel path for task group with "
|
142
|
-
f"{task_group_id=}. {task_group.
|
135
|
+
f"{task_group_id=}. {task_group.archive_path=} is "
|
143
136
|
"unset or does not exist."
|
144
137
|
)
|
145
138
|
logger.error(error_msg)
|
@@ -153,48 +146,52 @@ def deactivate_local(
|
|
153
146
|
)
|
154
147
|
return
|
155
148
|
|
156
|
-
# Recoverable situation: `
|
149
|
+
# Recoverable situation: `archive_path` was not yet copied
|
157
150
|
# over to the correct server-side folder
|
158
|
-
|
159
|
-
|
151
|
+
archive_path_parent_dir = Path(
|
152
|
+
task_group.archive_path
|
153
|
+
).parent
|
154
|
+
if archive_path_parent_dir != Path(task_group.path):
|
160
155
|
logger.warning(
|
161
|
-
f"{
|
162
|
-
f"{task_group.path}. NOTE: this should only "
|
156
|
+
f"{archive_path_parent_dir.as_posix()} differs "
|
157
|
+
f"from {task_group.path}. NOTE: this should only "
|
163
158
|
"happen for task groups created before 2.9.0."
|
164
159
|
)
|
165
160
|
|
166
|
-
if task_group.
|
161
|
+
if task_group.archive_path not in task_group.env_info:
|
167
162
|
raise ValueError(
|
168
|
-
f"Cannot find {task_group.
|
163
|
+
f"Cannot find {task_group.archive_path=} in "
|
169
164
|
"pip-freeze data. Exit."
|
170
165
|
)
|
171
166
|
|
172
167
|
logger.info(
|
173
168
|
f"Now copy wheel file into {task_group.path}."
|
174
169
|
)
|
175
|
-
|
170
|
+
new_archive_path = (
|
176
171
|
Path(task_group.path)
|
177
|
-
/ Path(task_group.
|
172
|
+
/ Path(task_group.archive_path).name
|
178
173
|
).as_posix()
|
179
|
-
shutil.copy(task_group.
|
180
|
-
logger.info(
|
174
|
+
shutil.copy(task_group.archive_path, new_archive_path)
|
175
|
+
logger.info(
|
176
|
+
f"Copied wheel file to {new_archive_path}."
|
177
|
+
)
|
181
178
|
|
182
|
-
task_group.
|
183
|
-
new_pip_freeze = task_group.
|
184
|
-
task_group.
|
185
|
-
|
179
|
+
task_group.archive_path = new_archive_path
|
180
|
+
new_pip_freeze = task_group.env_info.replace(
|
181
|
+
task_group.archive_path,
|
182
|
+
new_archive_path,
|
186
183
|
)
|
187
|
-
task_group.
|
184
|
+
task_group.env_info = new_pip_freeze
|
188
185
|
task_group = add_commit_refresh(obj=task_group, db=db)
|
189
186
|
logger.info(
|
190
|
-
"Updated `
|
187
|
+
"Updated `archive_path` and `env_info` "
|
191
188
|
"task-group attributes."
|
192
189
|
)
|
193
190
|
|
194
191
|
# Fail if `pip_freeze` includes "github.com", see
|
195
192
|
# https://github.com/fractal-analytics-platform/fractal-server/issues/2142
|
196
193
|
for forbidden_string in FORBIDDEN_DEPENDENCY_STRINGS:
|
197
|
-
if forbidden_string in task_group.
|
194
|
+
if forbidden_string in task_group.env_info:
|
198
195
|
raise ValueError(
|
199
196
|
"Deactivation and reactivation of task packages "
|
200
197
|
f"with direct {forbidden_string} dependencies "
|
@@ -0,0 +1,102 @@
|
|
1
|
+
import shutil
|
2
|
+
from pathlib import Path
|
3
|
+
from tempfile import TemporaryDirectory
|
4
|
+
|
5
|
+
from ..utils_background import add_commit_refresh
|
6
|
+
from ..utils_background import fail_and_cleanup
|
7
|
+
from ..utils_background import get_activity_and_task_group
|
8
|
+
from ..utils_pixi import SOURCE_DIR_NAME
|
9
|
+
from fractal_server.app.db import get_sync_db
|
10
|
+
from fractal_server.app.schemas.v2.task_group import TaskGroupActivityStatusV2
|
11
|
+
from fractal_server.logger import reset_logger_handlers
|
12
|
+
from fractal_server.logger import set_logger
|
13
|
+
from fractal_server.tasks.utils import get_log_path
|
14
|
+
from fractal_server.tasks.v2.utils_background import get_current_log
|
15
|
+
from fractal_server.utils import get_timestamp
|
16
|
+
|
17
|
+
|
18
|
+
def deactivate_local_pixi(
|
19
|
+
*,
|
20
|
+
task_group_activity_id: int,
|
21
|
+
task_group_id: int,
|
22
|
+
) -> None:
|
23
|
+
"""
|
24
|
+
Deactivate a pixi task group venv.
|
25
|
+
|
26
|
+
This function is run as a background task, therefore exceptions must be
|
27
|
+
handled.
|
28
|
+
|
29
|
+
Arguments:
|
30
|
+
task_group_id:
|
31
|
+
task_group_activity_id:
|
32
|
+
"""
|
33
|
+
|
34
|
+
LOGGER_NAME = f"{__name__}.ID{task_group_activity_id}"
|
35
|
+
|
36
|
+
with TemporaryDirectory() as tmpdir:
|
37
|
+
log_file_path = get_log_path(Path(tmpdir))
|
38
|
+
logger = set_logger(
|
39
|
+
logger_name=LOGGER_NAME,
|
40
|
+
log_file_path=log_file_path,
|
41
|
+
)
|
42
|
+
|
43
|
+
with next(get_sync_db()) as db:
|
44
|
+
success, task_group, activity = get_activity_and_task_group(
|
45
|
+
task_group_activity_id=task_group_activity_id,
|
46
|
+
task_group_id=task_group_id,
|
47
|
+
db=db,
|
48
|
+
)
|
49
|
+
if not success:
|
50
|
+
return
|
51
|
+
|
52
|
+
# Log some info
|
53
|
+
logger.debug("START")
|
54
|
+
|
55
|
+
for key, value in task_group.model_dump().items():
|
56
|
+
logger.debug(f"task_group.{key}: {value}")
|
57
|
+
|
58
|
+
source_dir = Path(task_group.path, SOURCE_DIR_NAME)
|
59
|
+
if not source_dir.exists():
|
60
|
+
error_msg = f"'{source_dir.as_posix()}' does not exist."
|
61
|
+
logger.error(error_msg)
|
62
|
+
fail_and_cleanup(
|
63
|
+
task_group=task_group,
|
64
|
+
task_group_activity=activity,
|
65
|
+
logger_name=LOGGER_NAME,
|
66
|
+
log_file_path=log_file_path,
|
67
|
+
exception=FileNotFoundError(error_msg),
|
68
|
+
db=db,
|
69
|
+
)
|
70
|
+
return
|
71
|
+
|
72
|
+
try:
|
73
|
+
|
74
|
+
activity.status = TaskGroupActivityStatusV2.ONGOING
|
75
|
+
activity = add_commit_refresh(obj=activity, db=db)
|
76
|
+
|
77
|
+
# Actually mark the task group as non-active
|
78
|
+
logger.info("Now setting `active=False`.")
|
79
|
+
task_group.active = False
|
80
|
+
task_group = add_commit_refresh(obj=task_group, db=db)
|
81
|
+
|
82
|
+
# Proceed with deactivation
|
83
|
+
logger.info(f"Now removing '{source_dir.as_posix()}'.")
|
84
|
+
shutil.rmtree(source_dir)
|
85
|
+
logger.info(f"All good, '{source_dir.as_posix()}' removed.")
|
86
|
+
activity.status = TaskGroupActivityStatusV2.OK
|
87
|
+
activity.log = get_current_log(log_file_path)
|
88
|
+
activity.timestamp_ended = get_timestamp()
|
89
|
+
activity = add_commit_refresh(obj=activity, db=db)
|
90
|
+
|
91
|
+
reset_logger_handlers(logger)
|
92
|
+
|
93
|
+
except Exception as e:
|
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=e,
|
100
|
+
db=db,
|
101
|
+
)
|
102
|
+
return
|