orchestrator-lso 2.3.1__tar.gz → 2.4.1__tar.gz
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.
- {orchestrator_lso-2.3.1 → orchestrator_lso-2.4.1}/PKG-INFO +1 -1
- {orchestrator_lso-2.3.1 → orchestrator_lso-2.4.1}/lso/__init__.py +1 -1
- {orchestrator_lso-2.3.1 → orchestrator_lso-2.4.1}/lso/execute.py +3 -3
- {orchestrator_lso-2.3.1 → orchestrator_lso-2.4.1}/lso/playbook.py +32 -8
- {orchestrator_lso-2.3.1 → orchestrator_lso-2.4.1}/lso/routes/execute.py +2 -2
- {orchestrator_lso-2.3.1 → orchestrator_lso-2.4.1}/lso/routes/playbook.py +11 -5
- {orchestrator_lso-2.3.1 → orchestrator_lso-2.4.1}/lso/schema.py +2 -2
- {orchestrator_lso-2.3.1 → orchestrator_lso-2.4.1}/lso/tasks.py +76 -16
- {orchestrator_lso-2.3.1 → orchestrator_lso-2.4.1}/pyproject.toml +3 -2
- {orchestrator_lso-2.3.1 → orchestrator_lso-2.4.1}/README.md +0 -0
- {orchestrator_lso-2.3.1 → orchestrator_lso-2.4.1}/lso/app.py +0 -0
- {orchestrator_lso-2.3.1 → orchestrator_lso-2.4.1}/lso/config.py +0 -0
- {orchestrator_lso-2.3.1 → orchestrator_lso-2.4.1}/lso/environment.py +0 -0
- {orchestrator_lso-2.3.1 → orchestrator_lso-2.4.1}/lso/routes/__init__.py +0 -0
- {orchestrator_lso-2.3.1 → orchestrator_lso-2.4.1}/lso/routes/default.py +0 -0
- {orchestrator_lso-2.3.1 → orchestrator_lso-2.4.1}/lso/utils.py +0 -0
- {orchestrator_lso-2.3.1 → orchestrator_lso-2.4.1}/lso/worker.py +0 -0
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
"""Module for handling the execution of arbitrary executables."""
|
|
15
15
|
|
|
16
16
|
import subprocess # noqa: S404
|
|
17
|
-
import uuid
|
|
18
17
|
from pathlib import Path
|
|
18
|
+
from uuid import UUID, uuid4
|
|
19
19
|
|
|
20
20
|
from pydantic import HttpUrl
|
|
21
21
|
|
|
@@ -30,12 +30,12 @@ def get_executable_path(executable_name: Path) -> Path:
|
|
|
30
30
|
return Path(settings.EXECUTABLES_ROOT_DIR) / executable_name
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
def run_executable_async(executable_path: Path, args: list[str], callback: HttpUrl | None) ->
|
|
33
|
+
def run_executable_async(executable_path: Path, args: list[str], callback: HttpUrl | None) -> UUID:
|
|
34
34
|
"""Dispatch the task for executing an arbitrary executable remotely.
|
|
35
35
|
|
|
36
36
|
Uses a ThreadPoolExecutor (for local execution) or a Celery worker (for distributed tasks).
|
|
37
37
|
"""
|
|
38
|
-
job_id =
|
|
38
|
+
job_id = uuid4()
|
|
39
39
|
callback_url = str(callback) if callback else None
|
|
40
40
|
if settings.EXECUTOR == ExecutorType.THREADPOOL:
|
|
41
41
|
executor = get_thread_pool()
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
|
|
14
14
|
"""Module that gathers common API responses and data models."""
|
|
15
15
|
|
|
16
|
-
import uuid
|
|
17
16
|
from pathlib import Path
|
|
18
17
|
from typing import Any
|
|
18
|
+
from uuid import UUID, uuid4
|
|
19
19
|
|
|
20
20
|
from pydantic import HttpUrl
|
|
21
21
|
|
|
@@ -33,8 +33,11 @@ def run_playbook(
|
|
|
33
33
|
playbook_path: Path,
|
|
34
34
|
extra_vars: dict[str, Any],
|
|
35
35
|
inventory: dict[str, Any] | str,
|
|
36
|
-
callback: HttpUrl,
|
|
37
|
-
|
|
36
|
+
callback: HttpUrl | None,
|
|
37
|
+
progress: HttpUrl | None,
|
|
38
|
+
*,
|
|
39
|
+
progress_is_incremental: bool,
|
|
40
|
+
) -> UUID:
|
|
38
41
|
"""Run an Ansible playbook against a specified inventory.
|
|
39
42
|
|
|
40
43
|
:param Path playbook_path: Playbook to be executed.
|
|
@@ -42,19 +45,40 @@ def run_playbook(
|
|
|
42
45
|
:param dict[str, Any] | str inventory: The inventory that the playbook is executed against.
|
|
43
46
|
:param HttpUrl callback: Callback URL where the playbook should send a status update when execution is completed.
|
|
44
47
|
This is used for workflow-orchestrator to continue with the next step in a workflow.
|
|
45
|
-
:return:
|
|
46
|
-
:rtype: :class:`fastapi.responses.JSONResponse`
|
|
48
|
+
:return UUID: Job ID of the launched playbook.
|
|
47
49
|
"""
|
|
48
|
-
job_id =
|
|
50
|
+
job_id = uuid4()
|
|
51
|
+
callback_str = None
|
|
52
|
+
progress_str = None
|
|
53
|
+
if callback:
|
|
54
|
+
callback_str = str(callback)
|
|
55
|
+
if progress:
|
|
56
|
+
progress_str = str(progress)
|
|
57
|
+
|
|
49
58
|
if settings.EXECUTOR == ExecutorType.THREADPOOL:
|
|
50
59
|
executor = get_thread_pool()
|
|
51
60
|
executor_handle = executor.submit(
|
|
52
|
-
run_playbook_proc_task,
|
|
61
|
+
run_playbook_proc_task,
|
|
62
|
+
str(job_id),
|
|
63
|
+
str(playbook_path),
|
|
64
|
+
extra_vars,
|
|
65
|
+
inventory,
|
|
66
|
+
callback_str,
|
|
67
|
+
progress_str,
|
|
68
|
+
progress_is_incremental=progress_is_incremental,
|
|
53
69
|
)
|
|
54
70
|
if settings.TESTING:
|
|
55
71
|
executor_handle.result()
|
|
56
72
|
|
|
57
73
|
elif settings.EXECUTOR == ExecutorType.WORKER:
|
|
58
|
-
run_playbook_proc_task.delay(
|
|
74
|
+
run_playbook_proc_task.delay(
|
|
75
|
+
str(job_id),
|
|
76
|
+
str(playbook_path),
|
|
77
|
+
extra_vars,
|
|
78
|
+
inventory,
|
|
79
|
+
callback_str,
|
|
80
|
+
progress_str,
|
|
81
|
+
progress_is_incremental=progress_is_incremental,
|
|
82
|
+
)
|
|
59
83
|
|
|
60
84
|
return job_id
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
"""FastAPI route for running arbitrary executables."""
|
|
15
15
|
|
|
16
16
|
import asyncio
|
|
17
|
-
import uuid
|
|
18
17
|
from pathlib import Path
|
|
19
18
|
from typing import Annotated
|
|
19
|
+
from uuid import uuid4
|
|
20
20
|
|
|
21
21
|
from fastapi import APIRouter, HTTPException, status
|
|
22
22
|
from pydantic import AfterValidator, BaseModel, HttpUrl
|
|
@@ -61,6 +61,6 @@ async def run_executable_endpoint(params: ExecutableRunParams) -> ExecutableRunR
|
|
|
61
61
|
job_id = run_executable_async(params.executable_name, params.args, params.callback)
|
|
62
62
|
return ExecutableRunResponse(job_id=job_id)
|
|
63
63
|
|
|
64
|
-
job_id =
|
|
64
|
+
job_id = uuid4()
|
|
65
65
|
result = await asyncio.to_thread(run_executable_sync, str(params.executable_name), params.args)
|
|
66
66
|
return ExecutableRunResponse(job_id=job_id, result=result)
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
|
|
16
16
|
import json
|
|
17
17
|
import tempfile
|
|
18
|
-
import uuid
|
|
19
18
|
from contextlib import redirect_stderr
|
|
20
19
|
from io import StringIO
|
|
21
20
|
from pathlib import Path
|
|
22
21
|
from typing import Annotated, Any
|
|
22
|
+
from uuid import UUID
|
|
23
23
|
|
|
24
24
|
import ansible_runner
|
|
25
25
|
from ansible.inventory.manager import InventoryManager
|
|
@@ -44,7 +44,7 @@ def _inventory_validator(inventory: dict[str, Any] | str) -> dict[str, Any] | st
|
|
|
44
44
|
"""
|
|
45
45
|
if not ansible_runner.utils.isinventory(inventory):
|
|
46
46
|
detail = "Invalid inventory provided. Should be a string, or JSON object."
|
|
47
|
-
raise HTTPException(status_code=status.
|
|
47
|
+
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=detail)
|
|
48
48
|
|
|
49
49
|
loader = DataLoader()
|
|
50
50
|
output = StringIO()
|
|
@@ -58,7 +58,7 @@ def _inventory_validator(inventory: dict[str, Any] | str) -> dict[str, Any] | st
|
|
|
58
58
|
output.seek(0)
|
|
59
59
|
error_messages = output.readlines()
|
|
60
60
|
if error_messages:
|
|
61
|
-
raise HTTPException(status_code=status.
|
|
61
|
+
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=error_messages)
|
|
62
62
|
|
|
63
63
|
return inventory
|
|
64
64
|
|
|
@@ -79,7 +79,7 @@ PlaybookName = Annotated[Path, AfterValidator(_playbook_path_validator)]
|
|
|
79
79
|
class PlaybookRunResponse(BaseModel):
|
|
80
80
|
"""PlaybookRunResponse domain model schema."""
|
|
81
81
|
|
|
82
|
-
job_id:
|
|
82
|
+
job_id: UUID
|
|
83
83
|
|
|
84
84
|
|
|
85
85
|
class PlaybookRunParams(BaseModel):
|
|
@@ -89,7 +89,11 @@ class PlaybookRunParams(BaseModel):
|
|
|
89
89
|
#: configuration option ``ANSIBLE_PLAYBOOKS_ROOT_DIR``.
|
|
90
90
|
playbook_name: PlaybookName
|
|
91
91
|
#: The address where LSO should call back to upon completion.
|
|
92
|
-
callback: HttpUrl
|
|
92
|
+
callback: HttpUrl | None = None
|
|
93
|
+
#: Optionally, the address where LSO should send progress updates as the playbook executes.
|
|
94
|
+
progress: HttpUrl | None = None
|
|
95
|
+
#: Optionally, whether progress updates should be incremental or not.
|
|
96
|
+
progress_is_incremental: bool = True
|
|
93
97
|
#: The inventory to run the playbook against. This inventory can also include any host vars, if needed. When
|
|
94
98
|
#: including host vars, it should be a dictionary. Can be a simple string containing hostnames when no host vars are
|
|
95
99
|
#: needed. In the latter case, multiple hosts should be separated with a ``\n`` newline character only.
|
|
@@ -114,6 +118,8 @@ def run_playbook_endpoint(params: PlaybookRunParams) -> PlaybookRunResponse:
|
|
|
114
118
|
extra_vars=params.extra_vars,
|
|
115
119
|
inventory=params.inventory,
|
|
116
120
|
callback=params.callback,
|
|
121
|
+
progress=params.progress,
|
|
122
|
+
progress_is_incremental=params.progress_is_incremental,
|
|
117
123
|
)
|
|
118
124
|
|
|
119
125
|
return PlaybookRunResponse(job_id=job_id)
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
|
|
14
14
|
"""Module for defining the schema for running arbitrary executables."""
|
|
15
15
|
|
|
16
|
-
import uuid
|
|
17
16
|
from enum import StrEnum
|
|
17
|
+
from uuid import UUID
|
|
18
18
|
|
|
19
19
|
from pydantic import BaseModel, model_validator
|
|
20
20
|
|
|
@@ -46,5 +46,5 @@ class ExecutionResult(BaseModel):
|
|
|
46
46
|
class ExecutableRunResponse(BaseModel):
|
|
47
47
|
"""Response for running an arbitrary executable."""
|
|
48
48
|
|
|
49
|
-
job_id:
|
|
49
|
+
job_id: UUID
|
|
50
50
|
result: ExecutionResult | None = None
|
|
@@ -18,11 +18,12 @@ the results to a specified callback URL.
|
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
20
|
import logging
|
|
21
|
+
from collections.abc import Callable
|
|
21
22
|
from typing import Any
|
|
22
23
|
from uuid import UUID
|
|
23
24
|
|
|
24
|
-
import ansible_runner
|
|
25
25
|
import requests
|
|
26
|
+
from ansible_runner import Runner, run
|
|
26
27
|
from starlette import status
|
|
27
28
|
|
|
28
29
|
from lso.config import settings
|
|
@@ -36,9 +37,72 @@ class CallbackFailedError(Exception):
|
|
|
36
37
|
"""Exception raised when a callback url can't be reached."""
|
|
37
38
|
|
|
38
39
|
|
|
40
|
+
def playbook_event_handler_factory(
|
|
41
|
+
progress: str | None, *, progress_is_incremental: bool
|
|
42
|
+
) -> Callable[[dict], bool] | None:
|
|
43
|
+
"""Create an event handler for Ansible playbook runs.
|
|
44
|
+
|
|
45
|
+
This is used to send incremental progress updates to the external system that called for this playbook to be run.
|
|
46
|
+
|
|
47
|
+
:param str progress: The progress URL where the external system expects to receive updates.
|
|
48
|
+
:param bool progress_is_incremental: Whether progress updates are sent incrementally, or only contain the latest
|
|
49
|
+
event data.
|
|
50
|
+
"""
|
|
51
|
+
events_stdout = []
|
|
52
|
+
|
|
53
|
+
def _playbook_event_handler(event: dict) -> bool:
|
|
54
|
+
if progress_is_incremental:
|
|
55
|
+
emit_body = event["stdout"].strip()
|
|
56
|
+
else:
|
|
57
|
+
events_stdout.append(event["stdout"].strip())
|
|
58
|
+
emit_body = events_stdout
|
|
59
|
+
|
|
60
|
+
requests.post(str(progress), json={"progress": emit_body}, timeout=settings.REQUEST_TIMEOUT_SEC)
|
|
61
|
+
return True
|
|
62
|
+
|
|
63
|
+
if progress:
|
|
64
|
+
return _playbook_event_handler
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def playbook_finished_handler_factory(callback: str | None, job_id: str) -> Callable[[Runner], None] | None:
|
|
69
|
+
"""Create an event handler for finished Ansible playbook runs.
|
|
70
|
+
|
|
71
|
+
Once Ansible runner is finished, it will call the handler method created by this factory before teardown.
|
|
72
|
+
|
|
73
|
+
:param str callback: The callback URL that ansible runner should report to.
|
|
74
|
+
:param str job_id: The job ID of this playbook run, used for reporting.
|
|
75
|
+
:return Callable: A handler method that sends one request to the callback URL.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def _playbook_finished_handler(runner: Runner) -> None:
|
|
79
|
+
payload = {
|
|
80
|
+
"status": runner.status,
|
|
81
|
+
"job_id": job_id,
|
|
82
|
+
"output": runner.stdout.readlines(),
|
|
83
|
+
"return_code": int(runner.rc),
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
response = requests.post(str(callback), json=payload, timeout=settings.REQUEST_TIMEOUT_SEC)
|
|
87
|
+
if not (status.HTTP_200_OK <= response.status_code < status.HTTP_300_MULTIPLE_CHOICES):
|
|
88
|
+
msg = f"Callback failed: {response.text}, url: {callback}"
|
|
89
|
+
raise CallbackFailedError(msg)
|
|
90
|
+
|
|
91
|
+
if callback:
|
|
92
|
+
return _playbook_finished_handler
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
39
96
|
@celery.task(name=RUN_PLAYBOOK) # type: ignore[misc]
|
|
40
97
|
def run_playbook_proc_task(
|
|
41
|
-
job_id: str,
|
|
98
|
+
job_id: str,
|
|
99
|
+
playbook_path: str,
|
|
100
|
+
extra_vars: dict[str, Any],
|
|
101
|
+
inventory: dict[str, Any] | str,
|
|
102
|
+
callback: str | None,
|
|
103
|
+
progress: str | None,
|
|
104
|
+
*,
|
|
105
|
+
progress_is_incremental: bool,
|
|
42
106
|
) -> None:
|
|
43
107
|
"""Celery task to run a playbook.
|
|
44
108
|
|
|
@@ -46,24 +110,20 @@ def run_playbook_proc_task(
|
|
|
46
110
|
:param str playbook_path: Path to the playbook to be executed.
|
|
47
111
|
:param dict[str, Any] extra_vars: Extra variables to pass to the playbook.
|
|
48
112
|
:param dict[str, Any] | str inventory: Inventory to run the playbook against.
|
|
49
|
-
:param
|
|
113
|
+
:param str callback: Callback URL for status updates.
|
|
114
|
+
:param str progress: URL for sending progress updates.
|
|
115
|
+
:param bool progress_is_incremental: Whether progress updates include all past progress.
|
|
50
116
|
:return: None
|
|
51
117
|
"""
|
|
52
118
|
msg = f"playbook_path: {playbook_path}, callback: {callback}"
|
|
53
119
|
logger.info(msg)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
response = requests.post(str(callback), json=payload, timeout=settings.REQUEST_TIMEOUT_SEC)
|
|
64
|
-
if not (status.HTTP_200_OK <= response.status_code < status.HTTP_300_MULTIPLE_CHOICES):
|
|
65
|
-
msg = f"Callback failed: {response.text}, url: {callback}"
|
|
66
|
-
raise CallbackFailedError(msg)
|
|
120
|
+
run(
|
|
121
|
+
playbook=playbook_path,
|
|
122
|
+
inventory=inventory,
|
|
123
|
+
extravars=extra_vars,
|
|
124
|
+
event_handler=playbook_event_handler_factory(progress, progress_is_incremental=progress_is_incremental),
|
|
125
|
+
finished_callback=playbook_finished_handler_factory(callback, job_id),
|
|
126
|
+
)
|
|
67
127
|
|
|
68
128
|
|
|
69
129
|
@celery.task(name=RUN_EXECUTABLE) # type: ignore[misc]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "orchestrator-lso"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.4.1"
|
|
4
4
|
description = "LSO, an API for remotely running Ansible playbooks."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -120,8 +120,9 @@ ignore = [
|
|
|
120
120
|
"D203",
|
|
121
121
|
"D213",
|
|
122
122
|
"N805",
|
|
123
|
-
"PLR0913",
|
|
124
123
|
"PLR0904",
|
|
124
|
+
"PLR0913",
|
|
125
|
+
"PLR0917",
|
|
125
126
|
"PLW1514",
|
|
126
127
|
"S104"
|
|
127
128
|
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|