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.
Files changed (51) hide show
  1. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/.bumpversion.cfg +1 -1
  2. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/.github/workflows/run-unit-tests.yaml +7 -3
  3. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/Dockerfile.example +1 -1
  4. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/PKG-INFO +48 -25
  5. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/README.md +37 -16
  6. orchestrator_lso-2.0.0/docs/LSO_banner.jpg +0 -0
  7. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/docs/source/_static/custom.css +2 -2
  8. orchestrator_lso-2.0.0/docs/source/_static/lso_logo.png +0 -0
  9. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/docs/source/conf.py +3 -2
  10. orchestrator_lso-2.0.0/env.example +23 -0
  11. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/lso/__init__.py +4 -10
  12. orchestrator_lso-2.0.0/lso/config.py +48 -0
  13. orchestrator_lso-2.0.0/lso/playbook.py +76 -0
  14. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/lso/routes/playbook.py +38 -10
  15. orchestrator_lso-2.0.0/lso/tasks.py +64 -0
  16. orchestrator_lso-2.0.0/lso/worker.py +52 -0
  17. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/pyproject.toml +10 -7
  18. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/test/conftest.py +25 -30
  19. orchestrator_lso-2.0.0/test/routes/test_playbook.py +224 -0
  20. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/test/test_ansible.py +0 -2
  21. orchestrator_lso-1.0.2/config.json.example +0 -3
  22. orchestrator_lso-1.0.2/lso/config.py +0 -70
  23. orchestrator_lso-1.0.2/lso/playbook.py +0 -122
  24. orchestrator_lso-1.0.2/test/routes/test_playbook.py +0 -120
  25. orchestrator_lso-1.0.2/test/test_config.py +0 -44
  26. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/.github/dependabot.yml +0 -0
  27. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/.github/styles/config/vocabularies/Sphinx/accept.txt +0 -0
  28. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/.github/styles/config/vocabularies/jargon/accept.txt +0 -0
  29. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/.github/workflows/publish-package.yaml +0 -0
  30. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/.github/workflows/run-linting-tests.yaml +0 -0
  31. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/.github/workflows/sphinx.yaml +0 -0
  32. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/.gitignore +0 -0
  33. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/.vale.ini +0 -0
  34. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/LICENSE +0 -0
  35. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/docs/Makefile +0 -0
  36. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/docs/source/index.rst +0 -0
  37. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/docs/source/module/config.rst +0 -0
  38. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/docs/source/module/playbook.rst +0 -0
  39. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/docs/source/module/routes/default.rst +0 -0
  40. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/docs/source/module/routes/index.rst +0 -0
  41. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/docs/source/module/routes/playbook.rst +0 -0
  42. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/docs/source/modules.rst +0 -0
  43. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/lso/app.py +0 -0
  44. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/lso/environment.py +0 -0
  45. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/lso/routes/__init__.py +0 -0
  46. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/lso/routes/default.py +0 -0
  47. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/setup.py +0 -0
  48. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/test/__init__.py +0 -0
  49. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/test/routes/__init__.py +0 -0
  50. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/test/routes/test_default.py +0 -0
  51. {orchestrator_lso-1.0.2 → orchestrator_lso-2.0.0}/test/test-playbook.yaml +0 -0
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 1.0.2
2
+ current_version = 2.0.0
3
3
  commit = False
4
4
  tag = False
5
5
  parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(rc(?P<build>\d+))?
@@ -25,6 +25,10 @@ jobs:
25
25
  env:
26
26
  FLIT_ROOT_INSTALL: 1
27
27
  - name: Run Unit tests
28
- run: pytest
29
- env:
30
- SETTINGS_FILENAME: dummy.json
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=="1.0.2"
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
1
+ Metadata-Version: 2.3
2
2
  Name: orchestrator-lso
3
- Version: 1.0.2
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~=2.3.4
29
- Requires-Dist: ansible~=9.3.0
30
- Requires-Dist: fastapi~=0.110.0
31
- Requires-Dist: httpx~=0.27.0
32
- Requires-Dist: jsonschema~=4.21.1
33
- Requires-Dist: uvicorn[standard]~=0.28.0
34
- Requires-Dist: requests~=2.31.0
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
- # Lightweight Service Orchestrator
60
+ ![Lightweight Service Orchestrator](./docs/LSO_banner.jpg)
61
+ [![Supported python versions](https://img.shields.io/pypi/pyversions/orchestrator-lso.svg?color=%2334D058)](https://pypi.org/project/orchestrator-lso)
62
+ [![Downloads](https://static.pepy.tech/badge/orchestrator-lso/month)](https://pepy.tech/project/orchestrator-lso)
63
+ [![codecov](https://codecov.io/github/workfloworchestrator/lso/graph/badge.svg?token=NVFHBBU3AR)](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
- version: "3.5"
79
- services:
80
- lso:
81
- image: my-lso:latest
82
- environment:
83
- SETTINGS_FILENAME: /app/config.json
84
- ANSIBLE_ROLES_PATH: /app/lso/ansible_roles
85
- volumes:
86
- - "/home/user/config.json:/app/config.json:ro"
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
- * A `config.json` that references to the location where the Ansible playbooks are stored **inside the container**.
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
- * Create a settings file, see `config.json.example` for an example.
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
- SETTINGS_FILENAME=/absolute/path/to/config.json python -m lso.app
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
- # Lightweight Service Orchestrator
1
+ ![Lightweight Service Orchestrator](./docs/LSO_banner.jpg)
2
+ [![Supported python versions](https://img.shields.io/pypi/pyversions/orchestrator-lso.svg?color=%2334D058)](https://pypi.org/project/orchestrator-lso)
3
+ [![Downloads](https://static.pepy.tech/badge/orchestrator-lso/month)](https://pepy.tech/project/orchestrator-lso)
4
+ [![codecov](https://codecov.io/github/workfloworchestrator/lso/graph/badge.svg?token=NVFHBBU3AR)](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
- version: "3.5"
23
- services:
24
- lso:
25
- image: my-lso:latest
26
- environment:
27
- SETTINGS_FILENAME: /app/config.json
28
- ANSIBLE_ROLES_PATH: /app/lso/ansible_roles
29
- volumes:
30
- - "/home/user/config.json:/app/config.json:ro"
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
- * A `config.json` that references to the location where the Ansible playbooks are stored **inside the container**.
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
- * Create a settings file, see `config.json.example` for an example.
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
- SETTINGS_FILENAME=/absolute/path/to/config.json python -m lso.app
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`.
@@ -1,10 +1,10 @@
1
1
  .wy-menu > p > span {
2
- color: rgb(237 21 86);
2
+ color: rgb(254 122 54);
3
3
  }
4
4
 
5
5
  section > dl > .sig-object {
6
6
  background: #e5e8e8 !important;
7
- color: rgb(237 21 86) !important;
7
+ color: rgb(254 122 54) !important;
8
8
  border-top: 3px solid rgb(167 179 180) !important;
9
9
  }
10
10
 
@@ -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(0 63 95)"}
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__ = "1.0.2"
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 config, environment
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
- """Override default settings with those found in the file read from environment variable `SETTINGS_FILENAME`.
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 format of the provided inventory by trying to parse it.
36
+ """Validate the provided inventory format.
35
37
 
36
- If an inventory can't be parsed without warnings or errors, these are returned to the user by means of an HTTP
37
- status 422 for 'unprocessable entity'.
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 ``ansible_playbooks_root_dir``.
64
- playbook_name: str
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) -> JSONResponse:
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
- return run_playbook(
87
- playbook_path=get_playbook_path(params.playbook_name),
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)