orchestrator-lso 2.3.0__tar.gz → 2.4.0__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.
Files changed (53) hide show
  1. {orchestrator_lso-2.3.0 → orchestrator_lso-2.4.0}/PKG-INFO +30 -45
  2. {orchestrator_lso-2.3.0 → orchestrator_lso-2.4.0}/README.md +21 -17
  3. {orchestrator_lso-2.3.0 → orchestrator_lso-2.4.0}/lso/__init__.py +2 -2
  4. {orchestrator_lso-2.3.0 → orchestrator_lso-2.4.0}/lso/app.py +1 -1
  5. {orchestrator_lso-2.3.0 → orchestrator_lso-2.4.0}/lso/config.py +1 -1
  6. {orchestrator_lso-2.3.0 → orchestrator_lso-2.4.0}/lso/environment.py +1 -1
  7. {orchestrator_lso-2.3.0 → orchestrator_lso-2.4.0}/lso/execute.py +16 -3
  8. orchestrator_lso-2.4.0/lso/playbook.py +143 -0
  9. {orchestrator_lso-2.3.0 → orchestrator_lso-2.4.0}/lso/routes/__init__.py +1 -1
  10. {orchestrator_lso-2.3.0 → orchestrator_lso-2.4.0}/lso/routes/default.py +1 -1
  11. {orchestrator_lso-2.3.0 → orchestrator_lso-2.4.0}/lso/routes/execute.py +15 -2
  12. {orchestrator_lso-2.3.0 → orchestrator_lso-2.4.0}/lso/routes/playbook.py +12 -6
  13. {orchestrator_lso-2.3.0 → orchestrator_lso-2.4.0}/lso/schema.py +2 -2
  14. {orchestrator_lso-2.3.0 → orchestrator_lso-2.4.0}/lso/tasks.py +19 -23
  15. {orchestrator_lso-2.3.0 → orchestrator_lso-2.4.0}/lso/utils.py +4 -0
  16. {orchestrator_lso-2.3.0 → orchestrator_lso-2.4.0}/lso/worker.py +1 -1
  17. {orchestrator_lso-2.3.0 → orchestrator_lso-2.4.0}/pyproject.toml +27 -25
  18. orchestrator_lso-2.3.0/.bumpversion.cfg +0 -16
  19. orchestrator_lso-2.3.0/.github/dependabot.yml +0 -11
  20. orchestrator_lso-2.3.0/.github/styles/config/vocabularies/Sphinx/accept.txt +0 -1
  21. orchestrator_lso-2.3.0/.github/styles/config/vocabularies/jargon/accept.txt +0 -10
  22. orchestrator_lso-2.3.0/.github/workflows/publish-package.yaml +0 -26
  23. orchestrator_lso-2.3.0/.github/workflows/run-linting-tests.yaml +0 -43
  24. orchestrator_lso-2.3.0/.github/workflows/run-unit-tests.yaml +0 -28
  25. orchestrator_lso-2.3.0/.github/workflows/sphinx.yaml +0 -43
  26. orchestrator_lso-2.3.0/.gitignore +0 -15
  27. orchestrator_lso-2.3.0/.vale.ini +0 -27
  28. orchestrator_lso-2.3.0/Dockerfile.example +0 -27
  29. orchestrator_lso-2.3.0/LICENSE +0 -202
  30. orchestrator_lso-2.3.0/docs/LSO_banner.jpg +0 -0
  31. orchestrator_lso-2.3.0/docs/Makefile +0 -20
  32. orchestrator_lso-2.3.0/docs/source/_static/custom.css +0 -13
  33. orchestrator_lso-2.3.0/docs/source/_static/lso_logo.png +0 -0
  34. orchestrator_lso-2.3.0/docs/source/conf.py +0 -79
  35. orchestrator_lso-2.3.0/docs/source/index.rst +0 -34
  36. orchestrator_lso-2.3.0/docs/source/module/config.rst +0 -6
  37. orchestrator_lso-2.3.0/docs/source/module/playbook.rst +0 -7
  38. orchestrator_lso-2.3.0/docs/source/module/routes/default.rst +0 -6
  39. orchestrator_lso-2.3.0/docs/source/module/routes/index.rst +0 -16
  40. orchestrator_lso-2.3.0/docs/source/module/routes/playbook.rst +0 -6
  41. orchestrator_lso-2.3.0/docs/source/modules.rst +0 -31
  42. orchestrator_lso-2.3.0/env.example +0 -21
  43. orchestrator_lso-2.3.0/lso/playbook.py +0 -60
  44. orchestrator_lso-2.3.0/test/__init__.py +0 -12
  45. orchestrator_lso-2.3.0/test/conftest.py +0 -79
  46. orchestrator_lso-2.3.0/test/routes/__init__.py +0 -12
  47. orchestrator_lso-2.3.0/test/routes/test_default.py +0 -33
  48. orchestrator_lso-2.3.0/test/routes/test_execute.py +0 -187
  49. orchestrator_lso-2.3.0/test/routes/test_playbook.py +0 -215
  50. orchestrator_lso-2.3.0/test/test-playbook.yaml +0 -9
  51. orchestrator_lso-2.3.0/test/test_execute.py +0 -126
  52. orchestrator_lso-2.3.0/test/test_playbook.py +0 -34
  53. orchestrator_lso-2.3.0/test/utils.py +0 -27
@@ -1,10 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: orchestrator-lso
3
- Version: 2.3.0
3
+ Version: 2.4.0
4
4
  Summary: LSO, an API for remotely running Ansible playbooks.
5
+ Author: GÉANT Orchestration and Automation Team
5
6
  Author-email: GÉANT Orchestration and Automation Team <goat@geant.org>
6
- Requires-Python: >=3.11,<3.14
7
- Description-Content-Type: text/markdown
8
7
  License-Expression: Apache-2.0
9
8
  Classifier: Intended Audience :: Information Technology
10
9
  Classifier: Intended Audience :: System Administrators
@@ -25,38 +24,20 @@ Classifier: Intended Audience :: Telecommunications Industry
25
24
  Classifier: Programming Language :: Python :: 3 :: Only
26
25
  Classifier: Programming Language :: Python :: 3.11
27
26
  Classifier: Programming Language :: Python :: 3.12
28
- License-File: LICENSE
29
- Requires-Dist: ansible-runner==2.4.1
27
+ Requires-Dist: ansible-runner==2.4.2
30
28
  Requires-Dist: ansible==10.7.0
31
- Requires-Dist: fastapi==0.116.1
29
+ Requires-Dist: fastapi==0.120.4
32
30
  Requires-Dist: httpx==0.28.1
33
- Requires-Dist: uvicorn[standard]==0.35.0
31
+ Requires-Dist: uvicorn[standard]==0.38.0
34
32
  Requires-Dist: requests==2.32.5
35
- Requires-Dist: pydantic-settings==2.10.1
33
+ Requires-Dist: pydantic-settings==2.11.0
36
34
  Requires-Dist: celery==5.5.3
37
35
  Requires-Dist: redis==5.3.1
38
- Requires-Dist: types-setuptools ; extra == "dev"
39
- Requires-Dist: types-requests ; extra == "dev"
40
- Requires-Dist: toml ; extra == "dev"
41
- Requires-Dist: types-toml ; extra == "dev"
42
- Requires-Dist: mypy_extensions ; extra == "dev"
43
- Requires-Dist: pre-commit ; extra == "dev"
44
- Requires-Dist: sphinx ; extra == "doc"
45
- Requires-Dist: sphinx-rtd-theme ; extra == "doc"
46
- Requires-Dist: docutils ; extra == "doc"
47
- Requires-Dist: pytest ; extra == "test"
48
- Requires-Dist: pytest-cov ; extra == "test"
49
- Requires-Dist: Faker ; extra == "test"
50
- Requires-Dist: responses ; extra == "test"
51
- Requires-Dist: mypy ; extra == "test"
52
- Requires-Dist: ruff ; extra == "test"
53
- Requires-Dist: jsonschema ; extra == "test"
54
- Requires-Dist: starlette ; extra == "test"
55
- Project-URL: Documentation, https://workfloworchestrator.org/lso/
36
+ Requires-Python: >=3.11, <3.14
37
+ Project-URL: Documentation, https://docs.gap.geant.org
38
+ Project-URL: Homepage, https://workfloworchestrator.org/lso/
56
39
  Project-URL: Source, https://github.com/workfloworchestrator/lso
57
- Provides-Extra: dev
58
- Provides-Extra: doc
59
- Provides-Extra: test
40
+ Description-Content-Type: text/markdown
60
41
 
61
42
  ![Lightweight Service Orchestrator](./docs/LSO_banner.jpg)
62
43
  [![Supported python versions](https://img.shields.io/pypi/pyversions/orchestrator-lso.svg?color=%2334D058)](https://pypi.org/project/orchestrator-lso)
@@ -65,10 +46,6 @@ Provides-Extra: test
65
46
 
66
47
  LSO: an API that allows for remotely executing Ansible playbooks.
67
48
 
68
- ## Code documentation
69
-
70
- Code documentation can be found at <https://workfloworchestrator.org/lso>
71
-
72
49
  ## Quick start
73
50
 
74
51
  This is a quick setup guide for running on your local machine.
@@ -104,7 +81,6 @@ This will expose the API on port 8000. The container requires some more files to
104
81
 
105
82
  ### Install the module
106
83
 
107
-
108
84
  As an alternative, below are a set of instructions for installing and running LSO directly on a machine.
109
85
 
110
86
  *One of these should be what you're looking for:*
@@ -112,24 +88,18 @@ As an alternative, below are a set of instructions for installing and running LS
112
88
  * Install the latest release
113
89
 
114
90
  ```bash
115
- python3 -m venv my-venv-directory
116
- . my-venv-directory/bin/activate
117
-
118
- pip install orchestrator-lso
91
+ uv venv --python 3.12
92
+ uv add orchestrator-lso
119
93
  ```
120
94
 
121
95
  * Install the source code
122
96
 
123
97
  ```bash
124
98
  git clone https://github.com/workfloworchestrator/lso.git && cd lso
125
- python3 -m venv my-venv-directory
126
- . my-venv-directory/bin/activate
99
+ uv venv --python 3.12
100
+ . .venv/bin/activate
127
101
 
128
- pip install flit
129
- flit install --deps production
130
-
131
- # Or, for the full development environment
132
- flit install --deps develop
102
+ uv sync --all-extras --dev
133
103
  ```
134
104
 
135
105
  ### Running the app
@@ -161,3 +131,18 @@ celery -A lso.worker worker --loglevel=info -Q lso-worker-queue
161
131
  2. ThreadPoolExecutor (Local Execution)
162
132
 
163
133
  For local concurrent tasks, set `EXECUTOR=threadpool` and configure `MAX_THREAD_POOL_WORKERS`.
134
+
135
+ ## Contributing
136
+
137
+ We use [uv](https://docs.astral.sh/uv/getting-started/installation/) to manage dependencies.
138
+
139
+ To get started, run:
140
+
141
+ ```
142
+ uv sync --all-extras --dev
143
+ pre-commit install
144
+ ```
145
+
146
+ ## Code documentation
147
+
148
+ Code documentation can be found at <https://workfloworchestrator.org/lso>
@@ -5,10 +5,6 @@
5
5
 
6
6
  LSO: an API that allows for remotely executing Ansible playbooks.
7
7
 
8
- ## Code documentation
9
-
10
- Code documentation can be found at <https://workfloworchestrator.org/lso>
11
-
12
8
  ## Quick start
13
9
 
14
10
  This is a quick setup guide for running on your local machine.
@@ -44,7 +40,6 @@ This will expose the API on port 8000. The container requires some more files to
44
40
 
45
41
  ### Install the module
46
42
 
47
-
48
43
  As an alternative, below are a set of instructions for installing and running LSO directly on a machine.
49
44
 
50
45
  *One of these should be what you're looking for:*
@@ -52,24 +47,18 @@ As an alternative, below are a set of instructions for installing and running LS
52
47
  * Install the latest release
53
48
 
54
49
  ```bash
55
- python3 -m venv my-venv-directory
56
- . my-venv-directory/bin/activate
57
-
58
- pip install orchestrator-lso
50
+ uv venv --python 3.12
51
+ uv add orchestrator-lso
59
52
  ```
60
53
 
61
54
  * Install the source code
62
55
 
63
56
  ```bash
64
57
  git clone https://github.com/workfloworchestrator/lso.git && cd lso
65
- python3 -m venv my-venv-directory
66
- . my-venv-directory/bin/activate
67
-
68
- pip install flit
69
- flit install --deps production
58
+ uv venv --python 3.12
59
+ . .venv/bin/activate
70
60
 
71
- # Or, for the full development environment
72
- flit install --deps develop
61
+ uv sync --all-extras --dev
73
62
  ```
74
63
 
75
64
  ### Running the app
@@ -100,4 +89,19 @@ celery -A lso.worker worker --loglevel=info -Q lso-worker-queue
100
89
  ```
101
90
  2. ThreadPoolExecutor (Local Execution)
102
91
 
103
- For local concurrent tasks, set `EXECUTOR=threadpool` and configure `MAX_THREAD_POOL_WORKERS`.
92
+ For local concurrent tasks, set `EXECUTOR=threadpool` and configure `MAX_THREAD_POOL_WORKERS`.
93
+
94
+ ## Contributing
95
+
96
+ We use [uv](https://docs.astral.sh/uv/getting-started/installation/) to manage dependencies.
97
+
98
+ To get started, run:
99
+
100
+ ```
101
+ uv sync --all-extras --dev
102
+ pre-commit install
103
+ ```
104
+
105
+ ## Code documentation
106
+
107
+ Code documentation can be found at <https://workfloworchestrator.org/lso>
@@ -1,4 +1,4 @@
1
- # Copyright 2023-2024 GÉANT Vereniging.
1
+ # Copyright 2023-2025 GÉANT Vereniging.
2
2
  # Licensed under the Apache License, Version 2.0 (the "License");
3
3
  # you may not use this file except in compliance with the License.
4
4
  # You may obtain a copy of the License at
@@ -13,7 +13,7 @@
13
13
 
14
14
  """LSO, an API for remotely running Ansible playbooks."""
15
15
 
16
- __version__ = "2.3.0"
16
+ __version__ = "2.4.0"
17
17
 
18
18
  import logging
19
19
 
@@ -1,4 +1,4 @@
1
- # Copyright 2023-2024 GÉANT Vereniging.
1
+ # Copyright 2023-2025 GÉANT Vereniging.
2
2
  # Licensed under the Apache License, Version 2.0 (the "License");
3
3
  # you may not use this file except in compliance with the License.
4
4
  # You may obtain a copy of the License at
@@ -1,4 +1,4 @@
1
- # Copyright 2023-2024 GÉANT Vereniging.
1
+ # Copyright 2023-2025 GÉANT Vereniging.
2
2
  # Licensed under the Apache License, Version 2.0 (the "License");
3
3
  # you may not use this file except in compliance with the License.
4
4
  # You may obtain a copy of the License at
@@ -1,4 +1,4 @@
1
- # Copyright 2023-2024 GÉANT Vereniging.
1
+ # Copyright 2023-2025 GÉANT Vereniging.
2
2
  # Licensed under the Apache License, Version 2.0 (the "License");
3
3
  # you may not use this file except in compliance with the License.
4
4
  # You may obtain a copy of the License at
@@ -1,8 +1,21 @@
1
+ # Copyright 2023-2025 GÉANT Vereniging.
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # You may obtain a copy of the License at
5
+ #
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ #
8
+ # Unless required by applicable law or agreed to in writing, software
9
+ # distributed under the License is distributed on an "AS IS" BASIS,
10
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ # See the License for the specific language governing permissions and
12
+ # limitations under the License.
13
+
1
14
  """Module for handling the execution of arbitrary executables."""
2
15
 
3
16
  import subprocess # noqa: S404
4
- import uuid
5
17
  from pathlib import Path
18
+ from uuid import UUID, uuid4
6
19
 
7
20
  from pydantic import HttpUrl
8
21
 
@@ -17,12 +30,12 @@ def get_executable_path(executable_name: Path) -> Path:
17
30
  return Path(settings.EXECUTABLES_ROOT_DIR) / executable_name
18
31
 
19
32
 
20
- def run_executable_async(executable_path: Path, args: list[str], callback: HttpUrl | None) -> uuid.UUID:
33
+ def run_executable_async(executable_path: Path, args: list[str], callback: HttpUrl | None) -> UUID:
21
34
  """Dispatch the task for executing an arbitrary executable remotely.
22
35
 
23
36
  Uses a ThreadPoolExecutor (for local execution) or a Celery worker (for distributed tasks).
24
37
  """
25
- job_id = uuid.uuid4()
38
+ job_id = uuid4()
26
39
  callback_url = str(callback) if callback else None
27
40
  if settings.EXECUTOR == ExecutorType.THREADPOOL:
28
41
  executor = get_thread_pool()
@@ -0,0 +1,143 @@
1
+ # Copyright 2023-2025 GÉANT Vereniging.
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # You may obtain a copy of the License at
5
+ #
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ #
8
+ # Unless required by applicable law or agreed to in writing, software
9
+ # distributed under the License is distributed on an "AS IS" BASIS,
10
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ # See the License for the specific language governing permissions and
12
+ # limitations under the License.
13
+
14
+ """Module that gathers common API responses and data models."""
15
+
16
+ import logging
17
+ from collections.abc import Callable
18
+ from pathlib import Path
19
+ from typing import Any
20
+ from uuid import UUID, uuid4
21
+
22
+ import requests
23
+ from ansible_runner import Runner
24
+ from pydantic import HttpUrl
25
+ from starlette import status
26
+
27
+ from lso.config import ExecutorType, settings
28
+ from lso.tasks import run_playbook_proc_task
29
+ from lso.utils import CallbackFailedError, get_thread_pool
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ def get_playbook_path(playbook_name: Path) -> Path:
35
+ """Get the path of a playbook on the local filesystem."""
36
+ return Path(settings.ANSIBLE_PLAYBOOKS_ROOT_DIR) / playbook_name
37
+
38
+
39
+ def playbook_event_handler_factory(progress: str, *, progress_is_incremental: bool) -> Callable[[dict], bool]:
40
+ """Create an event handler for Ansible playbook runs.
41
+
42
+ This is used to send incremental progress updates to the external system that called for this playbook to be run.
43
+
44
+ :param str progress: The progress URL where the external system expects to receive updates.
45
+ :param bool progress_is_incremental: Whether progress updates are sent incrementally, or only contain the latest
46
+ event data.
47
+ :return Callable[[dict], bool]]: A handler method that processes every Ansible playbook event.
48
+ """
49
+ events_stdout = []
50
+
51
+ def _playbook_event_handler(event: dict) -> bool:
52
+ if progress_is_incremental:
53
+ emit_body = event["stdout"].strip()
54
+ else:
55
+ events_stdout.append(event["stdout"].strip())
56
+ emit_body = events_stdout
57
+
58
+ requests.post(str(progress), json={"progress": emit_body}, timeout=settings.REQUEST_TIMEOUT_SEC)
59
+ return True
60
+
61
+ return _playbook_event_handler
62
+
63
+
64
+ def playbook_finished_handler_factory(callback: str, job_id: UUID) -> Callable[[Runner], None]:
65
+ """Create an event handler for finished Ansible playbook runs.
66
+
67
+ Once Ansible runner is finished, it will call the handler method created by this factory before teardown.
68
+
69
+ :param str callback: The callback URL that ansible runner should report to.
70
+ :param UUID job_id: The job ID of this playbook run, used for reporting.
71
+ :return Callable[[Runner], None]: A handler method that sends one request to the callback URL.
72
+ """
73
+
74
+ def _playbook_finished_handler(runner: Runner) -> None:
75
+ payload = {
76
+ "status": runner.status,
77
+ "job_id": str(job_id),
78
+ "output": runner.stdout.readlines(),
79
+ "return_code": int(runner.rc),
80
+ }
81
+
82
+ response = requests.post(str(callback), json=payload, timeout=settings.REQUEST_TIMEOUT_SEC)
83
+ if not (status.HTTP_200_OK <= response.status_code < status.HTTP_300_MULTIPLE_CHOICES):
84
+ msg = f"Callback failed: {response.text}, url: {callback}"
85
+ raise CallbackFailedError(msg)
86
+
87
+ return _playbook_finished_handler
88
+
89
+
90
+ def run_playbook(
91
+ playbook_path: Path,
92
+ extra_vars: dict[str, Any],
93
+ inventory: dict[str, Any] | str,
94
+ callback: HttpUrl | None,
95
+ progress: HttpUrl | None,
96
+ *,
97
+ progress_is_incremental: bool,
98
+ ) -> UUID:
99
+ """Run an Ansible playbook against a specified inventory.
100
+
101
+ :param Path playbook_path: Playbook to be executed.
102
+ :param dict[str, Any] extra_vars: Any extra vars needed for the playbook to run.
103
+ :param dict[str, Any] | str inventory: The inventory that the playbook is executed against.
104
+ :param HttpUrl callback: Callback URL where the playbook should send a status update when execution is completed.
105
+ This is used for workflow-orchestrator to continue with the next step in a workflow.
106
+ :return UUID: Job ID of the launched playbook.
107
+ """
108
+ msg = f"playbook_path: {playbook_path}"
109
+ job_id = uuid4()
110
+ callback_str = None
111
+ progress_str = None
112
+ event_handler = None
113
+ finished_callback = None
114
+
115
+ if callback:
116
+ callback_str = str(callback)
117
+ msg += f", callback URL: {callback_str}"
118
+ finished_callback = playbook_finished_handler_factory(callback_str, job_id)
119
+ if progress:
120
+ progress_str = str(progress)
121
+ msg += f", progress URL: {progress_str}"
122
+ event_handler = playbook_event_handler_factory(progress_str, progress_is_incremental=progress_is_incremental)
123
+
124
+ logger.info(msg)
125
+
126
+ if settings.EXECUTOR == ExecutorType.THREADPOOL:
127
+ executor = get_thread_pool()
128
+ executor_handle = executor.submit(
129
+ run_playbook_proc_task, str(playbook_path), extra_vars, inventory, event_handler, finished_callback
130
+ )
131
+ if settings.TESTING:
132
+ executor_handle.result()
133
+
134
+ elif settings.EXECUTOR == ExecutorType.WORKER:
135
+ run_playbook_proc_task.delay(
136
+ str(playbook_path),
137
+ extra_vars,
138
+ inventory,
139
+ event_handler,
140
+ finished_callback,
141
+ )
142
+
143
+ return job_id
@@ -1,4 +1,4 @@
1
- # Copyright 2023-2024 GÉANT Vereniging.
1
+ # Copyright 2023-2025 GÉANT Vereniging.
2
2
  # Licensed under the Apache License, Version 2.0 (the "License");
3
3
  # you may not use this file except in compliance with the License.
4
4
  # You may obtain a copy of the License at
@@ -1,4 +1,4 @@
1
- # Copyright 2023-2024 GÉANT Vereniging.
1
+ # Copyright 2023-2025 GÉANT Vereniging.
2
2
  # Licensed under the Apache License, Version 2.0 (the "License");
3
3
  # you may not use this file except in compliance with the License.
4
4
  # You may obtain a copy of the License at
@@ -1,9 +1,22 @@
1
+ # Copyright 2023-2025 GÉANT Vereniging.
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # You may obtain a copy of the License at
5
+ #
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ #
8
+ # Unless required by applicable law or agreed to in writing, software
9
+ # distributed under the License is distributed on an "AS IS" BASIS,
10
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ # See the License for the specific language governing permissions and
12
+ # limitations under the License.
13
+
1
14
  """FastAPI route for running arbitrary executables."""
2
15
 
3
16
  import asyncio
4
- import uuid
5
17
  from pathlib import Path
6
18
  from typing import Annotated
19
+ from uuid import uuid4
7
20
 
8
21
  from fastapi import APIRouter, HTTPException, status
9
22
  from pydantic import AfterValidator, BaseModel, HttpUrl
@@ -48,6 +61,6 @@ async def run_executable_endpoint(params: ExecutableRunParams) -> ExecutableRunR
48
61
  job_id = run_executable_async(params.executable_name, params.args, params.callback)
49
62
  return ExecutableRunResponse(job_id=job_id)
50
63
 
51
- job_id = uuid.uuid4()
64
+ job_id = uuid4()
52
65
  result = await asyncio.to_thread(run_executable_sync, str(params.executable_name), params.args)
53
66
  return ExecutableRunResponse(job_id=job_id, result=result)
@@ -1,4 +1,4 @@
1
- # Copyright 2023-2024 GÉANT Vereniging.
1
+ # Copyright 2023-2025 GÉANT Vereniging.
2
2
  # Licensed under the Apache License, Version 2.0 (the "License");
3
3
  # you may not use this file except in compliance with the License.
4
4
  # You may obtain a copy of the License at
@@ -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.HTTP_422_UNPROCESSABLE_ENTITY, detail=detail)
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.HTTP_422_UNPROCESSABLE_ENTITY, detail=error_messages)
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: uuid.UUID
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: uuid.UUID
49
+ job_id: UUID
50
50
  result: ExecutionResult | None = None
@@ -18,52 +18,48 @@ 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
29
30
  from lso.schema import ExecutableRunResponse
31
+ from lso.utils import CallbackFailedError
30
32
  from lso.worker import RUN_EXECUTABLE, RUN_PLAYBOOK, celery
31
33
 
32
34
  logger = logging.getLogger(__name__)
33
35
 
34
36
 
35
- class CallbackFailedError(Exception):
36
- """Exception raised when a callback url can't be reached."""
37
-
38
-
39
37
  @celery.task(name=RUN_PLAYBOOK) # type: ignore[misc]
40
38
  def run_playbook_proc_task(
41
- job_id: str, playbook_path: str, extra_vars: dict[str, Any], inventory: dict[str, Any] | str, callback: str
39
+ playbook_path: str,
40
+ extra_vars: dict[str, Any],
41
+ inventory: dict[str, Any] | str,
42
+ event_handler: Callable[[dict], bool] | None = None,
43
+ finished_callback: Callable[[Runner], None] | None = None,
42
44
  ) -> None:
43
45
  """Celery task to run a playbook.
44
46
 
45
- :param str job_id: Identifier of the job being executed.
46
47
  :param str playbook_path: Path to the playbook to be executed.
47
48
  :param dict[str, Any] extra_vars: Extra variables to pass to the playbook.
48
49
  :param dict[str, Any] | str inventory: Inventory to run the playbook against.
49
- :param HttpUrl callback: Callback URL for status updates.
50
+ :param Callable[[dict], bool] event_handler: Event handler method that is executed on every event while the playbook
51
+ runs.
52
+ :param Callable[[Runner], None] finished_callback: Callback handler method that is executed once the playbook run is
53
+ completed.
50
54
  :return: None
51
55
  """
52
- msg = f"playbook_path: {playbook_path}, callback: {callback}"
53
- logger.info(msg)
54
- ansible_playbook_run = ansible_runner.run(playbook=playbook_path, inventory=inventory, extravars=extra_vars)
55
-
56
- payload = {
57
- "status": ansible_playbook_run.status,
58
- "job_id": job_id,
59
- "output": ansible_playbook_run.stdout.readlines(),
60
- "return_code": int(ansible_playbook_run.rc),
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)
56
+ run(
57
+ playbook=playbook_path,
58
+ inventory=inventory,
59
+ extravars=extra_vars,
60
+ event_handler=event_handler,
61
+ finished_callback=finished_callback,
62
+ )
67
63
 
68
64
 
69
65
  @celery.task(name=RUN_EXECUTABLE) # type: ignore[misc]
@@ -20,6 +20,10 @@ from lso.config import settings
20
20
  _executor = None
21
21
 
22
22
 
23
+ class CallbackFailedError(Exception):
24
+ """Exception raised when a callback url can't be reached."""
25
+
26
+
23
27
  def get_thread_pool() -> ThreadPoolExecutor:
24
28
  """Initialize or return a cached ThreadPoolExecutor for local asynchronous execution."""
25
29
  global _executor # noqa: PLW0603
@@ -1,4 +1,4 @@
1
- # Copyright 2023-2024 GÉANT Vereniging.
1
+ # Copyright 2023-2025 GÉANT Vereniging.
2
2
  # Licensed under the Apache License, Version 2.0 (the "License");
3
3
  # you may not use this file except in compliance with the License.
4
4
  # You may obtain a copy of the License at