Skip to content

Commit 702d236

Browse files
Add update description runner endpoint
1 parent a57dc01 commit 702d236

5 files changed

Lines changed: 226 additions & 5 deletions

File tree

autosubmit_api/routers/v4/runners.py

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from enum import Enum
12
from typing import Any, Dict, List, Optional
23

34
from fastapi import APIRouter, Depends, HTTPException
@@ -99,6 +100,13 @@ class SetJobStatusBody(RunnerEndpointBody):
99100
command_params: SetJobStatusCmdParams
100101

101102

103+
class RunnerEndpointsCategory(str, Enum):
104+
SET_JOB_STATUS = "SET_JOB_STATUS"
105+
CREATE_EXPERIMENT = "CREATE_EXPERIMENT"
106+
RUNNER_RUN = "RUNNER_RUN"
107+
UPDATE_EXPERIMENT_DETAILS = "UPDATE_EXPERIMENT_DETAILS"
108+
109+
102110
def _endpoint_enabled(endpoint_name: str) -> bool:
103111
config = read_config_file()
104112
endpoints = config.get("RUNNER_CONFIGURATION", {}).get("ENDPOINTS", {})
@@ -114,7 +122,7 @@ async def set_job_status(
114122
"""
115123
Set the job status for an experiment using the specified runner profile.
116124
"""
117-
if not _endpoint_enabled("SET_JOB_STATUS"):
125+
if not _endpoint_enabled(RunnerEndpointsCategory.SET_JOB_STATUS.value):
118126
raise HTTPException(
119127
status_code=403,
120128
detail="The set-job-status endpoint is currently disabled.",
@@ -166,7 +174,7 @@ async def run_experiment(
166174
"""
167175
Run an experiment using the specified runner profile.
168176
"""
169-
if not _endpoint_enabled("RUNNER_RUN"):
177+
if not _endpoint_enabled(RunnerEndpointsCategory.RUNNER_RUN.value):
170178
raise HTTPException(
171179
status_code=403,
172180
detail="The run-experiment endpoint is currently disabled.",
@@ -218,7 +226,7 @@ async def get_runner_run_status(
218226
"""
219227
Get the status of the runner run for a given experiment ID.
220228
"""
221-
if not _endpoint_enabled("RUNNER_RUN"):
229+
if not _endpoint_enabled(RunnerEndpointsCategory.RUNNER_RUN.value):
222230
raise HTTPException(
223231
status_code=403,
224232
detail="The get-runner-run-status endpoint is currently disabled.",
@@ -264,7 +272,7 @@ async def stop_experiment(
264272
"""
265273
Stop an experiment using the specified runner profile.
266274
"""
267-
if not _endpoint_enabled("RUNNER_RUN"):
275+
if not _endpoint_enabled(RunnerEndpointsCategory.RUNNER_RUN.value):
268276
raise HTTPException(
269277
status_code=403,
270278
detail="The stop-experiment endpoint is currently disabled.",
@@ -310,7 +318,7 @@ async def create_experiment(
310318
"""
311319
Create an experiment using the specified runner profile.
312320
"""
313-
if not _endpoint_enabled("CREATE_EXPERIMENT"):
321+
if not _endpoint_enabled(RunnerEndpointsCategory.CREATE_EXPERIMENT.value):
314322
raise HTTPException(
315323
status_code=403,
316324
detail="The create-experiment endpoint is currently disabled.",
@@ -352,3 +360,59 @@ async def create_experiment(
352360
"expid": expid,
353361
"message": f"Experiment {expid} created successfully.",
354362
}
363+
364+
365+
class UpdateExperimentBody(RunnerEndpointBody):
366+
expid: str
367+
description: str
368+
369+
370+
@router.post(
371+
"/command/update-experiment-description", name="Update experiment description"
372+
)
373+
async def update_experiment_description(
374+
body: UpdateExperimentBody,
375+
user_id: Optional[str] = Depends(auth_token_dependency()),
376+
) -> Dict[str, Any]:
377+
"""
378+
Update the description of an experiment using the specified runner profile.
379+
"""
380+
if not _endpoint_enabled(RunnerEndpointsCategory.UPDATE_EXPERIMENT_DETAILS.value):
381+
raise HTTPException(
382+
status_code=403,
383+
detail="The update-experiment-description endpoint is currently disabled.",
384+
)
385+
386+
expid = body.expid
387+
description = body.description
388+
389+
logger.info(
390+
f"Updating description for experiment {expid} using profile {body.profile_name}"
391+
)
392+
393+
try:
394+
profile = process_profile(body.profile_name, body.profile_params)
395+
logger.debug(
396+
f"Processing profile: {body.profile_name}. Profile data: {profile}"
397+
)
398+
399+
runner_type, module_loader_type, modules = (
400+
profile.get("RUNNER_TYPE"),
401+
profile.get("MODULE_LOADER_TYPE"),
402+
profile.get("MODULES"),
403+
)
404+
405+
runner_extra_params = get_runner_extra_params(profile)
406+
407+
module_loader = get_module_loader(module_loader_type, modules)
408+
runner = get_runner(runner_type, module_loader, **runner_extra_params)
409+
await runner.update_description(expid, description)
410+
except Exception as exc:
411+
raise HTTPException(
412+
status_code=500,
413+
detail=f"Failed to update description for experiment {expid}: {exc}",
414+
)
415+
416+
return {
417+
"message": f"Description for experiment {expid} updated successfully.",
418+
}

autosubmit_api/runners/base.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,11 @@ async def set_job_status(
121121
:param check_wrapper: Whether to check the wrapper script.
122122
:param update_version: Whether to update the version.
123123
"""
124+
125+
async def update_description(self, expid: str, description: str):
126+
"""
127+
Update the description of an experiment.
128+
129+
:param expid: The experiment ID.
130+
:param description: The new description for the experiment.
131+
"""

autosubmit_api/runners/local_runner.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,3 +373,18 @@ async def set_job_status(
373373
except Exception as exc:
374374
logger.error(f"Command failed with error: {exc}")
375375
raise exc
376+
377+
async def update_description(self, expid: str, description: str):
378+
autosubmit_command = f'autosubmit updatedescrip {expid} "{description}"'
379+
wrapped_command = self.module_loader.generate_command(autosubmit_command)
380+
381+
try:
382+
logger.debug(f"Running command: {wrapped_command}")
383+
output = subprocess.check_output(
384+
wrapped_command, shell=True, text=True, executable="/bin/bash"
385+
).strip()
386+
logger.debug(f"Command output: {output}")
387+
return output
388+
except Exception as exc:
389+
logger.error(f"Command failed with error: {exc}")
390+
raise exc

autosubmit_api/runners/ssh_runner.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,3 +547,21 @@ async def set_job_status(
547547
except Exception as exc:
548548
logger.error(f"Command failed with error: {exc}")
549549
raise exc
550+
551+
async def update_description(self, expid: str, description: str):
552+
autosubmit_command = f'autosubmit updatedescrip {expid} "{description}"'
553+
prepared_command = self._prepare_command(autosubmit_command)
554+
555+
try:
556+
logger.debug(f"Running update description command: {prepared_command}")
557+
stdout, stderr, exit_code = self._execute_command(prepared_command)
558+
559+
if exit_code != 0:
560+
logger.error(f"Command failed with exit code {exit_code}: {stderr}")
561+
raise RuntimeError(f"Failed to update description: {stderr}")
562+
563+
logger.debug(f"Update description output: {stdout}")
564+
return stdout
565+
except Exception as exc:
566+
logger.error(f"Command failed with error: {exc}")
567+
raise exc

tests/test_endpoints_v4.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,6 +1188,122 @@ def test_no_description(self, fixture_fastapi_client: TestClient):
11881188
assert response.status_code != 200
11891189

11901190

1191+
class TestRunnerUpdateExperimentDescription:
1192+
endpoint = "/v4/runners/command/update-experiment-description"
1193+
1194+
def test_disabled_endpoint(self, fixture_fastapi_client: TestClient):
1195+
with patch(
1196+
"autosubmit_api.routers.v4.runners.read_config_file"
1197+
) as mock_read_config:
1198+
mock_read_config.return_value = {
1199+
"RUNNER_CONFIGURATION": {
1200+
"ENDPOINTS": {"UPDATE_EXPERIMENT_DETAILS": {"ENABLED": False}},
1201+
},
1202+
}
1203+
1204+
response = fixture_fastapi_client.post(
1205+
self.endpoint,
1206+
json={
1207+
"expid": "test_expid",
1208+
"description": "New description",
1209+
"profile_name": "ANY_PROFILE",
1210+
},
1211+
)
1212+
1213+
assert response.status_code == 403
1214+
assert "disabled" in response.json()["error_message"]
1215+
1216+
def test_valid_ssh_request(self, fixture_fastapi_client: TestClient):
1217+
with (
1218+
patch(
1219+
"autosubmit_api.runners.runner_config.read_config_file"
1220+
) as mock_read_config,
1221+
patch("autosubmit_api.routers.v4.runners.get_runner") as mock_get_runner,
1222+
):
1223+
mock_read_config.return_value = {
1224+
"RUNNER_CONFIGURATION": {
1225+
"PROFILES": {
1226+
"SSH_AUTOSUBMIT_DEV": {
1227+
"RUNNER_TYPE": "SSH",
1228+
"MODULE_LOADER_TYPE": "CONDA",
1229+
"MODULES": ["autosubmit"],
1230+
"SSH": {
1231+
"HOST": "bscesautosubmit03.bsc.es",
1232+
"PORT": 22,
1233+
},
1234+
}
1235+
}
1236+
}
1237+
}
1238+
1239+
mock_runner = MagicMock()
1240+
mock_runner.update_description = AsyncMock(return_value=None)
1241+
mock_get_runner.return_value = mock_runner
1242+
1243+
response = fixture_fastapi_client.post(
1244+
self.endpoint,
1245+
json={
1246+
"expid": "test_expid",
1247+
"description": "New description",
1248+
"profile_name": "SSH_AUTOSUBMIT_DEV",
1249+
"profile_params": {
1250+
"SSH": {
1251+
"USERNAME": "test_user",
1252+
}
1253+
},
1254+
},
1255+
)
1256+
1257+
assert response.status_code == 200
1258+
1259+
def test_invalid_profile(self, fixture_fastapi_client: TestClient):
1260+
response = fixture_fastapi_client.post(
1261+
self.endpoint,
1262+
json={
1263+
"expid": "test_expid",
1264+
"description": "New description",
1265+
"profile_name": "NON_EXISTENT_PROFILE",
1266+
},
1267+
)
1268+
1269+
assert response.status_code != 200
1270+
1271+
def test_runner_failure(self, fixture_fastapi_client: TestClient):
1272+
with (
1273+
patch(
1274+
"autosubmit_api.runners.runner_config.read_config_file"
1275+
) as mock_read_config,
1276+
patch("autosubmit_api.routers.v4.runners.get_runner") as mock_get_runner,
1277+
):
1278+
mock_read_config.return_value = {
1279+
"RUNNER_CONFIGURATION": {
1280+
"PROFILES": {
1281+
"LOCAL_AUTOSUBMIT_DEV": {
1282+
"RUNNER_TYPE": "LOCAL",
1283+
"MODULE_LOADER_TYPE": "NO_MODULE",
1284+
}
1285+
}
1286+
}
1287+
}
1288+
1289+
mock_runner = MagicMock()
1290+
mock_runner.update_description = AsyncMock(
1291+
side_effect=RuntimeError("Update failed")
1292+
)
1293+
mock_get_runner.return_value = mock_runner
1294+
1295+
response = fixture_fastapi_client.post(
1296+
self.endpoint,
1297+
json={
1298+
"expid": "test_expid",
1299+
"description": "New description",
1300+
"profile_name": "LOCAL_AUTOSUBMIT_DEV",
1301+
},
1302+
)
1303+
1304+
assert response.status_code == 500
1305+
1306+
11911307
class TestRunnerConfigurations:
11921308
@pytest.mark.parametrize(
11931309
"file_content, expected",

0 commit comments

Comments
 (0)