orchestrator-lso 1.0.2__tar.gz → 2.0.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.
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/.bumpversion.cfg +1 -1
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/.github/workflows/run-unit-tests.yaml +7 -3
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/Dockerfile.example +1 -1
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/PKG-INFO +48 -25
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/README.md +37 -16
- orchestrator_lso-2.0.0/docs/LSO_banner.jpg +0 -0
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/docs/source/_static/custom.css +2 -2
- orchestrator_lso-2.0.0/docs/source/_static/lso_logo.png +0 -0
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/docs/source/conf.py +3 -2
- orchestrator_lso-2.0.0/env.example +23 -0
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/lso/__init__.py +4 -10
- orchestrator_lso-2.0.0/lso/config.py +48 -0
- orchestrator_lso-2.0.0/lso/playbook.py +76 -0
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/lso/routes/playbook.py +38 -10
- orchestrator_lso-2.0.0/lso/tasks.py +64 -0
- orchestrator_lso-2.0.0/lso/worker.py +52 -0
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/pyproject.toml +10 -7
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/test/conftest.py +25 -30
- orchestrator_lso-2.0.0/test/routes/test_playbook.py +224 -0
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/test/test_ansible.py +0 -2
- orchestrator_lso-1.0.2/config.json.example +0 -3
- orchestrator_lso-1.0.2/lso/config.py +0 -70
- orchestrator_lso-1.0.2/lso/playbook.py +0 -122
- orchestrator_lso-1.0.2/test/routes/test_playbook.py +0 -120
- orchestrator_lso-1.0.2/test/test_config.py +0 -44
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/.github/dependabot.yml +0 -0
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/.github/styles/config/vocabularies/Sphinx/accept.txt +0 -0
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/.github/styles/config/vocabularies/jargon/accept.txt +0 -0
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/.github/workflows/publish-package.yaml +0 -0
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/.github/workflows/run-linting-tests.yaml +0 -0
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/.github/workflows/sphinx.yaml +0 -0
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/.gitignore +0 -0
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/.vale.ini +0 -0
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/LICENSE +0 -0
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/docs/Makefile +0 -0
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/docs/source/index.rst +0 -0
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/docs/source/module/config.rst +0 -0
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/docs/source/module/playbook.rst +0 -0
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/docs/source/module/routes/default.rst +0 -0
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/docs/source/module/routes/index.rst +0 -0
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/docs/source/module/routes/playbook.rst +0 -0
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/docs/source/modules.rst +0 -0
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/lso/app.py +0 -0
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/lso/environment.py +0 -0
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/lso/routes/__init__.py +0 -0
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/lso/routes/default.py +0 -0
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/setup.py +0 -0
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/test/__init__.py +0 -0
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/test/routes/__init__.py +0 -0
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/test/routes/test_default.py +0 -0
- {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/test/test-playbook.yaml +0 -0
|
@@ -25,6 +25,10 @@ jobs:
|
|
|
25
25
|
env:
|
|
26
26
|
FLIT_ROOT_INSTALL: 1
|
|
27
27
|
- name: Run Unit tests
|
|
28
|
-
run: pytest
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
run: pytest --cov-branch --cov=lso --cov-report=xml
|
|
29
|
+
- name: "Upload coverage to Codecov"
|
|
30
|
+
uses: codecov/codecov-action@v3
|
|
31
|
+
with:
|
|
32
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
33
|
+
fail_ci_if_error: true
|
|
34
|
+
files: ./coverage.xml
|
|
@@ -11,7 +11,7 @@ COPY ./ansible-galaxy-requirements.yaml ./ansible-galaxy-requirements.yaml
|
|
|
11
11
|
RUN apk add --update --no-cache gcc libc-dev libffi-dev openssh
|
|
12
12
|
|
|
13
13
|
# Install the LSO python package, and additional requirements
|
|
14
|
-
RUN pip install orchestrator-lso=="
|
|
14
|
+
RUN pip install orchestrator-lso=="2.0.0"
|
|
15
15
|
RUN pip install -r requirements.txt
|
|
16
16
|
|
|
17
17
|
# Install required Ansible Galaxy roles and collections
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: orchestrator-lso
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: LSO, an API for remotely running Ansible playbooks.
|
|
5
5
|
Author-email: GÉANT Orchestration and Automation Team <goat@geant.org>
|
|
6
6
|
Requires-Python: >=3.11,<3.13
|
|
@@ -25,13 +25,15 @@ Classifier: License :: OSI Approved :: Apache Software License
|
|
|
25
25
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
26
26
|
Classifier: Programming Language :: Python :: 3.11
|
|
27
27
|
Classifier: Programming Language :: Python :: 3.12
|
|
28
|
-
Requires-Dist: ansible-runner
|
|
29
|
-
Requires-Dist: ansible
|
|
30
|
-
Requires-Dist: fastapi
|
|
31
|
-
Requires-Dist: httpx
|
|
32
|
-
Requires-Dist:
|
|
33
|
-
Requires-Dist:
|
|
34
|
-
Requires-Dist:
|
|
28
|
+
Requires-Dist: ansible-runner==2.4.0
|
|
29
|
+
Requires-Dist: ansible==10.6.0
|
|
30
|
+
Requires-Dist: fastapi==0.115.5
|
|
31
|
+
Requires-Dist: httpx==0.27.2
|
|
32
|
+
Requires-Dist: uvicorn[standard]==0.32.0
|
|
33
|
+
Requires-Dist: requests==2.32.3
|
|
34
|
+
Requires-Dist: pydantic-settings==2.5.2
|
|
35
|
+
Requires-Dist: celery==5.4.0
|
|
36
|
+
Requires-Dist: redis==5.2.0
|
|
35
37
|
Requires-Dist: types-setuptools ; extra == "dev"
|
|
36
38
|
Requires-Dist: types-requests ; extra == "dev"
|
|
37
39
|
Requires-Dist: toml ; extra == "dev"
|
|
@@ -42,6 +44,7 @@ Requires-Dist: sphinx ; extra == "doc"
|
|
|
42
44
|
Requires-Dist: sphinx-rtd-theme ; extra == "doc"
|
|
43
45
|
Requires-Dist: docutils ; extra == "doc"
|
|
44
46
|
Requires-Dist: pytest ; extra == "test"
|
|
47
|
+
Requires-Dist: pytest-cov ; extra == "test"
|
|
45
48
|
Requires-Dist: Faker ; extra == "test"
|
|
46
49
|
Requires-Dist: responses ; extra == "test"
|
|
47
50
|
Requires-Dist: mypy ; extra == "test"
|
|
@@ -54,7 +57,10 @@ Provides-Extra: dev
|
|
|
54
57
|
Provides-Extra: doc
|
|
55
58
|
Provides-Extra: test
|
|
56
59
|
|
|
57
|
-
|
|
60
|
+

|
|
61
|
+
[](https://pypi.org/project/orchestrator-lso)
|
|
62
|
+
[](https://pepy.tech/project/orchestrator-lso)
|
|
63
|
+
[](https://codecov.io/github/workfloworchestrator/lso)
|
|
58
64
|
|
|
59
65
|
LSO: an API that allows for remotely executing Ansible playbooks.
|
|
60
66
|
|
|
@@ -75,23 +81,21 @@ To run LSO as a Docker container, build an image using the `Dockerfile.example`
|
|
|
75
81
|
Use the Docker image to then spin up an environment. An example Docker compose file is presented below:
|
|
76
82
|
|
|
77
83
|
```yaml
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
- "/home/user/ansible_inventory:/opt/ansible_inventory:ro"
|
|
88
|
-
- "~/.ssh/id_ed25519.pub:/root/.ssh/id_ed25519.pub:ro"
|
|
89
|
-
- "~/.ssh/id_ed25519:/root/.ssh/id_ed25519:ro"
|
|
84
|
+
services:
|
|
85
|
+
lso:
|
|
86
|
+
image: my-lso:latest
|
|
87
|
+
env_file:
|
|
88
|
+
.env # Load default environment variables from the .env file
|
|
89
|
+
volumes:
|
|
90
|
+
- "/home/user/ansible_inventory:/opt/ansible_inventory:ro"
|
|
91
|
+
- "~/.ssh/id_ed25519.pub:/root/.ssh/id_ed25519.pub:ro"
|
|
92
|
+
- "~/.ssh/id_ed25519:/root/.ssh/id_ed25519:ro"
|
|
90
93
|
```
|
|
91
94
|
|
|
92
95
|
This will expose the API on port 8000. The container requires some more files to be mounted:
|
|
93
96
|
|
|
94
|
-
*
|
|
97
|
+
* An .env file: Sets default environment variables, like ANSIBLE_PLAYBOOKS_ROOT_DIR for the location of Ansible playbooks **inside the container**.
|
|
98
|
+
* Environment variables: Specific configurations, such as ANSIBLE_ROLES_PATH, can be directly set in the environment section. This is ideal for values you may want to override without modifying the .env file.
|
|
95
99
|
* An Ansible inventory for all host and group variables that are used in the playbooks
|
|
96
100
|
* A public/private key pair for SSH authentication on external machines that are targeted by Ansible playbooks.
|
|
97
101
|
* Any Ansible-specific configuration (such as `collections_path`, `roles_path`, etc.) should be set using
|
|
@@ -129,11 +133,30 @@ As an alternative, below are a set of instructions for installing and running LS
|
|
|
129
133
|
|
|
130
134
|
### Running the app
|
|
131
135
|
|
|
132
|
-
*
|
|
136
|
+
* Set required environment variables; see `env.example` for reference.
|
|
133
137
|
* If necessary, set the environment variable `ANSIBLE_HOME` to a custom path.
|
|
134
138
|
* Run the app like this (`app.py` starts the server on port 44444):
|
|
135
139
|
|
|
136
140
|
```bash
|
|
137
|
-
|
|
141
|
+
source .env && python -m lso.app
|
|
138
142
|
```
|
|
139
143
|
|
|
144
|
+
### Task Execution Options
|
|
145
|
+
1. Celery (Distributed Execution)
|
|
146
|
+
|
|
147
|
+
- For distributed task execution, set `EXECUTOR=celery`.
|
|
148
|
+
- Add Celery config in your environment variables:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
CELERY_BROKER_URL=redis://localhost:6379/0
|
|
152
|
+
CELERY_RESULT_BACKEND=redis://localhost:6379/0
|
|
153
|
+
WORKER_QUEUE_NAME=lso-worker-queue # default value is None so you don't need this by default.
|
|
154
|
+
```
|
|
155
|
+
- Start a Celery worker:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
celery -A lso.worker worker --loglevel=info -Q lso-worker-queue
|
|
159
|
+
```
|
|
160
|
+
2. ThreadPoolExecutor (Local Execution)
|
|
161
|
+
|
|
162
|
+
For local concurrent tasks, set `EXECUTOR=threadpool` and configure `MAX_THREAD_POOL_WORKERS`.
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+

|
|
2
|
+
[](https://pypi.org/project/orchestrator-lso)
|
|
3
|
+
[](https://pepy.tech/project/orchestrator-lso)
|
|
4
|
+
[](https://codecov.io/github/workfloworchestrator/lso)
|
|
2
5
|
|
|
3
6
|
LSO: an API that allows for remotely executing Ansible playbooks.
|
|
4
7
|
|
|
@@ -19,23 +22,21 @@ To run LSO as a Docker container, build an image using the `Dockerfile.example`
|
|
|
19
22
|
Use the Docker image to then spin up an environment. An example Docker compose file is presented below:
|
|
20
23
|
|
|
21
24
|
```yaml
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
- "/home/user/ansible_inventory:/opt/ansible_inventory:ro"
|
|
32
|
-
- "~/.ssh/id_ed25519.pub:/root/.ssh/id_ed25519.pub:ro"
|
|
33
|
-
- "~/.ssh/id_ed25519:/root/.ssh/id_ed25519:ro"
|
|
25
|
+
services:
|
|
26
|
+
lso:
|
|
27
|
+
image: my-lso:latest
|
|
28
|
+
env_file:
|
|
29
|
+
.env # Load default environment variables from the .env file
|
|
30
|
+
volumes:
|
|
31
|
+
- "/home/user/ansible_inventory:/opt/ansible_inventory:ro"
|
|
32
|
+
- "~/.ssh/id_ed25519.pub:/root/.ssh/id_ed25519.pub:ro"
|
|
33
|
+
- "~/.ssh/id_ed25519:/root/.ssh/id_ed25519:ro"
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
This will expose the API on port 8000. The container requires some more files to be mounted:
|
|
37
37
|
|
|
38
|
-
*
|
|
38
|
+
* An .env file: Sets default environment variables, like ANSIBLE_PLAYBOOKS_ROOT_DIR for the location of Ansible playbooks **inside the container**.
|
|
39
|
+
* Environment variables: Specific configurations, such as ANSIBLE_ROLES_PATH, can be directly set in the environment section. This is ideal for values you may want to override without modifying the .env file.
|
|
39
40
|
* An Ansible inventory for all host and group variables that are used in the playbooks
|
|
40
41
|
* A public/private key pair for SSH authentication on external machines that are targeted by Ansible playbooks.
|
|
41
42
|
* Any Ansible-specific configuration (such as `collections_path`, `roles_path`, etc.) should be set using
|
|
@@ -73,10 +74,30 @@ As an alternative, below are a set of instructions for installing and running LS
|
|
|
73
74
|
|
|
74
75
|
### Running the app
|
|
75
76
|
|
|
76
|
-
*
|
|
77
|
+
* Set required environment variables; see `env.example` for reference.
|
|
77
78
|
* If necessary, set the environment variable `ANSIBLE_HOME` to a custom path.
|
|
78
79
|
* Run the app like this (`app.py` starts the server on port 44444):
|
|
79
80
|
|
|
80
81
|
```bash
|
|
81
|
-
|
|
82
|
+
source .env && python -m lso.app
|
|
82
83
|
```
|
|
84
|
+
|
|
85
|
+
### Task Execution Options
|
|
86
|
+
1. Celery (Distributed Execution)
|
|
87
|
+
|
|
88
|
+
- For distributed task execution, set `EXECUTOR=celery`.
|
|
89
|
+
- Add Celery config in your environment variables:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
CELERY_BROKER_URL=redis://localhost:6379/0
|
|
93
|
+
CELERY_RESULT_BACKEND=redis://localhost:6379/0
|
|
94
|
+
WORKER_QUEUE_NAME=lso-worker-queue # default value is None so you don't need this by default.
|
|
95
|
+
```
|
|
96
|
+
- Start a Celery worker:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
celery -A lso.worker worker --loglevel=info -Q lso-worker-queue
|
|
100
|
+
```
|
|
101
|
+
2. ThreadPoolExecutor (Local Execution)
|
|
102
|
+
|
|
103
|
+
For local concurrent tasks, set `EXECUTOR=threadpool` and configure `MAX_THREAD_POOL_WORKERS`.
|
|
Binary file
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
.wy-menu > p > span {
|
|
2
|
-
color: rgb(
|
|
2
|
+
color: rgb(254 122 54);
|
|
3
3
|
}
|
|
4
4
|
|
|
5
5
|
section > dl > .sig-object {
|
|
6
6
|
background: #e5e8e8 !important;
|
|
7
|
-
color: rgb(
|
|
7
|
+
color: rgb(254 122 54) !important;
|
|
8
8
|
border-top: 3px solid rgb(167 179 180) !important;
|
|
9
9
|
}
|
|
10
10
|
|
|
Binary file
|
|
@@ -47,7 +47,7 @@ def setup(app):
|
|
|
47
47
|
# -- Project information -----------------------------------------------------
|
|
48
48
|
|
|
49
49
|
project = "Lightweight Service Orchestrator"
|
|
50
|
-
copyright = "2023, GÉANT Vereniging"
|
|
50
|
+
copyright = "2023-2024, GÉANT Vereniging"
|
|
51
51
|
author = "GÉANT Orchestration & Automation Team"
|
|
52
52
|
|
|
53
53
|
# -- General configuration ---------------------------------------------------
|
|
@@ -66,8 +66,9 @@ exclude_patterns = []
|
|
|
66
66
|
|
|
67
67
|
html_theme = "sphinx_rtd_theme"
|
|
68
68
|
html_static_path = ["_static"]
|
|
69
|
-
html_theme_options = {"style_nav_header_background": "rgb(
|
|
69
|
+
html_theme_options = {"style_nav_header_background": "rgb(40 2 116)", "logo_only": True}
|
|
70
70
|
html_css_files = ["custom.css"]
|
|
71
|
+
html_logo = "_static/lso_logo.png"
|
|
71
72
|
|
|
72
73
|
|
|
73
74
|
# Both the class' and the ``__init__`` method's docstring are concatenated and inserted.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Environment configuration for LSO application
|
|
2
|
+
|
|
3
|
+
# Ansible configuration
|
|
4
|
+
ANSIBLE_PLAYBOOKS_ROOT_DIR="/path/to/ansible/playbooks"
|
|
5
|
+
ANSIBLE_ROLES_PATH="/app/lso/ansible_roles" # Set specific Ansible roles path
|
|
6
|
+
|
|
7
|
+
# Executor configuration
|
|
8
|
+
EXECUTOR="threadpool" # Options: "threadpool", "celery"
|
|
9
|
+
MAX_THREAD_POOL_WORKERS=10
|
|
10
|
+
|
|
11
|
+
# Request settings
|
|
12
|
+
REQUEST_TIMEOUT_SEC=10
|
|
13
|
+
|
|
14
|
+
# Celery configuration
|
|
15
|
+
CELERY_BROKER_URL="redis://localhost:6379/0"
|
|
16
|
+
CELERY_RESULT_BACKEND="redis://localhost:6379/0"
|
|
17
|
+
CELERY_TIMEZONE="Europe/Amsterdam"
|
|
18
|
+
CELERY_ENABLE_UTC=True
|
|
19
|
+
CELERY_RESULT_EXPIRES=3600
|
|
20
|
+
WORKER_QUEUE_NAME="lso-worker-queue"
|
|
21
|
+
|
|
22
|
+
# Debug/Testing
|
|
23
|
+
TESTING=False
|
|
@@ -13,24 +13,21 @@
|
|
|
13
13
|
|
|
14
14
|
"""LSO, an API for remotely running Ansible playbooks."""
|
|
15
15
|
|
|
16
|
-
__version__ = "
|
|
16
|
+
__version__ = "2.0.0"
|
|
17
17
|
|
|
18
18
|
import logging
|
|
19
19
|
|
|
20
20
|
from fastapi import FastAPI
|
|
21
21
|
from fastapi.middleware.cors import CORSMiddleware
|
|
22
22
|
|
|
23
|
-
from lso import
|
|
23
|
+
from lso import environment
|
|
24
24
|
from lso.routes.default import router as default_router
|
|
25
25
|
from lso.routes.playbook import router as playbook_router
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
def create_app() -> FastAPI:
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
|
-
:return: a new flask app instance
|
|
32
|
-
"""
|
|
33
|
-
app = FastAPI()
|
|
29
|
+
"""Initialise the :term:`LSO` app."""
|
|
30
|
+
app = FastAPI(docs_url="/api/doc", redoc_url="/api/redoc", openapi_url="/api/openapi.json")
|
|
34
31
|
|
|
35
32
|
app.add_middleware(
|
|
36
33
|
CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"]
|
|
@@ -39,9 +36,6 @@ def create_app() -> FastAPI:
|
|
|
39
36
|
app.include_router(default_router, prefix="/api")
|
|
40
37
|
app.include_router(playbook_router, prefix="/api/playbook")
|
|
41
38
|
|
|
42
|
-
# test that configuration parameters are loaded and available
|
|
43
|
-
config.load()
|
|
44
|
-
|
|
45
39
|
environment.setup_logging()
|
|
46
40
|
|
|
47
41
|
logging.info("FastAPI app initialized")
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Copyright 2023-2024 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 for loading and managing configuration settings for the LSO app.
|
|
15
|
+
|
|
16
|
+
Uses `pydantic`'s `BaseSettings` to load settings from environment variables.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
from enum import Enum
|
|
21
|
+
|
|
22
|
+
from pydantic_settings import BaseSettings
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ExecutorType(Enum):
|
|
26
|
+
"""Enum representing the types of executors available for task execution."""
|
|
27
|
+
|
|
28
|
+
WORKER = "celery"
|
|
29
|
+
THREADPOOL = "threadpool"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Config(BaseSettings):
|
|
33
|
+
"""The set of parameters required for running :term:`LSO`."""
|
|
34
|
+
|
|
35
|
+
TESTING: bool = False
|
|
36
|
+
ANSIBLE_PLAYBOOKS_ROOT_DIR: str = "/path/to/ansible/playbooks"
|
|
37
|
+
EXECUTOR: ExecutorType = ExecutorType.THREADPOOL
|
|
38
|
+
MAX_THREAD_POOL_WORKERS: int = min(32, (os.cpu_count() or 1) + 4)
|
|
39
|
+
REQUEST_TIMEOUT_SEC: int = 10
|
|
40
|
+
CELERY_BROKER_URL: str = "redis://localhost:6379/0"
|
|
41
|
+
CELERY_RESULT_BACKEND: str = "redis://localhost:6379/0"
|
|
42
|
+
CELERY_TIMEZONE: str = "Europe/Amsterdam"
|
|
43
|
+
CELERY_ENABLE_UTC: bool = True
|
|
44
|
+
CELERY_RESULT_EXPIRES: int = 3600
|
|
45
|
+
WORKER_QUEUE_NAME: str | None = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
settings = Config()
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Copyright 2023-2024 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 uuid
|
|
17
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from pydantic import HttpUrl
|
|
22
|
+
|
|
23
|
+
from lso.config import ExecutorType, settings
|
|
24
|
+
from lso.tasks import run_playbook_proc_task
|
|
25
|
+
|
|
26
|
+
_executor = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_thread_pool() -> ThreadPoolExecutor:
|
|
30
|
+
"""Get and optionally initialise a ThreadPoolExecutor.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
ThreadPoolExecutor
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
global _executor # noqa: PLW0603
|
|
37
|
+
if _executor is None:
|
|
38
|
+
_executor = ThreadPoolExecutor(max_workers=settings.MAX_THREAD_POOL_WORKERS)
|
|
39
|
+
|
|
40
|
+
return _executor
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_playbook_path(playbook_name: Path) -> Path:
|
|
44
|
+
"""Get the path of a playbook on the local filesystem."""
|
|
45
|
+
return Path(settings.ANSIBLE_PLAYBOOKS_ROOT_DIR) / playbook_name
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def run_playbook(
|
|
49
|
+
playbook_path: Path,
|
|
50
|
+
extra_vars: dict[str, Any],
|
|
51
|
+
inventory: dict[str, Any] | str,
|
|
52
|
+
callback: HttpUrl,
|
|
53
|
+
) -> uuid.UUID:
|
|
54
|
+
"""Run an Ansible playbook against a specified inventory.
|
|
55
|
+
|
|
56
|
+
:param Path playbook_path: playbook to be executed.
|
|
57
|
+
:param dict[str, Any] extra_vars: Any extra vars needed for the playbook to run.
|
|
58
|
+
:param dict[str, Any] | str inventory: The inventory that the playbook is executed against.
|
|
59
|
+
:param HttpUrl callback: Callback URL where the playbook should send a status update when execution is completed.
|
|
60
|
+
This is used for workflow-orchestrator to continue with the next step in a workflow.
|
|
61
|
+
:return: Result of playbook launch, this could either be successful or unsuccessful.
|
|
62
|
+
:rtype: :class:`fastapi.responses.JSONResponse`
|
|
63
|
+
"""
|
|
64
|
+
job_id = uuid.uuid4()
|
|
65
|
+
if settings.EXECUTOR == ExecutorType.THREADPOOL:
|
|
66
|
+
executor = get_thread_pool()
|
|
67
|
+
executor_handle = executor.submit(
|
|
68
|
+
run_playbook_proc_task, str(job_id), str(playbook_path), extra_vars, inventory, str(callback)
|
|
69
|
+
)
|
|
70
|
+
if settings.TESTING:
|
|
71
|
+
executor_handle.result()
|
|
72
|
+
|
|
73
|
+
elif settings.EXECUTOR == ExecutorType.WORKER:
|
|
74
|
+
run_playbook_proc_task.delay(str(job_id), str(playbook_path), extra_vars, inventory, str(callback))
|
|
75
|
+
|
|
76
|
+
return job_id
|
|
@@ -15,14 +15,16 @@
|
|
|
15
15
|
|
|
16
16
|
import json
|
|
17
17
|
import tempfile
|
|
18
|
+
import uuid
|
|
18
19
|
from contextlib import redirect_stderr
|
|
19
20
|
from io import StringIO
|
|
21
|
+
from pathlib import Path
|
|
20
22
|
from typing import Annotated, Any
|
|
21
23
|
|
|
24
|
+
import ansible_runner
|
|
22
25
|
from ansible.inventory.manager import InventoryManager
|
|
23
26
|
from ansible.parsing.dataloader import DataLoader
|
|
24
27
|
from fastapi import APIRouter, HTTPException, status
|
|
25
|
-
from fastapi.responses import JSONResponse
|
|
26
28
|
from pydantic import AfterValidator, BaseModel, HttpUrl
|
|
27
29
|
|
|
28
30
|
from lso.playbook import get_playbook_path, run_playbook
|
|
@@ -31,11 +33,19 @@ router = APIRouter()
|
|
|
31
33
|
|
|
32
34
|
|
|
33
35
|
def _inventory_validator(inventory: dict[str, Any] | str) -> dict[str, Any] | str:
|
|
34
|
-
"""Validate the
|
|
36
|
+
"""Validate the provided inventory format.
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
Attempts to parse the inventory to verify its validity. If the inventory cannot be parsed or the inventory
|
|
39
|
+
format is incorrect an HTTP 422 error is raised.
|
|
40
|
+
|
|
41
|
+
:param inventory: The inventory to validate, can be a dictionary or a string.
|
|
42
|
+
:return: The validated inventory if no errors are found.
|
|
43
|
+
:raises HTTPException: If parsing fails or the format is incorrect.
|
|
38
44
|
"""
|
|
45
|
+
if not ansible_runner.utils.isinventory(inventory):
|
|
46
|
+
detail = "Invalid inventory provided. Should be a string, or JSON object."
|
|
47
|
+
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=detail)
|
|
48
|
+
|
|
39
49
|
loader = DataLoader()
|
|
40
50
|
output = StringIO()
|
|
41
51
|
with tempfile.NamedTemporaryFile(mode="w+") as temp_inv, redirect_stderr(output):
|
|
@@ -53,15 +63,31 @@ def _inventory_validator(inventory: dict[str, Any] | str) -> dict[str, Any] | st
|
|
|
53
63
|
return inventory
|
|
54
64
|
|
|
55
65
|
|
|
66
|
+
def _playbook_path_validator(playbook_name: Path) -> Path:
|
|
67
|
+
playbook_path = get_playbook_path(playbook_name)
|
|
68
|
+
if not Path.exists(playbook_path):
|
|
69
|
+
msg = f"Filename '{playbook_path}' does not exist."
|
|
70
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=msg)
|
|
71
|
+
|
|
72
|
+
return playbook_path
|
|
73
|
+
|
|
74
|
+
|
|
56
75
|
PlaybookInventory = Annotated[dict[str, Any] | str, AfterValidator(_inventory_validator)]
|
|
76
|
+
PlaybookName = Annotated[Path, AfterValidator(_playbook_path_validator)]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class PlaybookRunResponse(BaseModel):
|
|
80
|
+
"""PlaybookRunResponse domain model schema."""
|
|
81
|
+
|
|
82
|
+
job_id: uuid.UUID
|
|
57
83
|
|
|
58
84
|
|
|
59
85
|
class PlaybookRunParams(BaseModel):
|
|
60
86
|
"""Parameters for executing an Ansible playbook."""
|
|
61
87
|
|
|
62
88
|
#: The filename of a playbook that's executed. It should be present inside the directory defined in the
|
|
63
|
-
#: configuration option ``
|
|
64
|
-
playbook_name:
|
|
89
|
+
#: configuration option ``ANSIBLE_PLAYBOOKS_ROOT_DIR``.
|
|
90
|
+
playbook_name: PlaybookName
|
|
65
91
|
#: The address where LSO should call back to upon completion.
|
|
66
92
|
callback: HttpUrl
|
|
67
93
|
#: The inventory to run the playbook against. This inventory can also include any host vars, if needed. When
|
|
@@ -74,8 +100,8 @@ class PlaybookRunParams(BaseModel):
|
|
|
74
100
|
extra_vars: dict[str, Any] = {}
|
|
75
101
|
|
|
76
102
|
|
|
77
|
-
@router.post("/")
|
|
78
|
-
def run_playbook_endpoint(params: PlaybookRunParams) ->
|
|
103
|
+
@router.post("/", response_model=PlaybookRunResponse, status_code=status.HTTP_201_CREATED)
|
|
104
|
+
def run_playbook_endpoint(params: PlaybookRunParams) -> PlaybookRunResponse:
|
|
79
105
|
"""Launch an Ansible playbook to modify or deploy a subscription instance.
|
|
80
106
|
|
|
81
107
|
The response will contain either a job ID, or error information.
|
|
@@ -83,9 +109,11 @@ def run_playbook_endpoint(params: PlaybookRunParams) -> JSONResponse:
|
|
|
83
109
|
:param PlaybookRunParams params: Parameters for executing a playbook.
|
|
84
110
|
:return JSONResponse: Response from the Ansible runner, including a run ID.
|
|
85
111
|
"""
|
|
86
|
-
|
|
87
|
-
playbook_path=
|
|
112
|
+
job_id = run_playbook(
|
|
113
|
+
playbook_path=params.playbook_name,
|
|
88
114
|
extra_vars=params.extra_vars,
|
|
89
115
|
inventory=params.inventory,
|
|
90
116
|
callback=params.callback,
|
|
91
117
|
)
|
|
118
|
+
|
|
119
|
+
return PlaybookRunResponse(job_id=job_id)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Copyright 2023-2024 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 defines tasks for executing Ansible playbooks asynchronously using Celery.
|
|
15
|
+
|
|
16
|
+
The primary task, `run_playbook_proc_task`, runs an Ansible playbook and sends a POST request with
|
|
17
|
+
the results to a specified callback URL.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
import ansible_runner
|
|
24
|
+
import requests
|
|
25
|
+
from starlette import status
|
|
26
|
+
|
|
27
|
+
from lso.config import settings
|
|
28
|
+
from lso.worker import RUN_PLAYBOOK, celery
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CallbackFailedError(Exception):
|
|
34
|
+
"""Exception raised when a callback url can't be reached."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@celery.task(name=RUN_PLAYBOOK) # type: ignore[misc]
|
|
38
|
+
def run_playbook_proc_task(
|
|
39
|
+
job_id: str, playbook_path: str, extra_vars: dict[str, Any], inventory: dict[str, Any] | str, callback: str
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Celery task to run a playbook.
|
|
42
|
+
|
|
43
|
+
:param str job_id: Identifier of the job being executed.
|
|
44
|
+
:param str playbook_path: Path to the playbook to be executed.
|
|
45
|
+
:param dict[str, Any] extra_vars: Extra variables to pass to the playbook.
|
|
46
|
+
:param dict[str, Any] | str inventory: Inventory to run the playbook against.
|
|
47
|
+
:param HttpUrl callback: Callback URL for status updates.
|
|
48
|
+
:return: None
|
|
49
|
+
"""
|
|
50
|
+
msg = f"playbook_path: {playbook_path}, callback: {callback}"
|
|
51
|
+
logger.info(msg)
|
|
52
|
+
ansible_playbook_run = ansible_runner.run(playbook=playbook_path, inventory=inventory, extravars=extra_vars)
|
|
53
|
+
|
|
54
|
+
payload = {
|
|
55
|
+
"status": ansible_playbook_run.status,
|
|
56
|
+
"job_id": job_id,
|
|
57
|
+
"output": ansible_playbook_run.stdout.readlines(),
|
|
58
|
+
"return_code": int(ansible_playbook_run.rc),
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
request_result = requests.post(str(callback), json=payload, timeout=settings.REQUEST_TIMEOUT_SEC)
|
|
62
|
+
if not status.HTTP_200_OK <= request_result.status_code < status.HTTP_300_MULTIPLE_CHOICES:
|
|
63
|
+
msg = f"Callback failed: {request_result.text}, url: {callback}"
|
|
64
|
+
raise CallbackFailedError(msg)
|